Merge branch 'main' into bug6239

This commit is contained in:
masha.kashirina
2026-04-24 14:44:31 +02:00
79 changed files with 1395 additions and 430 deletions
+1 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
@@ -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);
@@ -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");
}
}
@@ -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
@@ -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());
}
@@ -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>
&lt;CountryId&gt;AUS&lt;/CountryId&gt;
@@ -152,6 +152,87 @@ public interface ORCPublicCertificateDatabase {
&lt;RMSCode&gt;CLUB&lt;/RMSCode&gt;
* </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,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;
}
}
@@ -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
@@ -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();
}
}
@@ -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">
@@ -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);
}
}
@@ -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);
}
}
@@ -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>
@@ -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();
}
}
@@ -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);
}
}
@@ -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>
@@ -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
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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>
@@ -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());
}
@@ -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);
}
@@ -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);
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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);
}
}
}
@@ -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>
@@ -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;
}
}
@@ -29,6 +29,8 @@ public interface UserPreferencesView extends IsWidget {
BoatClassSelectionPresenter getFavoriteBoatClassesDataProvider();
CompetitorSelectionPresenter getFavoriteCompetitorsDataProvider();
MiscPreferencesPresenter getMiscPresenter();
}
}
@@ -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);
}
}
@@ -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());
@@ -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());
@@ -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);
@@ -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}
@@ -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);
@@ -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
@@ -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
@@ -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();
}
}
@@ -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;
}
}
@@ -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()));
}
@@ -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());
@@ -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
*
@@ -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);
}
}
});
}
/**
@@ -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;
@@ -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)
@@ -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);
@@ -233,4 +233,5 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages {
String clientCurrentlyLockedForUserCreation();
String unlockedSuccessfully();
String failedToUnlock();
String optOutOfFeatureAndCommunityEmails();
}
@@ -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
@@ -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
@@ -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,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;
}
@@ -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
@@ -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),
@@ -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());
}
@@ -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
@@ -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);
}
@@ -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,
@@ -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());
@@ -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;
@@ -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));
}
@@ -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>
View File
+1 -1
View File
@@ -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.
![Image not found](https://static.sapsailing.com/management-console/login/Login%20-%20Mobile.png)
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