From 3988775d41201bdecc4ce65d69322be5f8de14c6 Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Wed, 22 Jan 2025 13:09:08 +0100 Subject: [PATCH] feat: Add Site Layout Editor Portlet - MEED-8194 - Meeds-io/MIPs#175 This change will initiate a new porllet to allow site layout edition. --- .../io/meeds/layout/rest/PageLayoutRest.java | 2 +- .../io/meeds/layout/rest/SiteLayoutRest.java | 211 +++++++--- .../meeds/layout/rest/model/LayoutModel.java | 34 +- .../layout/rest/util/RestEntityBuilder.java | 7 +- .../layout/service/PageLayoutService.java | 7 +- .../layout/service/SiteLayoutService.java | 45 ++- .../meeds/layout/rest/SiteLayoutRestTest.java | 291 ++++++++++++- .../locale/portlet/LayoutEditor_en.properties | 10 + .../portlet/SiteManagement_en.properties | 5 +- .../main/webapp/WEB-INF/gatein-resources.xml | 49 ++- .../webapp/WEB-INF/jsp/siteLayoutEditor.jsp | 14 + .../src/main/webapp/WEB-INF/portlet.xml | 23 ++ .../components/coediting/Coediting.vue | 4 +- .../components/content/base/CellsDropBox.vue | 0 .../content/base/CellsSelectionBox.vue | 0 .../components/content/base/ContainerBase.vue | 52 +-- .../content/base/ContainerExtension.vue | 0 .../content/common/ApplicationCard.vue | 0 .../common/ApplicationCategoryCard.vue | 0 .../content/common/ApplicationMenu.vue | 0 .../content/common/CellResizeButton.vue | 0 .../components/content/common/SectionMenu.vue | 0 .../content/common/SectionSelectionGrid.vue | 0 .../common/SectionSelectionGridCell.vue | 0 .../content/container/Application.vue | 3 - .../components/content/container/Cell.vue | 2 +- .../content/container/Container.vue | 0 .../components/content/container/PageBody.vue | 63 +++ .../components/content/container/Section.vue | 0 .../components/content/container/Site.vue | 49 +++ .../content/container/SiteBannerCell.vue | 198 +++++++++ .../content/container/SiteBannerSection.vue | 113 ++++++ .../content/container/SiteMiddleBody.vue | 88 ++++ .../content/container/SiteSidebarCell.vue | 166 ++++++++ .../content/container/SiteSidebarSection.vue | 103 +++++ .../components/dialog/EditPortletDialog.vue | 0 .../drawer/AddApplicationDrawer.vue | 0 .../drawer/EditApplicationDrawer.vue | 0 .../SelectApplicationCategoryDrawer.vue | 0 .../form/BackgroundImageAttachment.vue | 0 .../components/form/BackgroundInput.vue | 0 .../components/form/BorderInput.vue | 0 .../components/form/BorderRadiusInput.vue | 0 .../components/form/BorderRadiusSelector.vue | 0 .../components/form/ColorPicker.vue | 0 .../components/form/MarginInput.vue | 0 .../components/form/SectionMarginInput.vue | 0 .../components/form/SectionTemplate.vue | 0 .../components/form/TextInput.vue | 0 .../extensions.js | 53 ++- .../vue-app/common-layout/initComponents.js | 109 +++++ .../js/CoeditingService.js | 0 .../js/LayoutUtils.js | 70 +++- .../main/webapp/vue-app/common-layout/main.js | 22 + .../services.js | 18 +- .../ManagePermissionsDrawer.vue | 0 .../PermissionTypeSelector.vue | 0 .../SiteAccessPermissions.vue | 0 .../manage-permissions/SiteEditPermission.vue | 0 .../site-navigation/NodeIconPickerDrawer.vue | 0 .../components/site-navigation/NodeItem.vue | 0 .../site-navigation/NodeItemMenu.vue | 0 .../components/site-navigation/NodesList.vue | 0 .../site-navigation/SiteNavigationDrawer.vue | 0 .../SiteNavigationElementDrawer.vue | 0 .../SiteNavigationExistingPageElement.vue | 0 .../SiteNavigationNewPageElement.vue | 0 .../SiteNavigationNewPageElementItem.vue | 0 .../SiteNavigationNewPageElementItemsList.vue | 0 .../SiteNavigationNodeDrawer.vue | 0 .../SiteNavigationPageElement.vue | 0 .../SiteNavigationPageSuggester.vue | 0 .../SiteNavigationScheduleDatePickers.vue | 0 .../SiteNavigationSiteSuggester.vue | 0 .../initComponents.js | 0 .../main.js | 0 .../vue-app/common/js/SiteLayoutService.js | 69 ++++ .../vue-app/layout-editor/initComponents.js | 67 --- .../main/webapp/vue-app/layout-editor/main.js | 4 +- .../webapp/vue-app/section-editor/main.js | 5 +- .../webapp/vue-app/section-template/main.js | 1 - .../components/SiteLayoutEditor.vue | 97 +++++ .../components/content/Content.vue | 382 ++++++++++++++++++ .../content/common/BannerSectionMenu.vue | 243 +++++++++++ .../content/common/SidebarSectionMenu.vue | 155 +++++++ .../components/toolbar/Toolbar.vue | 53 +++ .../toolbar/actions/HistoryButtons.vue | 88 ++++ .../toolbar/actions/MobilePreviewButton.vue | 55 +++ .../components/toolbar/actions/SaveButton.vue | 72 ++++ .../actions/SiteEditSectionsButton.vue | 40 ++ .../toolbar/actions/SitePropertiesButton.vue | 40 ++ .../site-layout-editor/initComponents.js | 50 +++ .../webapp/vue-app/site-layout-editor/main.js | 211 ++++++++++ .../site-management/components/main/Menu.vue | 78 +++- .../webapp/vue-app/site-management/main.js | 2 +- .../webapp/vue-app/site-navigation/main.js | 2 +- .../main/webapp/vue-app/site-template/main.js | 3 +- layout-webapp/webpack.prod.js | 1 + 98 files changed, 3314 insertions(+), 215 deletions(-) create mode 100644 layout-webapp/src/main/webapp/WEB-INF/jsp/siteLayoutEditor.jsp rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/coediting/Coediting.vue (97%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/base/CellsDropBox.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/base/CellsSelectionBox.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/base/ContainerBase.vue (86%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/base/ContainerExtension.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/ApplicationCard.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/ApplicationCategoryCard.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/ApplicationMenu.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/CellResizeButton.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/SectionMenu.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/SectionSelectionGrid.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/common/SectionSelectionGridCell.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/container/Application.vue (99%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/container/Cell.vue (99%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/container/Container.vue (100%) create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/PageBody.vue rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/content/container/Section.vue (100%) create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Site.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerCell.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerSection.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteMiddleBody.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarCell.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarSection.vue rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/dialog/EditPortletDialog.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/drawer/AddApplicationDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/drawer/EditApplicationDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/drawer/SelectApplicationCategoryDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/BackgroundImageAttachment.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/BackgroundInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/BorderInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/BorderRadiusInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/BorderRadiusSelector.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/ColorPicker.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/MarginInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/SectionMarginInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/SectionTemplate.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/components/form/TextInput.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/extensions.js (53%) create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/js/CoeditingService.js (100%) rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/js/LayoutUtils.js (94%) create mode 100644 layout-webapp/src/main/webapp/vue-app/common-layout/main.js rename layout-webapp/src/main/webapp/vue-app/{layout-editor => common-layout}/services.js (67%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/manage-permissions/ManagePermissionsDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/manage-permissions/PermissionTypeSelector.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/manage-permissions/SiteAccessPermissions.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/manage-permissions/SiteEditPermission.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/NodeIconPickerDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/NodeItem.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/NodeItemMenu.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/NodesList.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationElementDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationExistingPageElement.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationNewPageElement.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationNewPageElementItem.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationNewPageElementItemsList.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationNodeDrawer.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationPageElement.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationPageSuggester.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationScheduleDatePickers.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/components/site-navigation/SiteNavigationSiteSuggester.vue (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/initComponents.js (100%) rename layout-webapp/src/main/webapp/vue-app/{common-layout-components => common-sites}/main.js (100%) create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/SiteLayoutEditor.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/Content.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/BannerSectionMenu.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/SidebarSectionMenu.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/Toolbar.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/HistoryButtons.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/MobilePreviewButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SaveButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SiteEditSectionsButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SitePropertiesButton.vue create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/initComponents.js create mode 100644 layout-webapp/src/main/webapp/vue-app/site-layout-editor/main.js 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 db3bf1385..bfb4e8d31 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 @@ -195,7 +195,7 @@ public LayoutModel updatePageLayout( LayoutModel layoutModel) { try { pageLayoutService.updatePageLayout(pageRef, - RestEntityBuilder.fromLayoutModel(layoutModel), + layoutModel.toPage(), publish.orElse(false).booleanValue(), request.getRemoteUser()); return getPageLayout(request, pageRef, 0, false, expand); 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 a1417901b..d7cdbab38 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 @@ -20,7 +20,6 @@ package io.meeds.layout.rest; import java.util.Locale; -import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,18 +36,22 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.server.ResponseStatusException; import org.exoplatform.commons.ObjectAlreadyExistsException; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.model.ModelObject; import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.mop.SiteKey; +import org.exoplatform.portal.mop.service.LayoutService; import org.exoplatform.social.rest.entity.SiteEntity; import io.meeds.layout.model.NodeLabel; import io.meeds.layout.model.PermissionUpdateModel; import io.meeds.layout.model.SiteCreateModel; import io.meeds.layout.model.SiteUpdateModel; +import io.meeds.layout.rest.model.LayoutModel; import io.meeds.layout.rest.util.RestEntityBuilder; import io.meeds.layout.service.SiteLayoutService; @@ -67,14 +70,19 @@ public class SiteLayoutRest { @Autowired private SiteLayoutService siteLayoutService; + @Autowired + private LayoutService layoutService; + @GetMapping("{siteId}") @Operation(summary = "Gets a specific site by its id", description = "Gets site by id", method = "GET") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "304", description = "Not modified"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public ResponseEntity getSiteById( + WebRequest webRequest, HttpServletRequest request, @Parameter(description = "site id") @PathVariable("siteId") @@ -83,7 +91,8 @@ public ResponseEntity getSiteById( @RequestParam(name = "lang", required = false) String lang) throws Exception { try { - PortalConfig site = siteLayoutService.getSite(siteId, request.getRemoteUser()); + PortalConfig site = siteLayoutService.getSite(siteId, + request.getRemoteUser()); Locale locale; if (StringUtils.isBlank(lang)) { locale = request.getLocale(); @@ -93,9 +102,14 @@ public ResponseEntity getSiteById( SiteEntity siteEntity = RestEntityBuilder.toSiteEntity(site, request, locale); - return ResponseEntity.ok() - .eTag(String.valueOf(Objects.hash(siteEntity, locale))) - .body(siteEntity); + String eTag = String.valueOf(siteEntity.hashCode()); + if (webRequest.checkNotModified(eTag)) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); + } else { + return ResponseEntity.ok() + .eTag(eTag) + .body(siteEntity); + } } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -106,11 +120,13 @@ public ResponseEntity getSiteById( @GetMapping @Operation(summary = "Gets a specific site by its type and name", description = "Gets site its type and name", method = "GET") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "304", description = "Not modified"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public ResponseEntity getSite( + WebRequest webRequest, HttpServletRequest request, @Parameter(description = "site type") @RequestParam("siteType") @@ -122,19 +138,48 @@ public ResponseEntity getSite( @RequestParam(name = "lang", required = false) String lang) throws Exception { try { - PortalConfig site = siteLayoutService.getSite(new SiteKey(siteType, siteName), request.getRemoteUser()); - Locale locale; - if (StringUtils.isBlank(lang)) { - locale = request.getLocale(); + PortalConfig site = siteLayoutService.getSite(new SiteKey(siteType, siteName), + request.getRemoteUser()); + return getSiteById(webRequest, request, site.getId(), lang); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @GetMapping("layout") + @Operation(summary = "Gets a specific site layout by its key", description = "Gets a specific site layout by its key", method = "GET") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "304", description = "Not modified"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) + public ResponseEntity getSiteLayout( + WebRequest webRequest, + HttpServletRequest request, + @Parameter(description = "site type") + @RequestParam("siteType") + String siteType, + @Parameter(description = "site name") + @RequestParam("siteName") + String siteName, + @Parameter(description = "expand options", required = false) + @RequestParam(name = "expand", required = false) + String expand) { + try { + ModelObject modelObject = siteLayoutService.getSiteLayout(new SiteKey(siteType, siteName), + request.getRemoteUser()); + LayoutModel layoutModel = RestEntityBuilder.toLayoutModel(modelObject, layoutService, expand); + String eTag = String.valueOf(layoutModel.hashCode()); + if (webRequest.checkNotModified(eTag)) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } else { - locale = Locale.forLanguageTag(lang); + return ResponseEntity.ok() + .eTag(eTag) + .body(layoutModel); } - SiteEntity siteEntity = RestEntityBuilder.toSiteEntity(site, - request, - locale); - return ResponseEntity.ok() - .eTag(String.valueOf(Objects.hash(siteEntity, locale))) - .body(siteEntity); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -146,20 +191,20 @@ public ResponseEntity getSite( @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 = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @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") @RequestParam("siteType") String siteType, @Parameter(description = "site name") @RequestParam("siteName") String siteName) { try { - siteLayoutService.deleteSite(new SiteKey(siteType, siteName), request.getRemoteUser()); + siteLayoutService.deleteSite(new SiteKey(siteType, siteName), + request.getRemoteUser()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -171,16 +216,17 @@ public void deleteSite( @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 = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public void updateSite( HttpServletRequest request, @RequestBody SiteUpdateModel updateModel) { try { - siteLayoutService.updateSite(updateModel, request.getRemoteUser()); + siteLayoutService.updateSite(updateModel, + request.getRemoteUser()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -190,19 +236,19 @@ public void updateSite( @PatchMapping(value = "permissions") @Secured("users") - @Operation(summary = "Update a page access and edit permission", method = "PATCH", - description = "This updates the given page access and edit permission") + @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 = "Request fulfilled"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public void updateSitePermissions( HttpServletRequest request, @RequestBody PermissionUpdateModel permissionUpdateModel) { try { - siteLayoutService.updateSitePermissions(permissionUpdateModel, request.getRemoteUser()); + siteLayoutService.updateSitePermissions(permissionUpdateModel, + request.getRemoteUser()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -214,9 +260,9 @@ public void updateSitePermissions( @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 = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "409", description = "Conflict"), }) public ResponseEntity createSite( HttpServletRequest request, @@ -224,14 +270,77 @@ public ResponseEntity createSite( @RequestBody SiteCreateModel createModel) throws Exception { try { - PortalConfig site = siteLayoutService.createSite(createModel, request.getRemoteUser()); + PortalConfig site = siteLayoutService.createSite(createModel, + request.getRemoteUser()); SiteEntity siteEntity = RestEntityBuilder.toSiteEntity(site, request, request.getLocale()); return ResponseEntity.ok() - .eTag(String.valueOf(Objects.hash(siteEntity, request.getLocale()))) .body(siteEntity); } catch (ObjectAlreadyExistsException e) { + throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @PostMapping("draft") + @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 = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) + public ResponseEntity createDraftSite( + WebRequest webRequest, + HttpServletRequest request, + @Parameter(description = "site type") + @RequestParam("siteType") + String siteType, + @Parameter(description = "site name") + @RequestParam("siteName") + String siteName) throws Exception { + try { + siteLayoutService.createDraftSite(new SiteKey(siteType, siteName), + request.getRemoteUser()); + return getSite(webRequest, request, siteType, siteName, null); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @PutMapping("layout") + @Secured("users") + @Operation(summary = "Updates an existing site layout", method = "PUT", description = "This updates the designated site layout") + @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 ResponseEntity updateSiteLayout( + WebRequest webRequest, + HttpServletRequest request, + @Parameter(description = "site type") + @RequestParam("siteType") + String siteType, + @Parameter(description = "site name") + @RequestParam("siteName") + String siteName, + @Parameter(description = "expand options", required = false) + @RequestParam(name = "expand", required = false) + String expand, + @RequestBody + LayoutModel layoutModel) { + try { + siteLayoutService.updateSiteLayout(new SiteKey(siteType, siteName), + layoutModel.toSite(), + request.getRemoteUser()); + return getSiteLayout(webRequest, request, siteType, siteName, expand); + } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); @@ -241,9 +350,9 @@ public ResponseEntity createSite( @GetMapping("{siteId}/labels") @Operation(summary = "Retrieve site I18N labels", method = "GET", description = "This retrieves site labels") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public NodeLabel getSiteLabels( HttpServletRequest request, @@ -251,7 +360,8 @@ public NodeLabel getSiteLabels( @PathVariable("siteId") Long siteId) { try { - return siteLayoutService.getSiteLabels(siteId, request.getRemoteUser()); + return siteLayoutService.getSiteLabels(siteId, + request.getRemoteUser()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { @@ -262,9 +372,9 @@ public NodeLabel getSiteLabels( @GetMapping("{siteId}/descriptions") @Operation(summary = "Retrieve site I18N descriptions", method = "GET", description = "This retrieves site descriptions") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Request fulfilled"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not found"), + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), }) public NodeLabel getSiteDescriptions( HttpServletRequest request, @@ -272,7 +382,8 @@ public NodeLabel getSiteDescriptions( @PathVariable("siteId") Long siteId) { try { - return siteLayoutService.getSiteDescriptions(siteId, request.getRemoteUser()); + return siteLayoutService.getSiteDescriptions(siteId, + request.getRemoteUser()); } catch (ObjectNotFoundException e) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); } catch (IllegalAccessException e) { 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 01d7ae424..595a7bd87 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 @@ -40,7 +40,9 @@ import org.exoplatform.portal.config.model.ModelObject; import org.exoplatform.portal.config.model.ModelStyle; import org.exoplatform.portal.config.model.Page; +import org.exoplatform.portal.config.model.PageBody; import org.exoplatform.portal.config.model.PersistentApplicationState; +import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.config.model.TransientApplicationState; import org.exoplatform.portal.mop.page.PageKey; import org.exoplatform.portal.pom.spi.portlet.Portlet; @@ -57,6 +59,8 @@ @JsonInclude(value = Include.NON_EMPTY) public class LayoutModel { + private static final String PAGE_BODY_TEMPLATE = "PageBody"; + protected String id; protected String storageId; @@ -240,7 +244,11 @@ private void init(ModelObject model) { // NOSONAR this.textSubtitleFontStyle = cssStyle.getTextSubtitleFontStyle(); } - if (model instanceof Container container) { + if (model instanceof PageBody pageBody) { + this.storageId = pageBody.getStorageId(); + this.storageName = pageBody.getStorageName(); + this.template = PAGE_BODY_TEMPLATE; + } else if (model instanceof Container container) { this.id = container.getId(); this.storageId = container.getStorageId(); this.storageName = container.getStorageName(); @@ -328,10 +336,32 @@ public Page toPage() { return page; } + public PortalConfig toSite() { + PortalConfig site = new PortalConfig(storageId); + ModelObject modelObject = this.children == null ? new PageBody() : + this.children.stream() + .map(LayoutModel::toModelObject) + .findFirst() + .orElse(null); + if (modelObject instanceof Container container) { + site.setPortalLayout(container); + } else { + Container container = new Container(); + container.setChildren(new ArrayList<>()); + container.getChildren().add(modelObject); + site.setPortalLayout(container); + } + return site; + } + public static ModelObject toModelObject(LayoutModel layoutModel) { // NOSONAR ModelStyle cssStyle = mapToStyle(layoutModel); - if (StringUtils.isNotBlank(layoutModel.template)) { + if (StringUtils.equals(layoutModel.template, PAGE_BODY_TEMPLATE)) { + PageBody pageBody = new PageBody(layoutModel.getStorageId()); + pageBody.setStorageName(layoutModel.getStorageName()); + return pageBody; + } else if (StringUtils.isNotBlank(layoutModel.template)) { Container container = new Container(layoutModel.getStorageId()); container.setId(layoutModel.getId()); container.setStorageName(layoutModel.getStorageName()); diff --git a/layout-service/src/main/java/io/meeds/layout/rest/util/RestEntityBuilder.java b/layout-service/src/main/java/io/meeds/layout/rest/util/RestEntityBuilder.java index 35cca75b8..1a9e52f24 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/util/RestEntityBuilder.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/util/RestEntityBuilder.java @@ -31,7 +31,6 @@ 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.PortalConfig; import org.exoplatform.portal.mop.service.LayoutService; import org.exoplatform.social.rest.api.EntityBuilder; @@ -53,7 +52,7 @@ public static SiteEntity toSiteEntity(PortalConfig site, request, true, null, - true, + false, false, false, locale); @@ -105,8 +104,4 @@ private static void applyApplicationContentId(List children, } } - public static Page fromLayoutModel(LayoutModel layoutModel) { - return layoutModel.toPage(); - } - } diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java index 1103aae4f..07ef6c4d3 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PageLayoutService.java @@ -183,10 +183,9 @@ public Page getPageLayout(PageKey pageKey) { } @SneakyThrows - public PageContext createPage(PageCreateModel pageModel, - String username) throws ObjectNotFoundException, - IllegalAccessException, - IllegalArgumentException { + public PageContext createPage(PageCreateModel pageModel, String username) throws ObjectNotFoundException, + IllegalAccessException, + IllegalArgumentException { SiteKey siteKey = new SiteKey(pageModel.getPageSiteType(), pageModel.getPageSiteName()); PortalConfig portalConfig = layoutService.getPortalConfig(siteKey); if (portalConfig == null) { diff --git a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java index 90b06fb2a..0f2a01e33 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java @@ -33,6 +33,7 @@ import org.exoplatform.commons.exception.ObjectNotFoundException; import org.exoplatform.portal.config.UserPortalConfig; import org.exoplatform.portal.config.UserPortalConfigService; +import org.exoplatform.portal.config.model.ModelObject; import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.mop.SiteKey; import org.exoplatform.portal.mop.SiteType; @@ -53,6 +54,8 @@ @Service public class SiteLayoutService { + private static final String SITE_DOESNT_EXIST_MSG = "Site %s doesn't exist"; + @Autowired private LayoutService layoutService; @@ -82,6 +85,11 @@ public PortalConfig getSite(long siteId, String username) throws ObjectNotFoundE return portalConfig; } + public ModelObject getSiteLayout(SiteKey siteKey, String username) throws ObjectNotFoundException, IllegalAccessException { + PortalConfig site = getSite(siteKey, username); + return site.getPortalLayout(); + } + public PortalConfig getSite(SiteKey siteKey, String username) throws ObjectNotFoundException, IllegalAccessException { PortalConfig portalConfig = layoutService.getPortalConfig(siteKey); if (portalConfig == null) { @@ -136,6 +144,26 @@ public PortalConfig createSite(SiteCreateModel createModel, String username) thr return createdPortalConfig; } + public SiteKey createDraftSite(SiteKey siteKey, String username) throws ObjectNotFoundException, IllegalAccessException { + if (!aclService.canEditSite(siteKey, username)) { + throw new IllegalAccessException(String.format("Not allowed to edit site %s", siteKey)); + } + PortalConfig site = getSite(siteKey, username); + + String clonedSiteName = site.getType() + "_" + site.getName() + "_draft_" + username; + + PortalConfig draftPortalConfig = site.clone(); + draftPortalConfig.setType(PortalConfig.DRAFT); + draftPortalConfig.setName(clonedSiteName); + draftPortalConfig.resetStorage(); + SiteKey draftSiteKey = new SiteKey(draftPortalConfig.getType(), draftPortalConfig.getName()); + if (layoutService.getPortalConfig(draftSiteKey) != null) { + layoutService.remove(draftPortalConfig); + } + layoutService.create(draftPortalConfig); + return draftSiteKey; + } + public void updateSite(SiteUpdateModel updateModel, String username) throws IllegalAccessException, ObjectNotFoundException { SiteKey siteKey = new SiteKey(updateModel.getSiteType(), updateModel.getSiteName()); @@ -176,7 +204,7 @@ public void updateSitePermissions(PermissionUpdateModel permissionUpdateModel, SiteKey siteKey = new SiteKey(permissionUpdateModel.getSiteType(), permissionUpdateModel.getSiteName()); PortalConfig portalConfig = layoutService.getPortalConfig(siteKey); if (portalConfig == null) { - throw new ObjectNotFoundException(String.format("Site %s doesn't exist", siteKey)); + throw new ObjectNotFoundException(String.format(SITE_DOESNT_EXIST_MSG, siteKey)); } else if (!aclService.canEditSite(siteKey, username)) { throw new IllegalAccessException(String.format("Site permissions with key %s can't be edited by user %s", siteKey, @@ -191,6 +219,21 @@ public void updateSitePermissions(PermissionUpdateModel permissionUpdateModel, layoutService.save(portalConfig); } + public void updateSiteLayout(SiteKey siteKey, + PortalConfig site, + String username) throws IllegalAccessException, ObjectNotFoundException { + PortalConfig portalConfig = layoutService.getPortalConfig(siteKey); + if (portalConfig == null) { + throw new ObjectNotFoundException(String.format(SITE_DOESNT_EXIST_MSG, siteKey)); + } else if (!aclService.canEditSite(siteKey, username)) { + throw new IllegalAccessException(String.format("Site layout with key %s can't be edited by user %s", + siteKey, + username)); + } + portalConfig.setPortalLayout(site.getPortalLayout()); + layoutService.save(portalConfig); + } + public NodeLabel getSiteLabels(Long siteId, String username) throws ObjectNotFoundException, IllegalAccessException { return getSiteLabel(siteId, username, true); } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java index 7382694b5..1be61ddaa 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java @@ -18,38 +18,55 @@ */ package io.meeds.layout.rest; -import static org.mockito.Mockito.doReturn; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import org.exoplatform.commons.ObjectAlreadyExistsException; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.model.ModelObject; +import org.exoplatform.portal.config.model.PortalConfig; +import org.exoplatform.portal.mop.SiteKey; +import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.social.rest.entity.SiteEntity; import io.meeds.layout.model.NodeLabel; +import io.meeds.layout.rest.model.LayoutModel; +import io.meeds.layout.rest.util.RestEntityBuilder; import io.meeds.layout.service.SiteLayoutService; import io.meeds.spring.web.security.PortalAuthenticationManager; import io.meeds.spring.web.security.WebSecurityConfiguration; import jakarta.servlet.Filter; +import lombok.SneakyThrows; @SpringBootTest(classes = { SiteLayoutRest.class, PortalAuthenticationManager.class, }) @ContextConfiguration(classes = { WebSecurityConfiguration.class }) @@ -58,11 +75,32 @@ @ExtendWith(MockitoExtension.class) public class SiteLayoutRestTest { - private static final String REST_PATH = "/sites"; // NOSONAR + private static final String SIMPLE_USER = "simple"; + + private static final String TEST_PASSWORD = "testPassword"; + + private static final String SITE_NAME = "testsitename"; + + private static final SiteKey SITE_KEY = new SiteKey(PortalConfig.DRAFT, SITE_NAME); + + private static final String REST_PATH = "/sites"; // NOSONAR + + private static final String LAYOUT_REST_PATH = REST_PATH + "/layout"; + + private static final String LAYOUT_REST_PATH_WITH_PARAMS = LAYOUT_REST_PATH + "?siteType=DRAFT&siteName=" + SITE_NAME; + + private static final String GET_SITE_REST_PATH = REST_PATH + "?lang=fr&siteType=DRAFT&siteName=" + SITE_NAME; + + private static final String DELETE_LAYOUT_REST_PATH = REST_PATH + "?siteType=DRAFT&siteName=" + SITE_NAME; + + private static final String CREATE_DRAFT_LAYOUT_REST_PATH = REST_PATH + "/draft?siteType=DRAFT&siteName=" + SITE_NAME; @MockBean private SiteLayoutService siteLayoutService; + @MockBean + private LayoutService layoutService; + @Autowired private SecurityFilterChain filterChain; @@ -81,6 +119,246 @@ void setup() { .build(); } + @Test + void getSiteByIdWhenNotFound() throws Exception { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .getSite(2l, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(REST_PATH + "/2").with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + void getSiteByIdWhenIllegalAccess() throws Exception { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .getSite(2l, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(REST_PATH + "/2").with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void getSiteById() throws Exception { + PortalConfig site = mock(PortalConfig.class); + when(siteLayoutService.getSite(2l, SIMPLE_USER)).thenReturn(site); + try (MockedStatic restEntityBuilder = mockStatic(RestEntityBuilder.class)) { + restEntityBuilder.when(() -> RestEntityBuilder.toSiteEntity(any(), any(), any())).thenReturn(mock(SiteEntity.class)); + ResultActions response = mockMvc.perform(get(REST_PATH + "/2").with(testSimpleUser())); + response.andExpect(status().isOk()); + } + } + + @Test + void getSiteWhenNotFound() throws Exception { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .getSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(GET_SITE_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + void getSiteWhenIllegalAccess() throws Exception { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .getSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(GET_SITE_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void getSite() throws Exception { + PortalConfig site = mock(PortalConfig.class); + when(siteLayoutService.getSite(SITE_KEY, SIMPLE_USER)).thenReturn(site); + when(site.getId()).thenReturn(2l); + when(siteLayoutService.getSite(2l, SIMPLE_USER)).thenReturn(site); + try (MockedStatic restEntityBuilder = mockStatic(RestEntityBuilder.class)) { + restEntityBuilder.when(() -> RestEntityBuilder.toSiteEntity(any(), any(), any())).thenReturn(mock(SiteEntity.class)); + ResultActions response = mockMvc.perform(get(GET_SITE_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + } + } + + @Test + void getSiteLayoutWhenNotFound() throws Exception { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .getSiteLayout(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(LAYOUT_REST_PATH_WITH_PARAMS).with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + void getSiteLayoutWhenIllegalAccess() throws Exception { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .getSiteLayout(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(get(LAYOUT_REST_PATH_WITH_PARAMS).with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void getSiteLayout() throws Exception { + ModelObject modelObject = mock(ModelObject.class); + when(siteLayoutService.getSiteLayout(SITE_KEY, SIMPLE_USER)).thenReturn(modelObject); + LayoutModel layoutModel = mock(LayoutModel.class); + try (MockedStatic restEntityBuilder = mockStatic(RestEntityBuilder.class)) { + restEntityBuilder.when(() -> RestEntityBuilder.toLayoutModel(any(), any(), any())).thenReturn(layoutModel); + ResultActions response = mockMvc.perform(get(LAYOUT_REST_PATH_WITH_PARAMS).with(testSimpleUser())); + response.andExpect(status().isOk()); + } + } + + @Test + @SneakyThrows + void deleteSiteWhenNotFound() { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .deleteSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(delete(DELETE_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + void deleteSiteWhenIllegalAccess() { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .deleteSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(delete(DELETE_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + void deleteSite() { + ResultActions response = mockMvc.perform(delete(DELETE_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + } + + @Test + @SneakyThrows + void updateSiteWhenNotFound() { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .updateSite(any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(put(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + void updateSiteWhenIllegalAccess() { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .updateSite(any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(put(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + void updateSite() { + ResultActions response = mockMvc.perform(put(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isOk()); + } + + @Test + @SneakyThrows + void createSiteWhenNotFound() { + doThrow(ObjectAlreadyExistsException.class).when(siteLayoutService) + .createSite(any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(post(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isConflict()); + } + + @Test + @SneakyThrows + void createSiteWhenIllegalAccess() { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .createSite(any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(post(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + void createSite() { + ResultActions response = mockMvc.perform(post(REST_PATH).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isOk()); + } + + @Test + @SneakyThrows + void createDraftSiteWhenNotFound() { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .createDraftSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(post(CREATE_DRAFT_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + void createDraftSiteWhenIllegalAccess() { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .createDraftSite(SITE_KEY, SIMPLE_USER); + ResultActions response = mockMvc.perform(post(CREATE_DRAFT_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + void createDraftSite() { + PortalConfig site = mock(PortalConfig.class); + when(siteLayoutService.getSite(SITE_KEY, SIMPLE_USER)).thenReturn(site); + when(site.getId()).thenReturn(2l); + when(siteLayoutService.getSite(2l, SIMPLE_USER)).thenReturn(site); + try (MockedStatic restEntityBuilder = mockStatic(RestEntityBuilder.class)) { + restEntityBuilder.when(() -> RestEntityBuilder.toSiteEntity(any(), any(), any())).thenReturn(mock(SiteEntity.class)); + ResultActions response = mockMvc.perform(post(CREATE_DRAFT_LAYOUT_REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + } + } + + @Test + @SneakyThrows + void updateSiteLayoutWhenNotFound() { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .updateSiteLayout(eq(SITE_KEY), any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(put(LAYOUT_REST_PATH_WITH_PARAMS).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + void updateSiteLayoutWhenIllegalAccess() { + doThrow(IllegalAccessException.class).when(siteLayoutService) + .updateSiteLayout(eq(SITE_KEY), any(), eq(SIMPLE_USER)); + ResultActions response = mockMvc.perform(put(LAYOUT_REST_PATH_WITH_PARAMS).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + void updateSiteLayout() { + ModelObject modelObject = mock(ModelObject.class); + when(siteLayoutService.getSiteLayout(SITE_KEY, SIMPLE_USER)).thenReturn(modelObject); + LayoutModel layoutModel = mock(LayoutModel.class); + try (MockedStatic restEntityBuilder = mockStatic(RestEntityBuilder.class)) { + restEntityBuilder.when(() -> RestEntityBuilder.toLayoutModel(any(), any(), any())).thenReturn(layoutModel); + ResultActions response = mockMvc.perform(put(LAYOUT_REST_PATH_WITH_PARAMS).content("{}") + .contentType(MediaType.APPLICATION_JSON) + .with(testSimpleUser())); + response.andExpect(status().isOk()); + } + } + @Test void getSiteLabels() throws Exception { doThrow(ObjectNotFoundException.class).when(siteLayoutService) @@ -125,4 +403,9 @@ void getSiteDescriptions() throws Exception { .andExpect(jsonPath("$.defaultLanguage").value("en")); } + private RequestPostProcessor testSimpleUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("users")); + } + } 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 1b78d3d48..90587cd5c 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -459,3 +459,13 @@ layout.siteTemplate.spacePublic.description=Site suggested when admins want to c siteTemplate.label.editNavigation=Edit navigation siteTemplate.label.editCreatedNavigation.information=Template Saved! Now customize the layout and navigation for each site's page siteTemplate.label.editNavigation.information=Changes affect only new sites created using this template +layout.site.reminder.description.part1=Now, with just a few clicks, design your site using our full-fledged Site Builder and enjoy the benefits of a modern experience: +layout.site.reminder.description.part2=- Organize Site in Sections +layout.site.reminder.description.part3=- Manage Apps Display +layout.site.reminder.description.part4=- Preview Site Content +layout.siteSavedSuccessfully=Site Saved Successfully +layout.siteSavingError=An error occurred while saving site. Please try again later or contact the support services. +layout.editSiteName=Edit {0} +layout.editSite.portalPage=Portal Page +layout.editSiteProperties=Edit site design +layout.editSiteSections=Edit sections diff --git a/layout-webapp/src/main/resources/locale/portlet/SiteManagement_en.properties b/layout-webapp/src/main/resources/locale/portlet/SiteManagement_en.properties index c57d8a343..2a703b27b 100644 --- a/layout-webapp/src/main/resources/locale/portlet/SiteManagement_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/SiteManagement_en.properties @@ -11,12 +11,13 @@ siteManagement.label.confirmDelete.message=Are you sure you want to delete "{0}" siteManagement.drawer.properties.title=Site properties siteManagement.drawer.addSite.title=Add Site siteManagement.label.properties=Properties +siteManagement.label.editLayout=Properties siteManagement.label.siteName.title=Site name siteManagement.label.siteLabel.title=Site label siteManagement.label.siteLabel.placeholder=Give a name to your site siteManagement.label.siteDescription.title=Site description siteManagement.label.siteDescription.placeholder=Describe purpose to inform users via the sidebar -siteManagement.label.editLayout=Layout +siteManagement.label.editLayout=Edit Layout siteManagement.label.manageAccess=Permissions siteManagement.label.managePermissions=Manage permissions siteManagement.label.updatePermission.error=Error while updating permissions @@ -50,6 +51,8 @@ sites.label.sitePermissions=Site permissions sites.label.siteNavigation=Site navigation sites.filter.placeholder=Filter by name, description sites.label.system.noDelete=This default site cannot be deleted +sites.label.system.noLayoutEdit=This site cannot be edited +sites.label.meta.noLayoutEdit=This site is sharing the layout of your meta site sites.menu.open=Open sites.urlSlug.label=URL Slug sites.urlSlug.caption=This can't be updated once created 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 3b88fff40..effd2f20d 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -36,6 +36,13 @@ LayoutEditor + + layout + SiteLayoutEditor + Enterprise + LayoutEditor + + layout SectionEditor @@ -177,6 +184,46 @@ + + SiteLayoutEditor + + + + extensionRegistry + + + commonVueComponents + + + commonLayoutComponents + + + vueDraggable + + + translationField + + + attachImage + + + html2canvas + + + vue + + + vuetify + + + eXoVueI18n + + + + LayoutEditor @@ -224,7 +271,7 @@ layout-editor-group extensionRegistry diff --git a/layout-webapp/src/main/webapp/WEB-INF/jsp/siteLayoutEditor.jsp b/layout-webapp/src/main/webapp/WEB-INF/jsp/siteLayoutEditor.jsp new file mode 100644 index 000000000..750059914 --- /dev/null +++ b/layout-webapp/src/main/webapp/WEB-INF/jsp/siteLayoutEditor.jsp @@ -0,0 +1,14 @@ +<%@page import="io.meeds.layout.service.LayoutAclService"%> +<%@page import="org.exoplatform.container.ExoContainerContext"%> +<% + LayoutAclService aclService = ExoContainerContext.getService(LayoutAclService.class); + boolean isAdministrator = aclService.isAdministrator(request.getRemoteUser()); +%> +
+
+ +
+
diff --git a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml index 68c81cbbb..3f3f7eb03 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml @@ -63,6 +63,29 @@
+ + SiteLayoutEditor + Site Layout Editor Portlet + org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet + + portlet-view-dispatched-file-path + /WEB-INF/jsp/siteLayoutEditor.jsp + + + layout-css-class + no-layout-style + + + text/html + + en + locale.portlet.LayoutEditor + + Site Layout Editor + Site Layout Editor Management + + + LayoutEditor Layout Editor Portlet diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/coediting/Coediting.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/coediting/Coediting.vue similarity index 97% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/coediting/Coediting.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/coediting/Coediting.vue index 1974fa86c..1f9c26102 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/coediting/Coediting.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/coediting/Coediting.vue @@ -98,7 +98,7 @@ export default { return !!this.lockHolders?.length; }, hasDraft() { - return this.revision && this.revision !== this.value; + return this.revision && String(this.revision) !== String(this.value); }, display() { return this.disabled || (this.initialized && !this.locked && !this.hasDraft); @@ -206,7 +206,7 @@ export default { if (revision) { return this.$coeditingService.getRevision(this.objectType, this.objectId) .then(data => { - if (!data?.revision || revision === data.revision) { + if (!data?.revision || String(revision) === String(data.revision)) { return this.$coeditingService.setLock(this.objectType, this.objectId, `${revision}`) .then(() => this.draft = {revision}); } else { diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/CellsDropBox.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/CellsDropBox.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/CellsDropBox.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/CellsDropBox.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/CellsSelectionBox.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/CellsSelectionBox.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/CellsSelectionBox.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/CellsSelectionBox.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/ContainerBase.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue similarity index 86% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/ContainerBase.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue index 0576574bf..b2d860622 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/ContainerBase.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue @@ -28,36 +28,31 @@ :class="cssClass" :style="cssStyle" :options="dragOptions" - item-key="storageId" class="position-relative" @start="startMoving" @end="endMoving"> - - +
- - +
@@ -116,10 +111,19 @@ export default { }, data: () => ({ hover: false, - children: [], dragged: false, }), computed: { + children: { + get() { + return this.container.children?.slice?.() || []; + }, + set(value) { + if (!this.isCell && JSON.stringify(this.container.children) !== JSON.stringify(value)) { + this.$set(this.container, 'children', value?.filter?.(c => c)); + } + } + }, storageId() { return this.container.storageId; }, @@ -157,18 +161,20 @@ export default { return `${this.containerCssClass?.replace?.('layout-sticky-application', '') || ''} ${this.draggable && 'v-draggable' || ''} ${this.noChildren && 'position-relative' || ''}`; }, isCell() { - return this.container.template === this.$layoutUtils.cellTemplate; + return this.container.template === this.$layoutUtils.cellTemplate + || this.container.template === this.$layoutUtils.bannerCellTemplate + || this.container.template === this.$layoutUtils.sidebarCellTemplate; }, dragOptions() { - const dragOptions = { + return { group: `${this.container.template}`, draggable: '.draggable-container-flex', animation: 200, ghostClass: 'layout-moving-ghost-container', chosenClass: 'layout-moving-chosen-container', handle: this.isCell && '.draggable-cell' || '.draggable', + dataIdAttr: 'data-storage-id', }; - return dragOptions; }, }, watch: { diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/ContainerExtension.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerExtension.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/base/ContainerExtension.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerExtension.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationCard.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCard.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationCard.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationCategoryCard.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationCategoryCard.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationCategoryCard.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationMenu.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationMenu.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/ApplicationMenu.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/ApplicationMenu.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/CellResizeButton.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/CellResizeButton.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/CellResizeButton.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/CellResizeButton.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionMenu.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionMenu.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionMenu.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionMenu.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionSelectionGrid.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionSelectionGrid.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionSelectionGrid.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionSelectionGrid.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionSelectionGridCell.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionSelectionGridCell.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/common/SectionSelectionGridCell.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/common/SectionSelectionGridCell.vue 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/common-layout/components/content/container/Application.vue similarity index 99% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue index 6fbb70b01..28dc7bddf 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Application.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue @@ -98,9 +98,6 @@ export default { applicationId() { return this.container?.children?.[0]?.storageId || this.container?.storageId; }, - nodeId() { - return this.$root.draftNodeId; - }, nodeUri() { return this.$root.draftNodeUri; }, 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/common-layout/components/content/container/Cell.vue similarity index 99% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Cell.vue index 3841b2d85..b300d66a5 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Cell.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Cell.vue @@ -90,7 +90,7 @@ :height="hasApplication && 50 || '50%'" max-height="200" color="transparent" - class="full-width d-flex flex-wrap align-center justify-center px-2" + class="full-width d-flex flex-wrap align-center justify-center" flat>
fa-plus diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Container.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Container.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Container.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Container.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/PageBody.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/PageBody.vue new file mode 100644 index 000000000..af1b06e18 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/PageBody.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Section.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Section.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/Section.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Section.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Site.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Site.vue new file mode 100644 index 000000000..c5d6466d4 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Site.vue @@ -0,0 +1,49 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerCell.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerCell.vue new file mode 100644 index 000000000..f31ab3b89 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerCell.vue @@ -0,0 +1,198 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerSection.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerSection.vue new file mode 100644 index 000000000..31a953ef7 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteBannerSection.vue @@ -0,0 +1,113 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteMiddleBody.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteMiddleBody.vue new file mode 100644 index 000000000..58d95e160 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteMiddleBody.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarCell.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarCell.vue new file mode 100644 index 000000000..ac5fca9ce --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarCell.vue @@ -0,0 +1,166 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarSection.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarSection.vue new file mode 100644 index 000000000..1f8a75646 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/SiteSidebarSection.vue @@ -0,0 +1,103 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/dialog/EditPortletDialog.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/dialog/EditPortletDialog.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/dialog/EditPortletDialog.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/dialog/EditPortletDialog.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/AddApplicationDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/AddApplicationDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/AddApplicationDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/EditApplicationDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditApplicationDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/SelectApplicationCategoryDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/SelectApplicationCategoryDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/drawer/SelectApplicationCategoryDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/SelectApplicationCategoryDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BackgroundImageAttachment.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundImageAttachment.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BackgroundImageAttachment.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundImageAttachment.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BackgroundInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BackgroundInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderRadiusInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderRadiusInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderRadiusSelector.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusSelector.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/BorderRadiusSelector.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusSelector.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/ColorPicker.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/ColorPicker.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/ColorPicker.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/ColorPicker.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/MarginInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/MarginInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/MarginInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/MarginInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/SectionMarginInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionMarginInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/SectionMarginInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionMarginInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/SectionTemplate.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionTemplate.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/SectionTemplate.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionTemplate.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/TextInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextInput.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/components/form/TextInput.vue rename to layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextInput.vue diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/extensions.js b/layout-webapp/src/main/webapp/vue-app/common-layout/extensions.js similarity index 53% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/extensions.js rename to layout-webapp/src/main/webapp/vue-app/common-layout/extensions.js index 53f1a1bb9..e587393da 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/extensions.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/extensions.js @@ -18,7 +18,7 @@ */ extensionRegistry.registerExtension('layout-editor', 'container', { - rank: 1000, + rank: 1500, type: 'default', isValid: container => container?.template === 'Container', containerType: 'layout-editor-container', @@ -32,14 +32,63 @@ extensionRegistry.registerExtension('layout-editor', 'container', { }); extensionRegistry.registerExtension('layout-editor', 'container', { - rank: 500, + rank: 400, type: 'cell', isValid: container => container?.template === 'CellContainer', containerType: 'layout-editor-container-cell', }); +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 500, + type: 'Site', + isValid: container => container?.template === 'Site', + containerType: 'layout-editor-container-site', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 500, + type: 'PageBody', + isValid: container => container?.template === 'PageBody', + containerType: 'layout-editor-container-page-body', +}); + extensionRegistry.registerExtension('layout-editor', 'container', { rank: 600, + type: 'Sibebar', + isValid: container => container?.template === 'Sidebar', + containerType: 'layout-editor-container-site-sidebar-section', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 700, + type: 'SidebarCell', + isValid: container => container?.template === 'SidebarCell', + containerType: 'layout-editor-container-site-sidebar-cell', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 800, + type: 'Banner', + isValid: container => container?.template === 'Banner', + containerType: 'layout-editor-container-site-banner-section', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 900, + type: 'BannerCell', + isValid: container => container?.template === 'BannerCell', + containerType: 'layout-editor-container-site-banner-cell', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 1000, + type: 'SiteMiddleBody', + isValid: container => container?.template === 'SiteMiddleBody', + containerType: 'layout-editor-container-site-middle-section', +}); + +extensionRegistry.registerExtension('layout-editor', 'container', { + rank: 1500, type: 'application', isValid: container => !container.type && !container.template, containerType: 'layout-editor-container-application', diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js b/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js new file mode 100644 index 000000000..c8689637a --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js @@ -0,0 +1,109 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 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 BorderRadiusSelector from './components/form/BorderRadiusSelector.vue'; +import ColorPicker from './components/form/ColorPicker.vue'; +import BackgroundImageAttachment from './components/form/BackgroundImageAttachment.vue'; +import BackgroundInput from './components/form/BackgroundInput.vue'; +import TextInput from './components/form/TextInput.vue'; +import MarginInput from './components/form/MarginInput.vue'; +import SectionMarginInput from './components/form/SectionMarginInput.vue'; +import BorderInput from './components/form/BorderInput.vue'; +import BorderRadiusInput from './components/form/BorderRadiusInput.vue'; +import SectionTemplate from './components/form/SectionTemplate.vue'; + +import CellsDropBox from './components/content/base/CellsDropBox.vue'; +import CellsSelectionBox from './components/content/base/CellsSelectionBox.vue'; +import ContainerExtension from './components/content/base/ContainerExtension.vue'; +import ContainerBase from './components/content/base/ContainerBase.vue'; + +import SectionMenu from './components/content/common/SectionMenu.vue'; +import SectionSelectionGrid from './components/content/common/SectionSelectionGrid.vue'; +import SectionSelectionGridCell from './components/content/common/SectionSelectionGridCell.vue'; +import ApplicationCategoryCard from './components/content/common/ApplicationCategoryCard.vue'; +import ApplicationCard from './components/content/common/ApplicationCard.vue'; +import ApplicationMenu from './components/content/common/ApplicationMenu.vue'; +import CellResizeButton from './components/content/common/CellResizeButton.vue'; + +import Site from './components/content/container/Site.vue'; +import SiteBannerSection from './components/content/container/SiteBannerSection.vue'; +import SiteBannerCell from './components/content/container/SiteBannerCell.vue'; +import SiteSidebarSection from './components/content/container/SiteSidebarSection.vue'; +import SiteSidebarCell from './components/content/container/SiteSidebarCell.vue'; +import SiteMiddleBody from './components/content/container/SiteMiddleBody.vue'; +import PageBody from './components/content/container/PageBody.vue'; + +import Container from './components/content/container/Container.vue'; +import Section from './components/content/container/Section.vue'; +import Cell from './components/content/container/Cell.vue'; +import Application from './components/content/container/Application.vue'; + +import SelectApplicationCategoryDrawer from './components/drawer/SelectApplicationCategoryDrawer.vue'; +import AddApplicationDrawer from './components/drawer/AddApplicationDrawer.vue'; +import EditApplicationDrawer from './components/drawer/EditApplicationDrawer.vue'; + +import EditPortletDialog from './components/dialog/EditPortletDialog.vue'; + +import Coediting from './components/coediting/Coediting.vue'; + +const components = { + 'layout-editor-color-picker': ColorPicker, + 'layout-editor-border-radius-selector': BorderRadiusSelector, + 'layout-editor-container': Container, + 'layout-editor-container-extension': ContainerExtension, + 'layout-editor-container-base': ContainerBase, + 'layout-editor-container-section': Section, + 'layout-editor-section-template': SectionTemplate, + 'layout-editor-container-cell': Cell, + 'layout-editor-container-application': Application, + 'layout-editor-container-page-body': PageBody, + 'layout-editor-container-site': Site, + 'layout-editor-container-site-banner-section': SiteBannerSection, + 'layout-editor-container-site-banner-cell': SiteBannerCell, + 'layout-editor-container-site-sidebar-section': SiteSidebarSection, + 'layout-editor-container-site-sidebar-cell': SiteSidebarCell, + 'layout-editor-container-site-middle-section': SiteMiddleBody, + 'layout-editor-section-selection-grid': SectionSelectionGrid, + 'layout-editor-section-selection-grid-cell': SectionSelectionGridCell, + 'layout-editor-section-menu': SectionMenu, + 'layout-editor-application-category-select-drawer': SelectApplicationCategoryDrawer, + 'layout-editor-application-add-drawer': AddApplicationDrawer, + 'layout-editor-application-edit-drawer': EditApplicationDrawer, + 'layout-editor-portlet-edit-dialog': EditPortletDialog, + 'layout-editor-background-image-attachment': BackgroundImageAttachment, + 'layout-editor-background-input': BackgroundInput, + 'layout-editor-text-input': TextInput, + 'layout-editor-margin-input': MarginInput, + 'layout-editor-section-margin-input': SectionMarginInput, + 'layout-editor-border-input': BorderInput, + 'layout-editor-border-radius-input': BorderRadiusInput, + 'layout-editor-application-card': ApplicationCard, + 'layout-editor-application-category-card': ApplicationCategoryCard, + 'layout-editor-application-menu': ApplicationMenu, + 'layout-editor-cell-resize-button': CellResizeButton, + 'layout-editor-cells-selection-box': CellsSelectionBox, + 'layout-editor-cells-drop-box': CellsDropBox, + // TODO : to define in social to be a reusable component + 'coediting': Coediting, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/js/CoeditingService.js b/layout-webapp/src/main/webapp/vue-app/common-layout/js/CoeditingService.js similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/js/CoeditingService.js rename to layout-webapp/src/main/webapp/vue-app/common-layout/js/CoeditingService.js diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js b/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js similarity index 94% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js rename to layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js index dc39f5bf4..2296504db 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/js/LayoutUtils.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js @@ -25,6 +25,13 @@ export const simpleTemplate = 'system:/groovy/portal/webui/container/UIContainer export const gridTemplate = 'GridContainer'; export const flexTemplate = 'FlexContainer'; export const cellTemplate = 'CellContainer'; +export const siteTemplate = 'Site'; +export const siteBodyMiddleTemplate = 'SiteMiddleBody'; +export const pageBodyTemplate = 'PageBody'; +export const sidebarTemplate = 'Sidebar'; +export const sidebarCellTemplate = 'SidebarCell'; +export const bannerTemplate = 'Banner'; +export const bannerCellTemplate = 'BannerCell'; export const defaultMarginTop = 20; export const defaultMarginRight = 20; @@ -218,6 +225,9 @@ export function newParentContainer(layout) { } export function getApplications(container, applications) { + if (!container) { + return; + } if (!applications) { applications = []; } @@ -333,6 +343,55 @@ export function applyContainerStyle(container, containerStyle) { Vue.set(container, 'textSubtitleFontStyle', containerStyle.textSubtitleFontStyle || null); } +export function parseSite(layout) { + const compatible = layout.children + && layout.template === siteTemplate + && layout.children.every(c => c.template === sidebarTemplate || c.template === siteBodyMiddleTemplate) + && layout?.children?.[0]?.template === sidebarTemplate + && layout?.children?.[1]?.template === siteBodyMiddleTemplate + && layout?.children?.[2]?.template === sidebarTemplate; + if (!compatible) { + const applications = getApplications(layout); + layout.template = siteTemplate; + layout.children = [ + { + ...newContainer(sidebarTemplate), + children: [ + newContainer(sidebarCellTemplate), + ] + }, + { + ...newContainer(siteBodyMiddleTemplate), + children: [ + { + ...newContainer(bannerTemplate), + children: [ + newContainer(bannerCellTemplate), + newContainer(bannerCellTemplate), + newContainer(bannerCellTemplate), + ] + }, + newContainer(pageBodyTemplate), + { + ...newContainer(bannerTemplate), + children: [ + newContainer(bannerCellTemplate), + ] + } + ] + }, + { + ...newContainer(sidebarTemplate), + children: [ + newContainer(sidebarCellTemplate), + ] + } + ]; + return !applications?.length; + } + return true; +} + export function parseSections(layout) { const parentContainer = getParentContainer(layout); if (parentContainer) { @@ -707,7 +766,10 @@ function newContainer(template, cssClass, parentContainer, index) { } function parseSection(section) { - if ((section.template !== gridTemplate && section.template !== flexTemplate) + if ((section.template !== gridTemplate + && section.template !== flexTemplate + && section.template !== sidebarTemplate + && section.template !== bannerTemplate) || !section.children.length) { return; } @@ -723,8 +785,10 @@ function parseSection(section) { section.gap = parseGapClasses(section, 'grid-gap'); section.colsCount = section.colBreakpoints[currentBreakpoint]; section.rowsCount = section.rowBreakpoints[currentBreakpoint]; - // Compute cell indexes - parseMatrix(section); + if (section.template !== gridTemplate && section.template !== flexTemplate) { + // Compute cell indexes + parseMatrix(section); + } } export function parseContainerStyle(container) { diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/main.js b/layout-webapp/src/main/webapp/vue-app/common-layout/main.js new file mode 100644 index 000000000..02b909be8 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/main.js @@ -0,0 +1,22 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import './initComponents.js'; +import './services.js'; +import './extensions.js'; diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/services.js b/layout-webapp/src/main/webapp/vue-app/common-layout/services.js similarity index 67% rename from layout-webapp/src/main/webapp/vue-app/layout-editor/services.js rename to layout-webapp/src/main/webapp/vue-app/common-layout/services.js index b76167310..ac2ec3a1c 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/services.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/services.js @@ -1,7 +1,7 @@ /* * This file is part of the Meeds project (https://meeds.io/). * - * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * Copyright (C) 2020 - 2025 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 @@ -30,19 +30,3 @@ if (!Vue.prototype.$coeditingService) { value: coeditingService, }); } - -Vue.prototype.$updateApplicationVisibility = function(visible, element) { - if (!element) { - element = this?.$root?.$el; - } - if (!element?.className?.includes?.('PORTLET-FRAGMENT')) { - element = element?.parentElement; - } - if (element?.parentElement) { - if (visible) { - element.closest?.('.PORTLET-FRAGMENT')?.parentElement?.classList?.remove?.('hidden-sm-and-down'); - } else { - element.closest?.('.PORTLET-FRAGMENT')?.parentElement?.classList?.add?.('hidden-sm-and-down'); - } - } -}; diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/ManagePermissionsDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/ManagePermissionsDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/ManagePermissionsDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/ManagePermissionsDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/PermissionTypeSelector.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/PermissionTypeSelector.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/PermissionTypeSelector.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/PermissionTypeSelector.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/SiteAccessPermissions.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/SiteAccessPermissions.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/SiteAccessPermissions.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/SiteAccessPermissions.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/SiteEditPermission.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/SiteEditPermission.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/manage-permissions/SiteEditPermission.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/manage-permissions/SiteEditPermission.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeIconPickerDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeIconPickerDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeIconPickerDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeIconPickerDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeItem.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeItem.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeItem.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeItem.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeItemMenu.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeItemMenu.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodeItemMenu.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodeItemMenu.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodesList.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodesList.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/NodesList.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/NodesList.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationElementDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationElementDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationElementDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationElementDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationExistingPageElement.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationExistingPageElement.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationExistingPageElement.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationExistingPageElement.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElement.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElement.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElement.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElement.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElementItem.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElementItem.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElementItem.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElementItem.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElementItemsList.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElementItemsList.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNewPageElementItemsList.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNewPageElementItemsList.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNodeDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNodeDrawer.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationNodeDrawer.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationNodeDrawer.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationPageElement.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationPageElement.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationPageElement.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationPageElement.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationPageSuggester.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationPageSuggester.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationPageSuggester.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationPageSuggester.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationScheduleDatePickers.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationScheduleDatePickers.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationScheduleDatePickers.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationScheduleDatePickers.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationSiteSuggester.vue b/layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationSiteSuggester.vue similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/components/site-navigation/SiteNavigationSiteSuggester.vue rename to layout-webapp/src/main/webapp/vue-app/common-sites/components/site-navigation/SiteNavigationSiteSuggester.vue diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/initComponents.js b/layout-webapp/src/main/webapp/vue-app/common-sites/initComponents.js similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/initComponents.js rename to layout-webapp/src/main/webapp/vue-app/common-sites/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-sites/main.js similarity index 100% rename from layout-webapp/src/main/webapp/vue-app/common-layout-components/main.js rename to layout-webapp/src/main/webapp/vue-app/common-sites/main.js diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js b/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js index 2c0f89c32..df474b269 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js +++ b/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js @@ -50,6 +50,51 @@ export function createSite(siteName, siteId, siteLabel, siteDescription, display }); } +export function createDraftSite(siteType, siteName) { + const formData = new FormData(); + if (siteType) { + formData.append('siteType', siteType); + } + if (siteName) { + formData.append('siteName', siteName); + } + const params = new URLSearchParams(formData).toString(); + return fetch(`/layout/rest/sites/draft?${params}`, { + credentials: 'include', + method: 'POST', + }).then((resp) => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when creating site'); + } + }); +} + +export function getSiteLayout(siteType, siteName, expand) { + const formData = new FormData(); + if (siteType) { + formData.append('siteType', siteType); + } + if (siteName) { + formData.append('siteName', siteName); + } + if (expand) { + formData.append('expand', expand); + } + const params = new URLSearchParams(formData).toString(); + return fetch(`/layout/rest/sites/layout?${params}`, { + credentials: 'include', + method: 'GET', + }).then((resp) => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when creating site'); + } + }); +} + export function getSiteById(siteId, lang) { const formData = new FormData(); if (lang) { @@ -148,6 +193,30 @@ export function updateSite(siteName, siteType, siteLabel, siteDescription, displ }); } +export function updateSiteLayout(siteType, siteName, layout, expand) { + return fetch(`/layout/rest/sites/layout?siteType=${siteType}&siteName=${siteName}&expand=${expand || ''}`, { + method: 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + children: layout.children, + }), + }).then((resp) => { + if (resp?.ok) { + return resp.json(); + } else if (resp.status === 400) { + return resp.json() + .then(e => { + throw new Error(e?.message); + }); + } else { + throw new Error(resp.status); + } + }); +} + export function deleteSite(siteType, siteName) { const formData = new FormData(); formData.append('siteName', siteName); diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/initComponents.js b/layout-webapp/src/main/webapp/vue-app/layout-editor/initComponents.js index 552e7392c..8fa35181d 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/initComponents.js +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/initComponents.js @@ -30,48 +30,12 @@ import PagePreviewButton from './components/toolbar/actions/PagePreviewButton.vu import PageSpacePreviewButton from './components/toolbar/actions/PageSpacePreviewButton.vue'; import PagePropertiesButton from './components/toolbar/actions/PagePropertiesButton.vue'; -import BorderRadiusSelector from './components/form/BorderRadiusSelector.vue'; -import ColorPicker from './components/form/ColorPicker.vue'; -import BackgroundImageAttachment from './components/form/BackgroundImageAttachment.vue'; -import BackgroundInput from './components/form/BackgroundInput.vue'; -import TextInput from './components/form/TextInput.vue'; -import MarginInput from './components/form/MarginInput.vue'; -import SectionMarginInput from './components/form/SectionMarginInput.vue'; -import BorderInput from './components/form/BorderInput.vue'; -import BorderRadiusInput from './components/form/BorderRadiusInput.vue'; -import SectionTemplate from './components/form/SectionTemplate.vue'; - import Content from './components/content/Content.vue'; -import CellsDropBox from './components/content/base/CellsDropBox.vue'; -import CellsSelectionBox from './components/content/base/CellsSelectionBox.vue'; -import ContainerExtension from './components/content/base/ContainerExtension.vue'; -import ContainerBase from './components/content/base/ContainerBase.vue'; - -import SectionMenu from './components/content/common/SectionMenu.vue'; -import SectionSelectionGrid from './components/content/common/SectionSelectionGrid.vue'; -import SectionSelectionGridCell from './components/content/common/SectionSelectionGridCell.vue'; -import ApplicationCategoryCard from './components/content/common/ApplicationCategoryCard.vue'; -import ApplicationCard from './components/content/common/ApplicationCard.vue'; -import ApplicationMenu from './components/content/common/ApplicationMenu.vue'; -import CellResizeButton from './components/content/common/CellResizeButton.vue'; - -import Container from './components/content/container/Container.vue'; -import Section from './components/content/container/Section.vue'; -import Cell from './components/content/container/Cell.vue'; -import Application from './components/content/container/Application.vue'; - import EditSectionDrawer from './components/drawer/EditSectionDrawer.vue'; import AddSectionDrawer from './components/drawer/AddSectionDrawer.vue'; -import SelectApplicationCategoryDrawer from './components/drawer/SelectApplicationCategoryDrawer.vue'; -import AddApplicationDrawer from './components/drawer/AddApplicationDrawer.vue'; -import EditApplicationDrawer from './components/drawer/EditApplicationDrawer.vue'; import EditPageDrawer from './components/drawer/EditPageDrawer.vue'; -import EditPortletDialog from './components/dialog/EditPortletDialog.vue'; - -import Coediting from './components/coediting/Coediting.vue'; - const components = { 'layout-editor': LayoutEditor, 'layout-editor-toolbar': Toolbar, @@ -85,40 +49,9 @@ const components = { 'layout-editor-toolbar-page-properties-button': PagePropertiesButton, 'layout-editor-toolbar-mobile-preview-button': MobilePreviewButton, 'layout-editor-content': Content, - 'layout-editor-color-picker': ColorPicker, - 'layout-editor-border-radius-selector': BorderRadiusSelector, - 'layout-editor-container': Container, - 'layout-editor-container-extension': ContainerExtension, - 'layout-editor-container-base': ContainerBase, - 'layout-editor-container-section': Section, - 'layout-editor-section-template': SectionTemplate, - 'layout-editor-container-cell': Cell, - 'layout-editor-container-application': Application, - 'layout-editor-section-selection-grid': SectionSelectionGrid, - 'layout-editor-section-selection-grid-cell': SectionSelectionGridCell, 'layout-editor-section-add-drawer': AddSectionDrawer, 'layout-editor-section-edit-drawer': EditSectionDrawer, - 'layout-editor-section-menu': SectionMenu, - 'layout-editor-application-category-select-drawer': SelectApplicationCategoryDrawer, - 'layout-editor-application-add-drawer': AddApplicationDrawer, - 'layout-editor-application-edit-drawer': EditApplicationDrawer, 'layout-editor-page-edit-drawer': EditPageDrawer, - 'layout-editor-portlet-edit-dialog': EditPortletDialog, - 'layout-editor-background-image-attachment': BackgroundImageAttachment, - 'layout-editor-background-input': BackgroundInput, - 'layout-editor-text-input': TextInput, - 'layout-editor-margin-input': MarginInput, - 'layout-editor-section-margin-input': SectionMarginInput, - 'layout-editor-border-input': BorderInput, - 'layout-editor-border-radius-input': BorderRadiusInput, - 'layout-editor-application-card': ApplicationCard, - 'layout-editor-application-category-card': ApplicationCategoryCard, - 'layout-editor-application-menu': ApplicationMenu, - 'layout-editor-cell-resize-button': CellResizeButton, - 'layout-editor-cells-selection-box': CellsSelectionBox, - 'layout-editor-cells-drop-box': CellsDropBox, - // TODO : to define in social to be a reusable component - 'coediting': Coediting, }; for (const key in components) { 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 14b1154a0..73468bd1b 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,15 +18,13 @@ */ import './initComponents.js'; +import '../common-layout/main.js'; import '../common-page-layout/main.js'; import '../common-page-template/main.js'; import '../common-portlets/main.js'; import '../common-illustration/main.js'; import '../common-section-template/main.js'; -import './extensions.js'; -import './services.js'; - // get overridden components if exists if (extensionRegistry) { const components = extensionRegistry.loadComponents('LayoutEditor'); diff --git a/layout-webapp/src/main/webapp/vue-app/section-editor/main.js b/layout-webapp/src/main/webapp/vue-app/section-editor/main.js index f0e2cddc1..b902661c1 100644 --- a/layout-webapp/src/main/webapp/vue-app/section-editor/main.js +++ b/layout-webapp/src/main/webapp/vue-app/section-editor/main.js @@ -19,16 +19,13 @@ */ import './initComponents.js'; -import '../layout-editor/initComponents.js'; import '../common-page-layout/main.js'; import '../common-page-template/main.js'; import '../common-portlets/main.js'; import '../common-section-template/main.js'; import '../common-illustration/main.js'; - -import '../layout-editor/extensions.js'; -import '../layout-editor/services.js'; +import '../common-layout/main.js'; // get overridden components if exists if (extensionRegistry) { diff --git a/layout-webapp/src/main/webapp/vue-app/section-template/main.js b/layout-webapp/src/main/webapp/vue-app/section-template/main.js index 790670d98..1ebbe98cf 100644 --- a/layout-webapp/src/main/webapp/vue-app/section-template/main.js +++ b/layout-webapp/src/main/webapp/vue-app/section-template/main.js @@ -20,7 +20,6 @@ import './initComponents.js'; import '../common-illustration/main.js'; import '../common-section-template/main.js'; -import '../layout-editor/services.js'; // get overridden components if exists if (extensionRegistry) { diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/SiteLayoutEditor.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/SiteLayoutEditor.vue new file mode 100644 index 000000000..b56f57e13 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/SiteLayoutEditor.vue @@ -0,0 +1,97 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/Content.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/Content.vue new file mode 100644 index 000000000..8d3a4e0e9 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/Content.vue @@ -0,0 +1,382 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/BannerSectionMenu.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/BannerSectionMenu.vue new file mode 100644 index 000000000..08aad16ed --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/BannerSectionMenu.vue @@ -0,0 +1,243 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/SidebarSectionMenu.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/SidebarSectionMenu.vue new file mode 100644 index 000000000..2b1b8c5fb --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/content/common/SidebarSectionMenu.vue @@ -0,0 +1,155 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/Toolbar.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/Toolbar.vue new file mode 100644 index 000000000..cc8cc1eed --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/Toolbar.vue @@ -0,0 +1,53 @@ + + + diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/HistoryButtons.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/HistoryButtons.vue new file mode 100644 index 000000000..456bcef18 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/HistoryButtons.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/MobilePreviewButton.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/MobilePreviewButton.vue new file mode 100644 index 000000000..cfefccf55 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/MobilePreviewButton.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SaveButton.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SaveButton.vue new file mode 100644 index 000000000..a2e98bc7f --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SaveButton.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SiteEditSectionsButton.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SiteEditSectionsButton.vue new file mode 100644 index 000000000..da5c51a1b --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SiteEditSectionsButton.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SitePropertiesButton.vue b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SitePropertiesButton.vue new file mode 100644 index 000000000..6120eb713 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/components/toolbar/actions/SitePropertiesButton.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/initComponents.js b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/initComponents.js new file mode 100644 index 000000000..994ba2da1 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/initComponents.js @@ -0,0 +1,50 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 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 SiteLayoutEditor from './components/SiteLayoutEditor.vue'; + +import Toolbar from './components/toolbar/Toolbar.vue'; +import SaveButton from './components/toolbar/actions/SaveButton.vue'; +import HistoryButtons from './components/toolbar/actions/HistoryButtons.vue'; +import MobilePreviewButton from './components/toolbar/actions/MobilePreviewButton.vue'; +import SitePropertiesButton from './components/toolbar/actions/SitePropertiesButton.vue'; +import SiteEditSectionsButton from './components/toolbar/actions/SiteEditSectionsButton.vue'; + +import Content from './components/content/Content.vue'; +import SidebarSectionMenu from './components/content/common/SidebarSectionMenu.vue'; +import BannerSectionMenu from './components/content/common/BannerSectionMenu.vue'; + +const components = { + 'site-layout-editor': SiteLayoutEditor, + + 'site-layout-editor-toolbar': Toolbar, + 'site-layout-editor-toolbar-save-button': SaveButton, + 'site-layout-editor-toolbar-history-buttons': HistoryButtons, + 'site-layout-editor-toolbar-properties-button': SitePropertiesButton, + 'site-layout-editor-toolbar-edit-sections-button': SiteEditSectionsButton, + 'site-layout-editor-toolbar-mobile-preview-button': MobilePreviewButton, + + 'site-layout-editor-content': Content, + 'site-layout-editor-sidebar-section-menu': SidebarSectionMenu, + 'site-layout-editor-banner-section-menu': BannerSectionMenu, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/layout-webapp/src/main/webapp/vue-app/site-layout-editor/main.js b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/main.js new file mode 100644 index 000000000..334285bfd --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/site-layout-editor/main.js @@ -0,0 +1,211 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2025 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-layout/main.js'; +import '../common-page-layout/main.js'; +import '../common-page-template/main.js'; +import '../common-portlets/main.js'; +import '../common-illustration/main.js'; + +// get overridden components if exists +if (extensionRegistry) { + const components = extensionRegistry.loadComponents('SiteLayoutEditor'); + if (components && components.length > 0) { + components.forEach(cmp => { + Vue.component(cmp.componentName, cmp.componentOptions); + }); + } +} + +const appId = 'siteLayoutEditor'; + +//getting language of the PLF +const lang = eXo?.env.portal.language || 'en'; + +//should expose the locale ressources as REST API +const url = `/layout/i18n/locale.portlet.LayoutEditor?lang=${lang}`; + +export function init() { + exoi18n.loadLanguageAsync(lang, url) + .then(i18n => { + // init Vue app when locale ressources are ready + Vue.createApp({ + template: ``, + vuetify: Vue.prototype.vuetifyOptions, + i18n, + data: () => ({ + containerTypes: extensionRegistry.loadExtensions('layout-editor', 'container'), + collator: new Intl.Collator(eXo.env.portal.language, {numeric: true, sensitivity: 'base'}), + hoveredParentId: null, + hoveredSectionId: null, + hoveredSection: null, + hoveredApplication: null, + portletInstanceCategories: null, + portletInstances: null, + loadingPortletInstances: false, + branding: null, + displayMode: 'desktop', + layout: null, + site: null, + draftSite: null, + draftSiteId: null, + movingParentId: null, + movingParentDynamic: false, + drawerOpened: 0, + selectedSectionId: null, + selectedCellCoordinates: [], + selectedCells: [], + nextCellStorageId: null, + nextCellDiffWidth: null, + parentAppDimensions: false, + multiCellsSelect: false, + sectionHistory: null, + sectionRedo: null, + movingCell: null, + moveType: null, + startScrollX: 0, + startScrollY: 0, + diffScrollX: 0, + diffScrollY: 0, + gap: 20, + isAdministrator: eXo.env.portal.isAdministrator, + }), + computed: { + parentAppX() { + return this.parentAppDimensions?.x || 0; + }, + parentAppY() { + return this.parentAppDimensions?.y || 0; + }, + defaultContainer() { + return this.containerTypes.find(extension => extension.type === 'default'); + }, + isResize() { + return this.moveType === 'resize'; + }, + isMove() { + return this.moveType === 'drag'; + }, + isMultiSelect() { + return this.moveType === 'multiSelect'; + }, + selectedFirstRowIndex() { + return Math.min(...this.selectedCellCoordinates.map(c => c.rowIndex)); + }, + selectedFirstColIndex() { + return Math.min(...this.selectedCellCoordinates.map(c => c.colIndex)); + }, + mobileDisplayMode() { + return this.$root.displayMode === 'mobile'; + }, + desktopDisplayMode() { + return this.$root.displayMode === 'desktop'; + }, + siteId() { + return this.$layoutUtils.getQueryParam('siteId'); + }, + siteType() { + return this.site?.siteType; + }, + siteName() { + return this.site?.name; + }, + draftSiteType() { + return this.draftSite?.siteType; + }, + draftSiteName() { + return this.draftSite?.name; + }, + }, + watch: { + movingParentId() { + if (this.movingParentId) { + this.$root.$emit('layout-editor-moving-start', this.movingParentId); + } else { + this.$root.$emit('layout-editor-moving-end', this.movingParentId); + } + }, + }, + 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.refreshPortletInstances(); + this.$siteLayoutService.getSiteById(this.siteId) + .then(site => this.site = site); + this.$brandingService.getBrandingInformation() + .then(data => this.branding = data); + }, + mounted() { + this.$el?.closest?.('.PORTLET-FRAGMENT')?.classList?.remove?.('PORTLET-FRAGMENT'); + }, + methods: { + setDrawerOpened() { + this.drawerOpened++; + }, + 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'); + }, + updateParentAppDimensions() { + this.parentAppDimensions = document.querySelector('#siteLayoutEditor').getBoundingClientRect(); + }, + initScrollPosition() { + this.updateParentAppDimensions(); + this.startScrollX = this.parentAppDimensions.x; + this.startScrollY = this.parentAppDimensions.y; + this.diffScrollX = 0; + this.diffScrollY = 0; + }, + updateScrollPosition() { + this.updateParentAppDimensions(); + this.diffScrollX = this.parentAppDimensions.x - this.startScrollX; + this.diffScrollY = this.parentAppDimensions.y - this.startScrollY; + }, + initCellsSelection() { + this.selectedSectionId = null; + this.moveType = null; + this.selectedCells = []; + this.selectedCellCoordinates = []; + }, + resetMoving() { + this.parentAppDimensions = null; + this.moveType = null; + this.multiCellsSelect = false; + }, + }, + }, `#${appId}`, 'Site Layout Editor'); + }) + .finally(() => { + Vue.prototype.$utils.includeExtensions('LayoutEditorExtension'); + document.dispatchEvent(new CustomEvent('displayTopBarLoading')); + }); +} diff --git a/layout-webapp/src/main/webapp/vue-app/site-management/components/main/Menu.vue b/layout-webapp/src/main/webapp/vue-app/site-management/components/main/Menu.vue index 3570a726d..b3cbd8ecc 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-management/components/main/Menu.vue +++ b/layout-webapp/src/main/webapp/vue-app/site-management/components/main/Menu.vue @@ -7,6 +7,7 @@ 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 @@ -38,6 +39,38 @@ + + + {{ noLayoutEditTooltip }} + fa fa-cog - - {{ $t('siteManagement.label.properties') }} + + {{ $t('siteManagement.label.properties') }} - fas fa-sitemap + fas fa-project-diagram - - {{ $t('siteManagement.label.navigation') }} + + {{ $t('siteManagement.label.navigation') }} fas fa-shield-alt - - {{ $t('siteManagement.label.manageAccess') }} + + {{ $t('siteManagement.label.manageAccess') }} @@ -113,9 +142,10 @@ @@ -137,9 +167,10 @@ v-bind="attrs"> @@ -178,15 +209,30 @@ export default { isGlobalSite() { return this.site.name === 'global'; }, + isAdministrationSite() { + return this.site.name === 'administration'; + }, isPortalSite() { return this.site.siteType === 'PORTAL'; }, canEditSite() { return this.site.canEdit; }, + canEditSiteLayout() { + return this.canEditSite && !this.isMetaSite && !this.isGlobalSite && !this.isAdministrationSite && !this.site.displayed; + }, + siteId() { + return this.site.siteId; + }, canDelete() { return this.canEditSite && this.site?.properties?.removable !== 'false'; }, + editSiteLayoutLink() { + return this.canEditSite && `${eXo.env.portal.context}/${eXo.env.portal.portalName}/site-layout-editor?siteId=${this.siteId}`; + }, + noLayoutEditTooltip() { + return (this.isMetaSite || this.isGlobalSite || this.isAdministrationSite) && this.$t('sites.label.system.noLayoutEdit') || this.$t('sites.label.meta.noLayoutEdit'); + }, }, watch: { displayActionMenu() { diff --git a/layout-webapp/src/main/webapp/vue-app/site-management/main.js b/layout-webapp/src/main/webapp/vue-app/site-management/main.js index 5c92cbdaf..2fad06dbe 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-management/main.js +++ b/layout-webapp/src/main/webapp/vue-app/site-management/main.js @@ -18,7 +18,7 @@ */ import './initComponents.js'; -import '../common-layout-components/initComponents.js'; +import '../common-sites/main.js'; import '../common-site-template/main.js'; // get overridden components if exists diff --git a/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js b/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js index afd91ab3d..d073aaee8 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js +++ b/layout-webapp/src/main/webapp/vue-app/site-navigation/main.js @@ -18,7 +18,7 @@ */ import './initComponents.js'; -import '../common-layout-components/main.js'; +import '../common-sites/main.js'; import '../common-illustration/main.js'; // get overridden components if exists diff --git a/layout-webapp/src/main/webapp/vue-app/site-template/main.js b/layout-webapp/src/main/webapp/vue-app/site-template/main.js index c8e8fb5d3..06bf02b6e 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-template/main.js +++ b/layout-webapp/src/main/webapp/vue-app/site-template/main.js @@ -18,9 +18,10 @@ */ import './initComponents.js'; +import '../common-layout/services.js'; + import '../common-illustration/main.js'; import '../common-site-template/main.js'; -import '../layout-editor/services.js'; // get overridden components if exists if (extensionRegistry) { diff --git a/layout-webapp/webpack.prod.js b/layout-webapp/webpack.prod.js index 5b8f0f9da..0a4a8dd8d 100644 --- a/layout-webapp/webpack.prod.js +++ b/layout-webapp/webpack.prod.js @@ -7,6 +7,7 @@ const config = { commonLayoutComponents: './src/main/webapp/vue-app/common/main.js', siteNavigation: './src/main/webapp/vue-app/site-navigation/main.js', siteManagement: './src/main/webapp/vue-app/site-management/main.js', + siteLayoutEditor: './src/main/webapp/vue-app/site-layout-editor/main.js', layoutEditor: './src/main/webapp/vue-app/layout-editor/main.js', pageLayout: './src/main/webapp/vue-app/page-layout/main.js', pageTemplates: './src/main/webapp/vue-app/page-template/main.js',