From cc95129314720c46cb7c86d8e10f5c2918253c9d Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Fri, 31 May 2024 18:16:43 +0100 Subject: [PATCH] feat: Implement Portlet Instance Application and Renderer Service - MEED-6920 - Meeds-io/MIPs#139 (#93) This change will allow to dynamically display in the same page a Portlet instance selected by a request parameter. In addition, this will initialize the Portlet instance editor application. --- .../PortletInstancePreferencePlugin.java | 51 ++++ .../container/PortletInstanceAddOnPlugin.java | 64 ++++ .../PortletInstanceApplicationAdapter.java | 282 ++++++++++++++++++ .../ImagePortletInstancePreferencePlugin.java | 106 +++++++ .../LinkPortletInstancePreferencePlugin.java | 75 +++++ .../layout/rest/NavigationLayoutRest.java | 48 +-- .../io/meeds/layout/rest/PageLayoutRest.java | 52 ++-- .../meeds/layout/rest/PageTemplateRest.java | 26 +- .../rest/PortletInstanceCategoryRest.java | 32 +- .../layout/rest/PortletInstanceRest.java | 63 +++- .../io/meeds/layout/rest/PortletRest.java | 6 +- .../io/meeds/layout/rest/SiteLayoutRest.java | 35 ++- .../meeds/layout/rest/model/LayoutModel.java | 71 +++-- .../service/PortletInstanceRenderService.java | 259 ++++++++++++++++ .../service/PortletInstanceService.java | 8 +- .../PortletInstanceImportService.java | 25 +- ...ortletInstancePreferencePluginService.java | 56 ++++ .../layout/rest/PortletInstanceRestTest.java | 12 +- .../PortletInstanceRenderServiceTest.java | 241 +++++++++++++++ .../locale/portlet/LayoutEditor_en.properties | 3 + .../main/webapp/WEB-INF/gatein-resources.xml | 36 ++- .../src/main/webapp/WEB-INF/portlet.xml | 21 ++ .../portal/webui/container/UIPageLayout.gtmpl | 18 +- .../src/main/webapp/html/portletEditor.html | 7 + .../vue-app/common-illustration/main.js | 20 ++ .../vue-app/common-layout-components/main.js | 20 ++ .../webapp/vue-app/common-page-layout/main.js | 20 ++ .../vue-app/common-page-template/main.js | 20 ++ .../js/PortletInstanceService.js | 13 + .../vue-app/common/js/ApplicationUtils.js | 10 +- .../content/container/Application.vue | 4 +- .../components/content/container/Cell.vue | 2 +- .../drawer/AddApplicationDrawer.vue | 20 +- .../drawer/EditApplicationDrawer.vue | 4 +- .../vue-app/layout-editor/js/LayoutUtils.js | 2 + .../main/webapp/vue-app/layout-editor/main.js | 24 +- .../components/container/Application.vue | 37 ++- .../vue-app/page-templates-management/main.js | 3 +- .../components/PortletEditor.vue | 38 +++ .../components/content/Application.vue | 78 +++++ .../components/content/Cell.vue | 96 ++++++ .../components/content/CellResizeButton.vue | 57 ++++ .../components/content/Content.vue | 27 ++ .../components/toolbar/Toolbar.vue | 45 +++ .../components/toolbar/actions/SaveButton.vue | 58 ++++ .../vue-app/portlet-editor/initComponents.js | 42 +++ .../webapp/vue-app/portlet-editor/main.js | 68 +++++ .../portlets/components/instances/Menu.vue | 35 +-- .../src/main/webapp/vue-app/portlets/main.js | 3 +- .../webapp/vue-app/site-management/main.js | 1 - .../webapp/vue-app/site-navigation/main.js | 3 +- layout-webapp/webpack.prod.js | 1 + 52 files changed, 2158 insertions(+), 190 deletions(-) create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/PortletInstancePreferencePlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceAddOnPlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceApplicationAdapter.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/renderer/ImagePortletInstancePreferencePlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/plugin/renderer/LinkPortletInstancePreferencePlugin.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java create mode 100644 layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstancePreferencePluginService.java create mode 100644 layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java create mode 100644 layout-webapp/src/main/webapp/html/portletEditor.html create mode 100644 layout-webapp/src/main/webapp/vue-app/common-illustration/main.js create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout-components/main.js create mode 100644 layout-webapp/src/main/webapp/vue-app/common-page-layout/main.js create mode 100644 layout-webapp/src/main/webapp/vue-app/common-page-template/main.js create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Application.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js create mode 100644 layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/PortletInstancePreferencePlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/PortletInstancePreferencePlugin.java new file mode 100644 index 000000000..293a737b9 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/PortletInstancePreferencePlugin.java @@ -0,0 +1,51 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.plugin; + +import java.util.List; + +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.pom.spi.portlet.Portlet; + +import io.meeds.layout.model.PortletInstancePreference; + +/** + * A plugin that can be extended in order to inject a specific behavior for some + * portlets to let export preferences that allow to duplicate behavior from one + * application instance to another. + */ +public interface PortletInstancePreferencePlugin { + + /** + * return the portlet name for which the plugin can generate its preferences + */ + String getPortletName(); + + /** + * Computes the list of preferences to have the same view as the designated + * application instance + * + * @param application {@link Application} designated to extract its + * preferences + * @param preferences current {@link Portlet} preferences + * @return + */ + List generatePreferences(Application application, Portlet preferences); + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceAddOnPlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceAddOnPlugin.java new file mode 100644 index 000000000..8d9f11a18 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceAddOnPlugin.java @@ -0,0 +1,64 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.plugin.container; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.commons.addons.AddOnPlugin; +import org.exoplatform.commons.addons.AddOnService; +import org.exoplatform.portal.config.model.Application; + +import jakarta.annotation.PostConstruct; + +@Component +public class PortletInstanceAddOnPlugin extends AddOnPlugin { + + private static final String PORTLET_EDITOR_DYNAMIC_CONTAINER = "portlet-viewer"; + + @Autowired + private PortletInstanceApplicationAdapter portletInstanceApplicationAdapter; + + @Autowired + private AddOnService addonService; + + @PostConstruct + public void init() { + addonService.addPlugin(this); + } + + @Override + public int getPriority() { + return 1; + } + + @Override + public String getContainerName() { + return PORTLET_EDITOR_DYNAMIC_CONTAINER; + } + + @Override + public List> getApplications() { + return Collections.singletonList(portletInstanceApplicationAdapter); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceApplicationAdapter.java b/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceApplicationAdapter.java new file mode 100644 index 000000000..b7b1351ba --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/container/PortletInstanceApplicationAdapter.java @@ -0,0 +1,282 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.plugin.container; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import org.exoplatform.portal.application.PortalRequestContext; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.ApplicationState; +import org.exoplatform.portal.config.model.ApplicationType; +import org.exoplatform.portal.config.model.ModelStyle; +import org.exoplatform.portal.config.model.Properties; +import org.exoplatform.portal.config.serialize.PortletApplication; +import org.exoplatform.portal.pom.data.ApplicationData; +import org.exoplatform.portal.pom.spi.portlet.Portlet; + +import io.meeds.layout.service.PortletInstanceRenderService; + +import lombok.SneakyThrows; + +@Component +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class PortletInstanceApplicationAdapter extends PortletApplication { + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; + + private ThreadLocal application = new ThreadLocal<>(); + + @Override + public ModelStyle getCssStyle() { + return getApplication().getCssStyle(); + } + + @Override + public void setCssStyle(ModelStyle cssStyle) { + getApplication().setCssStyle(cssStyle); + } + + @Override + public ApplicationType getType() { + return ApplicationType.PORTLET; + } + + @Override + public String getWidth() { + return getApplication().getWidth(); + } + + @Override + public void setWidth(String s) { + getApplication().setWidth(s); + } + + @Override + public String getHeight() { + return getApplication().getHeight(); + } + + @Override + public void setHeight(String s) { + getApplication().setHeight(s); + } + + @Override + public String getId() { + return getApplication().getId(); + } + + @Override + public void setId(String value) { + getApplication().setId(value); + } + + @Override + public String[] getAccessPermissions() { + return getApplication().getAccessPermissions(); + } + + @Override + public void setAccessPermissions(String[] accessPermissions) { + getApplication().setAccessPermissions(accessPermissions); + } + + @Override + public boolean isModifiable() { + return getApplication().isModifiable(); + } + + @Override + public void setModifiable(boolean modifiable) { + getApplication().setModifiable(modifiable); + } + + @Override + public ApplicationState getState() { + return getApplication().getState(); + } + + @Override + public void setState(ApplicationState value) { + getApplication().setState(value); + } + + @Override + public boolean getShowInfoBar() { + return getApplication().getShowInfoBar(); + } + + @Override + public void setShowInfoBar(boolean b) { + getApplication().setShowInfoBar(b); + } + + @Override + public boolean getShowApplicationState() { + return getApplication().getShowApplicationState(); + } + + @Override + public void setShowApplicationState(boolean b) { + getApplication().setShowApplicationState(b); + } + + @Override + public boolean getShowApplicationMode() { + return getApplication().getShowApplicationMode(); + } + + @Override + public void setShowApplicationMode(boolean b) { + getApplication().setShowApplicationMode(b); + } + + @Override + public String getIcon() { + return getApplication().getIcon(); + } + + @Override + public void setIcon(String value) { + getApplication().setIcon(value); + } + + @Override + public String getDescription() { + return getApplication().getDescription(); + } + + @Override + public void setDescription(String des) { + getApplication().setDescription(des); + } + + @Override + public String getTitle() { + return getApplication().getTitle(); + } + + @Override + public void setTitle(String value) { + getApplication().setTitle(value); + } + + @Override + public Properties getProperties() { + return getApplication().getProperties(); + } + + @Override + public void setProperties(Properties properties) { + getApplication().setProperties(properties); + } + + @Override + public String getTheme() { + return getApplication().getTheme(); + } + + @Override + public void setTheme(String theme) { + getApplication().setTheme(theme); + } + + @Override + public String getCssClass() { + return getApplication().getCssClass(); + } + + @Override + public String getBorderColor() { + return getApplication().getBorderColor(); + } + + @Override + public ApplicationData build() { + return getApplication().build(); + } + + @Override + public void resetStorage() { + getApplication().resetStorage(); + } + + @Override + public void setCssClass(String cssClass) { + getApplication().setCssClass(cssClass); + } + + @Override + public void setBorderColor(String borderColor) { + getApplication().setBorderColor(borderColor); + } + + @Override + public String getStorageId() { + return getApplication().getStorageId(); + } + + @Override + public String getStorageName() { + return getApplication().getStorageName(); + } + + @Override + public void setStorageName(String storageName) { + getApplication().setStorageName(storageName); + } + + @SneakyThrows + public Application getApplication() { // NOSONAR + Application portletApplication = application.get(); + if (portletApplication == null) { + portletApplication = portletInstanceRenderService.getPortletInstanceApplication(getCurrentUserName(), + getPortletInstanceId(), + getApplicationStorageId()); + manageRequestCache(portletApplication); + } + return portletApplication; + } + + private void manageRequestCache(Application portletApplication) { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + if (requestContext != null) { + application.set(portletApplication); + requestContext.addOnRequestEnd(() -> application.remove()); + } + } + + private String getPortletInstanceId() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getParameter("portletInstanceId"); + } + + private String getApplicationStorageId() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getParameter("portal:componentId"); + } + + private String getCurrentUserName() { + PortalRequestContext requestContext = PortalRequestContext.getCurrentInstance(); + return requestContext == null ? null : requestContext.getRequest().getRemoteUser(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/renderer/ImagePortletInstancePreferencePlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/ImagePortletInstancePreferencePlugin.java new file mode 100644 index 000000000..5d78ad644 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/ImagePortletInstancePreferencePlugin.java @@ -0,0 +1,106 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.plugin.renderer; + +import static io.meeds.social.image.plugin.ImageAttachmentPlugin.OBJECT_TYPE; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.commons.file.model.FileItem; +import org.exoplatform.commons.file.services.FileService; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.pom.spi.portlet.Portlet; +import org.exoplatform.portal.pom.spi.portlet.Preference; +import org.exoplatform.social.attachment.AttachmentService; + +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; +import io.meeds.social.cms.model.CMSSetting; +import io.meeds.social.cms.service.CMSService; + +import lombok.SneakyThrows; + +@Service +public class ImagePortletInstancePreferencePlugin implements PortletInstancePreferencePlugin { + + private static final String CMS_SETTING_PREFERENCE_NAME = "name"; + + private static final String DATA_INIT_PREFERENCE_NAME = "data.init"; + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private CMSService cmsService; + + @Autowired + private FileService fileService; + + @Override + public String getPortletName() { + return "Image"; + } + + @Override + @SneakyThrows + public List generatePreferences(Application application, + Portlet preferences) { + String settingName = getCmsSettingName(preferences); + if (StringUtils.isBlank(settingName)) { + return Collections.emptyList(); + } + Long fileId = getImageFileId(settingName); + if (fileId == null) { + return Collections.emptyList(); + } + FileItem file = fileService.getFile(fileId); + if (file == null) { + return Collections.emptyList(); + } else { + String imageContent = Base64.encodeBase64String(file.getAsByte()); + return Collections.singletonList(new PortletInstancePreference(DATA_INIT_PREFERENCE_NAME, imageContent)); + } + } + + private Long getImageFileId(String settingName) { + CMSSetting setting = cmsService.getSetting(OBJECT_TYPE, settingName); + List fileIds = attachmentService.getAttachmentFileIds(OBJECT_TYPE, setting.getName()); + if (CollectionUtils.isEmpty(fileIds)) { + return null; + } else { + return Long.parseLong(fileIds.getFirst()); + } + } + + private String getCmsSettingName(Portlet preferences) { + if (preferences == null) { + return null; + } + Preference settingNamePreference = preferences.getPreference(CMS_SETTING_PREFERENCE_NAME); + return settingNamePreference == null ? null : settingNamePreference.getValue(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/plugin/renderer/LinkPortletInstancePreferencePlugin.java b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/LinkPortletInstancePreferencePlugin.java new file mode 100644 index 000000000..a7f0b9c8e --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/plugin/renderer/LinkPortletInstancePreferencePlugin.java @@ -0,0 +1,75 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.plugin.renderer; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.pom.spi.portlet.Portlet; +import org.exoplatform.portal.pom.spi.portlet.Preference; + +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; +import io.meeds.social.link.model.LinkData; +import io.meeds.social.link.service.LinkService; +import io.meeds.social.util.JsonUtils; + +import lombok.SneakyThrows; + +@Service +public class LinkPortletInstancePreferencePlugin implements PortletInstancePreferencePlugin { + + private static final String CMS_SETTING_PREFERENCE_NAME = "name"; + + private static final String DATA_INIT_PREFERENCE_NAME = "data.init"; + + @Autowired + private LinkService linkService; + + @Override + public String getPortletName() { + return "Links"; + } + + @Override + @SneakyThrows + public List generatePreferences(Application application, + Portlet preferences) { + String settingName = getCmsSettingName(preferences); + if (StringUtils.isBlank(settingName)) { + return Collections.emptyList(); + } + LinkData linkData = linkService.getLinkData(settingName); + return Collections.singletonList(new PortletInstancePreference(DATA_INIT_PREFERENCE_NAME, JsonUtils.toJsonString(linkData))); + } + + private String getCmsSettingName(Portlet preferences) { + if (preferences == null) { + return null; + } + Preference settingNamePreference = preferences.getPreference(CMS_SETTING_PREFERENCE_NAME); + return settingNamePreference == null ? null : settingNamePreference.getValue(); + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/rest/NavigationLayoutRest.java b/layout-service/src/main/java/io/meeds/layout/rest/NavigationLayoutRest.java index c69d0d798..5d2250255 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/NavigationLayoutRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/NavigationLayoutRest.java @@ -61,11 +61,12 @@ public class NavigationLayoutRest { @PostMapping @Secured("users") @Operation(summary = "Create a navigation node", method = "POST", description = "This creates the given navigation node") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "navigation node created"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "403", description = "User not authorized to create the navigation node"), - @ApiResponse(responseCode = "404", description = "Node not found"), - }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "400", description = "Invalid request input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public NodeData createNode( HttpServletRequest request, @RequestBody @@ -86,9 +87,10 @@ public NodeData createNode( @Operation(summary = "Creates a draft navigation node", method = "POST", description = "This creates a daft node with page based on an existing navigation node") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "draft navigation node created"), - @ApiResponse(responseCode = "403", description = "Forbidden operation"), - @ApiResponse(responseCode = "404", description = "Node not found"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public NodeData createDraftNode( HttpServletRequest request, @@ -107,10 +109,11 @@ public NodeData createDraftNode( @PutMapping("{nodeId}") @Secured("users") @Operation(summary = "Update a navigation node", method = "PUT", description = "This updates the given navigation node") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "navigation node updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "403", description = "User not authorized to update the navigation node"), - @ApiResponse(responseCode = "404", description = "Node not found"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "400", description = "Invalid request input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public void updateNode( HttpServletRequest request, @@ -133,9 +136,10 @@ public void updateNode( @DeleteMapping("{nodeId}") @Secured("users") @Operation(summary = "Delete a navigation node ", method = "DELETE", description = "This deletes the given navigation node") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "navigation node deleted"), - @ApiResponse(responseCode = "403", description = "User not authorized to delete the navigation node"), - @ApiResponse(responseCode = "404", description = "Node not found"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public void deleteNode( HttpServletRequest request, @@ -158,8 +162,9 @@ public void deleteNode( @Secured("users") @Operation(summary = "Undo delete a navigation node if not yet effectively deleted", method = "POST", description = "This undo deletes the given navigation node if not yet effectively deleted") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Request fulfilled"), - @ApiResponse(responseCode = "404", description = "Node not found"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public void undoDeleteNode( HttpServletRequest request, @@ -201,7 +206,8 @@ public void moveNode( @GetMapping("{nodeId}") @Operation(summary = "Retrieve node labels", method = "GET", description = "This retrieves node labels") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "Not found"), }) @@ -222,7 +228,8 @@ public NodeData getNode( @GetMapping("{nodeId}/uri") @Operation(summary = "Retrieve node uri", method = "GET", description = "This retrieves node Uri that will allow to access the page") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "Not found"), }) @@ -242,7 +249,8 @@ public String getNodeUri( @GetMapping("{nodeId}/labels") @Operation(summary = "Retrieve node labels", method = "GET", description = "This retrieves node labels") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "Not found"), }) diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PageLayoutRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PageLayoutRest.java index 1aac8f210..4d30924b7 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PageLayoutRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PageLayoutRest.java @@ -68,7 +68,9 @@ public class PageLayoutRest { @GetMapping @Operation(summary = "Retrieve pages", method = "GET", description = "This retrieves pages") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) public List getPages( HttpServletRequest request, @Parameter(description = "Portal site type, possible values: PORTAL, GROUP or USER", @@ -92,8 +94,11 @@ public List getPages( @GetMapping("layout") @Operation(summary = "Retrieve page layout by reference", method = "GET", description = "This retrieves page by reference") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public LayoutModel getPageLayout( HttpServletRequest request, @Parameter(description = "page reference", required = true) @@ -114,8 +119,11 @@ public LayoutModel getPageLayout( @GetMapping("byRef") @Operation(summary = "Retrieve page by reference", method = "GET", description = "This retrieves page by reference") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public PageContext getPage( HttpServletRequest request, @Parameter(description = "page reference", required = true) @@ -133,9 +141,11 @@ public PageContext getPage( @PostMapping @Secured("users") @Operation(summary = "Create a page", method = "POST", description = "This creates the page") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "page created"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "500", description = "Internal server error") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public PageContext createPage( HttpServletRequest request, @RequestBody @@ -152,9 +162,12 @@ public PageContext createPage( @PutMapping("layout") @Secured("users") @Operation(summary = "Updates an existing page layout", method = "PUT", description = "This updates the designated page layout") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "page created"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "500", description = "Internal server error") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "400", description = "Invalid request input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public LayoutModel updatePageLayout( HttpServletRequest request, @Parameter(description = "page display name", required = true) @@ -187,8 +200,11 @@ public LayoutModel updatePageLayout( @PatchMapping(name = "link", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) @Secured("users") @Operation(summary = "Update page link", method = "GET", description = "This updates page link") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updatePageLink( HttpServletRequest request, @Parameter(description = "page display name", required = true) @@ -210,11 +226,11 @@ public void updatePageLink( @Secured("users") @Operation(summary = "Update a page access and edit permission", method = "PATCH", description = "This updates the given page access and edit permission") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Page permissions updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Page not found"), - @ApiResponse(responseCode = "401", description = "Unauthorized operation"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updatePagePermissions( HttpServletRequest request, @Parameter(description = "Page reference", required = true) diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java index dd41caf2d..0f7d00c70 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java @@ -65,7 +65,9 @@ public List getPageTemplates(HttpServletRequest request) { @Secured("users") @Operation(summary = "Retrieve a page template designated by its id", method = "GET", description = "This will retrieve a page template designated by its id") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) public PageTemplate getPageTemplate( HttpServletRequest request, @Parameter(description = "Page template identifier") @@ -77,8 +79,10 @@ public PageTemplate getPageTemplate( @PostMapping @Secured("users") @Operation(summary = "Create a page template", method = "POST", description = "This creates a new page template") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "page created"), - @ApiResponse(responseCode = "400", description = "Invalid query input") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + }) public PageTemplate createPageTemplate( HttpServletRequest request, @RequestBody @@ -93,9 +97,11 @@ public PageTemplate createPageTemplate( @PutMapping("{id}") @Secured("users") @Operation(summary = "Update a page template", method = "PUT", description = "This updates an existing page template") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "page updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updatePageTemplate( HttpServletRequest request, @Parameter(description = "Page template identifier") @@ -116,9 +122,11 @@ public void updatePageTemplate( @DeleteMapping("{id}") @Secured("users") @Operation(summary = "Deletes a page template", method = "DELETE", description = "This deletes an existing page template") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "page deleted"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void deletePageTemplate( HttpServletRequest request, @Parameter(description = "Page template identifier") diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java index 791169701..0c97c7dee 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceCategoryRest.java @@ -57,7 +57,9 @@ public class PortletInstanceCategoryRest { @Secured("users") @Operation(summary = "Retrieve portlet instance categorys", method = "GET", description = "This retrieves portlet instance categorys") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) public List getPortletInstanceCategorys(HttpServletRequest request) { return portletInstanceService.getPortletInstanceCategories(request.getRemoteUser(), request.getLocale(), true); } @@ -66,7 +68,11 @@ public List getPortletInstanceCategorys(HttpServletRequ @Secured("users") @Operation(summary = "Retrieve a portlet instance category designated by its id", method = "GET", description = "This will retrieve a portlet instance category designated by its id") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public PortletInstanceCategory getPortletInstanceCategory( HttpServletRequest request, @Parameter(description = "Portlet instance category identifier") @@ -85,8 +91,10 @@ public PortletInstanceCategory getPortletInstanceCategory( @Secured("users") @Operation(summary = "Create a portlet instance category", method = "POST", description = "This creates a new portlet instance category") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "portlet instance category created"), - @ApiResponse(responseCode = "400", description = "Invalid query input") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + }) public PortletInstanceCategory createPortletInstanceCategory( HttpServletRequest request, @RequestBody @@ -102,9 +110,11 @@ public PortletInstanceCategory createPortletInstanceCategory( @Secured("users") @Operation(summary = "Update a portlet instance category", method = "PUT", description = "This updates an existing portlet instance category") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance category updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updatePortletInstanceCategory( HttpServletRequest request, @Parameter(description = "Portlet instance category identifier") @@ -126,9 +136,11 @@ public void updatePortletInstanceCategory( @Secured("users") @Operation(summary = "Deletes a portlet instance category", method = "DELETE", description = "This deletes an existing portlet instance category") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance category deleted"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void deletePortletInstanceCategory( HttpServletRequest request, @Parameter(description = "Portlet instance category identifier") diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java index 96dee9efc..bf0ec36fd 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletInstanceRest.java @@ -37,6 +37,8 @@ import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.service.PortletInstanceRenderService; import io.meeds.layout.service.PortletInstanceService; import io.swagger.v3.oas.annotations.Operation; @@ -52,12 +54,17 @@ public class PortletInstanceRest { @Autowired - private PortletInstanceService portletInstanceService; + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; @GetMapping @Secured("users") @Operation(summary = "Retrieve portlet instances", method = "GET", description = "This retrieves portlet instances") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + }) public List getPortletInstances( HttpServletRequest request, @Parameter(description = "Portlet instance category identifier") @@ -70,7 +77,11 @@ public List getPortletInstances( @Secured("users") @Operation(summary = "Retrieve a portlet instance designated by its id", method = "GET", description = "This will retrieve a portlet instance designated by its id") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public PortletInstance getPortletInstance( HttpServletRequest request, @Parameter(description = "Portlet instance identifier") @@ -85,11 +96,37 @@ public PortletInstance getPortletInstance( } } + @GetMapping("{id}/preferences") + @Secured("users") + @Operation(summary = "Retrieve a portlet instance preferences designated by its id", + method = "GET", + description = "This will retrieve a portlet instance preferences designated by its id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) + public List getPortletInstancePreferences( + HttpServletRequest request, + @Parameter(description = "Portlet instance identifier") + @PathVariable("id") + long id) { + try { + return portletInstanceRenderService.getPortletInstancePreferences(id, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN); + } + } + @PostMapping @Secured("users") @Operation(summary = "Create a portlet instance", method = "POST", description = "This creates a new portlet instance") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "portlet instance created"), - @ApiResponse(responseCode = "400", description = "Invalid query input") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + }) public PortletInstance createPortletInstance( HttpServletRequest request, @RequestBody @@ -104,9 +141,11 @@ public PortletInstance createPortletInstance( @PutMapping("{id}") @Secured("users") @Operation(summary = "Update a portlet instance", method = "PUT", description = "This updates an existing portlet instance") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updatePortletInstance( HttpServletRequest request, @Parameter(description = "Portlet instance identifier") @@ -127,9 +166,11 @@ public void updatePortletInstance( @DeleteMapping("{id}") @Secured("users") @Operation(summary = "Deletes a portlet instance", method = "DELETE", description = "This deletes an existing portlet instance") - @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "portlet instance deleted"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Object Not found") }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void deletePortletInstance( HttpServletRequest request, @Parameter(description = "Portlet instance identifier") diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java index bc1278ece..786364d8d 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PortletRest.java @@ -44,15 +44,15 @@ public class PortletRest { @GetMapping @Secured({ - "administrators", - "web-contributors", + "administrators", + "web-contributors", }) @Operation( summary = "Retrieve portlets", method = "GET", description = "This retrieves the list of available portlets in the platform") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) public List getPortlets() { return portletService.getPortlets(); diff --git a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java index b1f203540..d5854012d 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java @@ -71,7 +71,7 @@ public class SiteLayoutRest { @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "500", description = "Internal server error"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public ResponseEntity getSiteById( HttpServletRequest request, @@ -107,7 +107,7 @@ public ResponseEntity getSiteById( @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "500", description = "Internal server error"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public ResponseEntity getSite( HttpServletRequest request, @@ -144,8 +144,11 @@ public ResponseEntity getSite( @DeleteMapping @Secured("users") @Operation(summary = "Delete a site", method = "GET", description = "This deletes the given site") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void deleteSite( HttpServletRequest request, @Parameter(description = "site type") @@ -166,8 +169,11 @@ public void deleteSite( @PutMapping @Secured("users") @Operation(summary = "update a site", method = "PUT", description = "This updates the given site") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updateSite( HttpServletRequest request, @RequestBody @@ -185,11 +191,11 @@ public void updateSite( @Secured("users") @Operation(summary = "Update a page access and edit permission", method = "PATCH", description = "This updates the given page access and edit permission") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Page permissions updated"), - @ApiResponse(responseCode = "400", description = "Invalid query input"), - @ApiResponse(responseCode = "404", description = "Page not found"), - @ApiResponse(responseCode = "401", description = "Unauthorized operation"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public void updateSitePermissions( HttpServletRequest request, @RequestBody @@ -206,8 +212,11 @@ public void updateSitePermissions( @PostMapping @Secured("users") @Operation(summary = "create a site", method = "POST", description = "This create a new site") - @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "500", description = "Internal server error"), }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) public ResponseEntity createSite( HttpServletRequest request, @Parameter(description = "site to create", required = true) diff --git a/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java b/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java index 5843cd112..67d25d655 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/model/LayoutModel.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import com.fasterxml.jackson.annotation.JsonInclude; @@ -41,6 +42,8 @@ import org.exoplatform.portal.mop.page.PageKey; import org.exoplatform.portal.pom.spi.portlet.Portlet; +import io.meeds.layout.model.PortletInstancePreference; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -51,69 +54,71 @@ @JsonInclude(value = Include.NON_EMPTY) public class LayoutModel { - protected String id; + protected String id; - protected String storageId; + protected String storageId; - protected String storageName; + protected String storageName; - protected String name; + protected String name; - protected String icon; + protected String icon; - protected String template; + protected String template; - protected String factoryId; + protected String factoryId; - protected String title; + protected String title; - protected String description; + protected String description; - protected String width; + protected String width; - protected String height; + protected String height; - protected String cssClass; + protected String cssClass; - protected String borderColor; + protected String borderColor; - protected String[] accessPermissions; + protected String[] accessPermissions; // Specific to container - protected String profiles; + protected String profiles; + + protected String[] moveAppsPermissions; - protected String[] moveAppsPermissions; + protected String[] moveContainersPermissions; - protected String[] moveContainersPermissions; + protected List preferences; - protected List children; + protected List children; // Specific to applications - private String contentId; + private String contentId; - private boolean showInfoBar; + private boolean showInfoBar; - private boolean showApplicationState = true; + private boolean showApplicationState = true; - private boolean showApplicationMode = true; + private boolean showApplicationMode = true; // Specific to page - private String editPermission; + private String editPermission; @JsonProperty(access = Access.READ_ONLY) - private PageKey pageKey; + private PageKey pageKey; - private String ownerType; + private String ownerType; - private String ownerId; + private String ownerId; - private boolean showMaxWindow; + private boolean showMaxWindow; - private boolean hideSharedLayout; + private boolean hideSharedLayout; - private String type; + private String type; - private String link; + private String link; public LayoutModel(ModelObject model) { init(model); @@ -240,6 +245,12 @@ public static ModelObject toModelObject(LayoutModel layoutModel) { TransientApplicationState transientState = new TransientApplicationState<>(layoutModel.getContentId()); transientState.setOwnerId(layoutModel.getOwnerId()); transientState.setOwnerType(layoutModel.getOwnerType()); + if (CollectionUtils.isNotEmpty(layoutModel.getPreferences())) { + Portlet portlet = new Portlet(); + layoutModel.getPreferences() + .forEach(p -> portlet.setValue(p.getName(), p.getValue())); + transientState.setContentState(portlet); + } state = transientState; } else { throw new IllegalStateException("PortletInstance should either has a storageId or a contentId"); diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java new file mode 100644 index 000000000..37ba44d42 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceRenderService.java @@ -0,0 +1,259 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.gatein.pc.api.PortletInvoker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import org.exoplatform.commons.api.settings.SettingService; +import org.exoplatform.commons.api.settings.SettingValue; +import org.exoplatform.commons.api.settings.data.Context; +import org.exoplatform.commons.api.settings.data.Scope; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.Page; +import org.exoplatform.portal.config.model.TransientApplicationState; +import org.exoplatform.portal.config.serialize.PortletApplication; +import org.exoplatform.portal.mop.SiteKey; +import org.exoplatform.portal.mop.page.PageContext; +import org.exoplatform.portal.mop.page.PageKey; +import org.exoplatform.portal.mop.page.PageState; +import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.pom.spi.portlet.Portlet; +import org.exoplatform.portal.pom.spi.portlet.PortletBuilder; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; + +/** + * A plugin that is used to display a selected portlet instance in the context + * of the PortletEditor page until. This should be changed to use + * {@link PortletInvoker} to process 'view', 'edit' and 'serveResource' switch + * JSR-168 and JSR-286 requirements. But for now, to kkep WebUI based portlets + * working (which doesn't implement the JSRs portlet bridge), we will use this + * trick to allow displaying a portlet instance inside WebUI dynamic container. + */ +@Service +public class PortletInstanceRenderService { + + private static final Context CONTEXT = Context.GLOBAL.id("PORTLET_INSTANCE"); + + private static final Scope SCOPE = + Scope.APPLICATION.id("PORTLET_INSTANCE_APPLICATION"); + + private static final PageKey PORTLET_EDITOR_SYSTEM_PAGE_KEY = new PageKey(SiteKey.portal("global"), + "_portletEditor"); + + @Autowired + private SettingService settingService; + + @Autowired + private LayoutService layoutService; + + @Autowired + private PortletInstanceService portletInstanceService; + + private Application placeholderApplication; + + private Map preferencePlugins = new ConcurrentHashMap<>(); + + public void addPortletInstancePreferencePlugin(PortletInstancePreferencePlugin plugin) { + preferencePlugins.put(plugin.getPortletName(), plugin); + } + + public void removePortletInstancePreferencePlugin(String portletName) { + preferencePlugins.remove(portletName); + } + + public Application getPortletInstanceApplication(String username, // NOSONAR + String portletInstanceId, + String applicationStorageId) throws IllegalAccessException, + ObjectNotFoundException { + if (StringUtils.isNotBlank(portletInstanceId)) { + // Display the portlet instance by id + return getOrCreatePortletInstanceApplication(portletInstanceId, username); + } else if (StringUtils.isNotBlank(applicationStorageId)) { + // Display the app by storage id + return layoutService.getApplicationModel(applicationStorageId); + } else { + return getPlaceholderApplication(); + } + } + + public List getPortletInstancePreferences(long portletInstanceId, + String username) throws IllegalAccessException, + ObjectNotFoundException { + PortletInstance portletInstance = portletInstanceService.getPortletInstance(portletInstanceId, username, null, false); + if (portletInstance == null) { + throw new ObjectNotFoundException(String.format("Portlet Instance with id %s wasn't found", portletInstanceId)); + } + PortletInstancePreferencePlugin plugin = preferencePlugins.get(portletInstance.getContentId().split("/")[1]); + long applicationId = getPortletInstanceApplicationId(portletInstance.getId()); + Application application = layoutService.getApplicationModel(String.valueOf(applicationId)); + if (application == null) { + throw new ObjectNotFoundException(String.format("Application for Portlet Instance with id %s wasn't found", + portletInstanceId)); + } + Portlet preferences = layoutService.load(application.getState(), application.getType()); + if (plugin == null) { + if (preferences == null) { + return Collections.emptyList(); + } else { + List instancePreferences = new ArrayList<>(); + preferences.forEach(p -> instancePreferences.add(new PortletInstancePreference(p.getName(), p.getValue()))); + return instancePreferences; + } + } else { + return plugin.generatePreferences(application, preferences); + } + } + + private Application getOrCreatePortletInstanceApplication(String portletInstanceId, + String userName) throws IllegalAccessException, + ObjectNotFoundException { + PortletInstance portletInstance = portletInstanceService.getPortletInstance(Long.parseLong(portletInstanceId), + userName, + Locale.ENGLISH, + false); + if (portletInstance == null) { + throw new ObjectNotFoundException(String.format("Portlet instance with id %s wasn't found", portletInstanceId)); + } + long applicationId = getPortletInstanceApplicationId(portletInstance.getId()); + if (applicationId == 0) { + return createPortletInstanceApplication(portletInstance); + } else { + try { + return layoutService.getApplicationModel(String.valueOf(applicationId)); + } catch (Exception e) { + return createPortletInstanceApplication(portletInstance); + } + } + } + + @SuppressWarnings("unchecked") + private synchronized Application createPortletInstanceApplication(PortletInstance portletInstance) { + TransientApplicationState state = new TransientApplicationState<>(portletInstance.getContentId()); + + List permissions = portletInstance.getPermissions(); + List preferences = portletInstance.getPreferences(); + if (CollectionUtils.isNotEmpty(preferences)) { + PortletBuilder builder = new PortletBuilder(); + preferences.stream().forEach(pref -> builder.add(pref.getName(), pref.getValue())); + state.setContentState(builder.build()); + } + + PortletApplication portletApplication = new PortletApplication(); + portletApplication.setState(state); + portletApplication.setAccessPermissions(CollectionUtils.isEmpty(permissions) ? new String[] { UserACL.EVERYONE } : + permissions.toArray(new String[0])); + + Page page = getPortletInstanceSystemPage(); + Container container = (Container) page.getChildren().get(0); + ArrayList children = container.getChildren(); + int index; + if (CollectionUtils.isEmpty(children)) { + index = 0; + children = new ArrayList<>(); + } else { + index = children.size(); + children = new ArrayList<>(children); + } + children.add(portletApplication); + container.setChildren(children); + page.setChildren(new ArrayList<>(Collections.singletonList(container))); + layoutService.save(page); + + container = getPortletInstanceSystemContainer(); + Application application = (Application) container.getChildren().get(index); + savePortletInstanceApplicationId(Long.parseLong(application.getStorageId()), + portletInstance.getId()); + return application; + } + + private long getPortletInstanceApplicationId(long portletInstanceId) { + SettingValue settingValue = settingService.get(CONTEXT, SCOPE, String.valueOf(portletInstanceId)); + if (settingValue != null && settingValue.getValue() != null && StringUtils.isNotBlank(settingValue.getValue().toString())) { + return Long.parseLong(settingValue.getValue().toString()); + } else { + return 0; + } + } + + private void savePortletInstanceApplicationId(long applicationStorageId, long portletInstanceId) { + settingService.set(CONTEXT, + SCOPE, + String.valueOf(portletInstanceId), + SettingValue.create(applicationStorageId)); + } + + private Container getPortletInstanceSystemContainer() { + return (Container) getPortletInstanceSystemPage().getChildren().get(0); + } + + private Page getPortletInstanceSystemPage() { + Page page = layoutService.getPage(PORTLET_EDITOR_SYSTEM_PAGE_KEY); + if (page == null) { + page = new Page(); + page.setTitle("Portlet Editor Working Page"); + page.setEditPermission("manager:/platform/administrators"); + page.setPageId(PORTLET_EDITOR_SYSTEM_PAGE_KEY.format()); + + Container container = new Container(); + container.setTemplate("nop"); + page.setChildren(new ArrayList<>(Collections.singletonList(container))); + + PageState pageState = new PageState(page.getTitle(), + page.getDescription(), + false, + null, + Arrays.asList(UserACL.EVERYONE), + page.getEditPermission(), + Arrays.asList(UserACL.EVERYONE), + Arrays.asList(UserACL.EVERYONE)); + layoutService.save(new PageContext(PORTLET_EDITOR_SYSTEM_PAGE_KEY, pageState), page); + page = layoutService.getPage(PORTLET_EDITOR_SYSTEM_PAGE_KEY); + } + return page; + } + + private Application getPlaceholderApplication() { + if (placeholderApplication == null) { + placeholderApplication = new PortletApplication(); + placeholderApplication.setAccessPermissions(new String[] { UserACL.EVERYONE }); + placeholderApplication.setState(new TransientApplicationState<>("layout/PortletEditor")); + } + return placeholderApplication; + } + +} diff --git a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java index 369fef0dc..c5b40c371 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PortletInstanceService.java @@ -29,6 +29,7 @@ import org.springframework.stereotype.Service; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserACL; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.resources.LocaleConfigService; @@ -47,9 +48,10 @@ @Service public class PortletInstanceService { - private static final List EVERYONE_PERMISSIONS_LIST = Collections.singletonList("Everyone"); + private static final List EVERYONE_PERMISSIONS_LIST = Collections.singletonList(UserACL.EVERYONE); - private static final Log LOG = ExoLogger.getLogger(PortletInstanceService.class); + private static final Log LOG = + ExoLogger.getLogger(PortletInstanceService.class); @Autowired private LayoutAclService layoutAclService; @@ -114,7 +116,7 @@ public PortletInstance getPortletInstance(long id, if (!this.hasPermission(portletInstance, username)) { throw new IllegalAccessException(); } - if (expand) { + if (expand && locale != null) { computePortletInstanceAttributes(locale, portletInstance); } return portletInstance; diff --git a/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java index bd8ccbac6..77cf592dc 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstanceImportService.java @@ -198,16 +198,16 @@ protected void importDescriptor(PortletInstanceDescriptor descriptor) { protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d, long oldId) { String descriptorId = d.getNameId(); - LOG.info("Importing Portlet category instance {}", descriptorId); + LOG.debug("Importing Portlet category instance {}", descriptorId); try { PortletInstanceCategory category = savePortletInstanceCategory(d, oldId); - if (forceReimport || oldId == 0 || category.getId() != oldId) { - LOG.info("Importing Portlet instance category {} title translations", descriptorId); + if (category != null && (forceReimport || oldId == 0 || category.getId() != oldId)) { + LOG.debug("Importing Portlet instance category {} title translations", descriptorId); saveCategoryNames(d, category); // Mark as imported setCategorySettingValue(descriptorId, category.getId()); } - LOG.info("Importing Portlet instance category {} finished successfully", descriptorId); + LOG.debug("Importing Portlet instance category {} finished successfully", descriptorId); } catch (Exception e) { LOG.warn("An error occurred while importing portlet instance category {}", descriptorId, e); } @@ -215,16 +215,19 @@ protected void importPortletInstanceCategory(PortletInstanceCategoryDescriptor d protected void importPortletInstance(PortletInstanceDescriptor d, long oldId) { String descriptorId = d.getNameId(); - LOG.info("Importing Portlet instance {}", descriptorId); + LOG.debug("Importing Portlet instance {}", descriptorId); try { PortletInstance portletInstance = savePortletInstance(d, oldId); + if (portletInstance == null) { + return; + } if (forceReimport || oldId == 0 || portletInstance.getId() != oldId) { - LOG.info("Importing Portlet instance {} title translations", descriptorId); + LOG.debug("Importing Portlet instance {} title translations", descriptorId); saveNames(d, portletInstance); - LOG.info("Importing Portlet instance {} description translations", descriptorId); + LOG.debug("Importing Portlet instance {} description translations", descriptorId); saveDescriptions(d, portletInstance); if (StringUtils.isNotBlank(d.getIllustrationPath())) { - LOG.info("Importing Portlet instance {} illustration", descriptorId); + LOG.debug("Importing Portlet instance {} illustration", descriptorId); saveIllustration(portletInstance.getId(), d.getIllustrationPath()); } // Mark as imported @@ -280,6 +283,12 @@ protected PortletInstanceCategory savePortletInstanceCategory(PortletInstanceCat @SneakyThrows protected PortletInstance savePortletInstance(PortletInstanceDescriptor d, long oldId) { PortletDescriptor portlet = portletService.getPortlet(d.getPortletName()); + if (portlet == null) { + LOG.debug("Saving Portlet instance descriptor {} aborted since portlet {} doesn't exist.", + d.getNameId(), + d.getPortletName()); + return null; + } PortletInstance portletInstance = null; if (oldId > 0) { portletInstance = portletInstanceService.getPortletInstance(oldId); diff --git a/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstancePreferencePluginService.java b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstancePreferencePluginService.java new file mode 100644 index 000000000..e25b2a067 --- /dev/null +++ b/layout-service/src/main/java/io/meeds/layout/service/injection/PortletInstancePreferencePluginService.java @@ -0,0 +1,56 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.service.injection; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Service; + +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; +import io.meeds.layout.service.PortletInstanceRenderService; + +/** + * A class to initialize PortletInstanceService to avoid having cyclic + * dependency: PortletInstanceService -> Plugins -> Service Layer Components -> + * PortletInstanceService + */ +@Service +public class PortletInstancePreferencePluginService implements ApplicationContextAware { + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; + + private ApplicationContext applicationContext; + + @PostConstruct + public void init() { + applicationContext.getBeansOfType(PortletInstancePreferencePlugin.class) + .values() + .forEach(portletInstanceRenderService::addPortletInstancePreferencePlugin); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java index 0c41b0b84..48de611da 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java @@ -61,6 +61,7 @@ import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.service.PortletInstanceRenderService; import io.meeds.layout.service.PortletInstanceService; import io.meeds.spring.web.security.PortalAuthenticationManager; import io.meeds.spring.web.security.WebSecurityConfiguration; @@ -94,15 +95,18 @@ public class PortletInstanceRestTest { } @MockBean - private PortletInstanceService portletInstanceService; + private PortletInstanceService portletInstanceService; + + @MockBean + private PortletInstanceRenderService portletInstanceRenderService; @Autowired - private SecurityFilterChain filterChain; + private SecurityFilterChain filterChain; @Autowired - private WebApplicationContext context; + private WebApplicationContext context; - private MockMvc mockMvc; + private MockMvc mockMvc; @BeforeEach void setup() { diff --git a/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java new file mode 100644 index 000000000..69ed463b4 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/service/PortletInstanceRenderServiceTest.java @@ -0,0 +1,241 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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.layout.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import org.exoplatform.commons.api.settings.SettingService; +import org.exoplatform.commons.api.settings.SettingValue; +import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.Container; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.Page; +import org.exoplatform.portal.mop.page.PageContext; +import org.exoplatform.portal.mop.page.PageKey; +import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.pom.spi.portlet.Portlet; + +import io.meeds.layout.model.PortletInstance; +import io.meeds.layout.model.PortletInstancePreference; +import io.meeds.layout.plugin.PortletInstancePreferencePlugin; + +import lombok.SneakyThrows; + +@SpringBootTest(classes = { + PortletInstanceRenderService.class, +}) +@ExtendWith(MockitoExtension.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class PortletInstanceRenderServiceTest { + + private static final String CONTENT_ID = "test/content"; + + private static final String USERNAME = "test"; + + @MockBean + private LayoutAclService layoutAclService; + + @MockBean + private SettingService settingService; + + @MockBean + private LayoutService layoutService; + + @MockBean + private PortletInstanceService portletInstanceService; + + @Autowired + private PortletInstanceRenderService portletInstanceRenderService; + + @Mock + private Application application; + + @Mock + private PortletInstance portletInstance; + + @Mock + private Page page; + + @Mock + private Container container; + + @Mock + private PortletInstancePreferencePlugin plugin; + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByInstanceId() { + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(portletInstanceService.getPortletInstance(2, + USERNAME, + Locale.ENGLISH, + false)).thenReturn(portletInstance); + when(layoutService.getApplicationModel("3")).thenReturn(application); + when(portletInstance.getId()).thenReturn(2l); + + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null); + assertEquals(application, portletInstanceApplication); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationPlaceholder() { + assertNotNull(portletInstanceRenderService.getPortletInstanceApplication(null, null, null)); + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWhenNoPlugin() { + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + when(layoutService.getApplicationModel("3")).thenReturn(application); + Portlet portlet = new Portlet(); + when(layoutService.load(any(), any())).thenReturn(portlet); + portlet.setValue("test", "testValue"); + + List portletInstancePreferences = + portletInstanceRenderService.getPortletInstancePreferences(2, + USERNAME); + assertNotNull(portletInstancePreferences); + assertEquals(1, portletInstancePreferences.size()); + assertEquals("test", portletInstancePreferences.get(0).getName()); + assertEquals("testValue", portletInstancePreferences.get(0).getValue()); + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWithPlugin() { + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + when(layoutService.getApplicationModel("3")).thenReturn(application); + when(plugin.getPortletName()).thenReturn("content"); + portletInstanceRenderService.addPortletInstancePreferencePlugin(plugin); + try { + when(plugin.generatePreferences(any(), any())).thenReturn(Collections.singletonList(new PortletInstancePreference("test", + "value"))); + + List portletInstancePreferences = + portletInstanceRenderService.getPortletInstancePreferences(2, + USERNAME); + assertNotNull(portletInstancePreferences); + assertEquals(1, portletInstancePreferences.size()); + assertEquals("test", portletInstancePreferences.get(0).getName()); + assertEquals("value", portletInstancePreferences.get(0).getValue()); + } finally { + portletInstanceRenderService.removePortletInstancePreferencePlugin(plugin.getPortletName()); + } + } + + @Test + @SneakyThrows + public void getPortletInstancePreferencesWhenNoPluginNoPreferences() { + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(portletInstanceService.getPortletInstance(2, USERNAME, null, false)).thenReturn(portletInstance); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + when(portletInstance.getId()).thenReturn(2l); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + assertThrows(ObjectNotFoundException.class, () -> portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + + when(layoutService.getApplicationModel("3")).thenReturn(application); + assertNotNull(portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME)); + assertEquals(0, portletInstanceRenderService.getPortletInstancePreferences(2, USERNAME).size()); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByInstanceIdAndCreateApplication() { + assertThrows(ObjectNotFoundException.class, + () -> portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null)); + when(portletInstanceService.getPortletInstance(2, + USERNAME, + Locale.ENGLISH, + false)).thenReturn(portletInstance); + + PortletInstancePreference preference = new PortletInstancePreference(); + preference.setName("prefName"); + preference.setValue("prefValue"); + when(portletInstance.getPreferences()).thenReturn(Collections.singletonList(preference)); + when(portletInstance.getContentId()).thenReturn(CONTENT_ID); + + doAnswer(invocation -> { + when(layoutService.getPage(any(PageKey.class))).thenReturn(page); + ArrayList list = new ArrayList<>(); + list.add(container); + when(page.getChildren()).thenReturn(list); + + ArrayList apps = new ArrayList<>(); + apps.add(application); + when(container.getChildren()).thenReturn(apps); + return null; + }).when(layoutService).save(any(PageContext.class), any(Page.class)); + + doAnswer(invocation -> { + ArrayList children = invocation.getArgument(0); + children.set(children.size() - 1, application); + when(container.getChildren()).thenReturn(children); + when(application.getStorageId()).thenReturn("3"); + return null; + }).when(container).setChildren(any()); + + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, "2", null); + assertEquals(application, portletInstanceApplication); + } + + @Test + @SneakyThrows + public void getPortletInstanceApplicationByApplicationId() { + when(settingService.get(any(), any(), eq("2"))).thenReturn(new SettingValue("3")); + + when(layoutService.getApplicationModel("3")).thenReturn(application); + Application portletInstanceApplication = portletInstanceRenderService.getPortletInstanceApplication(USERNAME, null, "3"); + assertEquals(application, portletInstanceApplication); + } + +} diff --git a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties index 74fac0347..7b8ea45a6 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -178,6 +178,7 @@ portlets.menu.open=Open Menu portlets.label.instanceMenu={0} portlets.label.menu={0} portlets.label.closeMenu=Close Menu +portlets.label.editInstance=Edit Instance portlets.label.editLayout=Edit Layout portlets.label.system.noEditLayout=This portlet instance's layout cannot be updated portlets.label.editProperties=Edit Properties @@ -287,3 +288,5 @@ portlets.instancePreview=Preview portlets.uploadPreviewTitle=Upload an illustration for portlet instance portlets.label.createInstance=Create instance portlets.noPreviewAvailable=No preview available + +layout.editPortletInstance=Edit portlet instance {0} diff --git a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml index f8b312a89..121665df9 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -218,13 +218,13 @@ extensionRegistry - imageCropper + commonVueComponents commonLayoutComponents - commonVueComponents + imageCropper translationField @@ -309,5 +309,37 @@ + + PortletEditor + + portlet-editor-group + + + extensionRegistry + + + commonVueComponents + + + commonLayoutComponents + + + attachImage + + + vue + + + vuetify + + + eXoVueI18n + + + + diff --git a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml index 13672bd49..365aef8b0 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml @@ -78,6 +78,27 @@ + + PortletEditor + Portlet Editor Portlet + org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet + + portlet-view-dispatched-file-path + /html/portletEditor.html + + -1 + PUBLIC + + text/html + + en + locale.portlet.LayoutEditor + + Portlet Editor + Portlet Editor Management + + + PageTemplatesManagement Page Templates Management Portlet diff --git a/layout-webapp/src/main/webapp/groovy/portal/webui/container/UIPageLayout.gtmpl b/layout-webapp/src/main/webapp/groovy/portal/webui/container/UIPageLayout.gtmpl index 80c487683..6cf8588ec 100644 --- a/layout-webapp/src/main/webapp/groovy/portal/webui/container/UIPageLayout.gtmpl +++ b/layout-webapp/src/main/webapp/groovy/portal/webui/container/UIPageLayout.gtmpl @@ -1,7 +1,12 @@ <% - import org.exoplatform.portal.application.PortalRequestContext; + import java.util.List; + import java.util.ArrayList; + import io.meeds.layout.service.NavigationLayoutService; + import org.exoplatform.portal.application.PortalRequestContext; + import org.exoplatform.portal.webui.application.UIPortlet; + PortalRequestContext rcontext = PortalRequestContext.getCurrentInstance(); NavigationLayoutService navigationLayoutService = uicomponent.getApplicationComponent(NavigationLayoutService.class); @@ -9,6 +14,17 @@ def jsManager = _ctx.getRequestContext().getJavascriptManager(); jsManager.require("PORTLET/layout/PageLayout"); + + if (!rcontext.isMaximizePortlet()) { + List portlets = new ArrayList<>(); + rcontext.getUiPage().findComponentOfType(portlets, UIPortlet.class); + for (int i =0; i < portlets.size(); i++) { + def portletId = portlets.get(i).getId(); + if (portletId != null) { + rcontext.getResponse().addHeader("Link", "<" + rcontext.getRequest().getRequestURI() + "?maximizedPortletId=" + portletId + "&showMaxWindow=true&hideSharedLayout=true&maximizedPortletMode=VIEW>; rel=preload; as=fetch; crossorigin=use-credentials", false); + } + } + } %>
diff --git a/layout-webapp/src/main/webapp/html/portletEditor.html b/layout-webapp/src/main/webapp/html/portletEditor.html new file mode 100644 index 000000000..de9f3a29c --- /dev/null +++ b/layout-webapp/src/main/webapp/html/portletEditor.html @@ -0,0 +1,7 @@ +
+
+ +
+
diff --git a/layout-webapp/src/main/webapp/vue-app/common-illustration/main.js b/layout-webapp/src/main/webapp/vue-app/common-illustration/main.js new file mode 100644 index 000000000..22ea0a501 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-illustration/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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'; diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/main.js b/layout-webapp/src/main/webapp/vue-app/common-layout-components/main.js new file mode 100644 index 000000000..22ea0a501 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout-components/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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'; diff --git a/layout-webapp/src/main/webapp/vue-app/common-page-layout/main.js b/layout-webapp/src/main/webapp/vue-app/common-page-layout/main.js new file mode 100644 index 000000000..22ea0a501 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-page-layout/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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'; diff --git a/layout-webapp/src/main/webapp/vue-app/common-page-template/main.js b/layout-webapp/src/main/webapp/vue-app/common-page-template/main.js new file mode 100644 index 000000000..22ea0a501 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-page-template/main.js @@ -0,0 +1,20 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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'; diff --git a/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js index 496fa61c9..65afb0eb8 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js +++ b/layout-webapp/src/main/webapp/vue-app/common-portlets/js/PortletInstanceService.js @@ -43,6 +43,19 @@ export function getPortletInstance(id) { }); } +export function getPortletInstancePreferences(id) { + return fetch(`/layout/rest/portlet/instances/${id}/preferences`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving portlet instance preferences'); + } else { + return resp.json(); + } + }); +} + export function createPortletInstance(portletInstance) { return fetch('/layout/rest/portlet/instances', { credentials: 'include', diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js b/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js index b41a82492..94efeba70 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js +++ b/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js @@ -18,6 +18,11 @@ */ export function installApplication(navUri, applicationStorageId, applicationElement, applicationMode) { + return getApplicationContent(navUri, applicationStorageId, applicationMode) + .then(applicationContent => handleApplicationContent(applicationContent, applicationElement, applicationMode)); +} + +export function getApplicationContent(navUri, applicationStorageId, applicationMode) { return fetch(`/portal${navUri}?maximizedPortletId=${applicationStorageId}&showMaxWindow=true&hideSharedLayout=true&maximizedPortletMode=${applicationMode || 'VIEW'}`, { credentials: 'include', method: 'GET', @@ -29,11 +34,10 @@ export function installApplication(navUri, applicationStorageId, applicationElem } else { throw new Error('The retrieved page is not a portal page'); } - }) - .then(applicationContent => handleApplicationContent(applicationContent, applicationElement, applicationMode)); + }); } -function handleApplicationContent(applicationContent, applicationElement) { +export function handleApplicationContent(applicationContent, applicationElement) { const newHeadContent = applicationContent.substring(applicationContent.search('/g)[0].length, applicationContent.search('')); let newBodyContent = applicationContent.substring(applicationContent.search('/g)[0].length, applicationContent.lastIndexOf('')); newBodyContent = installNewCSS(newHeadContent, newBodyContent); diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue index edd9a8f8d..4d902ad8c 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue @@ -117,10 +117,10 @@ export default { return this.section?.template === this.$layoutUtils.flexTemplate; }, applicationTitle() { - return this.$root.applicationCategories?.flatMap?.(c => c.applications)?.find?.(a => a?.contentId === this.container?.contentId)?.displayName || this.container?.title || ''; + return this.$root.portletInstanceCategories?.flatMap?.(c => c.applications)?.find?.(a => a?.contentId === this.container?.contentId)?.displayName || this.container?.title || ''; }, applicationCategory() { - return this.applicationTitle && this.$root.applicationCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); + return this.applicationTitle && this.$root.portletInstanceCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); }, applicationCategoryTitle() { return this.applicationCategory?.displayName || ''; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue index 7f663f8e2..a85f00d43 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue @@ -176,7 +176,7 @@ export default { return !this.isDynamicSection && this.container?.children?.[0]?.title || ''; }, applicationCategory() { - return this.applicationTitle && this.$root.applicationCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); + return this.applicationTitle && this.$root.portletInstanceCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); }, applicationCategoryTitle() { return this.applicationCategory?.displayName || ''; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue index eb476eba6..cda35fc1d 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue @@ -23,6 +23,7 @@ ref="drawer" id="addApplicationDrawer" v-model="drawer" + :loading="$root.loadingPortletInstances" allow-expand right @closed="$root.$emit('layout-application-drawer-closed')"> @@ -41,7 +42,7 @@ focusable flat> c.label = this.$te(`layout.${c.name}`) ? this.$t(`layout.${c.name}`) : c.name); - return applicationCategories; + portletInstanceCategories() { + const portletInstanceCategories = this.$root.portletInstanceCategories.slice(); + portletInstanceCategories.forEach(c => c.label = this.$te(`layout.${c.name}`) ? this.$t(`layout.${c.name}`) : c.name); + return portletInstanceCategories; }, applications() { - return this.$root.allApplications; + return this.$root.portletInstances; }, otherApplications() { - return this.allApplications.filter(a => !this.applications.find(app => app.contentId === a.contentId)); + return this.portletInstances.filter(a => !this.applications.find(app => app.contentId === a.contentId)); }, otherCategories() { return this.otherApplications.reduce((otherCategories, application) => { @@ -110,6 +111,7 @@ export default { }, methods: { open() { + this.$root.$emit('layout-editor-portlet-instances-refresh'); this.$refs.drawer.endLoading(); this.$refs.drawer.open(); }, @@ -123,7 +125,7 @@ export default { loadMore() { this.$refs.drawer.startLoading(); this.$portletService.getPortlets() - .then(applications => this.allApplications = applications) + .then(applications => this.portletInstances = applications) .finally(() => { this.canLoadMore = false; this.$refs.drawer.endLoading(); diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue index e64d9f37d..73c1c5e68 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue @@ -307,10 +307,10 @@ export default { return this.container?.children?.[0]?.contentId || this.container?.contentId; }, application() { - return this.$root.allApplications?.find?.(a => a?.contentId === this.applicationContentId); + return this.$root.portletInstances?.find?.(a => a?.contentId === this.applicationContentId); }, applicationCategory() { - return this.applicationTitle && this.$root.applicationCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); + return this.applicationTitle && this.$root.portletInstanceCategories?.find?.(c => c?.applications?.find?.(a => a?.displayName === this.applicationTitle)); }, supportedModes() { return this.application?.supportedModes || []; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js b/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js index 48d38ebd6..9d710a850 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js @@ -88,6 +88,7 @@ export const applicationModel = { showInfoBar: false, showApplicationState: false, showApplicationMode: true, + preferences: null, theme: null, width: null, height: null, @@ -347,6 +348,7 @@ export function newApplication(parentContainer, appFromRegistry, append) { const application = JSON.parse(JSON.stringify(applicationModel)); application.contentId = appFromRegistry.contentId; application.title = appFromRegistry.displayName; + application.preferences = appFromRegistry.preferences; application.showApplicationMode = true; if (append && parentContainer.children) { parentContainer.children.push(application); diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js b/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js index 738235fc4..3c9a5fdc0 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/main.js @@ -18,8 +18,7 @@ */ import './initComponents.js'; -import '../common/initComponents.js'; -import '../common-page-template/initComponents.js'; +import '../common-page-template/main.js'; import '../common-portlets/main.js'; import './extensions.js'; @@ -27,7 +26,7 @@ import './services.js'; // get overridden components if exists if (extensionRegistry) { - const components = extensionRegistry.loadComponents('layoutEditor'); + const components = extensionRegistry.loadComponents('LayoutEditor'); if (components && components.length > 0) { components.forEach(cmp => { Vue.component(cmp.componentName, cmp.componentOptions); @@ -57,8 +56,9 @@ export function init() { hoveredSectionId: null, hoveredSection: null, hoveredApplication: null, - applicationCategories: null, - allApplications: null, + portletInstanceCategories: null, + portletInstances: null, + loadingPortletInstances: false, branding: null, displayMode: 'desktop', layout: null, @@ -143,12 +143,10 @@ export function init() { }, created() { document.addEventListener('extension-layout-editor-container-updated', this.refreshContainerTypes); + this.$on('layout-editor-portlet-instances-refresh', this.refreshPortletInstances); document.addEventListener('drawerOpened', this.setDrawerOpened); document.addEventListener('drawerClosed', this.setDrawerClosed); - this.$portletInstanceCategoryService.getPortletInstanceCategories() - .then(categories => this.applicationCategories = categories); - this.$portletInstanceService.getPortletInstances() - .then(applications => this.allApplications = applications.filter(a => !a.disabled)); + this.refreshPortletInstances(); this.$brandingService.getBrandingInformation() .then(data => this.branding = data); }, @@ -159,6 +157,14 @@ export function init() { setDrawerClosed() { this.drawerOpened--; }, + refreshPortletInstances() { + this.loadingPortletInstances = true; + return this.$portletInstanceCategoryService.getPortletInstanceCategories() + .then(categories => this.portletInstanceCategories = categories) + .then(() => this.$portletInstanceService.getPortletInstances()) + .then(applications => this.portletInstances = applications.filter(a => !a.disabled)) + .finally(() => this.loadingPortletInstances = false); + }, refreshContainerTypes() { this.containerTypes = extensionRegistry.loadExtensions('layout-editor', 'container'); }, diff --git a/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/Application.vue b/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/Application.vue index 30ae71fee..6d59bd6b9 100644 --- a/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/Application.vue +++ b/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/Application.vue @@ -38,6 +38,10 @@ export default { default: null, }, }, + data: () => ({ + applicationContent: null, + contentRetrieved: false, + }), computed: { nodeUri() { return this.$root.nodeUri; @@ -86,20 +90,43 @@ export default { return this.container.cssClass || ''; }, }, + watch: { + portletId() { + this.retrieveData(); + }, + nodeUri() { + this.retrieveData(); + }, + applicationContent() { + if (this.applicationContent) { + this.installApplication(); + } + }, + }, + created() { + this.retrieveData(); + }, mounted() { this.installApplication(); }, methods: { installApplication() { - if (this.$refs.content - && this.nodeUri) { - if (this.portletId) { - this.$applicationUtils.installApplication(this.nodeUri, this.portletId, this.$refs.content); - } else { + if (this.$refs.content && this.nodeUri) { + if (!this.portletId) { console.warn(`Application '${this.contentId}' doesn't have a storageId neither and id`); // eslint-disable-line no-console + } else if (this.applicationContent) { + this.$applicationUtils.handleApplicationContent(this.applicationContent, this.$refs.content); + this.applicationContent = null; } } }, + retrieveData() { + if (this.portletId && this.nodeUri && !this.contentRetrieved) { + this.contentRetrieved = true; + this.$applicationUtils.getApplicationContent(this.nodeUri, this.portletId) + .then(applicationContent => this.applicationContent = applicationContent); + } + }, hasUnit(length) { return Number.isNaN(Number(length)); }, diff --git a/layout-webapp/src/main/webapp/vue-app/page-templates-management/main.js b/layout-webapp/src/main/webapp/vue-app/page-templates-management/main.js index e1aea4841..a636e97fb 100644 --- a/layout-webapp/src/main/webapp/vue-app/page-templates-management/main.js +++ b/layout-webapp/src/main/webapp/vue-app/page-templates-management/main.js @@ -18,8 +18,7 @@ */ import './initComponents.js'; -import '../common/initComponents.js'; -import '../common-page-template/initComponents.js'; +import '../common-page-template/main.js'; import '../common-illustration/initComponents.js'; // get overridden components if exists diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue new file mode 100644 index 000000000..d5259360a --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/PortletEditor.vue @@ -0,0 +1,38 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Application.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Application.vue new file mode 100644 index 000000000..34bfcbd80 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Application.vue @@ -0,0 +1,78 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue new file mode 100644 index 000000000..9bd34d811 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Cell.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue new file mode 100644 index 000000000..c1b50ac18 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/CellResizeButton.vue @@ -0,0 +1,57 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue new file mode 100644 index 000000000..a9f693499 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/content/Content.vue @@ -0,0 +1,27 @@ + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue new file mode 100644 index 000000000..79c9684e9 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/Toolbar.vue @@ -0,0 +1,45 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue new file mode 100644 index 000000000..509c1c0f0 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/components/toolbar/actions/SaveButton.vue @@ -0,0 +1,58 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js new file mode 100644 index 000000000..f2bc4d6f8 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/initComponents.js @@ -0,0 +1,42 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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 PortletEditor from './components/PortletEditor.vue'; + +import Toolbar from './components/toolbar/Toolbar.vue'; +import SaveButton from './components/toolbar/actions/SaveButton.vue'; + +import Content from './components/content/Content.vue'; +import Cell from './components/content/Cell.vue'; +import CellResizeButton from './components/content/CellResizeButton.vue'; +import Application from './components/content/Application.vue'; + +const components = { + 'portlet-editor': PortletEditor, + 'portlet-editor-toolbar': Toolbar, + 'portlet-editor-toolbar-save-button': SaveButton, + 'portlet-editor-content': Content, + 'portlet-editor-cell': Cell, + 'portlet-editor-cell-resize-button': CellResizeButton, + 'portlet-editor-application': Application, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js new file mode 100644 index 000000000..db10787ac --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/portlet-editor/main.js @@ -0,0 +1,68 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 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 '../common-portlets/main.js'; + +// get overridden components if exists +if (extensionRegistry) { + const components = extensionRegistry.loadComponents('PortletEditor'); + if (components && components.length > 0) { + components.forEach(cmp => { + Vue.component(cmp.componentName, cmp.componentOptions); + }); + } +} + +const appId = 'portletEditor'; + +//getting language of the PLF +const lang = eXo?.env.portal.language || 'en'; + +//should expose the locale ressources as REST API +const url = `${eXo.env.portal.context}/${eXo.env.portal.rest}/i18n/bundle/locale.portlet.LayoutEditor-${lang}.json`; + +export function init() { + exoi18n.loadLanguageAsync(lang, url) + .then(i18n => { + // init Vue app when locale ressources are ready + Vue.createApp({ + template: ``, + vuetify: Vue.prototype.vuetifyOptions, + data: { + portletInstanceId: null, + portletInstance: null, + }, + i18n, + created() { + this.portletInstanceId = this.getQueryParam('id'); + this.$portletInstanceService.getPortletInstance(this.portletInstanceId) + .then(data => this.portletInstance = data) + .finally(() => this.$applicationLoaded()); + }, + methods: { + getQueryParam(paramName) { + const uri = window.location.search.substring(1); + const params = new URLSearchParams(uri); + return params.get(paramName); + }, + }, + }, `#${appId}`, 'Portlet Editor'); + }); +} diff --git a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue index f5d3fa620..434532a6c 100644 --- a/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue +++ b/layout-webapp/src/main/webapp/vue-app/portlets/components/instances/Menu.vue @@ -19,9 +19,8 @@ -->