mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-16 10:48:47 +00:00
Merge branch 'main' into bug6239
This commit is contained in:
Generated
+1
-1
@@ -266,7 +266,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ FROM eclipse-temurin:8-jdk
|
||||
# the configuration/github-download-release-assets.sh script to
|
||||
# obtain the tar.gz file for a specific or the latest "main" release.
|
||||
# To build with an SAP JVM 8 base image built using Dockerfile_sapjvm, use something like:
|
||||
#FROM sapjvm8:8.1.108
|
||||
#FROM sapjvm8:8.1.109
|
||||
ARG RELEASE
|
||||
LABEL maintainer=axel.uhl@sap.com
|
||||
LABEL org.opencontainers.image.description="Sailing Analytics with Java 8"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM buildpack-deps:bullseye
|
||||
ARG SAPJVM_VERSION=8.1.108
|
||||
ARG SAPJVM_VERSION=8.1.109
|
||||
LABEL maintainer=axel.uhl@sap.com
|
||||
# Download and extract the SAP JVM 8
|
||||
ENV PATH=${PATH}:/opt/sapjvm_8/bin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM sapmachine:25.0.1
|
||||
FROM sapmachine:25.0.3
|
||||
# This Dockerfile assumes that the release to use is provided as
|
||||
# ${RELEASE}.tar.gz in the current Docker workspace. Use, e.g.,
|
||||
# the configuration/github-download-release-assets.sh script to
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM sapmachine:25.0.1
|
||||
FROM sapmachine:25.0.3
|
||||
LABEL maintainer=axel.uhl@sap.com
|
||||
# Download and extract the release
|
||||
WORKDIR /home/sailing
|
||||
|
||||
+4
-2
@@ -15,6 +15,7 @@ import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
import com.sap.sailing.domain.common.orc.ORCCertificate;
|
||||
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CertificateFamily;
|
||||
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CertificateHandle;
|
||||
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CountryOverview;
|
||||
import com.sap.sailing.domain.orc.impl.ORCPublicCertificateDatabaseImpl;
|
||||
@@ -52,8 +53,9 @@ public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallb
|
||||
countryWithMostValidCertificates = StreamSupport
|
||||
.stream(db.getCountriesWithValidCertificates().spliterator(), /* parallel */ false)
|
||||
.max((c1, c2) -> c1.getCertCount() - c2.getCertCount()).get();
|
||||
Iterable<CertificateHandle> certificateHandles = db.search(countryWithMostValidCertificates.getIssuingCountry(),
|
||||
countryWithMostValidCertificates.getVPPYear(), null, null, null, null, /* includeInvalid */ false);
|
||||
Iterable<CertificateHandle> certificateHandles = Util.filter(db.search(countryWithMostValidCertificates.getIssuingCountry(),
|
||||
countryWithMostValidCertificates.getVPPYear(), null, null, null, null, /* includeInvalid */ false),
|
||||
certHandle->certHandle.getFamily() != CertificateFamily.ORC_LIGHT); // exclude LITE certificates
|
||||
final List<CertificateHandle> randomSubset = new ArrayList<>();
|
||||
Util.addAll(certificateHandles, randomSubset);
|
||||
Collections.shuffle(randomSubset);
|
||||
|
||||
+18
-4
@@ -2,9 +2,11 @@ package com.sap.sailing.domain.orc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -13,6 +15,8 @@ import com.sap.sailing.domain.common.orc.ORCCertificate;
|
||||
|
||||
|
||||
public class TestORCCertificateImporterJSON extends AbstractORCCertificateImporterTest {
|
||||
private static final Logger logger = Logger.getLogger(TestORCCertificateImporterJSON.class.getName());
|
||||
|
||||
@Test
|
||||
public void testSimpleLocalJSONFileRead() throws IOException, ParseException {
|
||||
testSimpleLocalFileRead("GER2019.json", "GER20041179");
|
||||
@@ -27,9 +31,19 @@ public class TestORCCertificateImporterJSON extends AbstractORCCertificateImport
|
||||
@Test
|
||||
public void testSimpleOnlineFileRead() throws IOException, ParseException, InterruptedException {
|
||||
Collection<ORCCertificate> certificates = FailIfNoValidOrcCertificateRule.getAvailableCerts();
|
||||
final ORCCertificate referenceCert = certificates.stream().findFirst().get();
|
||||
assertNotNull(referenceCert);
|
||||
assertTrue(referenceCert.getWindwardLeewardSpeedPrediction().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
|
||||
assertTrue(referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
|
||||
for (final ORCCertificate referenceCert : certificates) {
|
||||
assertNotNull(referenceCert);
|
||||
// some certificates are not fully filled with allowances for all types of PCS pre-sets; we need to check whether
|
||||
// the certificate at hand has those we need for this test; else, we keep going and use another one.
|
||||
// We're already excluding LITE certificates to reduce chances for this case
|
||||
if (referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]) != null) {
|
||||
assertTrue(referenceCert.getWindwardLeewardSpeedPrediction().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
|
||||
assertTrue(referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
|
||||
return;
|
||||
} else {
|
||||
logger.info("No valid GPH found in certificate "+referenceCert.getBoatName()+" with sail number "+referenceCert.getSailNumber());
|
||||
}
|
||||
}
|
||||
fail("Found only certificates with no valid long distance speed predictions; this seems unlikely and is probably an error");
|
||||
}
|
||||
}
|
||||
|
||||
+53
-44
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
@@ -153,51 +154,59 @@ public class TestORCPublicCertificateDatabase {
|
||||
@Test
|
||||
public void testGetCertificate() throws Exception {
|
||||
Collection<ORCCertificate> certificates = FailIfNoValidOrcCertificateRule.getAvailableCerts();
|
||||
final ORCCertificate cert = certificates.stream().findFirst().get();
|
||||
Iterable<CertificateHandle> certHandles = db.search(/* country */ null, LocalDate.now().getYear(), /* referenceNumber */ null, cert.getBoatName(),
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// there were certs; get one from the previous year
|
||||
certHandles = db.search(null, LocalDate.now().getYear()-1, null, cert.getBoatName(),
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
// some certificates seem to be lacking a GPH value; can't use those for this test; we're explicitly excluding LITE certificates already...
|
||||
for (final ORCCertificate cert : certificates) {
|
||||
if (cert.getGPH() != null) {
|
||||
Iterable<CertificateHandle> certHandles = db.search(/* country */ null, LocalDate.now().getYear(), /* referenceNumber */ null, cert.getBoatName(),
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// there were certs; get one from the previous year
|
||||
certHandles = db.search(null, LocalDate.now().getYear()-1, null, cert.getBoatName(),
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
}
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// there were certs; try searching by reference number
|
||||
certHandles = db.search(null, LocalDate.now().getYear(), cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
}
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// still nothing? Then try by reference number in previous year:
|
||||
certHandles = db.search(null, LocalDate.now().getYear()-1, cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
}
|
||||
Optional<CertificateHandle> certificateHandle = Optional.ofNullable(certHandles.iterator().hasNext() ? certHandles.iterator().next() : null);
|
||||
assertTrue(certificateHandle.isPresent(), "No certificate found for handle "+certificateHandle+
|
||||
" extracted from certificates "+certificates);
|
||||
final String referenceNumber = certificateHandle.get().getReferenceNumber();
|
||||
final CertificateHandle handle = db.getCertificateHandle(referenceNumber);
|
||||
final ORCCertificate result = db.getCertificate(referenceNumber, handle.getFamily());
|
||||
assertNotNull(result, "Unable to load certificate for reference number "+referenceNumber+" from handle "+certificateHandle);
|
||||
assertEquals(handle.getGPH(), result.getGPH().asSeconds(), 0.00001);
|
||||
// Use some tolerance as we found differences as much as 5s between the dxtDate in the handle coming from the XML search result
|
||||
// and the IssueDate field in the JSON. Both suggest to report millisecond accuracy, but dxtDate always seems to have the
|
||||
// milliseconds as "000" explaining many sub-second differences. But in some cases differences were significantly bigger.
|
||||
assertEquals(handle.getIssueDate().asMillis(), result.getIssueDate().asMillis(), 10000.0, "Issue dates of certificate with reference number "+referenceNumber+
|
||||
" varies between current year result handle ("+handle.getIssueDate()+") and certificate ("+
|
||||
result.getIssueDate()+").");
|
||||
assertEquals(handle.getSailNumber(), result.getSailNumber());
|
||||
return;
|
||||
} else {
|
||||
logger.info("No valid GPH found in certificate "+cert.getBoatName()+" with sail number "+cert.getSailNumber());
|
||||
}
|
||||
}
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// there were certs; try searching by reference number
|
||||
certHandles = db.search(null, LocalDate.now().getYear(), cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
}
|
||||
if (Util.isEmpty(certHandles)) {
|
||||
// still nothing? Then try by reference number in previous year:
|
||||
certHandles = db.search(null, LocalDate.now().getYear()-1, cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
|
||||
cert.getSailNumber(), /*
|
||||
* boat class name; could be set to cert.getBoatClassName() but there are
|
||||
* deviations in ORC DBs and query API, so leaving null:
|
||||
*/ null, /* includeInvalid */ false);
|
||||
}
|
||||
Optional<CertificateHandle> certificateHandle = Optional.ofNullable(certHandles.iterator().hasNext() ? certHandles.iterator().next() : null);
|
||||
assertTrue(certificateHandle.isPresent(), "No certificate found for handle "+certificateHandle+
|
||||
" extracted from certificates "+certificates);
|
||||
final String referenceNumber = certificateHandle.get().getReferenceNumber();
|
||||
final CertificateHandle handle = db.getCertificateHandle(referenceNumber);
|
||||
final ORCCertificate result = db.getCertificate(referenceNumber, handle.getFamily());
|
||||
assertNotNull(result, "Unable to load certificate for reference number "+referenceNumber+" from handle "+certificateHandle);
|
||||
assertEquals(handle.getGPH(), result.getGPH().asSeconds(), 0.00001);
|
||||
// Use some tolerance as we found differences as much as 5s between the dxtDate in the handle coming from the XML search result
|
||||
// and the IssueDate field in the JSON. Both suggest to report millisecond accuracy, but dxtDate always seems to have the
|
||||
// milliseconds as "000" explaining many sub-second differences. But in some cases differences were significantly bigger.
|
||||
assertEquals(handle.getIssueDate().asMillis(), result.getIssueDate().asMillis(), 10000.0, "Issue dates of certificate with reference number "+referenceNumber+
|
||||
" varies between current year result handle ("+handle.getIssueDate()+") and certificate ("+
|
||||
result.getIssueDate()+").");
|
||||
assertEquals(handle.getSailNumber(), result.getSailNumber());
|
||||
fail("No certificate found with a valid GPH; that seems very suspicious and lets this test case fail.");
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ public class SmartFutureCacheTest {
|
||||
sfc.triggerUpdate("humba", /* update interval */ null);
|
||||
try {
|
||||
// during the first call, expecting exception
|
||||
sfc.get("humba", /* waitForLatest */ true);
|
||||
fail("Expected RuntimeException because computeCacheUpdate threw one");
|
||||
final String result = sfc.get("humba", /* waitForLatest */ true);
|
||||
fail("Expected RuntimeException because computeCacheUpdate threw one; instead, it returned "+result);
|
||||
} catch (RuntimeException expected) {
|
||||
assertSame(ExecutionException.class, expected.getCause().getClass());
|
||||
}
|
||||
|
||||
+84
-3
@@ -105,7 +105,7 @@ public interface ORCPublicCertificateDatabase {
|
||||
ORCPublicCertificateDatabase INSTANCE = new ORCPublicCertificateDatabaseImpl();
|
||||
|
||||
public enum CertificateFamily {
|
||||
UNKNOWN(0, ""), ORC(1, "ORC"), SUPER_YACHT(2, "SY"), DOUBLE_HANDED(3, "DH"), MULTI_HULL(4, "Mu");
|
||||
UNKNOWN(0, ""), ORC(1, "ORC"), SUPER_YACHT(2, "SY"), DOUBLE_HANDED(3, "DH"), MULTI_HULL(4, "Mu"), NON_SPINNAKER(5, "NS"), ORC_LIGHT(6, "LITE");
|
||||
|
||||
private final int familyId;
|
||||
private final String familyQueryParamValue;
|
||||
@@ -137,8 +137,8 @@ public interface ORCPublicCertificateDatabase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data about valid certificates in a country, as obtained, e.g., from http://data.orc.org/public/WPub.dll/RMS. Such a
|
||||
* record, in its original XML representation, looks like this:
|
||||
* Data about valid certificates in a country, as obtained, e.g., from http://data.orc.org/public/WPub.dll/RMS. Such
|
||||
* a record, in its original XML representation, looks like this:
|
||||
*
|
||||
* <pre>
|
||||
<CountryId>AUS</CountryId>
|
||||
@@ -152,6 +152,87 @@ public interface ORCPublicCertificateDatabase {
|
||||
<RMSCode>CLUB</RMSCode>
|
||||
* </pre>
|
||||
*
|
||||
* Mappings of family and type work as follows:
|
||||
*
|
||||
* Used for parsing the ORC public API (WPub.dll) RMS/JSON outputs. Note: Family 6 (ORC Light) certificates
|
||||
* generally lack GPH and Performance Curve data required for PCS calculations.
|
||||
*
|
||||
* <table border="1">
|
||||
* <caption>ORC Family and Certificate Type Codes</caption>
|
||||
* <tr>
|
||||
* <th>Family ID</th>
|
||||
* <th>Family Name</th>
|
||||
* <th>certType</th>
|
||||
* <th>Certificate Name</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>1</td>
|
||||
* <td>ORC Standard</td>
|
||||
* <td>2</td>
|
||||
* <td>International (ORCi)</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>1</td>
|
||||
* <td>ORC Standard</td>
|
||||
* <td>3</td>
|
||||
* <td>Club</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>2</td>
|
||||
* <td>Super Yacht</td>
|
||||
* <td>7</td>
|
||||
* <td>ORCsy</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>3</td>
|
||||
* <td>Double Handed</td>
|
||||
* <td>8</td>
|
||||
* <td>DH International</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>3</td>
|
||||
* <td>Double Handed</td>
|
||||
* <td>9</td>
|
||||
* <td>DH Club</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>4</td>
|
||||
* <td>Multihull</td>
|
||||
* <td>15</td>
|
||||
* <td>Multihull International</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>4</td>
|
||||
* <td>Multihull</td>
|
||||
* <td>16</td>
|
||||
* <td>Multihull Club</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>5</td>
|
||||
* <td>Non Spinnaker</td>
|
||||
* <td>10</td>
|
||||
* <td>NS International</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>5</td>
|
||||
* <td>Non Spinnaker</td>
|
||||
* <td>11</td>
|
||||
* <td>NS Club</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>6</td>
|
||||
* <td>ORC Light</td>
|
||||
* <td>13</td>
|
||||
* <td>Light (Standard)</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>6</td>
|
||||
* <td>ORC Light</td>
|
||||
* <td>14</td>
|
||||
* <td>Light (Double Handed)</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*/
|
||||
public interface CountryOverview {
|
||||
|
||||
+6
-1
@@ -6,12 +6,14 @@ public class FavoritesResult implements Result {
|
||||
|
||||
private FavoriteBoatClassesDTO favoriteBoatClasses;
|
||||
private FavoriteCompetitorsDTO favoriteCompetitors;
|
||||
private boolean isSubscribedToFeatureAndCommunityUpdates;
|
||||
|
||||
protected FavoritesResult() {}
|
||||
|
||||
public FavoritesResult(FavoriteBoatClassesDTO favoriteBoatClasses, FavoriteCompetitorsDTO favoriteCompetitors) {
|
||||
public FavoritesResult(FavoriteBoatClassesDTO favoriteBoatClasses, FavoriteCompetitorsDTO favoriteCompetitors, boolean isSubscribedToFeatureAndCommunityUpdates) {
|
||||
this.favoriteBoatClasses = favoriteBoatClasses;
|
||||
this.favoriteCompetitors = favoriteCompetitors;
|
||||
this.isSubscribedToFeatureAndCommunityUpdates = isSubscribedToFeatureAndCommunityUpdates;
|
||||
}
|
||||
|
||||
public FavoriteBoatClassesDTO getFavoriteBoatClasses() {
|
||||
@@ -22,4 +24,7 @@ public class FavoritesResult implements Result {
|
||||
return favoriteCompetitors;
|
||||
}
|
||||
|
||||
public boolean getIsSubscribedToFeatureAndCommunityUpdates() {
|
||||
return isSubscribedToFeatureAndCommunityUpdates;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -25,7 +25,13 @@ public class GetFavoritesAction implements SailingAction<FavoritesResult> {
|
||||
@Override
|
||||
@GwtIncompatible
|
||||
public FavoritesResult execute(SailingDispatchContext ctx) throws DispatchException {
|
||||
return new FavoritesResult(getFavoriteBoatClasses(ctx), getFavoriteCompetitors(ctx));
|
||||
return new FavoritesResult(getFavoriteBoatClasses(ctx), getFavoriteCompetitors(ctx),
|
||||
!getDidOptOutOfFeatureAndCommunityEmails(ctx));
|
||||
}
|
||||
|
||||
@GwtIncompatible
|
||||
private boolean getDidOptOutOfFeatureAndCommunityEmails(SailingDispatchContext ctx) {
|
||||
return ctx.getSecurityService().getCurrentUser().getDidOptOutOfFeatureAndCommunityEmails();
|
||||
}
|
||||
|
||||
@GwtIncompatible
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.sap.sailing.gwt.home.communication.user.profile;
|
||||
|
||||
import com.google.gwt.core.shared.GwtIncompatible;
|
||||
import com.sap.sailing.gwt.home.communication.SailingAction;
|
||||
import com.sap.sailing.gwt.home.communication.SailingDispatchContext;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.HasWriteAction;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
import com.sap.sse.gwt.dispatch.shared.exceptions.DispatchException;
|
||||
import com.sap.sse.security.shared.UserManagementException;
|
||||
|
||||
public class SaveMiscEmailPreferences implements SailingAction<VoidResult>, HasWriteAction {
|
||||
private Boolean subscribeToFeatureAndCommunityUpdates;
|
||||
|
||||
protected SaveMiscEmailPreferences() {
|
||||
}
|
||||
|
||||
public SaveMiscEmailPreferences(final Boolean subscribeToFeatureAndCommunityUpdates) {
|
||||
this.subscribeToFeatureAndCommunityUpdates = subscribeToFeatureAndCommunityUpdates;
|
||||
}
|
||||
|
||||
@Override
|
||||
@GwtIncompatible
|
||||
public VoidResult execute(SailingDispatchContext ctx) throws DispatchException {
|
||||
try {
|
||||
final String username = ctx.getSecurityService().getCurrentUser().getName();
|
||||
ctx.getSecurityService().updateUserProperties(username, null, null, null,
|
||||
!subscribeToFeatureAndCommunityUpdates);
|
||||
} catch (UserManagementException e) {
|
||||
throw new DispatchException(e.getMessage());
|
||||
}
|
||||
return new VoidResult();
|
||||
}
|
||||
}
|
||||
+3
@@ -12,6 +12,9 @@
|
||||
This comes with general improvements in table selection/de-selection handling.</li>
|
||||
<li>Adding the "Race Rank" column to the leaderboard now consistently adds it after the selection
|
||||
checkbox column and displays it immediately when the settings dialog is confirmed.</li>
|
||||
<li>Users can now opt out of e-mail communication regarding features and community
|
||||
information. This will be honored when sending out such general information about
|
||||
the Sailing Analytics solution.</li>
|
||||
</ul>
|
||||
<h5 class="articleSubheadline">March 2026</h5>
|
||||
<ul class="bulletList">
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.333333333333333em;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
width: 1.333333333333333em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: auto;
|
||||
font-weight: 600;
|
||||
padding-right: 2.666666666666667em;
|
||||
line-height: 2em;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.loadingOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 60%;
|
||||
aspect-ratio: 1/1;
|
||||
border: 2px solid #ccc;
|
||||
border-top-color: #1976d2;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes
|
||||
spin {
|
||||
from {
|
||||
transform:rotate(0deg);
|
||||
} to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.checkboxtile;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import com.google.gwt.core.client.GWT;
|
||||
import com.google.gwt.dom.client.DivElement;
|
||||
import com.google.gwt.dom.client.Document;
|
||||
import com.google.gwt.event.logical.shared.ValueChangeHandler;
|
||||
import com.google.gwt.event.shared.HandlerRegistration;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.CheckBox;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.HasValue;
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
/**
|
||||
* @param onToggle
|
||||
* if null, toggle will be disabled
|
||||
*/
|
||||
public final class CheckBoxTile extends Composite implements HasValue<Boolean> {
|
||||
private static CheckBoxTileUiBinder uiBinder = GWT.create(CheckBoxTileUiBinder.class);
|
||||
|
||||
interface CheckBoxTileUiBinder extends UiBinder<Widget, CheckBoxTile> {
|
||||
}
|
||||
|
||||
@UiField
|
||||
CheckBoxTileResources res;
|
||||
@UiField
|
||||
Label labelUi;
|
||||
@UiField
|
||||
CheckBox toggleButtonUi;
|
||||
private DivElement loadingOverlay;
|
||||
|
||||
public CheckBoxTile(final String label, final boolean initialState,
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle) {
|
||||
super();
|
||||
CheckBoxTileResources.INSTANCE.css().ensureInjected();
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
labelUi.setText(label);
|
||||
initToggleButtonUi(initialState, onToggle);
|
||||
}
|
||||
|
||||
private void initToggleButtonUi(final boolean initialState,
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle) {
|
||||
toggleButtonUi.setValue(initialState);
|
||||
if (onToggle == null) {
|
||||
toggleButtonUi.setEnabled(false);
|
||||
}
|
||||
toggleButtonUi.getElement().getStyle().setProperty("position", "relative");
|
||||
final ValueChangeHandler<Boolean> loadingHandler = value -> {
|
||||
final Boolean newlyToggledValue = value.getValue();
|
||||
overlayLoadingSpinner();
|
||||
final AsyncCallback<VoidResult> callback = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
// undo failed toggle, false arg enforces silence of change handlers on this call
|
||||
toggleButtonUi.setValue(!newlyToggledValue, false);
|
||||
hideLoadingSpinner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
hideLoadingSpinner();
|
||||
}
|
||||
};
|
||||
onToggle.accept(newlyToggledValue, callback);
|
||||
};
|
||||
toggleButtonUi.addValueChangeHandler(loadingHandler);
|
||||
}
|
||||
|
||||
private void overlayLoadingSpinner() {
|
||||
toggleButtonUi.setEnabled(false);
|
||||
if (loadingOverlay == null) {
|
||||
initLoadingOverlay();
|
||||
}
|
||||
loadingOverlay.getStyle().setProperty("display", "flex");
|
||||
}
|
||||
|
||||
private void initLoadingOverlay() {
|
||||
// add elements
|
||||
loadingOverlay = Document.get().createDivElement();
|
||||
loadingOverlay.addClassName(res.css().loadingOverlay());
|
||||
final DivElement spinner = Document.get().createDivElement();
|
||||
spinner.addClassName(res.css().spinner());
|
||||
// add to canvas
|
||||
loadingOverlay.appendChild(spinner);
|
||||
toggleButtonUi.getElement().appendChild(loadingOverlay);
|
||||
}
|
||||
|
||||
private void hideLoadingSpinner() {
|
||||
if (loadingOverlay != null) {
|
||||
loadingOverlay.getStyle().setProperty("display", "none");
|
||||
toggleButtonUi.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Boolean b) {
|
||||
toggleButtonUi.setValue(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getValue() {
|
||||
return toggleButtonUi.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Boolean> handler) {
|
||||
return toggleButtonUi.addValueChangeHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Boolean value, boolean fireEvents) {
|
||||
toggleButtonUi.setValue(value, fireEvents);
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
|
||||
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui">
|
||||
<ui:with field="i18n" type="com.sap.sailing.gwt.ui.client.StringMessages" />
|
||||
<ui:with field="res" type="com.sap.sailing.gwt.home.shared.partials.checkboxtile.CheckBoxTileResources" />
|
||||
<g:HTMLPanel addStyleNames="{res.css.container}">
|
||||
<g:Label addStyleNames="{res.css.label}" ui:field="labelUi"/>
|
||||
<g:CheckBox addStyleNames="{res.css.checkbox}" ui:field="toggleButtonUi" />
|
||||
</g:HTMLPanel>
|
||||
</ui:UiBinder>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.checkboxtile;
|
||||
|
||||
import com.google.gwt.core.shared.GWT;
|
||||
import com.google.gwt.resources.client.ClientBundle;
|
||||
import com.google.gwt.resources.client.CssResource;
|
||||
|
||||
public interface CheckBoxTileResources extends ClientBundle {
|
||||
|
||||
public static final CheckBoxTileResources INSTANCE = GWT.create(CheckBoxTileResources.class);
|
||||
|
||||
@Source("CheckBoxTile.gss")
|
||||
LocalCss css();
|
||||
|
||||
public interface LocalCss extends CssResource {
|
||||
String container();
|
||||
String label();
|
||||
String checkbox();
|
||||
String loadingOverlay();
|
||||
String spinner();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.labeledbox;
|
||||
|
||||
import com.google.gwt.core.client.GWT;
|
||||
import com.google.gwt.dom.client.SpanElement;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
|
||||
public class LabeledBox extends Composite {
|
||||
@UiField
|
||||
SpanElement headerTitleUi;
|
||||
@UiField(provided = true)
|
||||
Widget childUi;
|
||||
|
||||
private static LabeledBoxUiBinder uiBinder = GWT.create(LabeledBoxUiBinder.class);
|
||||
|
||||
interface LabeledBoxUiBinder extends UiBinder<Widget, LabeledBox> {
|
||||
}
|
||||
|
||||
public LabeledBox(final String title, final Widget childUi) {
|
||||
this.childUi = childUi;
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
headerTitleUi.setInnerText(title);
|
||||
}
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
|
||||
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
|
||||
xmlns:g="urn:import:com.google.gwt.user.client.ui">
|
||||
<ui:with field="local_res"
|
||||
type="com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionResources" />
|
||||
<ui:style>
|
||||
|
||||
</ui:style>
|
||||
<g:HTMLPanel addStyleNames="{local_res.css.suggestions}">
|
||||
<div class="{local_res.css.suggestionsHeader}">
|
||||
<span class="{local_res.css.suggestionsHeaderTitle}"
|
||||
ui:field="headerTitleUi" />
|
||||
</div>
|
||||
<g:Widget ui:field="childUi" />
|
||||
</g:HTMLPanel>
|
||||
</ui:UiBinder>
|
||||
+6
-4
@@ -51,16 +51,18 @@ public abstract class AbstractSuggestedMultiSelectionPresenter<T, D extends Disp
|
||||
selectedItemsMap.clear();
|
||||
persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> getSelection() {
|
||||
return new HashSet<>(selectedItemsMap.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void initSelectedItems(Collection<T> selectedItems) {
|
||||
public final void initSelectedItems(Iterable<T> selectedItems) {
|
||||
selectedItemsMap.clear();
|
||||
for (T item : selectedItems) {
|
||||
selectedItemsMap.put(getKey(item), item);
|
||||
}
|
||||
for (D display : displays) {
|
||||
display.setSelectedItems(selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.multiselection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.IsWidget;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.dto.BoatClassDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.checkboxtile.CheckBoxTile;
|
||||
import com.sap.sailing.gwt.home.shared.partials.filter.AbstractSuggestBoxFilter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.labeledbox.LabeledBox;
|
||||
import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.BoatClassSelectionPresenter;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sse.gwt.client.Notification;
|
||||
import com.sap.sse.gwt.client.Notification.NotificationType;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public class BoatClassDisplayImpl implements IsWidget, BoatClassSelectionPresenter.Display {
|
||||
public final LabeledBox selectionUi;
|
||||
private final FlowPanel childUi;
|
||||
private final CheckBoxTile upcomingRacesUi;
|
||||
private final CheckBoxTile resultsUi;
|
||||
private final SuggestedMultiSelection<BoatClassDTO> filterUi;
|
||||
private final BoatClassSelectionPresenter presenter;
|
||||
|
||||
@Override
|
||||
public Widget asWidget() {
|
||||
return selectionUi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getNotifyAboutUpcomingRaces() {
|
||||
return upcomingRacesUi.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getNotifyAboutResults() {
|
||||
return resultsUi.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<BoatClassDTO> getSelection() {
|
||||
return filterUi.getSelection();
|
||||
}
|
||||
|
||||
public BoatClassDisplayImpl(final BoatClassSelectionPresenter presenter) {
|
||||
this.presenter = presenter;
|
||||
presenter.addDisplay(this);
|
||||
upcomingRacesUi = composeUpcomingRacesTile();
|
||||
resultsUi = composeResultsTile();
|
||||
filterUi = composeFilter();
|
||||
childUi = new FlowPanel();
|
||||
childUi.add(upcomingRacesUi);
|
||||
childUi.add(resultsUi);
|
||||
childUi.add(filterUi);
|
||||
final String title = StringMessages.INSTANCE.favoriteBoatClasses();
|
||||
selectionUi = new LabeledBox(title, childUi);
|
||||
}
|
||||
|
||||
private SuggestedMultiSelection<BoatClassDTO> composeFilter() {
|
||||
final SuggestedMultiSelection.WidgetFactory<BoatClassDTO> widgetFactory = new SuggestedMultiSelection.WidgetFactory<BoatClassDTO>() {
|
||||
@Override
|
||||
public IsWidget generateItemDescriptionWidget(BoatClassDTO item) {
|
||||
return new SuggestedMultiSelectionBoatClassItemDescription(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractSuggestBoxFilter<BoatClassDTO, BoatClassDTO> generateSuggestionSearchBar(
|
||||
Consumer<BoatClassDTO> selectionCallback) {
|
||||
final String text = StringMessages.INSTANCE.add(StringMessages.INSTANCE.boatClass());
|
||||
return new SuggestedMultiSelection.SelectableSuggestion<BoatClassDTO>(presenter, selectionCallback, text);
|
||||
}
|
||||
};
|
||||
final AsyncCallback<VoidResult> callbackWrappedWithToastNotification = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(StringMessages.INSTANCE.failedToModifyFavoredBoatClasses(), NotificationType.ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
Notification.notify(StringMessages.INSTANCE.succesfullyModifiedFavoredBoatClasses(), NotificationType.SUCCESS);
|
||||
}
|
||||
};
|
||||
presenter.setSelectionPersistenceCallback(callbackWrappedWithToastNotification);
|
||||
return new SuggestedMultiSelection<>(presenter, widgetFactory);
|
||||
}
|
||||
|
||||
private CheckBoxTile composeUpcomingRacesTile() {
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle = (isNowTrue, callback) -> {
|
||||
presenter.setNotifyAboutUpcomingRaces(isNowTrue, wrapCallbackWithToastResponse(isNowTrue, callback,
|
||||
StringMessages.INSTANCE.failedToSetStatusOfUpdatesOnUpcomingRacesForYourFavoredBoatClasses(),
|
||||
StringMessages.INSTANCE.youWillNowReceiveNotificationsForFavoriteBoatClassUpcomingRaces(),
|
||||
StringMessages.INSTANCE.youWillNotReceiveUpdatesOnUpcomingRacesForYourFavoredBoatClassesAnymore()));
|
||||
};
|
||||
final String title = StringMessages.INSTANCE.notificationAboutUpcomingRaces();
|
||||
return new CheckBoxTile(title, false, onToggle);
|
||||
}
|
||||
|
||||
private CheckBoxTile composeResultsTile() {
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle = (isNowTrue, callback) -> {
|
||||
presenter.setNotifyAboutResults(isNowTrue, wrapCallbackWithToastResponse(isNowTrue, callback,
|
||||
StringMessages.INSTANCE.failedToSetStatusOfUpdatesOnNewResultsForYourFavoredBoatClasses(),
|
||||
StringMessages.INSTANCE.youWillNowReceiveUpdatesOnNewResultsForYourFavoredBoatClasses(),
|
||||
StringMessages.INSTANCE.youWillNotReceiveNotificationsForFavoriteBoatClassNewResultsAnymore()));
|
||||
};
|
||||
final String title = StringMessages.INSTANCE.notificationAboutNewResults();
|
||||
return new CheckBoxTile(title, false, onToggle);
|
||||
}
|
||||
|
||||
private AsyncCallback<VoidResult> wrapCallbackWithToastResponse(final boolean isNowTrue,
|
||||
final AsyncCallback<VoidResult> callback, final String failText, final String passAndTrue,
|
||||
final String passAndFalse) {
|
||||
final AsyncCallback<VoidResult> callbackWrappedWithToastNotification = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(failText, NotificationType.ERROR);
|
||||
if (callback != null) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
final String message = isNowTrue ? passAndTrue : passAndFalse;
|
||||
Notification.notify(message, NotificationType.SUCCESS);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
return callbackWrappedWithToastNotification;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedItems(Iterable<BoatClassDTO> selectedItems) {
|
||||
filterUi.setSelectedItems(selectedItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initResults(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults, Collection<BoatClassDTO> selection) {
|
||||
upcomingRacesUi.setValue(notifyAboutUpcomingRaces);
|
||||
resultsUi.setValue(notifyAboutResults);
|
||||
filterUi.setSelectedItems(selection);
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.multiselection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.IsWidget;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.checkboxtile.CheckBoxTile;
|
||||
import com.sap.sailing.gwt.home.shared.partials.filter.AbstractSuggestBoxFilter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.labeledbox.LabeledBox;
|
||||
import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.CompetitorSelectionPresenter;
|
||||
import com.sap.sailing.gwt.ui.client.FlagImageResolver;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sse.gwt.client.Notification;
|
||||
import com.sap.sse.gwt.client.Notification.NotificationType;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public class CompetitorDisplayImpl implements IsWidget, CompetitorSelectionPresenter.Display {
|
||||
public final LabeledBox selectionUi;
|
||||
private final FlowPanel childUi;
|
||||
private final CheckBoxTile tileUi;
|
||||
private final SuggestedMultiSelection<SimpleCompetitorWithIdDTO> filterUi;
|
||||
private final CompetitorSelectionPresenter presenter;
|
||||
|
||||
@Override
|
||||
public Widget asWidget() {
|
||||
return selectionUi;
|
||||
}
|
||||
|
||||
public boolean getIsNotify() {
|
||||
return tileUi.getValue();
|
||||
}
|
||||
|
||||
public CompetitorDisplayImpl(CompetitorSelectionPresenter presenter, FlagImageResolver flagImageResolver) {
|
||||
this.presenter = presenter;
|
||||
presenter.addDisplay(this);
|
||||
tileUi = composeTile();
|
||||
filterUi = composeFilter(flagImageResolver);
|
||||
childUi = new FlowPanel();
|
||||
childUi.add(tileUi);
|
||||
childUi.add(filterUi);
|
||||
final String title = StringMessages.INSTANCE.favoriteCompetitors();
|
||||
selectionUi = new LabeledBox(title, childUi);
|
||||
}
|
||||
|
||||
private SuggestedMultiSelection<SimpleCompetitorWithIdDTO> composeFilter(FlagImageResolver flagImageResolver) {
|
||||
final SuggestedMultiSelection.WidgetFactory<SimpleCompetitorWithIdDTO> widgetFactory = new SuggestedMultiSelection.WidgetFactory<SimpleCompetitorWithIdDTO>() {
|
||||
@Override
|
||||
public IsWidget generateItemDescriptionWidget(SimpleCompetitorWithIdDTO item) {
|
||||
return new SuggestedMultiSelectionCompetitorItemDescription(item, flagImageResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractSuggestBoxFilter<SimpleCompetitorWithIdDTO, SimpleCompetitorWithIdDTO> generateSuggestionSearchBar(
|
||||
Consumer<SimpleCompetitorWithIdDTO> onSuggestionSelectedCallback) {
|
||||
final String text = StringMessages.INSTANCE.add(StringMessages.INSTANCE.competitor());
|
||||
return new SuggestedMultiSelection.SelectableSuggestion<SimpleCompetitorWithIdDTO>(presenter, onSuggestionSelectedCallback,
|
||||
text);
|
||||
}
|
||||
};
|
||||
// TODO add different messages to toast response here
|
||||
final AsyncCallback<VoidResult> selectionPersistenceCallback = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(StringMessages.INSTANCE.failedToModifyFavoriteCompetitors(), NotificationType.ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
Notification.notify(StringMessages.INSTANCE.favoriteCompetitorsModifiedSuccessfully(), NotificationType.SUCCESS);
|
||||
}
|
||||
};
|
||||
presenter.setSelectionPersistenceCallback(selectionPersistenceCallback);
|
||||
return new SuggestedMultiSelection<>(presenter, widgetFactory);
|
||||
}
|
||||
|
||||
private CheckBoxTile composeTile() {
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle = (isNowTrue, callback) -> {
|
||||
presenter.persistResults(isNowTrue, wrapCallbackWithToastResponse(isNowTrue, callback),
|
||||
presenter.getSelection());
|
||||
};
|
||||
final String title = StringMessages.INSTANCE.notificationAboutNewResults();
|
||||
return new CheckBoxTile(title, false, onToggle);
|
||||
}
|
||||
|
||||
private AsyncCallback<VoidResult> wrapCallbackWithToastResponse(final boolean isNowTrue,
|
||||
final AsyncCallback<VoidResult> callback) {
|
||||
final AsyncCallback<VoidResult> callbackWrappedWithToastNotification = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
final String failText = StringMessages.INSTANCE
|
||||
.failedToSetStatusOfNotificationsForFavoriteCompetitors();
|
||||
Notification.notify(failText, NotificationType.ERROR);
|
||||
if (callback != null) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
final String passAndTrue = StringMessages.INSTANCE
|
||||
.youWillNowReceiveNotificationsForFavoriteCompetitors();
|
||||
final String passAndFalse = StringMessages.INSTANCE
|
||||
.youWillNotReceiveNotificationsForFavoriteCompetitorsAnymore();
|
||||
final String message = isNowTrue ? passAndTrue : passAndFalse;
|
||||
Notification.notify(message, NotificationType.SUCCESS);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
return callbackWrappedWithToastNotification;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedItems(Iterable<SimpleCompetitorWithIdDTO> selectedItemsToSet) {
|
||||
filterUi.setSelectedItems(selectedItemsToSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initResults(boolean notifyAboutResults, Collection<SimpleCompetitorWithIdDTO> latestSelectedItems) {
|
||||
filterUi.setSelectedItems(latestSelectedItems);
|
||||
tileUi.setValue(notifyAboutResults, false);
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.multiselection;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.sap.sailing.gwt.home.shared.partials.checkboxtile.CheckBoxTile;
|
||||
import com.sap.sailing.gwt.home.shared.partials.labeledbox.LabeledBox;
|
||||
import com.sap.sailing.gwt.home.shared.places.user.profile.preferences.MiscPreferencesPresenter;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sse.gwt.client.Notification;
|
||||
import com.sap.sse.gwt.client.Notification.NotificationType;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public class MiscellaneousDisplayImpl {
|
||||
public final LabeledBox selectionUi;
|
||||
private final CheckBoxTile featureAndCommunityUpdates;
|
||||
|
||||
public MiscellaneousDisplayImpl(final MiscPreferencesPresenter presenter) {
|
||||
presenter.registerDisplay(this);
|
||||
// compose ui
|
||||
final String securityUpdatesTitle = StringMessages.INSTANCE.securityUpdates();
|
||||
final CheckBoxTile securityUpdates = new CheckBoxTile(securityUpdatesTitle, true, null);
|
||||
featureAndCommunityUpdates = composeFeatureAndCommunityUpdatesTile(presenter);
|
||||
final FlowPanel tileList = new FlowPanel();
|
||||
tileList.add(securityUpdates);
|
||||
tileList.add(featureAndCommunityUpdates);
|
||||
final String boxTitle = StringMessages.INSTANCE.miscellaneous();
|
||||
selectionUi = new LabeledBox(boxTitle, tileList);
|
||||
}
|
||||
|
||||
public void setIsSubscribedToFeatureAndCommunityUpdates(final boolean b, final boolean fireChangeHandlers) {
|
||||
featureAndCommunityUpdates.setValue(b, fireChangeHandlers);
|
||||
}
|
||||
|
||||
private AsyncCallback<VoidResult> wrapCallbackWithToastResponse(final boolean isNowTrue,
|
||||
final AsyncCallback<VoidResult> callback) {
|
||||
final AsyncCallback<VoidResult> callbackWrappedWithToastNotification = new AsyncCallback<VoidResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
final String failText = StringMessages.INSTANCE.failedToSetStatusOfFeatureAndCommunityUpdates();
|
||||
Notification.notify(failText, NotificationType.ERROR);
|
||||
if (callback != null) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
final String passAndTrue = StringMessages.INSTANCE.youWillNowReceiveFeatureAndCommunityUpdates();
|
||||
final String passAndFalse = StringMessages.INSTANCE
|
||||
.youWillNotReceiveFeatureAndCommunityUpdatesAnymore();
|
||||
final String message = isNowTrue ? passAndTrue : passAndFalse;
|
||||
Notification.notify(message, NotificationType.SUCCESS);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
return callbackWrappedWithToastNotification;
|
||||
}
|
||||
|
||||
private CheckBoxTile composeFeatureAndCommunityUpdatesTile(final MiscPreferencesPresenter presenter) {
|
||||
final BiConsumer<Boolean, AsyncCallback<VoidResult>> onToggle = (isNowTrue, callback) -> {
|
||||
final AsyncCallback<VoidResult> wrappedCallback = wrapCallbackWithToastResponse(isNowTrue, callback);
|
||||
presenter.updateIsSubscribedToFeatureAndCommunityUpdates(isNowTrue, wrappedCallback);
|
||||
};
|
||||
final String title = StringMessages.INSTANCE.featureAndCommunityUpdates();
|
||||
return new CheckBoxTile(title, false, onToggle);
|
||||
}
|
||||
}
|
||||
+58
-109
@@ -1,11 +1,12 @@
|
||||
package com.sap.sailing.gwt.home.shared.partials.multiselection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.gwt.core.client.GWT;
|
||||
import com.google.gwt.dom.client.DivElement;
|
||||
import com.google.gwt.dom.client.SpanElement;
|
||||
import com.google.gwt.event.dom.client.ClickEvent;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
@@ -13,18 +14,13 @@ import com.google.gwt.uibinder.client.UiHandler;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.HasValue;
|
||||
import com.google.gwt.user.client.ui.IsWidget;
|
||||
import com.google.gwt.user.client.ui.UIObject;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.dto.BoatClassDTO;
|
||||
import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.filter.AbstractAsyncSuggestBoxFilter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.filter.AbstractFilterWidget;
|
||||
import com.sap.sailing.gwt.home.shared.partials.filter.AbstractSuggestBoxFilter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionPresenter.SuggestionItemsCallback;
|
||||
import com.sap.sailing.gwt.ui.client.FlagImageResolver;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sse.gwt.client.suggestion.AbstractSuggestOracle;
|
||||
|
||||
/**
|
||||
@@ -42,103 +38,98 @@ public final class SuggestedMultiSelection<T> extends Composite implements Sugge
|
||||
interface SuggestedMultiSelectionUiBinder extends UiBinder<Widget, SuggestedMultiSelection<?>> {
|
||||
}
|
||||
|
||||
@UiField
|
||||
SpanElement headerTitleUi;
|
||||
@UiField
|
||||
FlowPanel notificationToggleContainerUi;
|
||||
@UiField
|
||||
DivElement contentSeparatorUi;
|
||||
@UiField(provided = true)
|
||||
AbstractFilterWidget<T, T> suggestionWidgetUi;
|
||||
AbstractFilterWidget<T, T> searchUi;
|
||||
@UiField
|
||||
Button removeAllButtonUi;
|
||||
@UiField
|
||||
FlowPanel itemContainerUi;
|
||||
FlowPanel selectedItemsUi;
|
||||
private final SuggestedMultiSelectionPresenter<T, ?> presenter;
|
||||
private final WidgetProvider<T> widgetProvider;
|
||||
private final WidgetFactory<T> widgetFactory;
|
||||
|
||||
private SuggestedMultiSelection(SuggestedMultiSelectionPresenter<T, ?> presenter,
|
||||
WidgetProvider<T> widgetProvider, String title) {
|
||||
SuggestedMultiSelectionResources.INSTANCE.css().ensureInjected();
|
||||
this.presenter = presenter;
|
||||
this.widgetProvider = widgetProvider;
|
||||
this.suggestionWidgetUi = widgetProvider.getSuggestBoxFilter(selectedItem -> {
|
||||
presenter.addSelection(selectedItem);
|
||||
SuggestedMultiSelection.this.addSelectedItem(selectedItem);
|
||||
});
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
headerTitleUi.setInnerText(title);
|
||||
UIObject.setVisible(contentSeparatorUi, false);
|
||||
this.updateUiState();
|
||||
public static interface WidgetFactory<T> {
|
||||
IsWidget generateItemDescriptionWidget(T item);
|
||||
AbstractSuggestBoxFilter<T, T> generateSuggestionSearchBar(Consumer<T> onSuggestionSelectedCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a checkbox with the given label to toggle notifications by notifying the provided callback.
|
||||
*
|
||||
* @param callback
|
||||
* {@link Consumer Callback} which get notified on checkbox toggles
|
||||
* @param label
|
||||
* {@link String Label} for the added checkbox
|
||||
* @return reference on the added checkbox
|
||||
*/
|
||||
public HasValue<Boolean> addNotificationToggle(Consumer<Boolean> callback, String label) {
|
||||
SuggestedMultiSelectionNotificationToggle notification = new SuggestedMultiSelectionNotificationToggle(label);
|
||||
notification.addValueChangeHandler(event -> callback.accept(event.getValue()));
|
||||
notificationToggleContainerUi.add(notification);
|
||||
UIObject.setVisible(contentSeparatorUi, true);
|
||||
return notification;
|
||||
public SuggestedMultiSelection(SuggestedMultiSelectionPresenter<T, ?> presenter,
|
||||
WidgetFactory<T> widgetFactory) {
|
||||
SuggestedMultiSelectionResources.INSTANCE.css().ensureInjected();
|
||||
this.presenter = presenter;
|
||||
this.widgetFactory = widgetFactory;
|
||||
this.searchUi = widgetFactory.generateSuggestionSearchBar(e -> {
|
||||
presenter.addSelection(e);
|
||||
selectedItemsUi.add(generateSelectedItemUi(e));
|
||||
});
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
}
|
||||
|
||||
@UiHandler("removeAllButtonUi")
|
||||
void onRemoveAllButtonClicked(ClickEvent event) {
|
||||
presenter.clearSelection();
|
||||
itemContainerUi.clear();
|
||||
this.updateUiState();
|
||||
selectedItemsUi.clear();
|
||||
UIObject.setVisible(contentSeparatorUi, false);
|
||||
removeAllButtonUi.setEnabled(false);
|
||||
}
|
||||
|
||||
public Set<T> getSelection(){
|
||||
return new HashSet<>(presenter.getSelection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedItems(Iterable<T> selectedItemsToSet) {
|
||||
itemContainerUi.clear();
|
||||
selectedItemsToSet.forEach(this::addSelectedItem);
|
||||
public void setSelectedItems(Iterable<T> toBeSet) {
|
||||
selectedItemsUi.clear();
|
||||
UIObject.setVisible(contentSeparatorUi, true);
|
||||
presenter.initSelectedItems(toBeSet);
|
||||
toBeSet.forEach(e -> {
|
||||
selectedItemsUi.add(generateSelectedItemUi(e));
|
||||
});
|
||||
final boolean areItemsNonEmpty = toBeSet.iterator().hasNext();
|
||||
removeAllButtonUi.setEnabled(areItemsNonEmpty);
|
||||
}
|
||||
|
||||
private void addSelectedItem(final T selectedItem) {
|
||||
itemContainerUi.add(new SuggestedMultiSelectionItem() {
|
||||
|
||||
|
||||
private SuggestedMultiSelectionItem generateSelectedItemUi(T selectedItem) {
|
||||
return new SuggestedMultiSelectionItem() {
|
||||
@Override
|
||||
protected IsWidget getItemDescriptionWidget() {
|
||||
return widgetProvider.getItemDescriptionWidget(selectedItem);
|
||||
return widgetFactory.generateItemDescriptionWidget(selectedItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoveItemRequsted() {
|
||||
presenter.removeSelection(selectedItem);
|
||||
if (presenter.getSelection().isEmpty()) {
|
||||
UIObject.setVisible(contentSeparatorUi, false);
|
||||
}
|
||||
this.removeFromParent();
|
||||
updateUiState();
|
||||
removeAllButtonUi.setEnabled(selectedItemsUi.getWidgetCount() > 0);
|
||||
}
|
||||
});
|
||||
this.updateUiState();
|
||||
};
|
||||
}
|
||||
|
||||
private void updateUiState() {
|
||||
removeAllButtonUi.setEnabled(itemContainerUi.getWidgetCount() > 0);
|
||||
}
|
||||
public static class SelectableSuggestion<T> extends AbstractAsyncSuggestBoxFilter<T, T> {
|
||||
private final Consumer<T> onPressedCallback;
|
||||
|
||||
private static class SuggestedMultiSelectionFilter<T> extends AbstractAsyncSuggestBoxFilter<T, T> {
|
||||
private final Consumer<T> selectionCallback;
|
||||
public SelectableSuggestion(final SuggestedMultiSelectionPresenter<T, ?> presenter,
|
||||
Consumer<T> onPressedCallback, String placeholderText) {
|
||||
super(buildOracle(presenter), placeholderText);
|
||||
this.onPressedCallback = onPressedCallback;
|
||||
}
|
||||
|
||||
private SuggestedMultiSelectionFilter(final SuggestedMultiSelectionPresenter<T, ?> presenter,
|
||||
Consumer<T> selectionCallback, String placeholderText) {
|
||||
super(new AbstractSuggestOracle<T>() {
|
||||
private static <T> AbstractSuggestOracle<T> buildOracle(final SuggestedMultiSelectionPresenter<T, ?> presenter) {
|
||||
return new AbstractSuggestOracle<T>() {
|
||||
@Override
|
||||
protected void getSuggestions(final Request request, final Callback callback,
|
||||
final Iterable<String> queryTokens) {
|
||||
presenter.getSuggestionItems(queryTokens, request.getLimit(), new SuggestionItemsCallback<T>() {
|
||||
final SuggestionItemsCallback<T> callback2 = new SuggestionItemsCallback<T>() {
|
||||
@Override
|
||||
public void setSuggestionItems(Collection<T> suggestionItems) {
|
||||
setSuggestions(request, callback, suggestionItems, queryTokens);
|
||||
}
|
||||
});
|
||||
};
|
||||
presenter.getSuggestionItems(queryTokens, request.getLimit(), callback2);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,55 +141,13 @@ public final class SuggestedMultiSelection<T> extends Composite implements Sugge
|
||||
protected String createSuggestionAdditionalDisplayString(T value) {
|
||||
return presenter.createSuggestionAdditionalDisplayString(value);
|
||||
}
|
||||
}, placeholderText);
|
||||
this.selectionCallback = selectionCallback;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void onSuggestionSelected(T selectedItem) {
|
||||
SuggestedMultiSelectionFilter.this.clear();
|
||||
selectionCallback.accept(selectedItem);
|
||||
SelectableSuggestion.this.clear();
|
||||
onPressedCallback.accept(selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private interface WidgetProvider<T> {
|
||||
IsWidget getItemDescriptionWidget(T item);
|
||||
AbstractSuggestBoxFilter<T, T> getSuggestBoxFilter(Consumer<T> selectionCallback);
|
||||
}
|
||||
|
||||
public static SuggestedMultiSelection<SimpleCompetitorWithIdDTO> forCompetitors(
|
||||
final SuggestedMultiSelectionPresenter<SimpleCompetitorWithIdDTO, ?> presenter, String headerTitle,
|
||||
FlagImageResolver flagImageResolver) {
|
||||
return new SuggestedMultiSelection<>(presenter, new WidgetProvider<SimpleCompetitorWithIdDTO>() {
|
||||
@Override
|
||||
public IsWidget getItemDescriptionWidget(SimpleCompetitorWithIdDTO item) {
|
||||
return new SuggestedMultiSelectionCompetitorItemDescription(item, flagImageResolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractSuggestBoxFilter<SimpleCompetitorWithIdDTO, SimpleCompetitorWithIdDTO> getSuggestBoxFilter(
|
||||
Consumer<SimpleCompetitorWithIdDTO> selectionCallback) {
|
||||
return new SuggestedMultiSelectionFilter<SimpleCompetitorWithIdDTO>(presenter, selectionCallback,
|
||||
StringMessages.INSTANCE.add(StringMessages.INSTANCE.competitor()));
|
||||
}
|
||||
}, headerTitle);
|
||||
}
|
||||
|
||||
public static SuggestedMultiSelection<BoatClassDTO> forBoatClasses(
|
||||
final SuggestedMultiSelectionPresenter<BoatClassDTO, ?> presenter, String headerTitle) {
|
||||
return new SuggestedMultiSelection<>(presenter, new WidgetProvider<BoatClassDTO>() {
|
||||
@Override
|
||||
public IsWidget getItemDescriptionWidget(BoatClassDTO item) {
|
||||
return new SuggestedMultiSelectionBoatClassItemDescription(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractSuggestBoxFilter<BoatClassDTO, BoatClassDTO> getSuggestBoxFilter(
|
||||
Consumer<BoatClassDTO> selectionCallback) {
|
||||
return new SuggestedMultiSelectionFilter<BoatClassDTO>(presenter, selectionCallback,
|
||||
StringMessages.INSTANCE.add(StringMessages.INSTANCE.boatClass()));
|
||||
}
|
||||
}, headerTitle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-6
@@ -5,19 +5,15 @@
|
||||
<ui:with field="res" type="com.sap.sailing.gwt.common.client.SharedResources" />
|
||||
<ui:with field="local_res" type="com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionResources" />
|
||||
<g:HTMLPanel addStyleNames="{local_res.css.suggestions}">
|
||||
<div class="{local_res.css.suggestionsHeader}">
|
||||
<span class="{local_res.css.suggestionsHeaderTitle}" ui:field="headerTitleUi" />
|
||||
</div>
|
||||
<div class="{local_res.css.suggestionsContent}">
|
||||
<g:FlowPanel ui:field="notificationToggleContainerUi" />
|
||||
<div ui:field="contentSeparatorUi" class="{local_res.css.suggestionsContentSeparator}"></div>
|
||||
<div class="{local_res.css.suggestionsContentToolbar}">
|
||||
<sp:filter.AbstractFilterWidget addStyleNames="{local_res.css.suggestionsAddFilter}" ui:field="suggestionWidgetUi" />
|
||||
<sp:filter.AbstractFilterWidget addStyleNames="{local_res.css.suggestionsAddFilter}" ui:field="searchUi" />
|
||||
<g:Button addStyleNames="{res.mainCss.button} {res.mainCss.buttonred} {local_res.css.suggestionsRemoveButton}" ui:field="removeAllButtonUi">
|
||||
<ui:text from="{i18n.removeAll}"/>
|
||||
</g:Button>
|
||||
</div>
|
||||
<g:FlowPanel addStyleNames="{local_res.css.suggestionsContentTable}" ui:field="itemContainerUi" />
|
||||
<g:FlowPanel addStyleNames="{local_res.css.suggestionsContentTable}" ui:field="selectedItemsUi" />
|
||||
</div>
|
||||
</g:HTMLPanel>
|
||||
</ui:UiBinder>
|
||||
+5
-2
@@ -4,6 +4,7 @@ import com.google.gwt.core.client.GWT;
|
||||
import com.google.gwt.dom.client.DivElement;
|
||||
import com.google.gwt.dom.client.Element;
|
||||
import com.google.gwt.dom.client.SpanElement;
|
||||
import com.google.gwt.safehtml.shared.SafeUri;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
@@ -24,8 +25,10 @@ public class SuggestedMultiSelectionCompetitorItemDescription extends Widget {
|
||||
public SuggestedMultiSelectionCompetitorItemDescription(SimpleCompetitorDTO competitor,
|
||||
FlagImageResolver flagImageResolver) {
|
||||
setElement(uiBinder.createAndBindUi(this));
|
||||
flagImageUi.getStyle().setBackgroundImage("url('" + flagImageResolver.getFlagImageUri(
|
||||
competitor.getFlagImageURL(), competitor.getTwoLetterIsoCountryCode()).asString() + "')");
|
||||
final String countryCode = competitor.getTwoLetterIsoCountryCode();
|
||||
final SafeUri flagImageUri = flagImageResolver.getFlagImageUri(competitor.getFlagImageURL(), countryCode);
|
||||
final String bgUrl = "url('" + flagImageUri.asString() + "')";
|
||||
flagImageUi.getStyle().setBackgroundImage(bgUrl);
|
||||
sailIdUi.setInnerText(competitor.getShortInfo());
|
||||
nameUi.setInnerText(competitor.getName());
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ class SuggestedMultiSelectionNotificationToggle extends Composite implements Has
|
||||
@UiField Label labelUi;
|
||||
@UiField CheckBox toggleButtonUi;
|
||||
|
||||
SuggestedMultiSelectionNotificationToggle(String label) {
|
||||
public SuggestedMultiSelectionNotificationToggle(String label) {
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
labelUi.setText(label);
|
||||
}
|
||||
|
||||
+4
-2
@@ -6,13 +6,15 @@ import com.google.gwt.view.client.ProvidesKey;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionPresenter.Display;
|
||||
|
||||
public interface SuggestedMultiSelectionPresenter<T, D extends Display<T>> extends ProvidesKey<T> {
|
||||
|
||||
|
||||
void addSelection(T item);
|
||||
|
||||
void removeSelection(T item);
|
||||
|
||||
void clearSelection();
|
||||
|
||||
Collection<T> getSelection();
|
||||
|
||||
void getSuggestionItems(Iterable<String> queryTokens, int limit, final SuggestionItemsCallback<T> callback);
|
||||
|
||||
String createSuggestionKeyString(T value);
|
||||
@@ -23,7 +25,7 @@ public interface SuggestedMultiSelectionPresenter<T, D extends Display<T>> exten
|
||||
|
||||
void persist();
|
||||
|
||||
void initSelectedItems(Collection<T> selectedItems);
|
||||
void initSelectedItems(Iterable<T> selectedItems);
|
||||
|
||||
interface SuggestionItemsCallback<T> {
|
||||
void setSuggestionItems(Collection<T> suggestionItems);
|
||||
|
||||
+23
-11
@@ -1,22 +1,34 @@
|
||||
package com.sap.sailing.gwt.home.shared.places.user.profile.preferences;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.sap.sailing.domain.common.dto.BoatClassDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionPresenter;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public interface BoatClassSelectionPresenter
|
||||
extends SuggestedMultiSelectionPresenter<BoatClassDTO, BoatClassSelectionPresenter.Display> {
|
||||
|
||||
void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces);
|
||||
|
||||
void setNotifyAboutResults(boolean notifyAboutResults);
|
||||
|
||||
void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults);
|
||||
|
||||
|
||||
void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces, AsyncCallback<VoidResult> callback);
|
||||
|
||||
void setNotifyAboutResults(boolean notifyAboutResults, AsyncCallback<VoidResult> callback);
|
||||
|
||||
void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults, Collection<BoatClassDTO> selection);
|
||||
|
||||
void persistResults(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults,
|
||||
AsyncCallback<VoidResult> callback, Collection<BoatClassDTO> latestSelectedItems);
|
||||
|
||||
void setSelectionPersistenceCallback(AsyncCallback<VoidResult> selectionCallback);
|
||||
|
||||
interface Display extends SuggestedMultiSelectionPresenter.Display<BoatClassDTO> {
|
||||
|
||||
void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces);
|
||||
|
||||
void setNotifyAboutResults(boolean notifyAboutResults);
|
||||
void initResults(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults, Collection<BoatClassDTO> selection);
|
||||
|
||||
boolean getNotifyAboutUpcomingRaces();
|
||||
|
||||
boolean getNotifyAboutResults();
|
||||
|
||||
Collection<BoatClassDTO> getSelection();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-4
@@ -1,17 +1,24 @@
|
||||
package com.sap.sailing.gwt.home.shared.places.user.profile.preferences;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelectionPresenter;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public interface CompetitorSelectionPresenter
|
||||
extends SuggestedMultiSelectionPresenter<SimpleCompetitorWithIdDTO, CompetitorSelectionPresenter.Display> {
|
||||
void persistResults(boolean notifyAboutResults, AsyncCallback<VoidResult> callback,
|
||||
Collection<SimpleCompetitorWithIdDTO> latestSelectedItems);
|
||||
|
||||
public void initResults(boolean notifyAboutResults, Collection<SimpleCompetitorWithIdDTO> latestSelectedItems);
|
||||
|
||||
void setNotifyAboutResults(boolean notifyAboutResults);
|
||||
|
||||
void initNotifications(boolean notifyAboutResults);
|
||||
public void setSelectionPersistenceCallback(AsyncCallback<VoidResult> selectionCallback);
|
||||
|
||||
public static interface Display extends SuggestedMultiSelectionPresenter.Display<SimpleCompetitorWithIdDTO> {
|
||||
void initResults(boolean notifyAboutResults, Collection<SimpleCompetitorWithIdDTO> latestSelectedItems);
|
||||
|
||||
void setNotifyAboutResults(boolean notifyAboutResults);
|
||||
public boolean getIsNotify();
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.sap.sailing.gwt.home.shared.places.user.profile.preferences;
|
||||
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.MiscellaneousDisplayImpl;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
public interface MiscPreferencesPresenter {
|
||||
void registerDisplay(MiscellaneousDisplayImpl display);
|
||||
|
||||
void updateIsSubscribedToFeatureAndCommunityUpdates(final boolean b, final AsyncCallback<VoidResult> callback);
|
||||
|
||||
/** get value via dispatch method, set first correct value onto checkbox */
|
||||
void initIsSubscribedToFeatureAndCommunityUpdates(final boolean b);
|
||||
}
|
||||
+24
-71
@@ -6,14 +6,13 @@ import com.google.gwt.resources.client.CssResource;
|
||||
import com.google.gwt.uibinder.client.UiBinder;
|
||||
import com.google.gwt.uibinder.client.UiField;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.HasValue;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.sap.sailing.domain.common.dto.BoatClassDTO;
|
||||
import com.sap.sailing.gwt.common.client.SharedResources;
|
||||
import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorWithIdDTO;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.SuggestedMultiSelection;
|
||||
import com.sap.sailing.gwt.home.shared.partials.labeledbox.LabeledBox;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.BoatClassDisplayImpl;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.CompetitorDisplayImpl;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.MiscellaneousDisplayImpl;
|
||||
import com.sap.sailing.gwt.ui.client.FlagImageResolver;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
|
||||
/**
|
||||
* Implementation of {@link UserPreferencesView} where users can change their preferred selections and notifications.
|
||||
@@ -24,84 +23,38 @@ public class UserPreferences extends Composite implements UserPreferencesView {
|
||||
|
||||
interface UserPreferencesUiBinder extends UiBinder<Widget, UserPreferences> {
|
||||
}
|
||||
|
||||
|
||||
interface Style extends CssResource {
|
||||
String edgeToEdge();
|
||||
}
|
||||
|
||||
@UiField Style style;
|
||||
@UiField SharedResources res;
|
||||
@UiField(provided = true) SuggestedMultiSelection<SimpleCompetitorWithIdDTO> favoriteCompetitorsSelctionUi;
|
||||
@UiField(provided = true) SuggestedMultiSelection<BoatClassDTO> favoriteBoatClassesSelctionUi;
|
||||
@UiField DivElement notificationsTextUi;
|
||||
|
||||
@UiField
|
||||
Style style;
|
||||
@UiField
|
||||
SharedResources res;
|
||||
@UiField(provided = true)
|
||||
LabeledBox favoriteCompetitorsSelctionUi;
|
||||
@UiField(provided = true)
|
||||
LabeledBox favoriteBoatClassesSelctionUi;
|
||||
@UiField(provided = true)
|
||||
LabeledBox miscUi;
|
||||
@UiField
|
||||
DivElement notificationsTextUi;
|
||||
final UserPreferencesView.Presenter presenter;
|
||||
|
||||
public UserPreferences(UserPreferencesView.Presenter presenter, FlagImageResolver flagImageResolver) {
|
||||
favoriteCompetitorsSelctionUi = new CompetitorDisplayImpl(
|
||||
presenter.getFavoriteCompetitorsDataProvider(), flagImageResolver).selectionUi;
|
||||
this.presenter = presenter;
|
||||
favoriteCompetitorsSelctionUi = new CompetitorDisplayImpl(presenter.getFavoriteCompetitorsDataProvider(),
|
||||
flagImageResolver).selectionUi;
|
||||
favoriteBoatClassesSelctionUi = new BoatClassDisplayImpl(
|
||||
presenter.getFavoriteBoatClassesDataProvider()).selectionUi;
|
||||
miscUi = (new MiscellaneousDisplayImpl(presenter.getMiscPresenter())).selectionUi;
|
||||
initWidget(uiBinder.createAndBindUi(this));
|
||||
// TODO hide notificationsTextUi if the user's mail address is already verified
|
||||
}
|
||||
|
||||
|
||||
public void setEdgeToEdge(boolean edgeToEdge) {
|
||||
favoriteBoatClassesSelctionUi.setStyleName(style.edgeToEdge(), edgeToEdge);
|
||||
favoriteCompetitorsSelctionUi.setStyleName(style.edgeToEdge(), edgeToEdge);
|
||||
favoriteBoatClassesSelctionUi.getElement().getParentElement().removeClassName(res.mediaCss().column());
|
||||
favoriteCompetitorsSelctionUi.getElement().getParentElement().removeClassName(res.mediaCss().column());
|
||||
}
|
||||
|
||||
private class CompetitorDisplayImpl implements CompetitorSelectionPresenter.Display {
|
||||
private final SuggestedMultiSelection<SimpleCompetitorWithIdDTO> selectionUi;
|
||||
private final HasValue<Boolean> notifyAboutResultsUi;
|
||||
|
||||
private CompetitorDisplayImpl(final CompetitorSelectionPresenter dataProvider,
|
||||
FlagImageResolver flagImageResolver) {
|
||||
selectionUi = SuggestedMultiSelection.forCompetitors(dataProvider, StringMessages.INSTANCE.favoriteCompetitors(), flagImageResolver);
|
||||
notifyAboutResultsUi = selectionUi.addNotificationToggle(dataProvider::setNotifyAboutResults,
|
||||
StringMessages.INSTANCE.notificationAboutNewResults());
|
||||
dataProvider.addDisplay(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedItems(Iterable<SimpleCompetitorWithIdDTO> selectedItems) {
|
||||
selectionUi.setSelectedItems(selectedItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutResults(boolean notifyAboutResults) {
|
||||
notifyAboutResultsUi.setValue(notifyAboutResults);
|
||||
}
|
||||
}
|
||||
|
||||
private class BoatClassDisplayImpl implements BoatClassSelectionPresenter.Display {
|
||||
private final SuggestedMultiSelection<BoatClassDTO> selectionUi;
|
||||
private final HasValue<Boolean> notifyAboutUpcomingRacesUi;
|
||||
private final HasValue<Boolean> notifyAboutResultsUi;
|
||||
|
||||
private BoatClassDisplayImpl(final BoatClassSelectionPresenter dataProvider) {
|
||||
selectionUi = SuggestedMultiSelection.forBoatClasses(dataProvider, StringMessages.INSTANCE.favoriteBoatClasses());
|
||||
notifyAboutUpcomingRacesUi = selectionUi.addNotificationToggle(dataProvider::setNotifyAboutUpcomingRaces,
|
||||
StringMessages.INSTANCE.notificationAboutUpcomingRaces());
|
||||
notifyAboutResultsUi = selectionUi.addNotificationToggle(dataProvider::setNotifyAboutResults,
|
||||
StringMessages.INSTANCE.notificationAboutNewResults());
|
||||
dataProvider.addDisplay(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectedItems(Iterable<BoatClassDTO> selectedItems) {
|
||||
selectionUi.setSelectedItems(selectedItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces) {
|
||||
notifyAboutUpcomingRacesUi.setValue(notifyAboutUpcomingRaces);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutResults(boolean notifyAboutResults) {
|
||||
notifyAboutResultsUi.setValue(notifyAboutResults);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,7 +1,9 @@
|
||||
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
|
||||
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"
|
||||
xmlns:um="urn:import:com.sap.sailing.gwt.home.shared.usermanagement.decorator"
|
||||
xmlns:ms="urn:import:com.sap.sailing.gwt.home.shared.partials.multiselection">
|
||||
xmlns:ms="urn:import:com.sap.sailing.gwt.home.shared.partials.multiselection"
|
||||
xmlns:cbt="urn:import:com.sap.sailing.gwt.home.shared.partials.checkboxtile"
|
||||
xmlns:lb="urn:import:com.sap.sailing.gwt.home.shared.partials.labeledbox">
|
||||
<ui:with field="i18n" type="com.sap.sailing.gwt.ui.client.StringMessages" />
|
||||
<ui:with field="res" type="com.sap.sailing.gwt.common.client.SharedResources" />
|
||||
<ui:style type="com.sap.sailing.gwt.home.shared.places.user.profile.preferences.UserPreferences.Style">
|
||||
@@ -16,10 +18,13 @@
|
||||
<ui:text from="{i18n.userNotificationsOnlyIfMailAddressIsVerified}" />
|
||||
</div>
|
||||
<div class="{res.mediaCss.column} {res.mediaCss.small12} {res.mediaCss.medium6} {res.mainCss.spacermargintopmediumsmall}">
|
||||
<ms:SuggestedMultiSelection ui:field="favoriteCompetitorsSelctionUi"/>
|
||||
<lb:LabeledBox ui:field="miscUi"/>
|
||||
</div>
|
||||
<div class="{res.mediaCss.column} {res.mediaCss.small12} {res.mediaCss.medium6} {res.mainCss.spacermargintopmediumsmall}">
|
||||
<ms:SuggestedMultiSelection ui:field="favoriteBoatClassesSelctionUi"/>
|
||||
<lb:LabeledBox ui:field="favoriteCompetitorsSelctionUi"/>
|
||||
</div>
|
||||
<div class="{res.mediaCss.column} {res.mediaCss.small12} {res.mediaCss.medium6} {res.mainCss.spacermargintopmediumsmall}">
|
||||
<lb:LabeledBox ui:field="favoriteBoatClassesSelctionUi"/>
|
||||
</div>
|
||||
</g:HTMLPanel>
|
||||
</ui:UiBinder>
|
||||
+102
-58
@@ -11,9 +11,11 @@ import com.sap.sailing.gwt.home.communication.user.profile.FavoritesResult;
|
||||
import com.sap.sailing.gwt.home.communication.user.profile.GetFavoritesAction;
|
||||
import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteBoatClassesAction;
|
||||
import com.sap.sailing.gwt.home.communication.user.profile.SaveFavoriteCompetitorsAction;
|
||||
import com.sap.sailing.gwt.home.communication.user.profile.SaveMiscEmailPreferences;
|
||||
import com.sap.sailing.gwt.home.shared.app.ClientFactoryWithDispatch;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedBoatClassMultiSelectionPresenter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.AbstractSuggestedCompetitorMultiSelectionPresenter;
|
||||
import com.sap.sailing.gwt.home.shared.partials.multiselection.MiscellaneousDisplayImpl;
|
||||
import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory;
|
||||
import com.sap.sse.gwt.dispatch.shared.commands.VoidResult;
|
||||
|
||||
@@ -29,17 +31,18 @@ public class UserPreferencesPresenter<C extends ClientFactoryWithDispatch & Erro
|
||||
implements UserPreferencesView.Presenter {
|
||||
|
||||
private final BoatClassSelectionPresenter boatClassSelectionPresenter = new BoatClassSelectionPresenterImpl();
|
||||
private final CompetitorSelectionPresenter competitorSelectionPresenter;
|
||||
final CompetitorSelectionPresenter competitorPresenter;
|
||||
private final MiscPreferencesPresenter miscPresenter = new MiscPresenterImpl();
|
||||
private final C clientFactory;
|
||||
|
||||
public UserPreferencesPresenter(C clientFactory) {
|
||||
this.clientFactory = clientFactory;
|
||||
this.competitorSelectionPresenter = new CompetitorSelectionPresenterImpl(clientFactory);
|
||||
competitorPresenter = new CompetitorSelectionPresenterImpl(clientFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadPreferences() {
|
||||
clientFactory.getDispatch().execute(new GetFavoritesAction(), new AsyncCallback<FavoritesResult>() {
|
||||
final AsyncCallback<FavoritesResult> callback = new AsyncCallback<FavoritesResult>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
clientFactory.createErrorView("Error while loading notification preferences!", caught);
|
||||
@@ -47,109 +50,150 @@ public class UserPreferencesPresenter<C extends ClientFactoryWithDispatch & Erro
|
||||
|
||||
@Override
|
||||
public void onSuccess(FavoritesResult result) {
|
||||
initFavoriteCompetitors(result.getFavoriteCompetitors());
|
||||
initFavoriteBoatClasses(result.getFavoriteBoatClasses());
|
||||
final boolean isNotify = result.getFavoriteCompetitors().isNotifyAboutResults();
|
||||
final Collection<SimpleCompetitorWithIdDTO> selection = result.getFavoriteCompetitors().getSelectedCompetitors();
|
||||
competitorPresenter.initResults(isNotify, selection);
|
||||
final FavoriteBoatClassesDTO favoriteBoatClasses = result.getFavoriteBoatClasses();
|
||||
boatClassSelectionPresenter.initNotifications(favoriteBoatClasses.isNotifyAboutUpcomingRaces(),
|
||||
favoriteBoatClasses.isNotifyAboutResults(), favoriteBoatClasses.getSelectedBoatClasses());
|
||||
miscPresenter.initIsSubscribedToFeatureAndCommunityUpdates(result.getIsSubscribedToFeatureAndCommunityUpdates());
|
||||
}
|
||||
});
|
||||
};
|
||||
clientFactory.getDispatch().execute(new GetFavoritesAction(), callback);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CompetitorSelectionPresenter getFavoriteCompetitorsDataProvider() {
|
||||
return competitorSelectionPresenter;
|
||||
public MiscPreferencesPresenter getMiscPresenter() {
|
||||
return miscPresenter;
|
||||
}
|
||||
|
||||
private class MiscPresenterImpl implements MiscPreferencesPresenter {
|
||||
MiscellaneousDisplayImpl display;
|
||||
|
||||
@Override
|
||||
public void registerDisplay(MiscellaneousDisplayImpl display) {
|
||||
this.display = display;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initIsSubscribedToFeatureAndCommunityUpdates(final boolean b) {
|
||||
if (display != null) {
|
||||
display.setIsSubscribedToFeatureAndCommunityUpdates(b, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIsSubscribedToFeatureAndCommunityUpdates(final boolean b, final AsyncCallback<VoidResult> callback) {
|
||||
clientFactory.getDispatch().execute(new SaveMiscEmailPreferences(b), callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoatClassSelectionPresenter getFavoriteBoatClassesDataProvider() {
|
||||
return boatClassSelectionPresenter;
|
||||
}
|
||||
|
||||
private void initFavoriteCompetitors(FavoriteCompetitorsDTO favoriteCompetitors) {
|
||||
competitorSelectionPresenter.initNotifications(favoriteCompetitors.isNotifyAboutResults());
|
||||
competitorSelectionPresenter.initSelectedItems(favoriteCompetitors.getSelectedCompetitors());
|
||||
}
|
||||
|
||||
private void initFavoriteBoatClasses(FavoriteBoatClassesDTO favoriteBoatClasses) {
|
||||
boatClassSelectionPresenter.initNotifications(favoriteBoatClasses.isNotifyAboutUpcomingRaces(),
|
||||
favoriteBoatClasses.isNotifyAboutResults());
|
||||
boatClassSelectionPresenter.initSelectedItems(favoriteBoatClasses.getSelectedBoatClasses());
|
||||
}
|
||||
|
||||
private class BoatClassSelectionPresenterImpl
|
||||
extends AbstractSuggestedBoatClassMultiSelectionPresenter<BoatClassSelectionPresenter.Display>
|
||||
implements BoatClassSelectionPresenter {
|
||||
|
||||
private boolean notifyAboutUpcomingRaces;
|
||||
private boolean notifyAboutResults;
|
||||
private AsyncCallback<VoidResult> selectionCallback;
|
||||
|
||||
@Override
|
||||
public void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults) {
|
||||
this.notifyAboutUpcomingRaces = notifyAboutUpcomingRaces;
|
||||
this.notifyAboutResults = notifyAboutResults;
|
||||
public void initNotifications(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults, Collection<BoatClassDTO> selection) {
|
||||
this.displays.forEach(display -> {
|
||||
display.setNotifyAboutUpcomingRaces(notifyAboutUpcomingRaces);
|
||||
display.setNotifyAboutResults(notifyAboutResults);
|
||||
display.initResults(notifyAboutUpcomingRaces, notifyAboutResults, selection);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces) {
|
||||
this.notifyAboutUpcomingRaces = notifyAboutUpcomingRaces;
|
||||
this.persist();
|
||||
public void setNotifyAboutUpcomingRaces(boolean notifyAboutUpcomingRaces, AsyncCallback<VoidResult> callback) {
|
||||
if (!this.displays.isEmpty()) {
|
||||
final BoatClassSelectionPresenter.Display display = (BoatClassSelectionPresenter.Display) this.displays
|
||||
.toArray()[0];
|
||||
final Collection<BoatClassDTO> selectedItems = display.getSelection();
|
||||
persistResults(notifyAboutUpcomingRaces, display.getNotifyAboutResults(), callback, selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutResults(boolean notifyAboutResults) {
|
||||
this.notifyAboutResults = notifyAboutResults;
|
||||
this.persist();
|
||||
public void setNotifyAboutResults(boolean notifyAboutResults, AsyncCallback<VoidResult> callback) {
|
||||
if (!this.displays.isEmpty()) {
|
||||
final BoatClassSelectionPresenter.Display display = (BoatClassSelectionPresenter.Display) this.displays
|
||||
.toArray()[0];
|
||||
final Collection<BoatClassDTO> selectedItems = display.getSelection();
|
||||
persistResults(display.getNotifyAboutUpcomingRaces(), notifyAboutResults, callback, selectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persist(Collection<BoatClassDTO> selectedItem) {
|
||||
final FavoriteBoatClassesDTO favorites = new FavoriteBoatClassesDTO(selectedItem, notifyAboutUpcomingRaces,
|
||||
notifyAboutResults);
|
||||
clientFactory.getDispatch().execute(new SaveFavoriteBoatClassesAction(favorites), new SaveAsyncCallback());
|
||||
protected void persist(Collection<BoatClassDTO> selectedItems) {
|
||||
if (!this.displays.isEmpty()) {
|
||||
final BoatClassSelectionPresenter.Display display = (BoatClassSelectionPresenter.Display) this.displays
|
||||
.toArray()[0];
|
||||
if (selectionCallback != null) {
|
||||
persistResults(display.getNotifyAboutUpcomingRaces(), display.getNotifyAboutResults(),
|
||||
selectionCallback, selectedItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persistResults(boolean notifyAboutUpcomingRaces, boolean notifyAboutResults,
|
||||
AsyncCallback<VoidResult> callback, Collection<BoatClassDTO> latestSelectedItems) {
|
||||
final FavoriteBoatClassesDTO favorites = new FavoriteBoatClassesDTO(latestSelectedItems,
|
||||
notifyAboutUpcomingRaces, notifyAboutResults);
|
||||
clientFactory.getDispatch().execute(new SaveFavoriteBoatClassesAction(favorites), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectionPersistenceCallback(AsyncCallback<VoidResult> selectionCallback) {
|
||||
this.selectionCallback = selectionCallback;
|
||||
}
|
||||
}
|
||||
|
||||
private class CompetitorSelectionPresenterImpl
|
||||
extends AbstractSuggestedCompetitorMultiSelectionPresenter<CompetitorSelectionPresenter.Display>
|
||||
implements CompetitorSelectionPresenter {
|
||||
|
||||
private boolean notifyAboutResults;
|
||||
|
||||
private AsyncCallback<VoidResult> selectionCallback;
|
||||
|
||||
private CompetitorSelectionPresenterImpl(ClientFactoryWithDispatch clientFactory) {
|
||||
super(clientFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initNotifications(boolean notifyAboutResults) {
|
||||
this.notifyAboutResults = notifyAboutResults;
|
||||
this.displays.forEach(display -> display.setNotifyAboutResults(notifyAboutResults));
|
||||
protected final void persist(Collection<SimpleCompetitorWithIdDTO> selectedItems) {
|
||||
if (!this.displays.isEmpty()) {
|
||||
final CompetitorSelectionPresenter.Display display = (CompetitorSelectionPresenter.Display) this.displays
|
||||
.toArray()[0];
|
||||
if (selectionCallback != null) {
|
||||
persistResults(display.getIsNotify(), selectionCallback, selectedItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotifyAboutResults(boolean notifyAboutResults) {
|
||||
this.notifyAboutResults = notifyAboutResults;
|
||||
this.persist();
|
||||
public void persistResults(boolean notifyAboutResults, AsyncCallback<VoidResult> callback,
|
||||
Collection<SimpleCompetitorWithIdDTO> latestSelectedItems) {
|
||||
final FavoriteCompetitorsDTO favorites = new FavoriteCompetitorsDTO(latestSelectedItems,
|
||||
notifyAboutResults);
|
||||
clientFactory.getDispatch().execute(new SaveFavoriteCompetitorsAction(favorites), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void persist(Collection<SimpleCompetitorWithIdDTO> selectedItem) {
|
||||
final FavoriteCompetitorsDTO favorites = new FavoriteCompetitorsDTO(selectedItem, notifyAboutResults);
|
||||
clientFactory.getDispatch().execute(new SaveFavoriteCompetitorsAction(favorites), new SaveAsyncCallback());
|
||||
public void initResults(boolean notifyAboutResults, Collection<SimpleCompetitorWithIdDTO> latestSelectedItems) {
|
||||
this.displays.forEach(display -> display.initResults(notifyAboutResults, latestSelectedItems));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSelectionPersistenceCallback(AsyncCallback<VoidResult> selectionCallback) {
|
||||
this.selectionCallback = selectionCallback;
|
||||
}
|
||||
}
|
||||
|
||||
private class SaveAsyncCallback implements AsyncCallback<VoidResult> {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
clientFactory.createErrorView("Error while saving notification preferences!", caught);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(VoidResult result) {
|
||||
}
|
||||
@Override
|
||||
public CompetitorSelectionPresenter getFavoriteCompetitorsDataProvider() {
|
||||
return competitorPresenter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -29,6 +29,8 @@ public interface UserPreferencesView extends IsWidget {
|
||||
BoatClassSelectionPresenter getFavoriteBoatClassesDataProvider();
|
||||
|
||||
CompetitorSelectionPresenter getFavoriteCompetitorsDataProvider();
|
||||
|
||||
MiscPreferencesPresenter getMiscPresenter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
package com.sap.sailing.gwt.home.shared.places.user.profile.sailorprofile.dataprovider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -107,11 +108,16 @@ public class SailorProfilesCompetitorSelectionPresenter implements EditModeChang
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initSelectedItems(Collection<SimpleCompetitorWithIdDTO> selectedItems) {
|
||||
public void initSelectedItems(Iterable<SimpleCompetitorWithIdDTO> selectedItems) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createSuggestionAdditionalDisplayString(SimpleCompetitorWithIdDTO value) {
|
||||
return competitorDataProvider.createSuggestionAdditionalDisplayString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SimpleCompetitorWithIdDTO> getSelection() {
|
||||
return new ArrayList<>(competitors);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -110,8 +110,8 @@ public class ManagementConsoleClientFactoryImpl implements ManagementConsoleClie
|
||||
private void changeDefaultTenantForCurrentUser(final StrippedUserGroupDTO serverTenant) {
|
||||
final UserDTO user = getUserService().getCurrentUser();
|
||||
getUserManagementWriteService().updateUserProperties(user.getName(), user.getFullName(),
|
||||
user.getCompany(), user.getLocale(), serverTenant.getId().toString(),
|
||||
new AsyncCallback<UserDTO>() {
|
||||
user.getCompany(), user.getLocale(), user.getDidOptOutOfFeatureAndCommunityEmails(),
|
||||
serverTenant.getId().toString(), new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Window.alert(caught.getMessage());
|
||||
|
||||
+2
-2
@@ -311,8 +311,8 @@ public class AdminConsoleActivity extends AbstractActivity implements AdminConso
|
||||
private void changeDefaultTenantForCurrentUser(final StrippedUserGroupDTO serverTenant) {
|
||||
final UserDTO user = clientFactory.getUserService().getCurrentUser();
|
||||
clientFactory.getUserManagementWriteService().updateUserProperties(user.getName(), user.getFullName(),
|
||||
user.getCompany(), user.getLocale(), serverTenant.getId().toString(),
|
||||
new AsyncCallback<UserDTO>() {
|
||||
user.getCompany(), user.getLocale(), user.getDidOptOutOfFeatureAndCommunityEmails(),
|
||||
serverTenant.getId().toString(), new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Window.alert(caught.getMessage());
|
||||
|
||||
+19
@@ -2556,6 +2556,25 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
|
||||
String ipsLockedForUserCreationAbuse();
|
||||
String unableToLoadIpsBlockedForUserCreationAbuse();
|
||||
String sourceCode();
|
||||
String miscellaneous();
|
||||
String securityUpdates();
|
||||
String featureAndCommunityUpdates();
|
||||
String youWillNowReceiveFeatureAndCommunityUpdates();
|
||||
String youWillNotReceiveFeatureAndCommunityUpdatesAnymore();
|
||||
String failedToSetStatusOfFeatureAndCommunityUpdates();
|
||||
String youWillNowReceiveNotificationsForFavoriteCompetitors();
|
||||
String youWillNotReceiveNotificationsForFavoriteCompetitorsAnymore();
|
||||
String failedToSetStatusOfNotificationsForFavoriteCompetitors();
|
||||
String youWillNowReceiveNotificationsForFavoriteBoatClassUpcomingRaces();
|
||||
String youWillNotReceiveUpdatesOnUpcomingRacesForYourFavoredBoatClassesAnymore();
|
||||
String failedToSetStatusOfUpdatesOnUpcomingRacesForYourFavoredBoatClasses();
|
||||
String youWillNowReceiveUpdatesOnNewResultsForYourFavoredBoatClasses();
|
||||
String youWillNotReceiveNotificationsForFavoriteBoatClassNewResultsAnymore();
|
||||
String failedToSetStatusOfUpdatesOnNewResultsForYourFavoredBoatClasses();
|
||||
String failedToModifyFavoriteCompetitors();
|
||||
String favoriteCompetitorsModifiedSuccessfully();
|
||||
String failedToModifyFavoredBoatClasses();
|
||||
String succesfullyModifiedFavoredBoatClasses();
|
||||
String revokeExplicitTrackingTimes();
|
||||
String confirmRevokeExplicitTrackingTimes(String leaderboardName);
|
||||
String errorRevokingExplicitTrackingTimes(String leaderboardName, String message);
|
||||
|
||||
+19
@@ -2592,6 +2592,25 @@ unlock=Unlock
|
||||
ipsLockedForUserCreationAbuse=IPs Locked for User Creation Abuse
|
||||
unableToLoadIpsBlockedForUserCreationAbuse=Unable to load IPs Blocked for User Creation Abuse
|
||||
sourceCode=Source Code
|
||||
miscellaneous=Miscellaneous
|
||||
securityUpdates=Security Updates
|
||||
featureAndCommunityUpdates=Feature and Community Updates
|
||||
youWillNowReceiveFeatureAndCommunityUpdates=You will now receive feature and community updates
|
||||
youWillNotReceiveFeatureAndCommunityUpdatesAnymore=You will not receive feature and community updates anymore
|
||||
failedToSetStatusOfFeatureAndCommunityUpdates=Failed to set status of feature and community updates
|
||||
failedToSetStatusOfNotificationsForFavoriteCompetitors=Failed to set status of notifications for favorite competitors
|
||||
youWillNowReceiveNotificationsForFavoriteCompetitors=You will now receive notifications for favorite competitors
|
||||
failedToSetStatusOfUpdatesOnUpcomingRacesForYourFavoredBoatClasses=Failed to set status of notifications for upcoming races of your favored boat classes
|
||||
youWillNowReceiveNotificationsForFavoriteBoatClassUpcomingRaces=You will now receive notifications for upcoming races of your favored boat classes
|
||||
youWillNotReceiveUpdatesOnUpcomingRacesForYourFavoredBoatClassesAnymore=You will not receive notifications for upcoming races of your favored boat classes anymore
|
||||
failedToSetStatusOfUpdatesOnNewResultsForYourFavoredBoatClasses=Failed to set status of notifications for new results of your favored boat classes
|
||||
youWillNowReceiveUpdatesOnNewResultsForYourFavoredBoatClasses=You will now receive notifications for new results of your favored boat classes
|
||||
youWillNotReceiveNotificationsForFavoriteBoatClassNewResultsAnymore=You will not receive notifications for new results of your favored boat classes anymore
|
||||
failedToModifyFavoriteCompetitors=Failed to modify favorite competitors
|
||||
favoriteCompetitorsModifiedSuccessfully=Favorite competitors modified successfully
|
||||
failedToModifyFavoredBoatClasses=Failed to modify favorite boat classes
|
||||
succesfullyModifiedFavoredBoatClasses=Successfully modified favored boat classes
|
||||
youWillNotReceiveNotificationsForFavoriteCompetitorsAnymore=You will not receive notifications for favorite competitors anymore
|
||||
revokeExplicitTrackingTimes=Revoke explicit tracking times for races with valid start and finishing times
|
||||
confirmRevokeExplicitTrackingTimes=Really revoke explicit tracking times for all races of leaderboard {0} that have valid start and finishing times?
|
||||
errorRevokingExplicitTrackingTimes=Error revoking explicit tracking times for leaderboard {0}: {1}
|
||||
|
||||
+20
-1
@@ -2586,6 +2586,25 @@ unlock=Entsperren
|
||||
ipsLockedForUserCreationAbuse=Wegen Missbrauchs bei der Benutzererstellung gesperrte IPs
|
||||
unableToLoadIpsBlockedForUserCreationAbuse=Wegen Missbrauchs der Benutzererstellung blockierte IPs können nicht geladen werden
|
||||
sourceCode=Quellcode
|
||||
miscellaneous=Verschiedenes
|
||||
securityUpdates=Sicherheitsupdates
|
||||
featureAndCommunityUpdates=Funktions- und Community-Updates
|
||||
youWillNowReceiveFeatureAndCommunityUpdates=Sie erhalten nun Funktions- und Community-Updates.
|
||||
youWillNotReceiveFeatureAndCommunityUpdatesAnymore=Sie erhalten keine Funktions- und Community-Updates mehr.
|
||||
failedToSetStatusOfFeatureAndCommunityUpdates=Fehler beim Festlegen des Status von Funktions- und Community-Updates
|
||||
failedToSetStatusOfNotificationsForFavoriteCompetitors=Status der Benachrichtigungen für bevorzugte Teilnehmer konnte nicht festgelegt werden.
|
||||
youWillNowReceiveNotificationsForFavoriteCompetitors=Sie erhalten nun Benachrichtigungen für Ihre bevorzugten Teilnehmer.
|
||||
youWillNotReceiveNotificationsForFavoriteCompetitorsAnymore=Sie erhalten keine Benachrichtigungen mehr für Ihre Lieblingskonkurrenten.
|
||||
failedToSetStatusOfUpdatesOnUpcomingRacesForYourFavoredBoatClasses=Fehler beim Festlegen des Benachrichtigungsstatus für bevorstehende Rennen Ihrer bevorzugten Bootsklasse
|
||||
youWillNowReceiveNotificationsForFavoriteBoatClassUpcomingRaces=Sie erhalten nun Benachrichtigungen für anstehende Rennen Ihrer bevorzugten Bootsklasse.
|
||||
youWillNotReceiveUpdatesOnUpcomingRacesForYourFavoredBoatClassesAnymore=Sie erhalten keine Benachrichtigungen mehr für bevorstehende Rennen Ihrer bevorzugten Bootsklasse.
|
||||
failedToSetStatusOfUpdatesOnNewResultsForYourFavoredBoatClasses=Fehler beim Festlegen des Benachrichtigungsstatus für neue Ergebnisse Ihrer bevorzugten Bootsklasse
|
||||
youWillNowReceiveUpdatesOnNewResultsForYourFavoredBoatClasses=Sie erhalten nun Benachrichtigungen über neue Ergebnisse Ihrer bevorzugten Bootsklasse.
|
||||
youWillNotReceiveNotificationsForFavoriteBoatClassNewResultsAnymore=Sie erhalten keine Benachrichtigungen mehr über neue Ergebnisse Ihrer bevorzugten Bootsklasse.
|
||||
failedToModifyFavoriteCompetitors=Fehler beim Setzen der Teilnemer-Favoriten
|
||||
favoriteCompetitorsModifiedSuccessfully=Teilnehmer-Favoriten erfolgreich aktualisiert
|
||||
failedToModifyFavoredBoatClasses=Die bevorzugten Bootsklassen konnten nicht angepasst werden.
|
||||
succesfullyModifiedFavoredBoatClasses=Die bevorzugten Bootsklassen wurden erfolgreich angepasst.
|
||||
revokeExplicitTrackingTimes=Explizite Tracking-Zeiten zurücksetzen für Rennen mit gültigen Start- und Endzeitpunkten
|
||||
confirmRevokeExplicitTrackingTimes=Wirklich explizite Tracking-Zeiten zurücksetzen für Rennen mit gültigen Start- und Endzeitpunkten der Rangliste {0}?
|
||||
errorRevokingExplicitTrackingTimes=Fehler beim Zurücksetzen expliziter Tracking-Zeiten für Rennen mit gültigen Start- und Endzeitpunkten der Rangliste {0}: {1}
|
||||
@@ -2595,4 +2614,4 @@ notRevokedTrackingTimesBecauseNotForTracking=Nicht widerrufene explizite Trackin
|
||||
notRevokedTrackingTimesBecauseOfMissingStartOrFinishTime=Nicht widerrufene explizite Tracking-Zeiten für Rennen mit ungültigen Start- oder Endzeitpunkten:
|
||||
noRegattaLeaderboard=Rangliste {0} ist keine Regatta-Rangliste.
|
||||
noAutomatedTrackingTimes=Rangliste {0} nutzt keine automatisierten Tracking-Zeiten, auf die zurückgesetzt werden könnte.
|
||||
unknownError=Unbekannter Fehler: {0}
|
||||
unknownError=Unbekannter Fehler: {0}
|
||||
|
||||
@@ -27,8 +27,13 @@
|
||||
<ul class="bulletList">
|
||||
<li>Tables with multi-selection support now have a select/de-select all checkbox in the table header.
|
||||
This comes with general improvements in table selection/de-selection handling.</li>
|
||||
<li>Users can now opt out of feature and community e-mails. This flag is available in
|
||||
Data Mining as a dimension for filtering and grouping.</li>
|
||||
<ii>When updates to user accounts are carried out using the API, not passing a property (such as
|
||||
company affiliation or preferred locale) will no longer reset that value to <tt>null</tt>
|
||||
but instead leave it unchanged.</li>
|
||||
<li>A new "Eraser" action has been added to the leaderboards table at the top of the
|
||||
Connectors / Smartphone Tracking panel. If the leadeboard is a "Regatta" leaderboard
|
||||
Connectors / Smartphone Tracking panel. If the leaderboard is a "Regatta" leaderboard
|
||||
and the corresponding Regatta is configured to control tracking times from race
|
||||
start/finish times, using the eraser action will remove any explicit start/end of tracking
|
||||
time specifications of the race logs of all races in the leaderboard that have valid
|
||||
|
||||
@@ -26,6 +26,7 @@ public class UserDTO extends
|
||||
private String locale;
|
||||
private List<AccountDTO> accounts;
|
||||
private boolean emailValidated;
|
||||
private boolean didOptOutOfFeatureAndCommunityEmails;
|
||||
private List<StrippedUserGroupDTO> groups;
|
||||
private TimePoint lockedUntil;
|
||||
private SecurityInformationDTO securityInformation = new SecurityInformationDTO();
|
||||
@@ -42,8 +43,8 @@ public class UserDTO extends
|
||||
* @param groups may be {@code null} which is equivalent to passing an empty groups collection
|
||||
*/
|
||||
public UserDTO(String name, String email, String fullName, String company, String locale, boolean emailValidated,
|
||||
List<AccountDTO> accounts, Iterable<RoleWithSecurityDTO> roles, StrippedUserGroupDTO defaultTenant,
|
||||
Iterable<WildcardPermissionWithSecurityDTO> permissions,
|
||||
boolean didOptOutOfFeatureAndCommunityEmails, List<AccountDTO> accounts, Iterable<RoleWithSecurityDTO> roles,
|
||||
StrippedUserGroupDTO defaultTenant, Iterable<WildcardPermissionWithSecurityDTO> permissions,
|
||||
Iterable<StrippedUserGroupDTO> groups, TimePoint lockedUntil) {
|
||||
super(name, permissions);
|
||||
this.defaultTenantForCurrentServer = defaultTenant;
|
||||
@@ -52,6 +53,7 @@ public class UserDTO extends
|
||||
this.company = company;
|
||||
this.locale = locale;
|
||||
this.emailValidated = emailValidated;
|
||||
this.didOptOutOfFeatureAndCommunityEmails = didOptOutOfFeatureAndCommunityEmails;
|
||||
this.accounts = accounts;
|
||||
this.groups = new ArrayList<>();
|
||||
Util.addAll(groups, this.groups);
|
||||
@@ -72,7 +74,8 @@ public class UserDTO extends
|
||||
final List<StrippedUserGroupDTO> groupsCopy = new ArrayList<StrippedUserGroupDTO>();
|
||||
Util.addAll(this.groups, groupsCopy);
|
||||
return new UserDTO(this.getName(), this.email, this.fullName, this.company, this.locale, this.emailValidated,
|
||||
accountsCopy, rolesCopy, this.defaultTenantForCurrentServer, permissionsCopy, groupsCopy, lockedUntil);
|
||||
this.didOptOutOfFeatureAndCommunityEmails, accountsCopy, rolesCopy, this.defaultTenantForCurrentServer,
|
||||
permissionsCopy, groupsCopy, lockedUntil);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,6 +143,10 @@ public class UserDTO extends
|
||||
return emailValidated;
|
||||
}
|
||||
|
||||
public boolean getDidOptOutOfFeatureAndCommunityEmails() {
|
||||
return didOptOutOfFeatureAndCommunityEmails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final AccessControlListDTO getAccessControlList() {
|
||||
return securityInformation.getAccessControlList();
|
||||
|
||||
@@ -97,6 +97,10 @@ public interface User extends SecurityUser<RoleDefinition, Role, UserGroup> {
|
||||
* specific elements as UIs or notification mails.
|
||||
*/
|
||||
Locale getLocale();
|
||||
|
||||
boolean getDidOptOutOfFeatureAndCommunityEmails();
|
||||
|
||||
void setDidOptOutOfFeatureAndCommunityEmails(boolean didOptOutOfFeatureAndCommunityEmails);
|
||||
|
||||
void setLocale(Locale locale);
|
||||
|
||||
|
||||
+2
-1
@@ -56,4 +56,5 @@ DataMiningReportsPreference=Data Mining Reports
|
||||
NumberOfDataMiningQueries=Number of Data Mining Queries
|
||||
NumberOfDataMiningReports=Number of Data Mining Reports
|
||||
FullName=Full Name
|
||||
EMail=E-Mail Address
|
||||
EMail=E-Mail Address
|
||||
DidOptOutOfFeatureAndCommunityEmails=Did opt out of Feature and Community Emails
|
||||
+2
-1
@@ -56,4 +56,5 @@ DataMiningReportsPreference=Data-Mining Berichte
|
||||
NumberOfDataMiningQueries=Anzahl Data-Mining Anfragen
|
||||
NumberOfDataMiningReports=Anzahl Data-Mining Berichte
|
||||
FullName=Ganzer Name
|
||||
EMail=e-Mail Adresse
|
||||
EMail=e-Mail Adresse
|
||||
DidOptOutOfFeatureAndCommunityEmails=Benutzer hat sich von Feature- und Community-E-Mails abgemeldet
|
||||
+5
@@ -108,4 +108,9 @@ public interface HasUserContext {
|
||||
final Session session = getSession();
|
||||
return session == null ? null : TimePoint.now().until(TimePoint.of(session.getLastAccessTime()).plus(Duration.ofMillis(session.getTimeout())));
|
||||
}
|
||||
|
||||
@Dimension(messageKey="DidOptOutOfFeatureAndCommunityEmails")
|
||||
default boolean didOptOutOfFeatureAndCommunityEmails() {
|
||||
return getUser().getDidOptOutOfFeatureAndCommunityEmails();
|
||||
}
|
||||
}
|
||||
|
||||
+22
-5
@@ -72,6 +72,8 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
private String passwordResetSecret;
|
||||
|
||||
private boolean emailValidated;
|
||||
|
||||
private boolean didOptOutOfFeatureAndCommunityEmails;
|
||||
|
||||
private final Map<AccountType, Account> accounts;
|
||||
|
||||
@@ -111,13 +113,14 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
public UserImpl(String name, String email, Map<String, UserGroup> defaultTenantForServer,
|
||||
Collection<Account> accounts, UserGroupProvider userGroupProvider, TimedLock timedLock) {
|
||||
this(name, email, /* fullName */ null, /* company */ null, /* locale */ null, /* is email validated */ false,
|
||||
/* password reset secret */ null, /* validation secret */ null, defaultTenantForServer, accounts,
|
||||
userGroupProvider, timedLock);
|
||||
/* did opt out of feature and community emails */ false, /* password reset secret */ null, /* validation secret */ null,
|
||||
defaultTenantForServer, accounts, userGroupProvider, timedLock);
|
||||
}
|
||||
|
||||
public UserImpl(String name, String email, String fullName, String company, Locale locale, Boolean emailValidated,
|
||||
String passwordResetSecret, String validationSecret, Map<String, UserGroup> defaultTenantForServer,
|
||||
Collection<Account> accounts, UserGroupProvider userGroupProvider, TimedLock timedLock) {
|
||||
public UserImpl(String name, String email, String fullName, String company, Locale locale, boolean emailValidated,
|
||||
boolean didOptOutOfFeatureAndCommunityEmails, String passwordResetSecret, String validationSecret,
|
||||
Map<String, UserGroup> defaultTenantForServer, Collection<Account> accounts,
|
||||
UserGroupProvider userGroupProvider, TimedLock timedLock) {
|
||||
super(name);
|
||||
this.timedLock = timedLock;
|
||||
this.defaultTenantForServer = defaultTenantForServer;
|
||||
@@ -128,6 +131,7 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
this.passwordResetSecret = passwordResetSecret;
|
||||
this.validationSecret = validationSecret;
|
||||
this.emailValidated = emailValidated;
|
||||
this.didOptOutOfFeatureAndCommunityEmails = didOptOutOfFeatureAndCommunityEmails;
|
||||
this.accounts = new HashMap<>();
|
||||
this.userGroupProvider = userGroupProvider;
|
||||
for (Account a : accounts) {
|
||||
@@ -252,6 +256,11 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDidOptOutOfFeatureAndCommunityEmails(boolean didOptOutOfFeatureAndCommunityEmails) {
|
||||
this.didOptOutOfFeatureAndCommunityEmails = didOptOutOfFeatureAndCommunityEmails;
|
||||
}
|
||||
|
||||
/**
|
||||
* The email address is set to not yet validated by resetting the
|
||||
* {@link #emailValidated} flag. A new {@link #validationSecret} is generated and returned which
|
||||
@@ -370,6 +379,9 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
builder.append("isEmailValidated()=");
|
||||
builder.append(isEmailValidated());
|
||||
builder.append(", ");
|
||||
builder.append("didOptOutOfFeatureAndCommunityEmails()=");
|
||||
builder.append(getDidOptOutOfFeatureAndCommunityEmails());
|
||||
builder.append(", ");
|
||||
if (getPermissions() != null) {
|
||||
builder.append("getPermissions()=");
|
||||
builder.append(getPermissions());
|
||||
@@ -472,4 +484,9 @@ public class UserImpl extends SecurityUserImpl<RoleDefinition, Role, UserGroup,
|
||||
public TimedLock getTimedLock() {
|
||||
return timedLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDidOptOutOfFeatureAndCommunityEmails() {
|
||||
return this.didOptOutOfFeatureAndCommunityEmails;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ public class PreferenceObjectBasedNotificationSetTest {
|
||||
Map<String, UserGroup> defaultTenantForServer = new ConcurrentHashMap<>();
|
||||
defaultTenantForServer.put(serverName, defaultTenantForSingleServer);
|
||||
store.createUser(username, email, new TimedLockImpl());
|
||||
store.updateUser(new UserImpl(username, email, null, null, null, true, null, null, defaultTenantForServer,
|
||||
store.updateUser(new UserImpl(username, email, null, null, null, true, false, null, null, defaultTenantForServer,
|
||||
Collections.emptySet(), /* userGroupProvider */ null, new TimedLockImpl()));
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -51,6 +51,7 @@ public class UserStoreWithPersistenceTest {
|
||||
private final String fullName = "Arno Nym";
|
||||
private final String company = "SAP SE";
|
||||
private final String email = "anonymous@sapsailing.com";
|
||||
private final boolean didOptOutOfFeatureAndCommunityEmails = false;
|
||||
private final String serverName = "dummyServer";
|
||||
private final String prefKey = "pk";
|
||||
private final String prefValue = "pv";
|
||||
@@ -107,8 +108,9 @@ public class UserStoreWithPersistenceTest {
|
||||
UserGroupImpl defaultTenant = createUserGroup();
|
||||
HashMap<String, UserGroup> defaultTenantForServers = new HashMap<>();
|
||||
defaultTenantForServers.put(serverName, defaultTenant);
|
||||
store.updateUser(new UserImpl(username, email, fullName, company, Locale.GERMAN, false, null, null,
|
||||
defaultTenantForServers, Collections.emptySet(), /* userGroupProvider */ null, new TimedLockImpl()));
|
||||
store.updateUser(new UserImpl(username, email, fullName, company, Locale.GERMAN, false,
|
||||
didOptOutOfFeatureAndCommunityEmails, null, null, defaultTenantForServers, Collections.emptySet(),
|
||||
/* userGroupProvider */ null, new TimedLockImpl()));
|
||||
newStore();
|
||||
User savedUser = store.getUserByName(username);
|
||||
assertEquals(username, savedUser.getName());
|
||||
|
||||
Executable → Regular
Executable → Regular
+3
-3
@@ -63,9 +63,9 @@ public interface AuthenticationManager {
|
||||
*/
|
||||
void logout();
|
||||
|
||||
void updateUserProperties(String fullName, String company, String localeName, String defaultTenantIdAsString,
|
||||
AsyncCallback<UserDTO> callback);
|
||||
|
||||
void updateUserProperties(String fullName, String company, String localeName, Boolean didOptOutFeatureAndCommunityEmails,
|
||||
String defaultTenantIdAsString, AsyncCallback<UserDTO> callback);
|
||||
|
||||
/**
|
||||
* Provide the {@link AuthenticationContext} for the current user
|
||||
*
|
||||
|
||||
+17
-17
@@ -203,27 +203,27 @@ public class AuthenticationManagerImpl implements AuthenticationManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUserProperties(String fullName, String company, String localeName, String defaultTenantIdAsString,
|
||||
final AsyncCallback<UserDTO> callback) {
|
||||
public void updateUserProperties(String fullName, String company, String localeName,
|
||||
Boolean didOptOutFeatureAndCommunityEmails, String defaultTenantIdAsString, final AsyncCallback<UserDTO> callback) {
|
||||
final UserDTO currentUser = getAuthenticationContext().getCurrentUser();
|
||||
final String username = currentUser.getName();
|
||||
final String locale = currentUser.getLocale();
|
||||
userManagementWriteService.updateUserProperties(username, fullName, company, localeName, defaultTenantIdAsString,
|
||||
new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
userManagementWriteService.updateUserProperties(username, fullName, company, localeName,
|
||||
didOptOutFeatureAndCommunityEmails, defaultTenantIdAsString, new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
callback.onFailure(caught);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(UserDTO result) {
|
||||
refreshUserInfo();
|
||||
callback.onSuccess(result);
|
||||
if (!Util.equalsWithNull(locale, localeName)) {
|
||||
redirectIfLocaleIsSetAndLocaleIsNotGivenInTheURL(localeName);
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onSuccess(UserDTO result) {
|
||||
refreshUserInfo();
|
||||
callback.onSuccess(result);
|
||||
if (!Util.equalsWithNull(locale, localeName)) {
|
||||
redirectIfLocaleIsSetAndLocaleIsNotGivenInTheURL(localeName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-4
@@ -18,10 +18,9 @@ import com.sap.sse.security.ui.client.premium.PaywallResolver;
|
||||
public class AuthenticationContextImpl implements AuthenticationContext {
|
||||
|
||||
private final UserDTO currentUser;
|
||||
private final static UserDTO ANONYMOUS = new UserDTO("Anonymous", "", "", "", null, false, new ArrayList<AccountDTO>(),
|
||||
new ArrayList<RoleWithSecurityDTO>(), /* default tenant */ null,
|
||||
new ArrayList<WildcardPermissionWithSecurityDTO>(),
|
||||
/* groups */ null, /* lockedUntil */ null);
|
||||
private final static UserDTO ANONYMOUS = new UserDTO("Anonymous", "", "", "", null, false, false,
|
||||
new ArrayList<AccountDTO>(), new ArrayList<RoleWithSecurityDTO>(), /* default tenant */ null,
|
||||
new ArrayList<WildcardPermissionWithSecurityDTO>(), /* groups */ null, /* lockedUntil */ null);
|
||||
private final UserService userService;
|
||||
private final PaywallResolver paywallResolver;
|
||||
|
||||
|
||||
+8
-1
@@ -67,8 +67,15 @@ public interface UserManagementWriteService extends UserManagementService {
|
||||
void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL)
|
||||
throws UserManagementException, MailException, org.apache.shiro.authz.UnauthorizedException;
|
||||
|
||||
/**
|
||||
* @param username must not be null
|
||||
* @param fullName when null, no update will be processed to the respective parameter
|
||||
* @param company when null, no update will be processed to the respective parameter
|
||||
* @param locale when null, no update will be processed to the respective parameter
|
||||
* @param didOptOutOfFeatureAndCommunityEmails when null, no update will be processed to the respective parameter
|
||||
*/
|
||||
UserDTO updateUserProperties(String username, String fullName, String company, String localeName,
|
||||
String defaultTenantIdAsString)
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails, String defaultTenantIdAsString)
|
||||
throws UserManagementException, org.apache.shiro.authz.UnauthorizedException;
|
||||
|
||||
void resetPassword(String username, String eMailAddress, String baseURL)
|
||||
|
||||
+8
-1
@@ -51,8 +51,15 @@ public interface UserManagementWriteServiceAsync extends UserManagementServiceAs
|
||||
|
||||
void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL, AsyncCallback<Void> callback);
|
||||
|
||||
/**
|
||||
* @param username must not be null
|
||||
* @param fullName when null, no update will be processed to the respective parameter
|
||||
* @param company when null, no update will be processed to the respective parameter
|
||||
* @param locale when null, no update will be processed to the respective parameter
|
||||
* @param didOptOutOfFeatureAndCommunityEmails when null, no update will be processed to the respective parameter
|
||||
*/
|
||||
void updateUserProperties(String username, String fullName, String company, String localeName,
|
||||
String defaultTenantIdAsString, AsyncCallback<UserDTO> callback);
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails, String defaultTenantIdAsString, AsyncCallback<UserDTO> callback);
|
||||
|
||||
void createRoleDefinition(String roleDefinitionIdAsString, String name, AsyncCallback<RoleDefinitionDTO> callback);
|
||||
|
||||
|
||||
+1
@@ -233,4 +233,5 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages {
|
||||
String clientCurrentlyLockedForUserCreation();
|
||||
String unlockedSuccessfully();
|
||||
String failedToUnlock();
|
||||
String optOutOfFeatureAndCommunityEmails();
|
||||
}
|
||||
|
||||
+2
-1
@@ -239,4 +239,5 @@ lockedUntil=Locked until
|
||||
passwordAuthenticationCurrentlyLockedForUser=Password authentication is currently locked for this user due to too many failed login attempts. Please try again later.
|
||||
clientCurrentlyLockedForUserCreation=Client is currently locked for user creation after too many user creations by the same client. Please try again later.
|
||||
unlockedSuccessfully=Unlocked successfully
|
||||
failedToUnlock=Failed to unlock
|
||||
failedToUnlock=Failed to unlock
|
||||
optOutOfFeatureAndCommunityEmails=Opt out of Feature and Community Updates emails
|
||||
+2
-1
@@ -238,4 +238,5 @@ lockedUntil=Gesperrt bis
|
||||
passwordAuthenticationCurrentlyLockedForUser=Passwort-Authentifizierung ist für diesen Benutzer derzeit aufgrund zu vieler fehlgeschlagener Versuche gesperrt. Bitter später erneut versuchen.
|
||||
clientCurrentlyLockedForUserCreation=Benutzererstellung für den aktuellen Client ist derzeit wegen zu vieler neuer Nutzer vom selben Client gesperrt. Bitte später erneut versuchen.
|
||||
unlockedSuccessfully=Erfolgreich entsperrt.
|
||||
failedToUnlock=Entsperren fehlgeschlagen
|
||||
failedToUnlock=Entsperren fehlgeschlagen
|
||||
optOutOfFeatureAndCommunityEmails=Abmeldung von E-Mails mit Funktions- und Community-Updates
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class EditUserRolesAndPermissionsDialog extends DataEntryDialog<Void> {
|
||||
}
|
||||
|
||||
private UserDTO createUserAdapter(final String selectedUsername, final RolesAndPermissionsForUserDTO dto) {
|
||||
return new UserDTO(selectedUsername, null, null, null, null, false, null, dto.getRoles(), null,
|
||||
return new UserDTO(selectedUsername, null, null, null, null, false, false, null, dto.getRoles(), null,
|
||||
dto.getPermissions(), null, null);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -9,6 +9,7 @@ import com.google.gwt.event.dom.client.ClickEvent;
|
||||
import com.google.gwt.event.dom.client.ClickHandler;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.CheckBox;
|
||||
import com.google.gwt.user.client.ui.DecoratorPanel;
|
||||
import com.google.gwt.user.client.ui.FlexTable;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
@@ -49,6 +50,7 @@ public class UserEditDialog extends DataEntryDialog<UserDTO> {
|
||||
private final TextBox fullName;
|
||||
private final TextBox company;
|
||||
private final TextBox email;
|
||||
private final CheckBox optOutOfFeatureAndCommunityEmailsCheckbox;
|
||||
private final VerticalPanel accountPanels;
|
||||
|
||||
private final UserService userService;
|
||||
@@ -73,6 +75,9 @@ public class UserEditDialog extends DataEntryDialog<UserDTO> {
|
||||
this.email = createTextBox(userToEdit.getEmail(), 70);
|
||||
this.fullName = createTextBox(userToEdit.getFullName(), 70);
|
||||
this.company = createTextBox(userToEdit.getCompany(), 70);
|
||||
this.optOutOfFeatureAndCommunityEmailsCheckbox = new CheckBox(stringMessages.optOutOfFeatureAndCommunityEmails(),
|
||||
userToEdit.getDidOptOutOfFeatureAndCommunityEmails());
|
||||
optOutOfFeatureAndCommunityEmailsCheckbox.setValue(userToEdit.getDidOptOutOfFeatureAndCommunityEmails());
|
||||
this.accountPanels = new VerticalPanel();
|
||||
for (AccountDTO a : userToEdit.getAccounts()) {
|
||||
DecoratorPanel accountPanelDecorator = new DecoratorPanel();
|
||||
@@ -155,8 +160,8 @@ public class UserEditDialog extends DataEntryDialog<UserDTO> {
|
||||
permissions.add((WildcardPermissionWithSecurityDTO) permission);
|
||||
}
|
||||
final UserDTO user = new UserDTO(userToEdit.getName(), email.getText(), fullName.getText(), company.getText(),
|
||||
userToEdit.getLocale(), userToEdit.isEmailValidated(), userToEdit.getAccounts(),
|
||||
userToEdit.getRoles(), userToEdit.getDefaultTenant(), permissions,
|
||||
userToEdit.getLocale(), userToEdit.isEmailValidated(), optOutOfFeatureAndCommunityEmailsCheckbox.getValue(),
|
||||
userToEdit.getAccounts(), userToEdit.getRoles(), userToEdit.getDefaultTenant(), permissions,
|
||||
userToEdit.getUserGroups(), userToEdit.getLockedUntil());
|
||||
return user;
|
||||
}
|
||||
@@ -172,6 +177,8 @@ public class UserEditDialog extends DataEntryDialog<UserDTO> {
|
||||
result.setWidget(2, 1, email);
|
||||
result.setWidget(3, 0, new Label(stringMessages.company()));
|
||||
result.setWidget(3, 1, company);
|
||||
result.setWidget(4, 0, new Label(stringMessages.optOutOfFeatureAndCommunityEmails()));
|
||||
result.setWidget(4, 1, optOutOfFeatureAndCommunityEmailsCheckbox);
|
||||
result.setWidget(4, 0, accountPanels);
|
||||
return result;
|
||||
}
|
||||
|
||||
+25
-2
@@ -20,6 +20,7 @@ import com.google.gwt.cell.client.SafeHtmlCell;
|
||||
import com.google.gwt.core.client.Callback;
|
||||
import com.google.gwt.safehtml.shared.SafeHtml;
|
||||
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
|
||||
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
|
||||
import com.google.gwt.user.cellview.client.AbstractCellTable;
|
||||
import com.google.gwt.user.cellview.client.Column;
|
||||
import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler;
|
||||
@@ -97,6 +98,14 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
TextColumn<UserDTO> fullNameColumn = new AbstractSortableTextColumn<UserDTO>(user->user.getFullName(), userColumnListHandler);
|
||||
TextColumn<UserDTO> emailColumn = new AbstractSortableTextColumn<UserDTO>(user->user.getEmail(), userColumnListHandler);
|
||||
TextColumn<UserDTO> emailValidatedColumn = new AbstractSortableTextColumn<UserDTO>(user->user.isEmailValidated() ? stringMessages.yes() : stringMessages.no(), userColumnListHandler);
|
||||
TextColumn<UserDTO> optOutColumn = new AbstractSortableTextColumn<UserDTO>(user->user.getDidOptOutOfFeatureAndCommunityEmails() ? stringMessages.yes() : stringMessages.no(), userColumnListHandler);
|
||||
optOutColumn.setSortable(true);
|
||||
userColumnListHandler.setComparator(optOutColumn, new Comparator<UserDTO>() {
|
||||
@Override
|
||||
public int compare(UserDTO r1, UserDTO r2) {
|
||||
return Boolean.compare(r1.getDidOptOutOfFeatureAndCommunityEmails(), r2.getDidOptOutOfFeatureAndCommunityEmails());
|
||||
}
|
||||
});
|
||||
TextColumn<UserDTO> companyColumn = new AbstractSortableTextColumn<UserDTO>(user->user.getCompany(), userColumnListHandler);
|
||||
Column<UserDTO, SafeHtml> groupsColumn = new Column<UserDTO, SafeHtml>(new SafeHtmlCell()) {
|
||||
@Override
|
||||
@@ -177,6 +186,9 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
strings.add(t.getFullName());
|
||||
strings.add(t.getEmail());
|
||||
strings.add(t.getCompany());
|
||||
if (t.getDidOptOutOfFeatureAndCommunityEmails()) {
|
||||
strings.add(stringMessages.optOutOfFeatureAndCommunityEmails());
|
||||
}
|
||||
Util.addAll(Util.map(t.getRoles(), RoleWithSecurityDTO::getName), strings);
|
||||
Util.addAll(Util.map(t.getUserGroups(), StrippedUserGroupDTO::getName), strings);
|
||||
return strings;
|
||||
@@ -195,6 +207,7 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
table.addColumn(fullNameColumn, stringMessages.name());
|
||||
table.addColumn(emailColumn, stringMessages.email());
|
||||
table.addColumn(emailValidatedColumn, stringMessages.validated());
|
||||
table.addColumn(optOutColumn, composeOptOutColumnHeaderWithTooltipOnHover(stringMessages));
|
||||
table.addColumn(companyColumn, stringMessages.company());
|
||||
table.addColumn(groupsColumn, stringMessages.groups());
|
||||
table.addColumn(rolesColumn, stringMessages.roles());
|
||||
@@ -205,6 +218,16 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
table.ensureDebugId("UsersTable");
|
||||
}
|
||||
|
||||
private SafeHtml composeOptOutColumnHeaderWithTooltipOnHover(StringMessages stringMessages) {
|
||||
final String fullTitle = stringMessages.optOutOfFeatureAndCommunityEmails();
|
||||
final String tooltip = new SafeHtmlBuilder().appendEscaped(fullTitle).toSafeHtml().asString();
|
||||
final SafeHtml baseHtml = SafeHtmlUtils.fromString(fullTitle.substring(0, 10) + "...");
|
||||
return new SafeHtmlBuilder()
|
||||
.appendHtmlConstant("<span title=\"" + tooltip + "\">")
|
||||
.append(baseHtml)
|
||||
.appendHtmlConstant("</ span>").toSafeHtml();
|
||||
}
|
||||
|
||||
private AccessControlledActionsColumn<UserDTO, DefaultActionsImagesBarCell> composeUserActionColumn(
|
||||
StringMessages stringMessages, ErrorReporter errorReporter) {
|
||||
final HasPermissions type = SecuredSecurityTypes.USER;
|
||||
@@ -338,8 +361,8 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
final UserEditDialog dialog = new UserEditDialog(originalUser, new DialogCallback<UserDTO>() {
|
||||
@Override
|
||||
public void ok(final UserDTO user) {
|
||||
getUserManagementWriteService().updateUserProperties(user.getName(), user.getFullName(), user.getCompany(),
|
||||
user.getLocale(),
|
||||
getUserManagementWriteService().updateUserProperties(user.getName(), user.getFullName(),
|
||||
user.getCompany(), user.getLocale(), user.getDidOptOutOfFeatureAndCommunityEmails(),
|
||||
user.getDefaultTenant() != null ? user.getDefaultTenant().getId().toString() : null,
|
||||
new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -91,8 +91,9 @@ public class SecurityDTOFactory {
|
||||
.collect(Collectors.toList());
|
||||
userDTO = new UserDTO(user.getName(), user.getEmail(), user.getFullName(), user.getCompany(),
|
||||
user.getLocale() != null ? user.getLocale().toLanguageTag() : null, user.isEmailValidated(),
|
||||
accountDTOs, createRolesDTOs(filteredRoles, fromOriginalToStrippedDownUser,
|
||||
fromOriginalToStrippedDownUserGroup, securityService, user),
|
||||
user.getDidOptOutOfFeatureAndCommunityEmails(), accountDTOs,
|
||||
createRolesDTOs(filteredRoles, fromOriginalToStrippedDownUser, fromOriginalToStrippedDownUserGroup,
|
||||
securityService, user),
|
||||
/* default tenant filled in later */ null,
|
||||
getSecuredPermissions(filteredPermissions, user, securityService),
|
||||
createStrippedUserGroupDTOsFromUserGroups(securityService.getUserGroupsOfUser(user),
|
||||
|
||||
+6
-4
@@ -320,11 +320,13 @@ public class UserManagementWriteServiceImpl extends UserManagementServiceImpl im
|
||||
|
||||
@Override
|
||||
public UserDTO updateUserProperties(final String username, String fullName, String company, String localeName,
|
||||
String defaultTenant) throws UserManagementException {
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails, String defaultTenant) throws UserManagementException {
|
||||
getSecurityService().checkCurrentUserUpdatePermission(getSecurityService().getCurrentUser());
|
||||
getSecurityService().updateUserProperties(username, fullName, company,
|
||||
getLocaleFromLocaleName(localeName));
|
||||
getSecurityService().setDefaultTenantForCurrentServerForUser(username, UUID.fromString(defaultTenant));
|
||||
getSecurityService().updateUserProperties(username, fullName, company, getLocaleFromLocaleName(localeName),
|
||||
didOptOutOfFeatureAndCommunityEmails);
|
||||
if (defaultTenant != null) {
|
||||
getSecurityService().setDefaultTenantForCurrentServerForUser(username, UUID.fromString(defaultTenant));
|
||||
}
|
||||
return securityDTOFactory.createUserDTOFromUser(getSecurityService().getUserByName(username),
|
||||
getSecurityService());
|
||||
}
|
||||
|
||||
+18
-14
@@ -47,21 +47,25 @@ public class UserDetailsPresenter implements AbstractUserDetails.Presenter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSaveChangesRequest(String fullName, String company, String locale, String defaultTenantIdAsString) {
|
||||
authenticationManager.updateUserProperties(fullName, company, locale, defaultTenantIdAsString,
|
||||
public void handleSaveChangesRequest(String fullName, String company, String locale,
|
||||
String defaultTenantIdAsString) {
|
||||
authenticationManager.updateUserProperties(fullName, company, locale,
|
||||
/* don' t change the "opt out of feature and community mails" setting */ null, defaultTenantIdAsString,
|
||||
new AsyncCallback<UserDTO>() {
|
||||
@Override
|
||||
public void onSuccess(UserDTO result) {
|
||||
Notification.notify(i18n_sec.successfullyUpdatedUserProperties(
|
||||
authenticationManager.getAuthenticationContext().getCurrentUser().getName()),
|
||||
NotificationType.INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(i18n_sec.errorUpdatingUserProperties(caught.getMessage()), NotificationType.ERROR);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onSuccess(UserDTO result) {
|
||||
Notification.notify(
|
||||
i18n_sec.successfullyUpdatedUserProperties(
|
||||
authenticationManager.getAuthenticationContext().getCurrentUser().getName()),
|
||||
NotificationType.INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(i18n_sec.errorUpdatingUserProperties(caught.getMessage()),
|
||||
NotificationType.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-2
@@ -7,6 +7,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -284,6 +285,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
final String company = (String) userDBObject.get(FieldNames.User.COMPANY.name());
|
||||
final String localeRaw = (String) userDBObject.get(FieldNames.User.LOCALE.name());
|
||||
final Locale locale = localeRaw != null ? Locale.forLanguageTag(localeRaw) : null;
|
||||
final Boolean didOptOutOfFeatureAndCommunityEmails = Optional.ofNullable((Boolean) userDBObject
|
||||
.get(FieldNames.User.DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS.name())).orElse(false);
|
||||
final Boolean emailValidated = (Boolean) userDBObject.get(FieldNames.User.EMAIL_VALIDATED.name());
|
||||
final String passwordResetSecret = (String) userDBObject.get(FieldNames.User.PASSWORD_RESET_SECRET.name());
|
||||
final String validationSecret = (String) userDBObject.get(FieldNames.User.VALIDATION_SECRET.name());
|
||||
@@ -357,8 +360,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
Document accountsMap = (Document) userDBObject.get(FieldNames.User.ACCOUNTS.name());
|
||||
Map<AccountType, Account> accounts = createAccountMapFromdDBObject(accountsMap);
|
||||
User result = new UserImpl(username, email, fullName, company, locale,
|
||||
emailValidated == null ? false : emailValidated, passwordResetSecret, validationSecret, defaultTenant,
|
||||
accounts.values(), userGroupProvider, timedLock);
|
||||
emailValidated == null ? false : emailValidated, didOptOutOfFeatureAndCommunityEmails, passwordResetSecret,
|
||||
validationSecret, defaultTenant, accounts.values(), userGroupProvider, timedLock);
|
||||
for (final Role role : roles) {
|
||||
result.addRole(role);
|
||||
}
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ public class FieldNames {
|
||||
DEFAULT_TENANT_IDS,
|
||||
EMAIL_VALIDATED,
|
||||
PASSWORD_RESET_SECRET,
|
||||
DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS,
|
||||
VALIDATION_SECRET,
|
||||
DEFAULT_TENANT_SERVER,
|
||||
DEFAULT_TENANT_GROUP,
|
||||
|
||||
+1
@@ -212,6 +212,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
dbUser.put(FieldNames.User.FULLNAME.name(), user.getFullName());
|
||||
dbUser.put(FieldNames.User.COMPANY.name(), user.getCompany());
|
||||
dbUser.put(FieldNames.User.LOCALE.name(), user.getLocale() != null ? user.getLocale().toLanguageTag() : null);
|
||||
dbUser.put(FieldNames.User.DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS.name(), user.getDidOptOutOfFeatureAndCommunityEmails());
|
||||
dbUser.put(FieldNames.User.EMAIL_VALIDATED.name(), user.isEmailValidated());
|
||||
dbUser.put(FieldNames.User.PASSWORD_RESET_SECRET.name(), user.getPasswordResetSecret());
|
||||
dbUser.put(FieldNames.User.VALIDATION_SECRET.name(), user.getValidationSecret());
|
||||
|
||||
+10
@@ -245,4 +245,14 @@ public class UserProxy implements User {
|
||||
public TimedLock getTimedLock() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getDidOptOutOfFeatureAndCommunityEmails() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDidOptOutOfFeatureAndCommunityEmails(boolean didOptOutOfFeatureAndCommunityEmails) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,15 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
|
||||
|
||||
void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL) throws UserManagementException;
|
||||
|
||||
void updateUserProperties(String username, String fullName, String company, Locale locale) throws UserManagementException;
|
||||
/**
|
||||
* @param username must not be null
|
||||
* @param fullName when null, no update will be processed to the respective parameter
|
||||
* @param company when null, no update will be processed to the respective parameter
|
||||
* @param locale when null, no update will be processed to the respective parameter
|
||||
* @param didOptOutOfFeatureAndCommunityEmails when null, no update will be processed to the respective parameter
|
||||
*/
|
||||
void updateUserProperties(String username, String fullName, String company, Locale locale,
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails) throws UserManagementException;
|
||||
|
||||
void resetUserTimedLock(String username) throws UserManagementException;
|
||||
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ public interface ReplicableSecurityService extends SecurityService {
|
||||
|
||||
Void internalUpdateSimpleUserPassword(String username, byte[] salt, String hashedPasswordBase64);
|
||||
|
||||
Void internalUpdateUserProperties(String username, String fullName, String company, Locale locale);
|
||||
Void internalUpdateUserProperties(String username, String fullName, String company, Locale locale,
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails);
|
||||
|
||||
Void internalResetUserTimedLock(String username);
|
||||
|
||||
|
||||
@@ -1157,7 +1157,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
|
||||
// the new user becomes its owner to ensure the user role is working correctly
|
||||
// the default tenant is the owning tenant to allow users having admin role for a specific server tenant to also be able to delete users
|
||||
apply(new SetOwnershipOperation(result.getIdentifier(), username, groupOwningUser==null?null:groupOwningUser.getId(), username));
|
||||
updateUserProperties(username, fullName, company, locale);
|
||||
updateUserProperties(username, fullName, company, locale, /* didOptOutOfFeatureAndCommunityEmails */ false);
|
||||
// email has been set during creation already; the following call will trigger the e-mail validation process
|
||||
updateSimpleUserEmail(username, email, validationBaseURL);
|
||||
return result;
|
||||
@@ -1288,20 +1288,31 @@ implements ReplicableSecurityService, ClearStateTestSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUserProperties(String username, String fullName, String company, Locale locale) throws UserManagementException {
|
||||
public void updateUserProperties(String username, String fullName, String company, Locale locale,
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails) throws UserManagementException {
|
||||
final User user = store.getUserByName(username);
|
||||
if (user == null) {
|
||||
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
|
||||
}
|
||||
apply(new UpdateUserPropertiesOperation(username, fullName, company, locale));
|
||||
apply(new UpdateUserPropertiesOperation(username, fullName, company, locale, didOptOutOfFeatureAndCommunityEmails));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void internalUpdateUserProperties(String username, String fullName, String company, Locale locale) {
|
||||
public Void internalUpdateUserProperties(String username, String fullName, String company, Locale locale,
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails) {
|
||||
final User user = store.getUserByName(username);
|
||||
user.setFullName(fullName);
|
||||
user.setCompany(company);
|
||||
user.setLocale(locale);
|
||||
if (fullName != null) {
|
||||
user.setFullName(fullName);
|
||||
}
|
||||
if (company != null) {
|
||||
user.setCompany(company);
|
||||
}
|
||||
if (locale != null) {
|
||||
user.setLocale(locale);
|
||||
}
|
||||
if (didOptOutOfFeatureAndCommunityEmails != null) {
|
||||
user.setDidOptOutOfFeatureAndCommunityEmails(didOptOutOfFeatureAndCommunityEmails);
|
||||
}
|
||||
store.updateUser(user);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public class SecurityResource extends AbstractSecurityResource {
|
||||
public static final String COMPANY = "company";
|
||||
public static final String FULL_NAME = "fullName";
|
||||
public static final String EMAIL = "email";
|
||||
public static final String OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS = "opt_out_of_feature_and_community_emails";
|
||||
private static final String SECURITY_UI_URL_PATH = "/security/ui/";
|
||||
public static final String USERNAME = "username";
|
||||
public static final String PASSWORD = "password";
|
||||
@@ -366,8 +367,8 @@ public class SecurityResource extends AbstractSecurityResource {
|
||||
@Path(USER_METHOD)
|
||||
@Produces("text/plain;charset=UTF-8")
|
||||
public Response updateUser(@Context UriInfo uriInfo, @QueryParam(USERNAME) String username,
|
||||
@QueryParam(EMAIL) String email, @QueryParam(FULL_NAME) String fullName,
|
||||
@QueryParam(COMPANY) String company) {
|
||||
@QueryParam(EMAIL) String email, @QueryParam(OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS) Boolean optOutOfFeatureAndCommunityEmails,
|
||||
@QueryParam(FULL_NAME) String fullName, @QueryParam(COMPANY) String company) {
|
||||
if (!getSecurityService().hasCurrentUserUpdatePermission(getSecurityService().getUserByName(username))) {
|
||||
return Response.status(Status.UNAUTHORIZED).build();
|
||||
} else {
|
||||
@@ -376,7 +377,8 @@ public class SecurityResource extends AbstractSecurityResource {
|
||||
if (user == null) {
|
||||
return Response.status(Status.PRECONDITION_FAILED).entity("User "+username+" not known").build();
|
||||
} else {
|
||||
getSecurityService().updateUserProperties(username, fullName, company, user.getLocale());
|
||||
getSecurityService().updateUserProperties(username, fullName, company, user.getLocale(),
|
||||
optOutOfFeatureAndCommunityEmails);
|
||||
if (!Util.equalsWithNull(user.getEmail(), email)) {
|
||||
getSecurityService().updateSimpleUserEmail(username, email, getEmailValidationBaseURL(uriInfo));
|
||||
}
|
||||
|
||||
+9
-2
@@ -7,20 +7,27 @@ import com.sap.sse.security.impl.ReplicableSecurityService;
|
||||
public class UpdateUserPropertiesOperation implements SecurityOperation<Void> {
|
||||
private static final long serialVersionUID = -6267523788529623080L;
|
||||
protected final String username;
|
||||
/** When null, an update to this property will not be processed */
|
||||
protected final String fullName;
|
||||
/** When null, an update to this property will not be processed */
|
||||
protected final String company;
|
||||
/** When null, an update to this property will not be processed */
|
||||
protected final Locale locale;
|
||||
/** When null, an update to this property will not be processed */
|
||||
protected final Boolean didOptOutOfFeatureAndCommunityEmails;
|
||||
|
||||
public UpdateUserPropertiesOperation(String username, String fullName, String company, Locale locale) {
|
||||
public UpdateUserPropertiesOperation(String username, String fullName, String company, Locale locale,
|
||||
Boolean didOptOutOfFeatureAndCommunityEmails) {
|
||||
this.username = username;
|
||||
this.fullName = fullName;
|
||||
this.company = company;
|
||||
this.locale = locale;
|
||||
this.didOptOutOfFeatureAndCommunityEmails = didOptOutOfFeatureAndCommunityEmails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void internalApplyTo(ReplicableSecurityService toState) throws Exception {
|
||||
toState.internalUpdateUserProperties(username, fullName, company, locale);
|
||||
toState.internalUpdateUserProperties(username, fullName, company, locale, didOptOutOfFeatureAndCommunityEmails);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,16 +29,36 @@ cannot be used to update the password. See <a href="change_password.html"><code>
|
||||
<tr>
|
||||
<td>Mandatory parameters:</td>
|
||||
<td>
|
||||
<div>username</div>
|
||||
<div>email</div>
|
||||
<div>fullName</div>
|
||||
<div>company</div>
|
||||
<div>username - The username of the user to update (required)</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example:</td>
|
||||
<td><code>curl -X PUT "http://127.0.0.1:8888/security/api/restsecurity/user?username=uhl&email=axel.uhl@sap.com&fullName=Axel%20Uhl&company=SAP%20SE"</code><br>
|
||||
Will provide a JSON document as explained above.</td>
|
||||
<td>Optional parameters:</td>
|
||||
<td>
|
||||
<div>email - The user's email address</div>
|
||||
<div>fullName - The user's full name</div>
|
||||
<div>company - The user's company name</div>
|
||||
<div>opt_out_of_feature_and_community_emails - Boolean flag to opt out of feature and community emails</div>
|
||||
<br>
|
||||
<strong>Important:</strong> If a parameter is omitted or passed as null, that property will <strong>not be updated</strong>.
|
||||
Only parameters explicitly provided with non-null values will be updated. This allows for partial updates
|
||||
where you can update only specific fields without affecting others.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Examples:</td>
|
||||
<td>
|
||||
<div><strong>Update all properties:</strong></div>
|
||||
<code>curl -X PUT "http://127.0.0.1:8888/security/api/restsecurity/user?username=uhl&email=axel.uhl@sap.com&fullName=Axel%20Uhl&company=SAP%20SE"</code><br>
|
||||
<br>
|
||||
<div><strong>Update only email (other properties remain unchanged):</strong></div>
|
||||
<code>curl -X PUT "http://127.0.0.1:8888/security/api/restsecurity/user?username=uhl&email=newemail@sap.com"</code><br>
|
||||
<br>
|
||||
<div><strong>Update only company and opt-out flag:</strong></div>
|
||||
<code>curl -X PUT "http://127.0.0.1:8888/security/api/restsecurity/user?username=uhl&company=New%20Company&opt_out_of_feature_and_community_emails=true"</code><br>
|
||||
<br>
|
||||
Will provide a JSON document as explained above.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
|
||||
Executable → Regular
@@ -29,7 +29,7 @@ Displays designs displaying the login pages for the management-console.
|
||||
The View is already implemented. Overall Authorization and related event propagation is missing.
|
||||
|
||||

|
||||
See https://static.sapsailing.com/management-console/login for a full list of designs.
|
||||
See [https://static.sapsailing.com/management-console/login](https://static.sapsailing.com/management-console/login) for a full list of designs.
|
||||
|
||||
### Event
|
||||
|
||||
|
||||
Reference in New Issue
Block a user