diff --git a/component/api/pom.xml b/component/api/pom.xml index aebe76911ca..c3f3acf9629 100644 --- a/component/api/pom.xml +++ b/component/api/pom.xml @@ -57,11 +57,6 @@ org.exoplatform.commons commons-comet-service - org.exoplatform.commons diff --git a/component/notification/pom.xml b/component/notification/pom.xml index e97755537c9..cecd7e521cb 100644 --- a/component/notification/pom.xml +++ b/component/notification/pom.xml @@ -25,7 +25,6 @@ org.exoplatform.social 6.5.x-SNAPSHOT - org.exoplatform.social social-component-notification eXo PLF:: Social Notification Component eXo Social Notification Component @@ -58,9 +57,11 @@ org.apache.maven.plugins maven-surefire-plugin + alphabetical **/InitContainerTestSuite.java **/InitContainerWithSettingsTestSuite.java + **/InitContainerTestSuiteRest.java diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/NotificationSettingsRestService.java b/component/notification/src/main/java/io/meeds/social/notification/rest/NotificationSettingsRestService.java new file mode 100644 index 00000000000..bc28d8dc966 --- /dev/null +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/NotificationSettingsRestService.java @@ -0,0 +1,554 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; + +import org.exoplatform.commons.api.notification.channel.AbstractChannel; +import org.exoplatform.commons.api.notification.channel.ChannelManager; +import org.exoplatform.commons.api.notification.model.*; +import org.exoplatform.commons.api.notification.model.UserSetting.FREQUENCY; +import org.exoplatform.commons.api.notification.plugin.config.PluginConfig; +import io.meeds.social.notification.rest.model.ChannelActivationChoice; +import io.meeds.social.notification.rest.model.EmailDigestChoice; +import io.meeds.social.notification.rest.model.UserNotificationSettings; +import org.exoplatform.commons.api.notification.service.setting.PluginSettingService; +import org.exoplatform.commons.api.notification.service.setting.UserSettingService; +import org.exoplatform.portal.application.localization.LocalizationFilter; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.services.resources.ResourceBundleService; +import org.exoplatform.services.rest.resource.ResourceContainer; +import org.exoplatform.services.security.ConversationState; +import io.swagger.v3.oas.annotations.tags.Tag; + +import org.exoplatform.services.rest.http.PATCH; + +@Path("notifications/settings") +@Tag(name = "notifications/settings", description = "Managing users notifications settings") +public class NotificationSettingsRestService implements ResourceContainer { + + private static final String NOTIFICATION_LABEL_CHANNEL_DEFAULT = "UINotification.label.channel.default"; + + private static final String MAIN_RESOURCE_BUNDLE_NAME = "locale.portlet.UserNotificationPortlet"; + + private static final Log LOG = ExoLogger.getLogger(NotificationSettingsRestService.class); + + private static final String DAILY = "Daily"; + + private static final String WEEKLY = "Weekly"; + + private static final String NEVER = "Never"; + + private ResourceBundleService resourceBundleService; + + private PluginSettingService pluginSettingService; + + private ChannelManager channelManager; + + private UserSettingService userSettingService; + + private UserACL userACL; + + public NotificationSettingsRestService(ResourceBundleService resourceBundleService, + PluginSettingService pluginSettingService, + ChannelManager channelManager, + UserSettingService userSettingService, + UserACL userACL) { + this.resourceBundleService = resourceBundleService; + this.pluginSettingService = pluginSettingService; + this.channelManager = channelManager; + this.userSettingService = userSettingService; + this.userACL = userACL; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed("administrators") + @Operation( + summary = "Get default notification settings", + description = "Get default notification settings", + method = "GET") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + } + ) + public Response getSettings() { + UserSetting setting = userSettingService.getDefaultSettings(); + UserNotificationSettings notificationSettings = mapToRestModel(setting, false); + notificationSettings.setSenderName(pluginSettingService.getEmailSenderName()); + notificationSettings.setSenderEmail(pluginSettingService.getEmailSenderEmail()); + return Response.ok(notificationSettings).build(); + } + + @PATCH + @RolesAllowed("administrators") + @Operation( + summary = "Change enablement status of Channel for all users", + description = "Change enablement status of Channel for all users", + method = "PATCH" + ) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "Request fulfilled") + } + ) + public Response saveEmailSender( + @Parameter(description = "Company name", required = true) + @FormParam("name") + String name, + @Parameter(description = "Company email", required = true) + @FormParam("email") + String email) { + try { + pluginSettingService.saveEmailSender(name, email); + return Response.noContent().build(); + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); + } + } + + @GET + @Path("{id}") + @Produces(MediaType.APPLICATION_JSON) + @RolesAllowed("users") + @Operation( + summary = "Gets all notification settings of a user", + description = "Gets all notification settings of a user", + method = "GET") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "500", description = "Internal server error") + } + ) + public Response getSettings( + @Parameter( + description = "User name that will be used to retrieve its settings. " + + "If current user is and administrator, it will be able to retrieve settings of all users", + required = true + ) @PathParam("id") String username) { + boolean isAdmin = userACL.isSuperUser() || userACL.isUserInGroup(userACL.getAdminGroups()); + boolean isSameUser = ConversationState.getCurrent().getIdentity().getUserId().equals(username); + if (!isAdmin && !isSameUser) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + UserSetting setting = userSettingService.get(username); + UserNotificationSettings notificationSettings = mapToRestModel(setting, true); + return Response.ok(notificationSettings).build(); + } + + @PATCH + @Path("plugin/{pluginId}") + @RolesAllowed("administrators") + @Operation( + summary = "Change enablement status of Channel for all users", + description = "Change enablement status of Channel for all users", + method = "PATCH" + ) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "Request fulfilled") + } + ) + public Response savePluginSetting( + @Parameter(description = "Notification plugin Id", required = true) + @PathParam("pluginId") + String pluginId, + @Parameter(description = "Notification digest to use for corresponding plugin Id", required = true) + @FormParam("channels") + String channels) { + String[] channelsArray = StringUtils.split(channels, ','); + Map channelsStatus = + Arrays.stream(channelsArray) + .collect(Collectors.toMap(channelStatus -> StringUtils.split(channelStatus, "=")[0], + channelStatus -> StringUtils.split(channelStatus, "=")[1])); + for (Map.Entry channelByStatus : channelsStatus.entrySet()) { + String channelId = channelByStatus.getKey(); + boolean channelActive = Boolean.parseBoolean(channelByStatus.getValue()); + pluginSettingService.saveActivePlugin(channelId, pluginId, channelActive); + } + return Response.noContent().build(); + } + + @PATCH + @Path("{id}/plugin/{pluginId}") + @RolesAllowed("users") + @Operation( + summary = "Change enablement status of Channel for a user", + description = "Change enablement status of Channel for a user", + method = "PATCH") + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "Request fulfilled"), + @ApiResponse(responseCode = "500", description = "Internal server error") + } + ) + public Response savePluginSetting( + @Parameter( + description = "User name that will be used to save its settings.", + required = true + ) @PathParam("id") String username, + @Parameter( + description = "Notification plugin Id", + required = true + ) @PathParam("pluginId") String pluginId, + @Parameter( + description = "Notification digest to use for corresponding plugin Id", + required = true + ) @FormParam("channels") String channels, + @Parameter( + description = "Notification digest to use for corresponding plugin Id", + required = true + ) @FormParam("digest") String digest) { + + boolean isAdmin = userACL.isSuperUser() || userACL.isUserInGroup(userACL.getAdminGroups()); + boolean isSameUser = ConversationState.getCurrent().getIdentity().getUserId().equals(username); + if (!isAdmin && !isSameUser) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + try { + UserSetting setting = userSettingService.get(username); + + // digest + if (WEEKLY.equals(digest)) { + setting.addPlugin(pluginId, FREQUENCY.WEEKLY); + } else if (DAILY.equals(digest)) { + setting.addPlugin(pluginId, FREQUENCY.DAILY); + } else { + setting.removePlugin(pluginId, FREQUENCY.WEEKLY); + setting.removePlugin(pluginId, FREQUENCY.DAILY); + } + + // channels + String[] channelsArray = StringUtils.split(channels, ','); + Map channelsStatus = Arrays.stream(channelsArray) + .collect(Collectors.toMap(channelStatus -> StringUtils.split(channelStatus, + "=")[0], + channelStatus -> StringUtils.split(channelStatus, + "=")[1])); + for (Map.Entry channelByStatus : channelsStatus.entrySet()) { + if (Boolean.parseBoolean(channelByStatus.getValue())) { + setting.addChannelPlugin(channelByStatus.getKey(), pluginId); + } else { + setting.removeChannelPlugin(channelByStatus.getKey(), pluginId); + } + } + userSettingService.save(setting); + } catch (Exception e) { + return Response.serverError().entity("Exception in switching state of plugin " + pluginId + ". " + e.toString()).build(); + } + return Response.noContent().build(); + } + + @PATCH + @Path("channel/{channelId}") + @RolesAllowed("administrators") + @Operation( + summary = "Change enablement status of Channel for all users", + description = "Change enablement status of Channel for all users", + method = "PATCH" + ) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "Request fulfilled") + } + ) + public Response saveChannelStatus( + @Parameter(description = "Channel Id like MAIL_CHANNEL, WEB_CHANNEL...", required = true) + @PathParam("channelId") + String channelId, + @Parameter(description = "Enable/disable a channel", required = true) + @FormParam("enable") + boolean enable) { + pluginSettingService.saveChannelStatus(channelId, enable); + return Response.noContent().build(); + } + + @PATCH + @Path("{id}/channel/{channelId}") + @RolesAllowed("users") + @Operation( + summary = "Change enablement status of Channel for a user", + description = "Change enablement status of Channel for a user", + method = "PATCH") + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "Request fulfilled"), + @ApiResponse(responseCode = "500", description = "Internal server error") + } + ) + public Response saveActiveStatus( + @Parameter( + description = "User name that will be used to save its settings.", + required = true + ) @PathParam("id") String username, + @Parameter( + description = "Channel Id like MAIL_CHANNEL, WEB_CHANNEL...", + required = true + ) @PathParam("channelId") String channelId, + @Parameter( + description = "Enable/disable a channel", + required = true + ) @FormParam("enable") boolean enable) { + + boolean isAdmin = userACL.isSuperUser() || userACL.isUserInGroup(userACL.getAdminGroups()); + boolean isSameUser = ConversationState.getCurrent().getIdentity().getUserId().equals(username); + if (!isAdmin && !isSameUser) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + try { + UserSetting setting = userSettingService.get(username); + if (enable) { + setting.setChannelActive(channelId); + } else { + setting.removeChannelActive(channelId); + } + userSettingService.save(setting); + } catch (Exception e) { + return Response.serverError().entity("Exception in switching state of provider " + channelId + ". " + e.toString()).build(); + } + return Response.noContent().build(); + } + + private Map buildDigestDescriptions(Context context) { + Map options = new HashMap<>(); + options.put(DAILY, context.appRes("UINotification.description.Daily")); + options.put(WEEKLY, context.appRes("UINotification.description.Weekly")); + return options; + } + + private Map buildDigestLabels(Context context) { + Map options = new HashMap<>(); + options.put(DAILY, context.appRes("UINotification.label.Daily")); + options.put(WEEKLY, context.appRes("UINotification.label.Weekly")); + options.put(NEVER, context.appRes("UINotification.label.Never")); + return options; + } + + private List getChannels(boolean activeChannelsOnly) { + List channels = new ArrayList<>(); + for (AbstractChannel channel : channelManager.getChannels()) { + String channelId = channel.getId(); + if (!activeChannelsOnly || pluginSettingService.isChannelActive(channelId)) { + channels.add(channelId); + } + } + return channels; + } + + private Map computeChannelStatuses(UserSetting setting, List channels) { + Map channelStatus = new HashMap<>(); + for (String channelId : channels) { + channelStatus.put(channelId, setting != null && setting.isChannelGloballyActive(channelId)); + } + return channelStatus; + } + + private boolean computeChoices(UserSetting userSetting, // NOSONAR + List channels, + List groups, + Map channelStatus, + List emailDigestChoices, + List channelCheckBoxList) { + boolean hasActivePlugin = false; + for (GroupProvider groupProvider : groups) { + for (PluginInfo pluginInfo : groupProvider.getPluginInfos()) { + String pluginId = pluginInfo.getType(); + for (String channelId : channels) { + hasActivePlugin = true; + boolean isChannelActive = channelStatus.get(channelId) + && pluginSettingService.isActive(channelId, pluginId) + && userSetting.isChannelGloballyActive(channelId); + channelCheckBoxList.add(new ChannelActivationChoice(channelId, + pluginId, + pluginSettingService.isAllowed(channelId, pluginId), + isChannelActive && userSetting.isActive(channelId, pluginId), + isChannelActive)); + if (UserSetting.EMAIL_CHANNEL.equals(channelId)) { + emailDigestChoices.add(new EmailDigestChoice(channelId, + pluginId, + getValue(userSetting, pluginId), + isChannelActive)); + } + } + } + } + return hasActivePlugin; + } + + private String getValue(UserSetting setting, String pluginId) { + if (setting != null && setting.isInWeekly(pluginId)) { + return WEEKLY; + } else if (setting != null && setting.isInDaily(pluginId)) { + return DAILY; + } else { + return NEVER; + } + } + + private UserNotificationSettings mapToRestModel(UserSetting setting, boolean activeChannelsOnly) { + Locale userLocale = LocalizationFilter.getCurrentLocale(); + if (userLocale == null) { + userLocale = Locale.ENGLISH; + } + + String[] sharedResourceBundles = resourceBundleService.getSharedResourceBundleNames(); + String[] resourceBundles = ArrayUtils.insert(0, sharedResourceBundles, MAIN_RESOURCE_BUNDLE_NAME); + ResourceBundle resourceBundle = resourceBundleService.getResourceBundle(resourceBundles, userLocale); + Context context = new Context(resourceBundle, userLocale); + + List channels = getChannels(activeChannelsOnly); + Map channelStatus = computeChannelStatuses(setting, channels); + List groups = pluginSettingService.getGroupPlugins(); + Map groupsLabels = groups.stream() + .collect(Collectors.toMap(GroupProvider::getGroupId, + group -> context.pluginRes(group.getResourceBundleKey(), + group.getGroupId()))); + Map pluginLabels = groups.stream() + .flatMap(group -> group.getPluginInfos().stream()) + .collect(Collectors.toMap(PluginInfo::getType, + plugin -> context.pluginRes("UINotification.title." + + plugin.getType(), plugin.getType()))); + Map channelLabels = channels.stream().collect(Collectors.toMap(Function.identity(), channel -> { + String channelKey = context.getChannelKey(channel); + String key = "UINotification.label.channel-" + channelKey; + if (resourceBundle != null && resourceBundle.containsKey(key)) { + return resourceBundle.getString(key); + } else if (resourceBundle != null && resourceBundle.containsKey(NOTIFICATION_LABEL_CHANNEL_DEFAULT)) { + return resourceBundle.getString(NOTIFICATION_LABEL_CHANNEL_DEFAULT).replace("{0}", channelKey); + } + return channelKey; + })); + Map channelDescriptions = channels.stream().collect(Collectors.toMap(Function.identity(), channel -> { + String channelKey = context.getChannelKey(channel); + String key = "UINotification.description.channel-" + channelKey; + if (resourceBundle != null && resourceBundle.containsKey(key)) { + return resourceBundle.getString(key); + } else if (resourceBundle != null && resourceBundle.containsKey("UINotification.description.channel.default")) { + return resourceBundle.getString("UINotification.description.channel.default").replace("{0}", channelKey); + } + return StringUtils.EMPTY; + })); + + Map digestLabels = buildDigestLabels(context); + Map digestDescriptions = buildDigestDescriptions(context); + List emailDigestChoices = new ArrayList<>(); + List channelCheckBoxList = new ArrayList<>(); + boolean hasActivePlugin = computeChoices(setting, channels, groups, channelStatus, emailDigestChoices, channelCheckBoxList); + + return new UserNotificationSettings(groups, + groupsLabels, + pluginLabels, + channelLabels, + channelDescriptions, + digestLabels, + digestDescriptions, + hasActivePlugin, + emailDigestChoices, + channelCheckBoxList, + channelStatus, + channels); + } + + public class Context { + ResourceBundle resourceBundle; + + Locale userLocale; + + public Context(ResourceBundle resourceBundle, Locale userLocale) { + this.resourceBundle = resourceBundle; + this.userLocale = userLocale; + } + + public String appRes(String key) { + try { + return resourceBundle.getString(key).replace("'", "'").replace("\"", """); + } catch (java.util.MissingResourceException e) { + if (key.indexOf("checkbox-") > -1) { + return appRes("UINotification.label.checkbox.default").replace("{0}", capitalizeFirstLetter(key.split("-")[1])); + } + if (key.indexOf("channel-") > -1) { + return appRes(NOTIFICATION_LABEL_CHANNEL_DEFAULT).replace("{0}", capitalizeFirstLetter(key.split("-")[1])); + } + } catch (Exception e) { + LOG.debug("Error when get resource bundle key " + key, e); + } + return capitalizeFirstLetter(key.substring(key.lastIndexOf('.') + 1)); + } + + private String getBundlePath(String id, String key) { // NOSONAR + PluginConfig pluginConfig = pluginSettingService.getPluginConfig(id); + if (pluginConfig != null) { + return pluginConfig.getBundlePath(); + } + // + List groups = pluginSettingService.getGroupPlugins(); + for (GroupProvider groupProvider : groups) { + List pluginInfos = groupProvider.getPluginInfos(); + if (groupProvider.getGroupId().equals(id) && pluginInfos != null && !pluginInfos.isEmpty()) { + for (PluginInfo pluginInfo : pluginInfos) { + if (StringUtils.isNotBlank(pluginInfo.getBundlePath())) { + ResourceBundle resBundle = resourceBundleService.getResourceBundle(pluginInfo.getBundlePath(), userLocale); + if (resBundle != null && resBundle.containsKey(key)) { + return pluginInfo.getBundlePath(); + } + } + } + } + } + + PluginConfig defaultPluginConfig = pluginSettingService.getPluginConfig("DigestDailyPlugin"); + return defaultPluginConfig == null ? null : defaultPluginConfig.getBundlePath(); + } + + public String pluginRes(String key, String id) { + String path = getBundlePath(id, key); + ResourceBundle pluginResourceBundle = StringUtils.isBlank(path) ? null + : resourceBundleService.getResourceBundle(path, userLocale); + return pluginResourceBundle != null && pluginResourceBundle.containsKey(key) ? pluginResourceBundle.getString(key) : id; + } + + public String getChannelKey(String channelId) { + return channelId.replace("_CHANNEL", "").toLowerCase(); + } + + public String capitalizeFirstLetter(String original) { + return original.length() <= 1 ? original : original.substring(0, 1).toUpperCase() + original.substring(1); + } + } + +} diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java b/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java new file mode 100644 index 00000000000..27976e5fdd4 --- /dev/null +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/WebNotificationRestService.java @@ -0,0 +1,166 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.exoplatform.services.rest.http.PATCH; +import java.util.List; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.exoplatform.commons.api.notification.model.NotificationInfo; +import org.json.JSONArray; +import org.json.JSONObject; + +import org.exoplatform.commons.api.notification.NotificationMessageUtils; +import org.exoplatform.commons.api.notification.model.WebNotificationFilter; +import org.exoplatform.commons.api.notification.service.WebNotificationService; +import org.exoplatform.services.log.ExoLogger; +import org.exoplatform.services.log.Log; +import org.exoplatform.services.rest.resource.ResourceContainer; +import org.exoplatform.services.security.ConversationState; + +/** + * Provides REST Services in order to perform all read/write operations related + * to web notifications. + */ + +@Path("notifications/webNotifications") +@Tag(name = "notifications/webNotifications", description = "Manage web notifications") +public class WebNotificationRestService implements ResourceContainer { + + private static final Log LOG = ExoLogger.getLogger(WebNotificationRestService.class); + + private WebNotificationService webNftService; + + public WebNotificationRestService(WebNotificationService webNftService) { + this.webNftService = webNftService; + } + + @GET + @RolesAllowed("users") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get notifications list", + description = "This gets the list of the notifications", + method = "GET") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Notifications list returned"), + @ApiResponse(responseCode = "404", description = "Notifications list not found"), + @ApiResponse(responseCode = "500", description = "Internal server error") }) + public Response getNotifications() { + int maxItemsInPopover = NotificationMessageUtils.getMaxItemsInPopover(); + JSONArray notificationsJsonArray = new JSONArray(); + JSONObject response = new JSONObject(); + String currentUser = ConversationState.getCurrent().getIdentity().getUserId(); + if (currentUser == null) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + List notifications = webNftService.get(new WebNotificationFilter(currentUser, true), 0, maxItemsInPopover); + for (String notification : notifications) { + JSONObject notificationJsonObject = new JSONObject(); + notificationJsonObject.put("notification", notification); + notificationsJsonArray.put(notificationJsonObject); + } + int badge = webNftService.getNumberOnBadge(currentUser); + response.put("notifications", notificationsJsonArray); + response.put("badge", badge); + return Response.ok(response.toString(), MediaType.APPLICATION_JSON).build(); + } + + @PATCH + @Path("{id}") + @Consumes(MediaType.TEXT_PLAIN) + @RolesAllowed("users") + @Operation( + summary = "Update notification", + description = "Perform some patch operations on notifications", + method = "PATCH") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Notification updated"), + @ApiResponse(responseCode = "400", description = "Invalid query input"), + @ApiResponse(responseCode = "500", description = "Internal server error") }) + public Response updateNotifications(@Parameter(description = "notification operation", required = true) String operation, + @Parameter(description = "notification id", required = true) @PathParam("id") String notificationId) { + + String currentUser = ConversationState.getCurrent().getIdentity().getUserId(); + try { + if (operation == null) { + LOG.warn("Notification operation should be not null"); + return Response.status(Response.Status.BAD_REQUEST).build(); + } + + if (currentUser == null) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + + if (operation.equals("markAsRead")) { + if (notificationId == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } else { + NotificationInfo notification = webNftService.getNotificationInfo(notificationId); + if (currentUser.equals(notification.getTo())) { + webNftService.markRead(notificationId); + } else { + LOG.warn("User {} is not allowed to mark notification {} as read", currentUser, notificationId); + return Response.status(Response.Status.UNAUTHORIZED).build(); + + } + } + } + + if (operation.equals("hide")) { + if (notificationId == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } else { + NotificationInfo notification = webNftService.getNotificationInfo(notificationId); + if (currentUser.equals(notification.getTo())) { + webNftService.hidePopover(notificationId); + } else { + LOG.warn("User {} is not allowed to hide notification {}", currentUser, notificationId); + return Response.status(Response.Status.UNAUTHORIZED).build(); + + } + } + } + + if (operation.equals("resetNew")) { + webNftService.resetNumberOnBadge(currentUser); + } + + if (operation.equals("markAllAsRead")) { + webNftService.markAllRead(currentUser); + webNftService.resetNumberOnBadge(currentUser); + } + return Response.noContent().build(); + } catch (Exception e) { + LOG.error("Error when trying to patch operation {} on notification {} for user {}", operation, notificationId, currentUser, e); + return Response.serverError().build(); + } + } +} diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/model/ChannelActivationChoice.java b/component/notification/src/main/java/io/meeds/social/notification/rest/model/ChannelActivationChoice.java new file mode 100644 index 00000000000..8d1d38e94f2 --- /dev/null +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/model/ChannelActivationChoice.java @@ -0,0 +1,84 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest.model; + +public class ChannelActivationChoice { + + private String channelId; + + private String pluginId; + + private boolean allowed; + + private boolean active; + + private boolean channelActive; + + public ChannelActivationChoice(String channelId, + String pluginId, + boolean allowed, + boolean active, + boolean channelActive) { + this.channelId = channelId; + this.pluginId = pluginId; + this.allowed = allowed; + this.active = active; + this.channelActive = channelActive; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + public boolean isChannelActive() { + return channelActive; + } + + public void setChannelActive(boolean channelActive) { + this.channelActive = channelActive; + } + +} diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/model/EmailDigestChoice.java b/component/notification/src/main/java/io/meeds/social/notification/rest/model/EmailDigestChoice.java new file mode 100644 index 00000000000..0d1ee396189 --- /dev/null +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/model/EmailDigestChoice.java @@ -0,0 +1,71 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest.model; + +public class EmailDigestChoice { + String channelId; + + String pluginId; + + String value; + + boolean channelActive; + + public EmailDigestChoice(String channelId, + String pluginId, + String value, + boolean channelActive) { + this.channelId = channelId; + this.pluginId = pluginId; + this.value = value; + this.channelActive = channelActive; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } + + public String getPluginId() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public boolean isChannelActive() { + return channelActive; + } + + public void setChannelActive(boolean channelActive) { + this.channelActive = channelActive; + } + +} diff --git a/component/notification/src/main/java/io/meeds/social/notification/rest/model/UserNotificationSettings.java b/component/notification/src/main/java/io/meeds/social/notification/rest/model/UserNotificationSettings.java new file mode 100644 index 00000000000..c7ca6ed6c18 --- /dev/null +++ b/component/notification/src/main/java/io/meeds/social/notification/rest/model/UserNotificationSettings.java @@ -0,0 +1,194 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest.model; + +import java.util.List; +import java.util.Map; + +import org.exoplatform.commons.api.notification.model.GroupProvider; +import org.exoplatform.commons.api.notification.model.UserSetting; + +import lombok.Getter; +import lombok.Setter; + +public class UserNotificationSettings { + private List groups; + + private Map groupsLabels; + + private Map pluginLabels; + + private Map channelLabels; + + private Map channelDescriptions; + + private Map digestLabels; + + private Map digestDescriptions; + + private boolean hasActivePlugin = false; + + private List emailDigestChoices = null; + + private List channelCheckBoxList = null; + + private Map channelStatus; + + private List channels; + + private String emailChannel = UserSetting.EMAIL_CHANNEL; + + @Getter + @Setter + private String senderEmail; + + @Getter + @Setter + private String senderName; + + public UserNotificationSettings(List groups, + Map groupsLabels, + Map pluginLabels, + Map channelLabels, + Map channelDescriptions, + Map digestLabels, + Map digestDescriptions, + boolean hasActivePlugin, + List emailDigestChoices, + List channelCheckBoxList, + Map channelStatus, + List channels) { + this.groups = groups; + this.groupsLabels = groupsLabels; + this.pluginLabels = pluginLabels; + this.channelLabels = channelLabels; + this.channelDescriptions = channelDescriptions; + this.digestLabels = digestLabels; + this.digestDescriptions = digestDescriptions; + this.hasActivePlugin = hasActivePlugin; + this.emailDigestChoices = emailDigestChoices; + this.channelCheckBoxList = channelCheckBoxList; + this.channelStatus = channelStatus; + this.channels = channels; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + public Map getChannelLabels() { + return channelLabels; + } + + public void setChannelLabels(Map channelLabels) { + this.channelLabels = channelLabels; + } + + public Map getChannelDescriptions() { + return channelDescriptions; + } + + public void setChannelDescriptions(Map channelDescriptions) { + this.channelDescriptions = channelDescriptions; + } + + public Map getDigestLabels() { + return digestLabels; + } + + public void setDigestLabels(Map digestLabels) { + this.digestLabels = digestLabels; + } + + public Map getDigestDescriptions() { + return digestDescriptions; + } + + public void setDigestDescriptions(Map digestDescriptions) { + this.digestDescriptions = digestDescriptions; + } + + public boolean isHasActivePlugin() { + return hasActivePlugin; + } + + public void setHasActivePlugin(boolean hasActivePlugin) { + this.hasActivePlugin = hasActivePlugin; + } + + public List getEmailDigestChoices() { + return emailDigestChoices; + } + + public void setEmailDigestChoices(List emailDigestChoices) { + this.emailDigestChoices = emailDigestChoices; + } + + public Map getGroupsLabels() { + return groupsLabels; + } + + public void setGroupsLabels(Map groupsLabels) { + this.groupsLabels = groupsLabels; + } + + public Map getPluginLabels() { + return pluginLabels; + } + + public void setPluginLabels(Map pluginLabels) { + this.pluginLabels = pluginLabels; + } + + public List getChannelCheckBoxList() { + return channelCheckBoxList; + } + + public void setChannelCheckBoxList(List channelCheckBoxList) { + this.channelCheckBoxList = channelCheckBoxList; + } + + public Map getChannelStatus() { + return channelStatus; + } + + public void setChannelStatus(Map channelStatus) { + this.channelStatus = channelStatus; + } + + public List getChannels() { + return channels; + } + + public void setChannels(List channels) { + this.channels = channels; + } + + public String getEmailChannel() { + return emailChannel; + } + + public void setEmailChannel(String emailChannel) { + this.emailChannel = emailChannel; + } + +} diff --git a/component/notification/src/main/java/org/exoplatform/social/notification/plugin/NewUserPlugin.java b/component/notification/src/main/java/org/exoplatform/social/notification/plugin/NewUserPlugin.java index 40cd8e92b48..5f6b8b61569 100644 --- a/component/notification/src/main/java/org/exoplatform/social/notification/plugin/NewUserPlugin.java +++ b/component/notification/src/main/java/org/exoplatform/social/notification/plugin/NewUserPlugin.java @@ -19,8 +19,6 @@ import org.exoplatform.commons.api.notification.NotificationContext; import org.exoplatform.commons.api.notification.model.NotificationInfo; import org.exoplatform.commons.api.notification.plugin.BaseNotificationPlugin; -import org.exoplatform.commons.api.notification.service.setting.UserSettingService; -import org.exoplatform.commons.utils.CommonsUtils; import org.exoplatform.container.xml.InitParams; import org.exoplatform.social.core.identity.model.Profile; @@ -41,11 +39,6 @@ public NotificationInfo makeNotification(NotificationContext ctx) { Profile profile = ctx.value(SocialNotificationUtils.PROFILE); String remoteId = profile.getIdentity().getRemoteId(); try { - UserSettingService userSettingService = CommonsUtils.getService(UserSettingService.class); - // - userSettingService.initDefaultSettings(remoteId); - // - return NotificationInfo.instance() .key(getId()) .with(SocialNotificationUtils.REMOTE_ID.getKey(), remoteId) @@ -53,6 +46,7 @@ public NotificationInfo makeNotification(NotificationContext ctx) { .setFrom(remoteId) .end(); } catch (Exception e) { + ctx.setException(e); return null; } } diff --git a/component/notification/src/test/java/io/meeds/social/notification/rest/NotificationSettingsRestServiceTest.java b/component/notification/src/test/java/io/meeds/social/notification/rest/NotificationSettingsRestServiceTest.java new file mode 100644 index 00000000000..76fba7611cf --- /dev/null +++ b/component/notification/src/test/java/io/meeds/social/notification/rest/NotificationSettingsRestServiceTest.java @@ -0,0 +1,356 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.MultivaluedMap; + +import org.mockito.ArgumentMatcher; + +import org.exoplatform.commons.api.notification.NotificationContext; +import org.exoplatform.commons.api.notification.channel.AbstractChannel; +import org.exoplatform.commons.api.notification.channel.ChannelManager; +import org.exoplatform.commons.api.notification.channel.template.AbstractTemplateBuilder; +import org.exoplatform.commons.api.notification.channel.template.TemplateProvider; +import org.exoplatform.commons.api.notification.model.ChannelKey; +import org.exoplatform.commons.api.notification.model.GroupProvider; +import org.exoplatform.commons.api.notification.model.PluginInfo; +import org.exoplatform.commons.api.notification.model.PluginKey; +import org.exoplatform.commons.api.notification.model.UserSetting; +import org.exoplatform.commons.api.notification.plugin.config.PluginConfig; +import io.meeds.social.notification.rest.model.UserNotificationSettings; +import org.exoplatform.commons.api.notification.service.setting.PluginSettingService; +import org.exoplatform.commons.api.notification.service.setting.UserSettingService; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.services.rest.impl.ContainerResponse; +import org.exoplatform.services.rest.impl.EnvironmentContext; +import org.exoplatform.services.rest.impl.MultivaluedMapImpl; +import org.exoplatform.services.security.ConversationState; +import org.exoplatform.services.security.Identity; +import org.exoplatform.services.test.mock.MockHttpServletRequest; +import org.exoplatform.settings.jpa.JPAUserSettingServiceImpl; +import org.exoplatform.social.service.rest.BaseRestServicesTestCase; + +public class NotificationSettingsRestServiceTest extends BaseRestServicesTestCase { // NOSONAR + + private static final String USER_1 = "testuser1"; + + private static final String USER_2 = "testuser2"; + + private static final String CHANNEL_ID = "channelId"; + + private static final String GROUP_PROVIDER_ID = "groupId"; + + private static final String PLUGIN_ID = "pluginId"; + + private static final PluginInfo PLUGIN_PROVIDER = new PluginInfo(); + + private static final PluginConfig PLUGIN_CONFIG = new PluginConfig(); + static { + PLUGIN_PROVIDER.setType(PLUGIN_ID); + PLUGIN_CONFIG.setPluginId(PLUGIN_ID); + PLUGIN_CONFIG.setGroupId(GROUP_PROVIDER_ID); + } + + private static final List PLUGINS = Collections.singletonList(PLUGIN_PROVIDER); + + private static final GroupProvider GROUP_PROVIDER = new GroupProvider(GROUP_PROVIDER_ID); + static { + GROUP_PROVIDER.setPluginInfos(PLUGINS); + } + + private static final List GROUPS = Collections.singletonList(GROUP_PROVIDER); + + private static final List EMPTY_GROUP_PROVIDER = + Collections.singletonList(new GroupProvider(GROUP_PROVIDER_ID)); + + private static final List CHANNELS = Collections.singletonList(newChannel()); + + private ChannelManager channelManager; + + private UserSettingService userSettingService; + + private UserACL userACL; + + private PluginSettingService pluginSettingService; + + @Override + protected Class getComponentClass() { + return NotificationSettingsRestService.class; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + pluginSettingService = mock(PluginSettingService.class); + channelManager = mock(ChannelManager.class); + userSettingService = mock(UserSettingService.class); + userACL = mock(UserACL.class); + + when(userACL.isSuperUser()).thenReturn(false); + when(userACL.getAdminGroups()).thenReturn("admins"); + + when(channelManager.getChannels()).thenReturn(CHANNELS); + + when(pluginSettingService.getGroupPlugins()).thenReturn(GROUPS); + when(pluginSettingService.getPluginConfig(eq(PLUGIN_ID))).thenReturn(PLUGIN_CONFIG); + when(pluginSettingService.isChannelActive(CHANNEL_ID)).thenReturn(true); + + UserSetting userSetting = new UserSetting(); + userSetting.setChannelActive(CHANNEL_ID); + userSetting.setEnabled(true); + userSetting.setUserId(USER_1); + userSetting.setChannelPlugins(CHANNEL_ID, Collections.singletonList(PLUGIN_ID)); + when(userSettingService.get(eq(USER_1))).thenReturn(userSetting); + + getContainer().unregisterComponent(PluginSettingService.class); + getContainer().unregisterComponent(ChannelManager.class); + getContainer().unregisterComponent(UserSettingService.class); + getContainer().unregisterComponent(JPAUserSettingServiceImpl.class); + getContainer().unregisterComponent(UserACL.class); + + getContainer().registerComponentInstance(PluginSettingService.class.getName(), pluginSettingService); + getContainer().registerComponentInstance(ChannelManager.class.getName(), channelManager); + getContainer().registerComponentInstance(UserSettingService.class.getName(), userSettingService); + getContainer().registerComponentInstance(UserACL.class.getName(), userACL); + } + + @Override + public void tearDown() throws Exception { + getContainer().unregisterComponent(PluginSettingService.class.getName()); + getContainer().unregisterComponent(ChannelManager.class.getName()); + getContainer().unregisterComponent(UserSettingService.class.getName()); + getContainer().unregisterComponent(UserACL.class.getName()); + super.tearDown(); + } + + public void testUnauthorizedNotSameUserGetSettings() throws Exception { + // Given + String path = getPath(USER_1, ""); + MockHttpServletRequest httpRequest = new MockHttpServletRequest(path, + null, + 0, + "GET", + null); + + EnvironmentContext envctx = new EnvironmentContext(); + envctx.put(HttpServletRequest.class, httpRequest); + + startSessionAs(USER_2); + + // When + ContainerResponse resp = launcher.service("GET", + path, + "", + null, + null, + envctx); + + // Then + assertEquals(String.valueOf(resp.getEntity()), 401, resp.getStatus()); // NOSONAR + } + + public void testAdminGetSettings() throws Exception { + // Given + String path = getPath(USER_1, ""); + MockHttpServletRequest httpRequest = new MockHttpServletRequest(path, + null, + 0, + "GET", + null); + + EnvironmentContext envctx = new EnvironmentContext(); + envctx.put(HttpServletRequest.class, httpRequest); + + startSessionAs(USER_2); + when(userACL.isUserInGroup(eq("admins"))).thenReturn(true); + + // When + ContainerResponse resp = launcher.service("GET", + path, + "", + null, + null, + envctx); + + // Then + assertEquals(String.valueOf(resp.getEntity()), 200, resp.getStatus()); + UserNotificationSettings notificationSettings = (UserNotificationSettings) resp.getEntity(); + assertNotNull(notificationSettings); + } + + public void testGetSettingsSameUser() throws Exception { + // Given + String path = getPath(USER_1, ""); + MockHttpServletRequest httpRequest = new MockHttpServletRequest(path, + null, + 0, + "GET", + null); + + EnvironmentContext envctx = new EnvironmentContext(); + envctx.put(HttpServletRequest.class, httpRequest); + + startSessionAs(USER_1); + + // When + ContainerResponse resp = launcher.service("GET", + path, + "", + null, + null, + envctx); + + // Then + assertEquals(String.valueOf(resp.getEntity()), 200, resp.getStatus()); + UserNotificationSettings notificationSettings = (UserNotificationSettings) resp.getEntity(); + assertNotNull(notificationSettings); + assertNotNull(notificationSettings.getChannels()); + assertEquals(1, notificationSettings.getChannels().size()); + assertNotNull(notificationSettings.getGroups()); + assertEquals(1, notificationSettings.getGroups().size()); + assertNotNull(notificationSettings.getGroups().get(0)); + assertEquals(GROUP_PROVIDER_ID, notificationSettings.getGroups().get(0).getGroupId()); + assertNotNull(notificationSettings.getGroups().get(0).getGroupId()); + assertNotNull(notificationSettings.getChannelStatus()); + assertEquals(1, notificationSettings.getChannelStatus().size()); + assertTrue(notificationSettings.getChannelStatus().get(CHANNEL_ID)); + } + + public void testGetSettingsSameUserEmptyPlugins() throws Exception { + // Given + when(pluginSettingService.getGroupPlugins()).thenReturn(EMPTY_GROUP_PROVIDER); + + // Ensure no error is raised + testGetSettingsSameUser(); + } + + public void testSaveDisableChannel() throws Exception { + // Given + String path = getPath(USER_1, "channel/" + CHANNEL_ID); + MockHttpServletRequest httpRequest = new MockHttpServletRequest(path, + null, + 0, + "PATCH", + null); + + EnvironmentContext envctx = new EnvironmentContext(); + envctx.put(HttpServletRequest.class, httpRequest); + + startSessionAs(USER_1); + + // When + ContainerResponse resp = launcher.service("PATCH", + path, + "", + getFormHeaders(), + ("enable=false").getBytes(), + envctx); + + // Then + assertEquals(String.valueOf(resp.getEntity()), 204, resp.getStatus()); + verify(userSettingService, times(1)).save(any()); + } + + public void testSaveSettings() throws Exception { // NOSONAR + // Given + String path = getPath(USER_1, "plugin/" + PLUGIN_ID); + MockHttpServletRequest httpRequest = new MockHttpServletRequest(path, + null, + 0, + "PATCH", + null); + + EnvironmentContext envctx = new EnvironmentContext(); + envctx.put(HttpServletRequest.class, httpRequest); + + startSessionAs(USER_1); + + // When + ContainerResponse resp = launcher.service("PATCH", + path, + "", + getFormHeaders(), + ("channels=" + CHANNEL_ID + "=true&digest=Weekly").getBytes(), + envctx); + + // Then + assertEquals(String.valueOf(resp.getEntity()), 204, resp.getStatus()); + verify(userSettingService, times(1)).save(argThat(userSetting -> + userSetting.isActive(CHANNEL_ID, PLUGIN_ID) && userSetting.isChannelGloballyActive(CHANNEL_ID) + && userSetting.isInWeekly(PLUGIN_ID))); + } + + private MultivaluedMap getFormHeaders() { + MultivaluedMap headers = new MultivaluedMapImpl(); + headers.putSingle("Content-Type", "application/x-www-form-urlencoded"); + return headers; + } + + private String getPath(String username, String prefix) { + return "/notifications/settings/" + username + "/" + prefix; + } + + private void startSessionAs(String username) { + Identity identity = new Identity(username); + ConversationState state = new ConversationState(identity); + ConversationState.setCurrent(state); + } + + private static AbstractChannel newChannel() { + return new AbstractChannel() { + + @Override + public void registerTemplateProvider(TemplateProvider provider) { + throw new UnsupportedOperationException(); + } + + @Override + protected AbstractTemplateBuilder getTemplateBuilderInChannel(PluginKey key) { + throw new UnsupportedOperationException(); + } + + @Override + public ChannelKey getKey() { + return ChannelKey.key(CHANNEL_ID); + } + + @Override + public String getId() { + return CHANNEL_ID; + } + + @Override + public void dispatch(NotificationContext ctx, String userId) { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java b/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java new file mode 100644 index 00000000000..6e97fdb1842 --- /dev/null +++ b/component/notification/src/test/java/io/meeds/social/notification/rest/WebNotificationRestServiceTest.java @@ -0,0 +1,110 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.social.notification.rest; + +import org.exoplatform.commons.api.notification.model.NotificationInfo; +import org.exoplatform.commons.api.notification.service.WebNotificationService; +import org.exoplatform.services.security.ConversationState; +import org.exoplatform.services.security.Identity; +import org.exoplatform.social.service.rest.BaseRestServicesTestCase; + +import javax.ws.rs.core.Response; + +import org.mockito.Mock; +import static org.mockito.Mockito.*; + +public class WebNotificationRestServiceTest extends BaseRestServicesTestCase { // NOSONAR + + @Mock + WebNotificationService webNotificationService; + + @Override + protected Class getComponentClass() { + return WebNotificationRestService.class; + } + + public void testUnauthorizedMarkAsRead() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("mary"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = new WebNotificationRestService(webNotificationService); + Response response = webNotificationRestService.updateNotifications("markAsRead", "1"); + + assertEquals(401, response.getStatus()); // NOSONAR + } + + public void testAuthorizedMarkAsRead() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("john"); + + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = new WebNotificationRestService(webNotificationService); + Response response = webNotificationRestService.updateNotifications("markAsRead", "1"); + + assertEquals(204, response.getStatus()); + + } + + public void testUnauthorizedHide() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("mary"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = new WebNotificationRestService(webNotificationService); + Response response = webNotificationRestService.updateNotifications("hide", "1"); + + assertEquals(response.getStatus(), 401); + + } + + public void testAuthorizedHide() { + startSessionAs("john"); + + webNotificationService = mock(WebNotificationService.class); + + NotificationInfo notificationInfo = new NotificationInfo(); + notificationInfo.setTo("john"); + when(webNotificationService.getNotificationInfo(anyString())).thenReturn(notificationInfo); + + WebNotificationRestService webNotificationRestService = new WebNotificationRestService(webNotificationService); + Response response = webNotificationRestService.updateNotifications("hide", "1"); + + assertEquals(204, response.getStatus()); + + } + + private void startSessionAs(String username) { + Identity identity = new Identity(username); + ConversationState state = new ConversationState(identity); + ConversationState.setCurrent(state); + } + +} diff --git a/component/notification/src/test/java/org/exoplatform/social/notification/InitContainerTestSuiteRest.java b/component/notification/src/test/java/org/exoplatform/social/notification/InitContainerTestSuiteRest.java new file mode 100644 index 00000000000..14d3d46c835 --- /dev/null +++ b/component/notification/src/test/java/org/exoplatform/social/notification/InitContainerTestSuiteRest.java @@ -0,0 +1,50 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.exoplatform.social.notification; + +import org.exoplatform.commons.testing.BaseExoContainerTestSuite; +import org.exoplatform.commons.testing.ConfigTestCase; + +import io.meeds.social.notification.rest.NotificationSettingsRestServiceTest; +import io.meeds.social.notification.rest.WebNotificationRestServiceTest; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +@RunWith(Suite.class) +@SuiteClasses({ + WebNotificationRestServiceTest.class, + NotificationSettingsRestServiceTest.class, +}) +@ConfigTestCase(AbstractCoreTest.class) +public class InitContainerTestSuiteRest extends BaseExoContainerTestSuite { + + @BeforeClass + public static void setUp() throws Exception { + initConfiguration(InitContainerTestSuiteRest.class); + beforeSetup(); + } + + @AfterClass + public static void tearDown() { + afterTearDown(); + } +} diff --git a/extension/war/src/main/webapp/WEB-INF/conf/portal/group/platform/administrators/pages.xml b/extension/war/src/main/webapp/WEB-INF/conf/portal/group/platform/administrators/pages.xml index df223ceff1d..1db9f606baf 100644 --- a/extension/war/src/main/webapp/WEB-INF/conf/portal/group/platform/administrators/pages.xml +++ b/extension/war/src/main/webapp/WEB-INF/conf/portal/group/platform/administrators/pages.xml @@ -115,6 +115,7 @@ + notification Notifications Administration @@ -124,9 +125,8 @@ *:/platform/administrators - commons-extension - NotificationsAdminJuzuPortlet - + social-portlet + NotificationAdministration Notifications Administration manager:/platform/administrators @@ -136,4 +136,5 @@ + diff --git a/extension/war/src/main/webapp/WEB-INF/conf/social-extension/social/notification-configuration.xml b/extension/war/src/main/webapp/WEB-INF/conf/social-extension/social/notification-configuration.xml index 044f17c080d..7b3abe9d53f 100644 --- a/extension/war/src/main/webapp/WEB-INF/conf/social-extension/social/notification-configuration.xml +++ b/extension/war/src/main/webapp/WEB-INF/conf/social-extension/social/notification-configuration.xml @@ -9,6 +9,14 @@ org.exoplatform.social.notification.impl.SpaceWebNotificationServiceImpl + + io.meeds.social.notification.rest.NotificationSettingsRestService + + + + io.meeds.social.notification.rest.WebNotificationRestService + + org.exoplatform.social.notification.service.SpaceWebNotificationService diff --git a/translations.properties b/translations.properties index a8ab1b409e9..bd028c83d10 100644 --- a/translations.properties +++ b/translations.properties @@ -52,6 +52,8 @@ webapp/SpacesOverview.properties=webapp/portlet/src/main/resources/locale/portle webapp/SuggestionsPortlet.properties=webapp/portlet/src/main/resources/locale/portlet/social/SuggestionsPortlet_en.properties webapp/UserSettings.properties=webapp/portlet/src/main/resources/locale/portlet/social/UserSettings_en.properties webapp/Login.properties=webapp/portlet/src/main/resources/locale/portlet/Login_en.properties +webapp/UserNotificationPortlet.properties=webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_en.properties +webapp/NotificationAdministration.properties=webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_en.properties # navigation navigation/users.properties=extension/war/src/main/resources/locale/navigation/group/platform/users_en.properties diff --git a/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_en.properties b/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_en.properties new file mode 100644 index 00000000000..25e5a7d78dc --- /dev/null +++ b/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_en.properties @@ -0,0 +1,33 @@ +NotificationAdmin.title=Manage notifications settings +NotificationAdmin.sender.title=Notification Sender +NotificationAdmin.sender.subtitle=Set name and email of the notification sender +NotificationAdmin.sender.drawer.message=Notification will be sent using the characteristics below +NotificationAdmin.sender.drawer.name=Name +NotificationAdmin.sender.drawer.address=Address +NotificationAdmin.allowedNotifications.title=Allowed notification channels +NotificationAdmin.allowedNotifications.subtitle=Precise notification channels that users would be allowed to use. +NotificationAdmin.MAIL_CHANNEL.title=Enable email notifications for all +NotificationAdmin.WEB_CHANNEL.title=Enable onsite notifications for all +NotificationAdmin.SPACE_WEB_CHANNEL.title=Enable unread activities notifications for all +NotificationAdmin.PUSH_CHANNEL.title=Enable push notifications for all +NotificationAdmin.MAIL_CHANNEL.name=Email notifications +NotificationAdmin.WEB_CHANNEL.name=On-site notifications +NotificationAdmin.SPACE_WEB_CHANNEL.name=Unread activities notifications +NotificationAdmin.PUSH_CHANNEL.name=Mobile push notifications +NotificationAdmin.allowedNotificationChannels.title=Allowed notification channels per type +NotificationAdmin.invalidSenderEmail=Invalid email +NotificationAdmin.invalidSenderName=Invalid name. Please use alphanumeric ' - _ characters only +NotificationAdmin.NewUserPlugin=A new user joins the platform +NotificationAdmin.RelationshipReceivedRequestPlugin=A connection request is sent +NotificationAdmin.SpaceInvitationPlugin=An invitation to join a space is done +NotificationAdmin.RequestJoinSpacePlugin=A request to join a space is done +NotificationAdmin.ActivityMentionPlugin=User is mentioned in a message +NotificationAdmin.SharedActivitySpaceStreamPlugin=An activity is shared to a space +NotificationAdmin.PostActivityPlugin=An activity is posted on a personal stream +NotificationAdmin.PostActivitySpaceStreamPlugin=An activity is posted on a space stream +NotificationAdmin.EditActivityPlugin=An activity is edited +NotificationAdmin.ActivityCommentPlugin=An activity is commented +NotificationAdmin.EditCommentPlugin=A comment is edited +NotificationAdmin.ActivityReplyToCommentPlugin=A reply to a comment is added +NotificationAdmin.LikePlugin=A like on activity is added +NotificationAdmin.LikeCommentPlugin=A like on comment is added diff --git a/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_fr.properties b/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_fr.properties new file mode 100644 index 00000000000..a1bab298334 --- /dev/null +++ b/webapp/portlet/src/main/resources/locale/portlet/NotificationAdministration_fr.properties @@ -0,0 +1,29 @@ +NotificationAdmin.title=Manage notifications settings +NotificationAdmin.sender.title=Notification Sender +NotificationAdmin.sender.subtitle=Set name and email of the notification sender +NotificationAdmin.sender.drawer.message=Notification will be sent using the characteristics below +NotificationAdmin.sender.drawer.name=Name +NotificationAdmin.sender.drawer.address=Address +NotificationAdmin.allowedNotifications.title=Allowed notification channels +NotificationAdmin.allowedNotifications.subtitle=Precise notification channels that users would be allowed to use. +NotificationAdmin.MAIL_CHANNEL.title=Enable email notifications for all +NotificationAdmin.WEB_CHANNEL.title=Enable onsite notifications for all +NotificationAdmin.SPACE_WEB_CHANNEL.title=Enable unread activities notifications for all +NotificationAdmin.PUSH_CHANNEL.title=Enable push notifications for all +NotificationAdmin.allowedNotificationChannels.title=Allowed notification channels per type +NotificationAdmin.invalidSenderEmail=Invalid email +NotificationAdmin.invalidSenderName=Invalid name. Please use alphanumeric ' - _ characters only +NotificationAdmin.NewUserPlugin=Un nouvel utilisateur rejoint la plateforme +NotificationAdmin.RelationshipReceivedRequestPlugin=Une demande de connexion est envoy\u00E9e +NotificationAdmin.SpaceInvitationPlugin=Une invitation \u00E0 rejoindre un espace est envoy\u00E9e +NotificationAdmin.RequestJoinSpacePlugin=Une demande pour rejoindre un espace est transmise +NotificationAdmin.ActivityMentionPlugin=Un utilisateur est mentionn\u00E9 dans un espace +NotificationAdmin.SharedActivitySpaceStreamPlugin=Une activit\u00E9 est partag\u00E9e dans un espace +NotificationAdmin.PostActivityPlugin=Une activit\u00E9 est publi\u00E9e sur le fil personnel +NotificationAdmin.PostActivitySpaceStreamPlugin=Une activit\u00E9 est publi\u00E9e sur le fil d'un espace +NotificationAdmin.EditActivityPlugin=Une activit\u00E9 a \u00E9t\u00E9 modifi\u00E9e +NotificationAdmin.ActivityCommentPlugin=Une activit\u00E9 est comment\u00E9e +NotificationAdmin.EditCommentPlugin=Un commentaire a \u00E9t\u00E9 modifi\u00E9 +NotificationAdmin.ActivityReplyToCommentPlugin=Une r\u00E9ponse \u00E0 un commentaire est ajout\u00E9e +NotificationAdmin.LikePlugin=Un j'aime sur une activit\u00E9 est ajout\u00E9 +NotificationAdmin.LikeCommentPlugin=Un j'aime sur un commentaire est ajout\u00E9 diff --git a/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_en.properties b/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_en.properties new file mode 100644 index 00000000000..3782c68f1e2 --- /dev/null +++ b/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_en.properties @@ -0,0 +1,50 @@ +##################################################################################### +# Notification Settings # +##################################################################################### +UINotification.label.NotificationsDescription=We will notify you whenever something involving you happens. The settings below will let you control how you want to be notified. +UINotification.label.NotificationSettings=Notification Settings + +UINotification.label.channel-mail=Notify me by email +UINotification.description.channel-mail=You will be notified by an email sent to your private mail box +UINotification.label.channel-web=Notify me on-site +UINotification.label.channel-space_web=Notify me when unread activities +UINotification.description.channel-space_web=You will be notified in the left menu and in the stream +UINotification.description.channel-web=You'll receive a notification directly on the website. +UINotification.label.channel.default=Notify me by {0} +UINotification.description.channel.default=You will be notified by {0} + +UINotification.label.Empty=No providers +UINotification.label.NotifyMeWhen=Notify me when +UINotification.label.HowToGetNotification=How to get notifications + +UINotification.label.NoNotifications=No notifications +UINotification.label.checkbox-mail=Send me an email right away +UINotification.label.selectBox-mail=Send me a digest email +UINotification.label.checkbox-web=See on site +UINotification.label.checkbox.default=See on {0} + +UINotification.label.Never=Never +UINotification.label.Daily=Daily +UINotification.description.Daily=Daily digest email notification +UINotification.label.Weekly=Weekly +UINotification.description.Weekly=Weekly digest email notification +UINotification.label.Monthly=Monthly +UINotification.description.Monthly=Monthly digest email notification + +UINotification.title.Information=Information +UINotification.title.Confirmation=Confirmation + +UINotification.msg.NoPluginIsActive=All notifications have been disabled by an administrator. +UINotification.msg.SaveOKSetting=Your changes have been saved. +UINotification.msg.SaveNOKSetting=Your changes cannot be saved. +UINotification.msg.ResetSetting=All your notification settings will be reset to default values. Your previous settings will be lost. + +UINotification.action.Save=Save +# +UINotification.action.Reset=Reset +UINotification.action.Confirm=Confirm +UINotification.action.Cancel=Cancel +UINotification.action.OK=OK +UINotification.action.Close=Close +UINotification.action.switch.on=Yes +UINotification.action.switch.off=No diff --git a/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_fr.properties b/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_fr.properties new file mode 100644 index 00000000000..a56ee9ed9e9 --- /dev/null +++ b/webapp/portlet/src/main/resources/locale/portlet/UserNotificationPortlet_fr.properties @@ -0,0 +1,50 @@ +##################################################################################### +# Notification Settings # +##################################################################################### +UINotification.label.NotificationsDescription=Nous pouvons vous notifier \u00E0 chaque fois qu'il arrive quelque chose vous concernant. Les param\u00E8tres ci-dessous vous permettront de contr\u00F4ler la fa\u00E7on dont vous souhaitez \u00EAtre averti(e). +UINotification.label.NotificationSettings=Param\u00E8tres de Notification + +UINotification.label.channel-mail=E-mail +UINotification.description.channel-mail=Vous recevrez une notification par email +UINotification.label.channel-web=Site +UINotification.label.channel-space_web=Me notifier en cas d'activit\u00E9 non lue +UINotification.description.channel-space_web=Vous serez notifi\u00E9 dans le menu de gauche et dans le fil d'activit\u00E9s +UINotification.description.channel-web=Vous recevrez une notification directement sur le site +UINotification.label.channel.default=Me notifier par {0} +UINotification.description.channel.default=Vous serez notifi\u00E9 par {0} + +UINotification.label.Empty=Aucun fournisseur +UINotification.label.NotifyMeWhen=Me notifier lorsque... +UINotification.label.HowToGetNotification=Comment \u00EAtre notifi\u00E9 ? + +UINotification.label.NoNotifications=Ne pas me notifier +UINotification.label.checkbox-mail=Envoyez moi un email imm\u00E9diatement +UINotification.label.selectBox-mail=Envoyez-moi un email r\u00E9sum\u00E9 +UINotification.label.checkbox-web=Affichez la notification sur le site +UINotification.label.checkbox.default=Voir sur {0} + +UINotification.label.Never=Jamais +UINotification.label.Daily=Quotidien +UINotification.description.Daily=R\u00E9capitulatif quotidien des notifications par email +UINotification.label.Weekly=Hebdomadaire +UINotification.description.Weekly=R\u00E9capitulatif hebdomadaire des notifications par email +UINotification.label.Monthly=Mensuel +UINotification.description.Monthly=R\u00E9capitulatif mensuel des notifications par email + +UINotification.title.Information=Information: +UINotification.title.Confirmation=Confirmation + +UINotification.msg.NoPluginIsActive=Toutes les notifications ont \u00E9t\u00E9 d\u00E9sactiv\u00E9es par un administrateur. +UINotification.msg.SaveOKSetting=Vos modifications ont \u00E9t\u00E9 enregistr\u00E9es. +UINotification.msg.SaveNOKSetting=Impossible d'enregistrer vos modifications. +UINotification.msg.ResetSetting=Tous vos param\u00E8tres de notification seront r\u00E9initialis\u00E9s aux valeurs par d\u00E9faut. Vos param\u00E8tres actuels seront perdus. + +UINotification.action.Save=Enregistrer +# +UINotification.action.Reset=R\u00E9initialiser +UINotification.action.Confirm=Confirmer +UINotification.action.Cancel=Annuler +UINotification.action.OK=Valider +UINotification.action.Close=Fermer +UINotification.action.switch.on=Oui +UINotification.action.switch.off=Non diff --git a/webapp/portlet/src/main/webapp/WEB-INF/gatein-resources.xml b/webapp/portlet/src/main/webapp/WEB-INF/gatein-resources.xml index da0930b4fff..66632290a48 100644 --- a/webapp/portlet/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/webapp/portlet/src/main/webapp/WEB-INF/gatein-resources.xml @@ -249,7 +249,7 @@ 2 - + social-portlet IntranetNotificationsPortlet @@ -265,15 +265,7 @@ /skin/css/portlet/TopBarNotification/Style.css 1 - - - commons-extension - NotificationsAdminJuzuPortlet - Enterprise - /skin/css/portlet/uiNotificationsAdmin/Style.css - 1 - - + @@ -1044,6 +1036,31 @@ + + NotificationAdministration + + + + commonVueComponents + + + vue + + + vuetify + + + eXoVueI18n + + + extensionRegistry + + + + UserSettingSecurity diff --git a/webapp/portlet/src/main/webapp/WEB-INF/jsp/profileSettings.jsp b/webapp/portlet/src/main/webapp/WEB-INF/jsp/portlet/profileSettings.jsp similarity index 100% rename from webapp/portlet/src/main/webapp/WEB-INF/jsp/profileSettings.jsp rename to webapp/portlet/src/main/webapp/WEB-INF/jsp/portlet/profileSettings.jsp diff --git a/webapp/portlet/src/main/webapp/WEB-INF/jsp/portlet/userSettingNotifications.jsp b/webapp/portlet/src/main/webapp/WEB-INF/jsp/portlet/userSettingNotifications.jsp deleted file mode 100644 index 3615f3c58e6..00000000000 --- a/webapp/portlet/src/main/webapp/WEB-INF/jsp/portlet/userSettingNotifications.jsp +++ /dev/null @@ -1,12 +0,0 @@ -
-
- - -
-
\ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/WEB-INF/portlet.xml b/webapp/portlet/src/main/webapp/WEB-INF/portlet.xml index ebeab9ee926..db579f5c845 100644 --- a/webapp/portlet/src/main/webapp/WEB-INF/portlet.xml +++ b/webapp/portlet/src/main/webapp/WEB-INF/portlet.xml @@ -162,7 +162,6 @@ Who is on line Who is on line - Juzu social intranet
@@ -313,11 +312,15 @@ org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet portlet-view-dispatched-file-path - /WEB-INF/jsp/portlet/userSettingNotifications.jsp + /html/userSettingNotifications.html + -1 + PUBLIC text/html + en + locale.portlet.UserNotificationPortlet User Setting notifications @@ -781,7 +784,7 @@ org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet portlet-view-dispatched-file-path - /WEB-INF/jsp/profileSettings.jsp + /WEB-INF/jsp/portlet/profileSettings.jsp text/html @@ -841,4 +844,23 @@ + + NotificationAdministration + org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet + + portlet-view-dispatched-file-path + /html/notificationAdministration.html + + -1 + PUBLIC + + text/html + + en + locale.portlet.NotificationAdministration + + Notification Administration + + + diff --git a/webapp/portlet/src/main/webapp/html/notificationAdministration.html b/webapp/portlet/src/main/webapp/html/notificationAdministration.html new file mode 100644 index 00000000000..a537baed890 --- /dev/null +++ b/webapp/portlet/src/main/webapp/html/notificationAdministration.html @@ -0,0 +1,9 @@ +
+
+ +
+
\ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/html/userSettingNotifications.html b/webapp/portlet/src/main/webapp/html/userSettingNotifications.html new file mode 100644 index 00000000000..953939eddd2 --- /dev/null +++ b/webapp/portlet/src/main/webapp/html/userSettingNotifications.html @@ -0,0 +1,9 @@ +
+
+ +
+
\ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/skin/less/portlet/uiNotificationsAdmin/Style.less b/webapp/portlet/src/main/webapp/skin/less/portlet/uiNotificationsAdmin/Style.less deleted file mode 100644 index c6b9ef51906..00000000000 --- a/webapp/portlet/src/main/webapp/skin/less/portlet/uiNotificationsAdmin/Style.less +++ /dev/null @@ -1,117 +0,0 @@ -/** - - This file is part of the Meeds project (https://meeds.io/). - - Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 3 of the License, or (at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public License - along with this program; if not, write to the Free Software Foundation, - Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -*/ - -.uiNotificationAdmin { - - @import "../../deprecated/Style.less"; - - @import "../../variables.less"; - @import "../../mixins.less"; - - .senderForm { - label { - line-height: 27px; - margin-bottom: 0; - margin-right: 6px ~'; /** orientation=lt */ '; - margin-left: 6px ~'; /** orientation=rt */ '; - } - } - .uiGrid.table { - thead { - th:last-child { - width: 40%; - } - } - tr.disable { - color: @tableBackgroundAccent; - } - tr { - td { - &:first-child{ - >label{ - padding-left: 20px; - } - } - } - td .view .edit-mode, - td .view .save-setting { - display: none; - } - td .edit .view-mode, - td .edit .edit-setting { - display: none; - } - - } - } - - .view-mode, .edit-mode { - > div { - padding: 2px 0; - display: flex; - } - } - .view-mode { - - [class^="uiIconPLF"] { - display: flex; - min-width: 20px; - margin-right: 7px ~'; /** orientation=lt */ '; - margin-left: 7px ~'; /** orientation=rt */ '; - - &:before { - margin: 0 auto; - } - } - > span { - color: @baseColorLight; - } - } - .edit-mode { - .uiCheckbox { - margin-left: 0 ~'; /** orientation=lt */ '; - margin-right: 0 ~'; /** orientation=rt */ '; - .checkbox + span { - padding-left: 6px ~'; /** orientation=lt */ '; - padding-right: 6px ~'; /** orientation=rt */ '; - } - } - } - .plugin-container { - position: relative; - .right-view { - position: absolute; - top: 50%; - right: 0 ~'; /** orientation=lt */ '; - left: 0 ~'; /** orientation=rt */ '; - .translate(0, -50%); - } - } - .inputContainer { - margin: 0 auto; - } -} - - -@media (max-width: 480px) { - .uiNotificationAdmin { - padding: 15px 10px; - } -} \ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationAdministration.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationAdministration.vue new file mode 100644 index 00000000000..64d66b03d78 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationAdministration.vue @@ -0,0 +1,49 @@ + + + + diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationChannels.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationChannels.vue new file mode 100644 index 00000000000..97ee5c16ebe --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationChannels.vue @@ -0,0 +1,50 @@ + + \ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationContact.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationContact.vue new file mode 100644 index 00000000000..886b2c7ce41 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationContact.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationPlugins.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationPlugins.vue new file mode 100644 index 00000000000..e3414d56453 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/NotificationPlugins.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/contact/NotificationContactDrawer.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/contact/NotificationContactDrawer.vue new file mode 100644 index 00000000000..df9cbcd774e --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/contact/NotificationContactDrawer.vue @@ -0,0 +1,146 @@ + + + diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPlugin.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPlugin.vue new file mode 100644 index 00000000000..e12e160e9dd --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPlugin.vue @@ -0,0 +1,68 @@ + + + + diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginDrawer.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginDrawer.vue new file mode 100644 index 00000000000..1c4b979d266 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginDrawer.vue @@ -0,0 +1,110 @@ + + + + diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginGroup.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginGroup.vue new file mode 100644 index 00000000000..147871ecaa5 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/components/plugin/NotificationPluginGroup.vue @@ -0,0 +1,42 @@ + + + + diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/initComponents.js b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/initComponents.js new file mode 100644 index 00000000000..038f3b6c2a3 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/initComponents.js @@ -0,0 +1,45 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import NotificationAdministration from './components/NotificationAdministration.vue'; + +import NotificationPlugins from './components/NotificationPlugins.vue'; +import NotificationChannels from './components/NotificationChannels.vue'; +import NotificationContact from './components/NotificationContact.vue'; + +import NotificationPluginGroup from './components/plugin/NotificationPluginGroup.vue'; +import NotificationPlugin from './components/plugin/NotificationPlugin.vue'; +import NotificationPluginDrawer from './components/plugin/NotificationPluginDrawer.vue'; + +import NotificationContactDrawer from './components/contact/NotificationContactDrawer.vue'; + +const components = { + 'notification-administration': NotificationAdministration, + 'notification-administration-plugins': NotificationPlugins, + 'notification-administration-channels': NotificationChannels, + 'notification-administration-contact': NotificationContact, + 'notification-administration-plugin': NotificationPlugin, + 'notification-administration-plugin-group': NotificationPluginGroup, + 'notification-administration-plugin-drawer': NotificationPluginDrawer, + 'notification-administration-contact-drawer': NotificationContactDrawer, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/js/NotificationAdministration.js b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/js/NotificationAdministration.js new file mode 100644 index 00000000000..359f1401795 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/js/NotificationAdministration.js @@ -0,0 +1,86 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export function getSettings() { + return fetch(`${eXo.env.portal.context}/${eXo.env.portal.rest}/notifications/settings`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error getting notification settings'); + } + }); +} + +export function saveSenderEmail(name, email) { + const formData = new FormData(); + formData.append('name', name); + formData.append('email', email); + const params = new URLSearchParams(formData).toString(); + return fetch(`${eXo.env.portal.context}/${eXo.env.portal.rest}/notifications/settings`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params + }).then(resp => { + if (!resp?.ok) { + if (resp?.status === 400) { + return resp.text().then(e => { + throw new Error(e); + }); + } else { + throw new Error('Error saving plugin settings'); + } + } + }); +} + +export function savePluginSettings(pluginId, channels) { + return fetch(`${eXo.env.portal.context}/${eXo.env.portal.rest}/notifications/settings/plugin/${pluginId}`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `channels=${channels}` + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error saving plugin settings'); + } + }); +} + +export function saveChannelStatus(channelId, enable) { + return fetch(`${eXo.env.portal.context}/${eXo.env.portal.rest}/notifications/settings/channel/${channelId}`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `enable=${enable}` + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error saving channel setting'); + } + }); +} diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/main.js b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/main.js new file mode 100644 index 00000000000..0764e6908f2 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/main.js @@ -0,0 +1,61 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import './initComponents.js'; +import './services.js'; + +// get overrided components if exists +if (extensionRegistry) { + const components = extensionRegistry.loadComponents('NotificationAdministration'); + if (components && components.length > 0) { + components.forEach(cmp => { + Vue.component(cmp.componentName, cmp.componentOptions); + }); + } +} + +document.dispatchEvent(new CustomEvent('displayTopBarLoading')); + +//getting language of user +const lang = eXo && eXo.env.portal.language || 'en'; + +//should expose the locale ressources as REST API +const urls = [ + `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.NotificationAdministration-${lang}.json`, + `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.UserNotificationPortlet-${lang}.json`, + `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.social.UserSettings-${lang}.json`, +]; + +const appId = 'NotificationAdministration'; + +export function init(settings) { + exoi18n.loadLanguageAsync(lang, urls).then(i18n => { + Vue.createApp({ + data: { + settings: settings, + }, + mounted() { + document.dispatchEvent(new CustomEvent('hideTopBarLoading')); + }, + template: ``, + i18n, + vuetify: Vue.prototype.vuetifyOptions, + }, `#${appId}`, 'Notification Administration'); + }); +} diff --git a/webapp/portlet/src/main/webapp/vue-apps/notification-administration/services.js b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/services.js new file mode 100644 index 00000000000..cd8b12e8fc9 --- /dev/null +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-administration/services.js @@ -0,0 +1,24 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import * as notificationAdministration from './js/NotificationAdministration.js'; + +window.Object.defineProperty(Vue.prototype, '$notificationAdministration', { + value: notificationAdministration, +}); diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationChannel.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationChannel.vue similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationChannel.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationChannel.vue diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationDrawer.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationDrawer.vue similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationDrawer.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationDrawer.vue diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationGroup.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationGroup.vue similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationGroup.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationGroup.vue diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationPlugin.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationPlugin.vue similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationPlugin.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationPlugin.vue diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotifications.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotifications.vue similarity index 96% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotifications.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotifications.vue index fd3c4eedf42..2de58b96387 100644 --- a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotifications.vue +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotifications.vue @@ -73,6 +73,9 @@ export default { return fetch(`${eXo.env.portal.context}/${eXo.env.portal.rest}/notifications/settings/${eXo.env.portal.userName}`) .then(resp => resp && resp.ok && resp.json()) .then(settings => { + if (this.displayed && !settings?.channels?.length) { + this.displayed = false; + } this.notificationSettings = settings; return this.$nextTick(); }) diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationsWindow.vue b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationsWindow.vue similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/components/UserSettingNotificationsWindow.vue rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/components/UserSettingNotificationsWindow.vue diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/initComponents.js b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/initComponents.js similarity index 100% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/initComponents.js rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/initComponents.js diff --git a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/main.js b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/main.js similarity index 95% rename from webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/main.js rename to webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/main.js index 79ae717566a..329b7087b25 100644 --- a/webapp/portlet/src/main/webapp/vue-apps/user-setting-notifications/main.js +++ b/webapp/portlet/src/main/webapp/vue-apps/notification-user-settings/main.js @@ -17,7 +17,7 @@ const lang = eXo && eXo.env.portal.language || 'en'; //should expose the locale ressources as REST API const urls = [ - `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.notification.UserNotificationPortlet-${lang}.json`, + `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.UserNotificationPortlet-${lang}.json`, `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.social.UserSettings-${lang}.json` ]; diff --git a/webapp/portlet/webpack.common.js b/webapp/portlet/webpack.common.js index 58806cf22c9..c9c998dab68 100644 --- a/webapp/portlet/webpack.common.js +++ b/webapp/portlet/webpack.common.js @@ -45,7 +45,7 @@ let config = { profileContactInformation: './src/main/webapp/vue-apps/profile-contact-information/main.js', profileWorkExperience: './src/main/webapp/vue-apps/profile-work-experience/main.js', userSettingLanguage: './src/main/webapp/vue-apps/user-setting-language/main.js', - userSettingNotifications: './src/main/webapp/vue-apps/user-setting-notifications/main.js', + userSettingNotifications: './src/main/webapp/vue-apps/notification-user-settings/main.js', userSettingSecurity: './src/main/webapp/vue-apps/user-setting-security/main.js', userSettingTimezone: './src/main/webapp/vue-apps/user-setting-timezone/main.js', spaceInfos: './src/main/webapp/vue-apps/space-infos-app/main.js', @@ -73,6 +73,7 @@ let config = { generalSettings: './src/main/webapp/vue-apps/general-settings/main.js', imageCropper: './src/main/webapp/vue-apps/component-image-crop/main.js', translationField: './src/main/webapp/vue-apps/component-translation-field/main.js', + notificationAdministration: './src/main/webapp/vue-apps/notification-administration/main.js', }, module: { rules: [