diff --git a/crowdin.yml b/crowdin.yml index 818ddc187..114bcd3ff 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -69,4 +69,22 @@ files: [ "escape_special_characters": 0, "escape_quotes" : 0, }, + { + "source" : "/layout-webapp/src/main/resources/locale/portlet/Analytics_en.properties", + + "translation" : "%original_path%/%file_name%!_%locale_with_underscore%.%file_extension%", + "translation_replace" : { + "_en!": "","ar_SA": "ar","ar_OM": "aro","az_AZ": "az","ca_ES": "ca","ceb_PH": "ceb", + "co_FR": "co","cs_CZ": "cs","de_DE": "de","el_GR": "el","en_US": "en","es_ES": "es_ES","eu_ES": "eu","fa_IR": "fa", + "fi_FI": "fi","fil_PH": "fil","fr_FR": "fr","hi_IN": "hi","hu_HU": "hu","id_ID": "id","it_IT": "it","ja_JP": "ja", + "kab_KAB": "kab","ko_KR": "ko","lt_LT": "lt","ms_MY": "ms","nl_NL": "nl","no_NO": "no","pcm_NG": "pcm","pl_PL": "pl", + "pt_BR": "pt_BR","pt_PT": "pt_PT","ro_RO": "ro","ru_RU": "ru","sk_SK": "sk","sl_SI": "sl","sq_AL": "sq", + "sv_SE": "sv_SE","th_TH": "th","tl_PH": "tl","tr_TR": "tr","uk_UA": "uk","ur_IN": "ur_IN","vi_VN": "vi", + "zh_CN": "zh_CN","zh_TW": "zh_TW", + }, + "dest" : "add__ons/layout/portlet/Analytics.properties", + "update_option" : "update_as_unapproved", + "escape_special_characters": 0, + "escape_quotes" : 0, + }, ] \ No newline at end of file diff --git a/layout-service/src/main/java/io/meeds/layout/entity/PageTemplateEntity.java b/layout-service/src/main/java/io/meeds/layout/entity/PageTemplateEntity.java index 1a04dd9e7..e5c9caefb 100644 --- a/layout-service/src/main/java/io/meeds/layout/entity/PageTemplateEntity.java +++ b/layout-service/src/main/java/io/meeds/layout/entity/PageTemplateEntity.java @@ -40,9 +40,18 @@ public class PageTemplateEntity { @SequenceGenerator(name = "SEQ_PAGE_TEMPLATE_ID", sequenceName = "SEQ_PAGE_TEMPLATE_ID", allocationSize = 1) @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_PAGE_TEMPLATE_ID") @Column(name = "ID") - protected Long id; + protected Long id; + + @Column(name = "DISABLED") + private boolean disabled; + + @Column(name = "SYSTEM_PAGE_TEMPLATE") + private boolean system; + + @Column(name = "CATEGORY") + private String category; @Column(name = "CONTENT") - private String content; + private String content; } diff --git a/layout-service/src/main/java/io/meeds/layout/model/PageTemplate.java b/layout-service/src/main/java/io/meeds/layout/model/PageTemplate.java index 21a14b2b9..5e998f93a 100644 --- a/layout-service/src/main/java/io/meeds/layout/model/PageTemplate.java +++ b/layout-service/src/main/java/io/meeds/layout/model/PageTemplate.java @@ -27,19 +27,32 @@ @AllArgsConstructor public class PageTemplate { - private long id; + private long id; - private String content; + private boolean disabled; - private String name; + private boolean system; - private String description; + private String category; - private long illustrationId; + private String content; - public PageTemplate(long id, String content) { + private String name; + + private String description; + + private long illustrationId; + + public PageTemplate(long id, + boolean disabled, + boolean system, + String category, + String content) { this.id = id; this.content = content; + this.disabled = disabled; + this.system = system; + this.category = category; } } diff --git a/layout-service/src/main/java/io/meeds/layout/model/PageTemplateDescriptor.java b/layout-service/src/main/java/io/meeds/layout/model/PageTemplateDescriptor.java index aae40baca..eaef9ca9c 100644 --- a/layout-service/src/main/java/io/meeds/layout/model/PageTemplateDescriptor.java +++ b/layout-service/src/main/java/io/meeds/layout/model/PageTemplateDescriptor.java @@ -37,6 +37,10 @@ public class PageTemplateDescriptor { private Map descriptions; + private String category; + + private boolean system; + private String illustrationPath; private String layoutPath; diff --git a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java index 9bf8320cc..556e6821d 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/PageTemplateRest.java @@ -61,6 +61,19 @@ public List getPageTemplates(HttpServletRequest request) { return pageTemplateService.getPageTemplates(request.getLocale(), true); } + @GetMapping("{id}") + @Secured("users") + @Operation(summary = "Retrieve a page template designated by its id", method = "GET", + description = "This will retrieve a page template designated by its id") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Request fulfilled"), }) + public PageTemplate getPageTemplate( + HttpServletRequest request, + @Parameter(description = "Page template identifier") + @PathVariable("id") + long id) { + return pageTemplateService.getPageTemplate(id, request.getLocale(), true); + } + @PostMapping @Secured("users") @Operation(summary = "Create a page template", method = "POST", description = "This creates a new page template") diff --git a/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java index 80bf52e5c..73a422de6 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/NavigationLayoutService.java @@ -268,7 +268,7 @@ public NodeLabel getNodeLabels(long nodeId, String username) throws ObjectNotFou String label = nodeData.getState().getLabel(); if (ExpressionUtil.isResourceBindingExpression(label)) { SiteKey siteKey = nodeData.getSiteKey(); - ResourceBundle nodeLabelResourceBundle = resourceBundleManager.getNavigationResourceBundle(locale.toLanguageTag(), + ResourceBundle nodeLabelResourceBundle = resourceBundleManager.getNavigationResourceBundle(getLocaleName(locale), siteKey.getTypeName(), siteKey.getName()); if (nodeLabelResourceBundle != null) { @@ -448,4 +448,8 @@ public NodeLabel toNodeLabel(Map nodeLabels) { return nodeLabel; } + private String getLocaleName(Locale locale) { + return locale.toLanguageTag().replace("-", "_"); // Use same name as localeConfigService + } + } 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 2093fc652..feaeaf34b 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 @@ -291,6 +291,9 @@ private Page createPageInstance(String siteType, if (pageTemplate == null) { throw new ObjectNotFoundException("pageTemplate not found"); } + if (pageTemplate.isDisabled()) { + throw new IllegalArgumentException("pageTemplate with designated Id is disabled"); + } page = userPortalConfigService.createPageTemplate(EMPTY_PAGE_TEMPLATE, siteType, siteName); diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java index 76dfeae1e..126ccdbc2 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateImportService.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import org.apache.commons.io.IOUtils; @@ -49,6 +50,7 @@ import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.services.resources.ResourceBundleService; import org.exoplatform.services.security.ConversationState; import org.exoplatform.social.attachment.AttachmentService; import org.exoplatform.social.attachment.model.UploadedAttachmentDetail; @@ -71,13 +73,17 @@ @Component public class PageTemplateImportService { - private static final Scope PAGE_TEMPLATE_IMPORT_SCOPE = Scope.APPLICATION.id("PAGE_TEMPLATE_IMPORT"); + private static final Scope PAGE_TEMPLATE_IMPORT_SCOPE = Scope.APPLICATION.id("PAGE_TEMPLATE_IMPORT"); - private static final Context PAGE_TEMPLATE_CONTEXT = Context.GLOBAL.id("PAGE_TEMPLATE"); + private static final Context PAGE_TEMPLATE_CONTEXT = Context.GLOBAL.id("PAGE_TEMPLATE"); - private static final Log LOG = ExoLogger.getLogger(PageTemplateImportService.class); + private static final String PAGE_TEMPLATE_VERSION = "version"; - private static final Random RANDOM = new Random(); + private static final long PAGE_TEMPLATE_IMPORT_VERSION = 1; + + private static final Log LOG = ExoLogger.getLogger(PageTemplateImportService.class); + + private static final Random RANDOM = new Random(); @Autowired private LayoutAclService layoutAclService; @@ -97,6 +103,9 @@ public class PageTemplateImportService { @Autowired private SettingService settingService; + @Autowired + private ResourceBundleService resourceBundleService; + @Autowired private ConfigurationManager configurationManager; @@ -121,6 +130,13 @@ public void init() { @ContainerTransactional public void importPageTemplates() { LOG.info("Importing Page Templates"); + if (!forceReimportTemplates + && getSettingValue(PAGE_TEMPLATE_VERSION) != PAGE_TEMPLATE_IMPORT_VERSION) { + forceReimportTemplates = true; + } + + ConversationState currentConversationState = ConversationState.getCurrent(); + ConversationState.setCurrent(layoutAclService.getSuperUserConversationState()); try { Enumeration templateFiles = PortalContainer.getInstance() .getPortalClassLoader() @@ -139,7 +155,11 @@ public void importPageTemplates() { .forEach(this::importDescriptor); } catch (Exception e) { LOG.warn("An error occurred while importing page templates", e); + } finally { + ConversationState.setCurrent(currentConversationState); } + setSettingValue(PAGE_TEMPLATE_VERSION, PAGE_TEMPLATE_IMPORT_VERSION); + LOG.info("Importing Page Templates finished successfully"); } protected List parseDescriptors(URL url) { @@ -155,21 +175,19 @@ protected List parseDescriptors(URL url) { protected void importDescriptor(PageTemplateDescriptor descriptor) { String descriptorId = descriptor.getId(); - long oldTemplateId = getAlreadyImportedTemplateId(descriptorId); - if (forceReimportTemplates || oldTemplateId == 0) { - importPageTemplate(descriptor, oldTemplateId); + long existingTemplateId = getSettingValue(descriptorId); + if (forceReimportTemplates || existingTemplateId == 0) { + importPageTemplate(descriptor, existingTemplateId); } else { LOG.info("Ignore re-importing Page Template {}", descriptorId); } } protected void importPageTemplate(PageTemplateDescriptor d, long oldTemplateId) { - ConversationState currentConversationState = ConversationState.getCurrent(); - ConversationState.setCurrent(layoutAclService.getSuperUserConversationState()); + LOG.info("Importing Page Template {}", d.getId()); try { - LOG.info("Importing Page Template {}", d.getId()); PageTemplate pageTemplate = createPageTemplate(d, oldTemplateId); - if (oldTemplateId == 0 || pageTemplate.getId() != oldTemplateId) { + if (forceReimportTemplates || oldTemplateId == 0 || pageTemplate.getId() != oldTemplateId) { LOG.info("Importing Page Template {} title translations", d.getId()); saveTemplateNames(d, pageTemplate); LOG.info("Importing Page Template {} description translations", d.getId()); @@ -177,20 +195,22 @@ protected void importPageTemplate(PageTemplateDescriptor d, long oldTemplateId) LOG.info("Importing Page Template {} illustration", d.getId()); saveTemplateIllustration(pageTemplate.getId(), d.getIllustrationPath()); // Mark as imported - settingService.set(PAGE_TEMPLATE_CONTEXT, - PAGE_TEMPLATE_IMPORT_SCOPE, - d.getId(), - SettingValue.create(String.valueOf(pageTemplate.getId()))); + setSettingValue(d.getId(), pageTemplate.getId()); } LOG.info("Importing Page Template {} finished successfully", d.getId()); } catch (Exception e) { LOG.warn("An error occurred while importing page template {}", d.getId(), e); - } finally { - ConversationState.setCurrent(currentConversationState); } } protected void saveTemplateNames(PageTemplateDescriptor d, PageTemplate pageTemplate) { + try { + translationService.deleteTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, + pageTemplate.getId(), + PageTemplateTranslationPlugin.TITLE_FIELD_NAME); + } catch (Exception e) { // NOSONAR + // Normal, when not exists + } d.getNames() .forEach((k, v) -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, pageTemplate.getId(), @@ -209,6 +229,13 @@ protected void saveTemplateNames(PageTemplateDescriptor d, PageTemplate pageTemp } protected void saveTemplateDescriptions(PageTemplateDescriptor d, PageTemplate pageTemplate) { + try { + translationService.deleteTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, + pageTemplate.getId(), + PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME); + } catch (Exception e) { // NOSONAR + // Normal, when not exists + } d.getDescriptions() .forEach((k, v) -> saveTranslationLabel(PageTemplateTranslationPlugin.OBJECT_TYPE, pageTemplate.getId(), @@ -232,11 +259,12 @@ protected PageTemplate createPageTemplate(PageTemplateDescriptor d, long oldTemp if (oldTemplateId > 0) { pageTemplate = pageTemplateService.getPageTemplate(oldTemplateId); } - boolean isNew = false; - if (pageTemplate == null) { + boolean isNew = pageTemplate == null; + if (isNew) { pageTemplate = new PageTemplate(); - isNew = true; } + pageTemplate.setCategory(d.getCategory()); + pageTemplate.setSystem(d.isSystem()); try (InputStream is = configurationManager.getInputStream(d.getLayoutPath())) { String xml = IOUtil.getStreamContentAsString(is); Container layout = fromXML(xml); @@ -251,6 +279,12 @@ protected PageTemplate createPageTemplate(PageTemplateDescriptor d, long oldTemp @SneakyThrows protected void saveTranslationLabel(String objectType, long id, String fieldName, Locale locale, String label) { + if (PortalContainer.getInstanceIfPresent() != null) { + String i18nLabel = getI18NLabel(label, locale); + if (i18nLabel != null) { + label = i18nLabel; + } + } translationService.saveTranslationLabel(objectType, id, fieldName, @@ -287,9 +321,32 @@ protected Container fromXML(String xml) { return obj.getObject(); } - protected long getAlreadyImportedTemplateId(String descriptorId) { + protected String getI18NLabel(String label, Locale locale) { + try { + ResourceBundle resourceBundle = + resourceBundleService.getResourceBundle("locale.portlet.Portlets", + locale, + PortalContainer.getInstance() + .getPortalClassLoader()); + if (resourceBundle != null && resourceBundle.containsKey(label)) { + return resourceBundle.getString(label); + } + } catch (Exception e) { + LOG.debug("Resource Bundle not found with locale {}", locale, e); + } + return null; + } + + protected void setSettingValue(String name, long value) { + settingService.set(PAGE_TEMPLATE_CONTEXT, + PAGE_TEMPLATE_IMPORT_SCOPE, + name, + SettingValue.create(String.valueOf(value))); + } + + protected long getSettingValue(String name) { try { - SettingValue settingValue = settingService.get(PAGE_TEMPLATE_CONTEXT, PAGE_TEMPLATE_IMPORT_SCOPE, descriptorId); + SettingValue settingValue = settingService.get(PAGE_TEMPLATE_CONTEXT, PAGE_TEMPLATE_IMPORT_SCOPE, name); return settingValue == null || settingValue.getValue() == null ? 0l : Long.parseLong(settingValue.getValue().toString()); } catch (NumberFormatException e) { return 0l; diff --git a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java index fb373ac17..782aa19dd 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/PageTemplateService.java @@ -70,19 +70,19 @@ public List getPageTemplates(boolean expand) { public List getPageTemplates(Locale locale, boolean expand) { List pageTemplates = pageTemplateStorage.getPageTemplates(); if (expand) { - pageTemplates.forEach(pageTemplate -> { - pageTemplate.setName(getLabel(pageTemplate.getId(), PageTemplateTranslationPlugin.TITLE_FIELD_NAME, locale)); - pageTemplate.setDescription(getLabel(pageTemplate.getId(), PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME, locale)); - List attachmentFileIds = attachmentService.getAttachmentFileIds(PageTemplateAttachmentPlugin.OBJECT_TYPE, - String.valueOf(pageTemplate.getId())); - if (CollectionUtils.isNotEmpty(attachmentFileIds)) { - pageTemplate.setIllustrationId(Long.parseLong(attachmentFileIds.get(0))); - } - }); + pageTemplates.forEach(pageTemplate -> computePageTemplateAttributes(locale, pageTemplate)); } return pageTemplates; } + public PageTemplate getPageTemplate(long id, Locale locale, boolean expand) { + PageTemplate pageTemplate = pageTemplateStorage.getPageTemplate(id); + if (expand) { + computePageTemplateAttributes(locale, pageTemplate); + } + return pageTemplate; + } + public PageTemplate getPageTemplate(long id) { return pageTemplateStorage.getPageTemplate(id); } @@ -102,6 +102,13 @@ public void deletePageTemplate(long templateId, String username) throws IllegalA if (!layoutAclService.isAdministrator(username)) { throw new IllegalAccessException("User isn't authorized to create a page template"); } + PageTemplate pageTemplate = getPageTemplate(templateId); + if (pageTemplate == null) { + throw new ObjectNotFoundException("Page template doesn't exist"); + } + if (pageTemplate.isSystem()) { + throw new IllegalAccessException("Can't delete a system page template"); + } deletePageTemplate(templateId); } @@ -157,4 +164,14 @@ private String getLabel(long templateId, String fieldName, Locale locale) { } } + private void computePageTemplateAttributes(Locale locale, PageTemplate pageTemplate) { + pageTemplate.setName(getLabel(pageTemplate.getId(), PageTemplateTranslationPlugin.TITLE_FIELD_NAME, locale)); + pageTemplate.setDescription(getLabel(pageTemplate.getId(), PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME, locale)); + List attachmentFileIds = attachmentService.getAttachmentFileIds(PageTemplateAttachmentPlugin.OBJECT_TYPE, + String.valueOf(pageTemplate.getId())); + if (CollectionUtils.isNotEmpty(attachmentFileIds)) { + pageTemplate.setIllustrationId(Long.parseLong(attachmentFileIds.get(0))); + } + } + } diff --git a/layout-service/src/main/java/io/meeds/layout/storage/PageTemplateStorage.java b/layout-service/src/main/java/io/meeds/layout/storage/PageTemplateStorage.java index 7328a9518..58d8a8755 100644 --- a/layout-service/src/main/java/io/meeds/layout/storage/PageTemplateStorage.java +++ b/layout-service/src/main/java/io/meeds/layout/storage/PageTemplateStorage.java @@ -37,28 +37,54 @@ public class PageTemplateStorage { public List getPageTemplates() { List entities = pageTemplateDAO.findAll(); - return entities.stream().map(entity -> new PageTemplate(entity.getId(), entity.getContent())).toList(); + return entities.stream() + .map(e -> new PageTemplate(e.getId(), + e.isDisabled(), + e.isSystem(), + e.getCategory(), + e.getContent())) + .toList(); } public PageTemplate getPageTemplate(long id) { return pageTemplateDAO.findById(id) - .map(e -> new PageTemplate(e.getId(), e.getContent())) + .map(e -> new PageTemplate(e.getId(), + e.isDisabled(), + e.isSystem(), + e.getCategory(), + e.getContent())) .orElse(null); } public PageTemplate createPageTemplate(PageTemplate pageTemplate) { - PageTemplateEntity entity = new PageTemplateEntity(null, pageTemplate.getContent()); + PageTemplateEntity entity = new PageTemplateEntity(null, + pageTemplate.isDisabled(), + pageTemplate.isSystem(), + pageTemplate.getCategory(), + pageTemplate.getContent()); entity = pageTemplateDAO.save(entity); - return new PageTemplate(entity.getId(), entity.getContent()); + return new PageTemplate(entity.getId(), + entity.isDisabled(), + entity.isSystem(), + entity.getCategory(), + entity.getContent()); } public PageTemplate updatePageTemplate(PageTemplate pageTemplate) throws ObjectNotFoundException { if (!pageTemplateDAO.existsById(pageTemplate.getId())) { throw new ObjectNotFoundException("Page template doesn't exist"); } - PageTemplateEntity entity = new PageTemplateEntity(pageTemplate.getId(), pageTemplate.getContent()); + PageTemplateEntity entity = new PageTemplateEntity(pageTemplate.getId(), + pageTemplate.isDisabled(), + pageTemplate.isSystem(), + pageTemplate.getCategory(), + pageTemplate.getContent()); entity = pageTemplateDAO.save(entity); - return new PageTemplate(entity.getId(), entity.getContent()); + return new PageTemplate(entity.getId(), + entity.isDisabled(), + entity.isSystem(), + entity.getCategory(), + entity.getContent()); } public void deletePageTemplate(long templateId) throws ObjectNotFoundException { diff --git a/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml b/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml index 3137dd2a8..771a4c1e7 100644 --- a/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml +++ b/layout-service/src/main/resources/changelog/layout-rdbms.db.changelog-1.0.0.xml @@ -46,4 +46,48 @@ + + + + + + + + + + + + + + + ALTER TABLE LAYOUT_PAGE_TEMPLATES MODIFY COLUMN CONTENT LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + + + + + ALTER TABLE LAYOUT_PAGE_TEMPLATES MODIFY COLUMN CATEGORY VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + + + + + + + + \ No newline at end of file diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java new file mode 100644 index 000000000..05d5f6b2a --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java @@ -0,0 +1,261 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.layout.rest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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 com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.PageTemplate; +import io.meeds.layout.service.PageTemplateService; +import io.meeds.spring.web.security.PortalAuthenticationManager; +import io.meeds.spring.web.security.WebSecurityConfiguration; + +import jakarta.servlet.Filter; +import lombok.SneakyThrows; + +@SpringBootTest(classes = { PageTemplateRest.class, PortalAuthenticationManager.class, }) +@ContextConfiguration(classes = { WebSecurityConfiguration.class }) +@AutoConfigureWebMvc +@AutoConfigureMockMvc(addFilters = false) +@ExtendWith(MockitoExtension.class) +public class PageTemplateRestTest { + + private static final String REST_PATH = "/pageTemplates"; // NOSONAR + + private static final String SIMPLE_USER = "simple"; + + private static final String TEST_PASSWORD = "testPassword"; + + static final ObjectMapper OBJECT_MAPPER; + + static { + // Workaround when Jackson is defined in shared library with different + // version and without artifact jackson-datatype-jsr310 + OBJECT_MAPPER = JsonMapper.builder() + .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + OBJECT_MAPPER.registerModule(new JavaTimeModule()); + } + + @MockBean + private PageTemplateService pageTemplateService; + + @Autowired + private SecurityFilterChain filterChain; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChain.getFilters().toArray(new Filter[0])) + .build(); + } + + @Test + void getPageTemplatesAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(pageTemplateService); + } + + @Test + void getPageTemplatesWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH).with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(pageTemplateService).getPageTemplates(any(), anyBoolean()); + } + + @Test + void getPageTemplateAnonymously() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(pageTemplateService); + } + + @Test + void getPageTemplateWithUser() throws Exception { + ResultActions response = mockMvc.perform(get(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + verify(pageTemplateService).getPageTemplate(eq(1l), any(), eq(true)); + } + + @Test + void createPageTemplateAnonymously() throws Exception { + ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PageTemplate())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(pageTemplateService); + } + + @Test + void createPageTemplateWithUser() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(pageTemplate)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + verify(pageTemplateService).createPageTemplate(pageTemplate, SIMPLE_USER); + } + + @Test + void createPageTemplateWithUserForbidden() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + when(pageTemplateService.createPageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) + .content(asJsonString(pageTemplate)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePageTemplateAnonymously() throws Exception { + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PageTemplate())) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + verifyNoInteractions(pageTemplateService); + } + + @Test + void updatePageTemplateWithUser() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(pageTemplate)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isOk()); + + pageTemplate.setId(1l); + verify(pageTemplateService).updatePageTemplate(pageTemplate, SIMPLE_USER); + } + + @Test + void updatePageTemplateWithUserForbidden() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + pageTemplate.setId(1l); + when(pageTemplateService.updatePageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(IllegalAccessException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(pageTemplate)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isForbidden()); + } + + @Test + void updatePageTemplateWithUserNotFound() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + pageTemplate.setId(1l); + when(pageTemplateService.updatePageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); + + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) + .content(asJsonString(pageTemplate)) + .contentType(MediaType.APPLICATION_JSON)); + response.andExpect(status().isNotFound()); + } + + @Test + void deletePageTemplateAnonymously() throws Exception { + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1")); + response.andExpect(status().isForbidden()); + verifyNoInteractions(pageTemplateService); + } + + @Test + void deletePageTemplateWithUser() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isOk()); + + pageTemplate.setId(1l); + verify(pageTemplateService).deletePageTemplate(1l, SIMPLE_USER); + } + + @Test + void deletePageTemplateWithUserForbidden() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + pageTemplate.setId(1l); + doThrow(IllegalAccessException.class).when(pageTemplateService).deletePageTemplate(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isForbidden()); + } + + @Test + void deletePageTemplateWithUserNotFound() throws Exception { + PageTemplate pageTemplate = new PageTemplate(); + pageTemplate.setId(1l); + doThrow(ObjectNotFoundException.class).when(pageTemplateService).deletePageTemplate(1l, SIMPLE_USER); + + ResultActions response = mockMvc.perform(delete(REST_PATH + "/1").with(testSimpleUser())); + response.andExpect(status().isNotFound()); + } + + private RequestPostProcessor testSimpleUser() { + return user(SIMPLE_USER).password(TEST_PASSWORD) + .authorities(new SimpleGrantedAuthority("users")); + } + + @SneakyThrows + public static String asJsonString(final Object obj) { + return OBJECT_MAPPER.writeValueAsString(obj); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java index 4a10714e8..f5e0100cd 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateImportServiceTest.java @@ -30,6 +30,7 @@ import org.exoplatform.commons.api.settings.SettingService; import org.exoplatform.container.configuration.ConfigurationManager; import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.services.resources.ResourceBundleService; import org.exoplatform.social.attachment.AttachmentService; import io.meeds.layout.plugin.PageTemplateAttachmentPlugin; @@ -58,6 +59,9 @@ public class PageTemplateImportServiceTest { @MockBean private SettingService settingService; + @MockBean + private ResourceBundleService resourceBundleService; + @MockBean private ConfigurationManager configurationManager; @@ -72,7 +76,9 @@ public class PageTemplateImportServiceTest { @Test public void init() { - assertDoesNotThrow(() -> pageTemplateImportService.init(), "Shouldn't stop the container initialization if page templates fails"); - assertDoesNotThrow(() -> pageTemplateImportService.importPageTemplates(), "Shouldn't stop the container initialization if page templates fails"); + assertDoesNotThrow(() -> pageTemplateImportService.init(), + "Shouldn't stop the container initialization if page templates fails"); + assertDoesNotThrow(() -> pageTemplateImportService.importPageTemplates(), + "Shouldn't stop the container initialization if page templates fails"); } } diff --git a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java index ea7ecc8c6..2486b4362 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/PageTemplateServiceTest.java @@ -55,12 +55,14 @@ import io.meeds.social.translation.service.TranslationService; @SpringBootTest(classes = { - PageTemplateService.class, + PageTemplateService.class, }) @ExtendWith(MockitoExtension.class) public class PageTemplateServiceTest { - private static final String LAYOUT_CONTENT = "...layout..."; + private static final String LAYOUT_CONTENT = "...layout..."; + + private static final String LAYOUT_CATEGORY = "CATEGORY"; @MockBean private LayoutAclService layoutAclService; @@ -89,13 +91,13 @@ public class PageTemplateServiceTest { @Autowired private PageTemplateService pageTemplateService; - private String testuser = "testuser"; + private String testuser = "testuser"; @Test public void getPageTemplates() { when(pageTemplate.getId()).thenReturn(2l); when(pageTemplate.getContent()).thenReturn(LAYOUT_CONTENT); - + when(pageTemplateStorage.getPageTemplates()).thenReturn(Collections.singletonList(pageTemplate)); List pageTemplates = pageTemplateService.getPageTemplates(); assertNotNull(pageTemplates); @@ -109,7 +111,7 @@ public void getPageTemplates() { @Test public void getPageTemplatesWithExpand() throws ObjectNotFoundException { - PageTemplate template = new PageTemplate(2l, LAYOUT_CONTENT); + PageTemplate template = new PageTemplate(2l, false, false, LAYOUT_CATEGORY, LAYOUT_CONTENT); when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); @@ -166,13 +168,74 @@ public void getPageTemplatesWithExpand() throws ObjectNotFoundException { assertEquals(1, pageTemplates.size()); assertEquals(enDesc, pageTemplates.get(0).getDescription()); - when(attachmentService.getAttachmentFileIds(PageTemplateAttachmentPlugin.OBJECT_TYPE, "2")).thenReturn(Collections.singletonList("32")); + when(attachmentService.getAttachmentFileIds(PageTemplateAttachmentPlugin.OBJECT_TYPE, + "2")).thenReturn(Collections.singletonList("32")); pageTemplates = pageTemplateService.getPageTemplates(Locale.GERMAN, true); assertNotNull(pageTemplates); assertEquals(1, pageTemplates.size()); assertEquals(32l, pageTemplates.get(0).getIllustrationId()); } + @Test + public void getPageTemplateWithExpand() throws ObjectNotFoundException { + PageTemplate template = new PageTemplate(2l, false, false, LAYOUT_CATEGORY, LAYOUT_CONTENT); + when(localeConfigService.getDefaultLocaleConfig()).thenReturn(defaultLocaleConfig); + when(defaultLocaleConfig.getLocale()).thenReturn(Locale.ENGLISH); + + when(pageTemplateStorage.getPageTemplate(2l)).thenReturn(template); + + PageTemplate retrievedPageTemplate = pageTemplateService.getPageTemplate(2l); + assertNotNull(retrievedPageTemplate); + assertEquals(template.getId(), retrievedPageTemplate.getId()); + assertEquals(template.getContent(), retrievedPageTemplate.getContent()); + assertNull(retrievedPageTemplate.getName()); + assertNull(retrievedPageTemplate.getDescription()); + assertEquals(0l, retrievedPageTemplate.getIllustrationId()); + + when(translationService.getTranslationField(PageTemplateTranslationPlugin.OBJECT_TYPE, + template.getId(), + PageTemplateTranslationPlugin.TITLE_FIELD_NAME)).thenThrow(ObjectNotFoundException.class); + retrievedPageTemplate = pageTemplateService.getPageTemplate(2l, Locale.FRENCH, true); + assertNotNull(retrievedPageTemplate); + + reset(translationService); + + TranslationField titleTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PageTemplateTranslationPlugin.OBJECT_TYPE, + template.getId(), + PageTemplateTranslationPlugin.TITLE_FIELD_NAME)).thenReturn(titleTranslationField); + retrievedPageTemplate = pageTemplateService.getPageTemplate(2l, Locale.FRENCH, true); + assertNotNull(retrievedPageTemplate); + assertEquals(template.getId(), retrievedPageTemplate.getId()); + assertEquals(template.getContent(), retrievedPageTemplate.getContent()); + assertNull(retrievedPageTemplate.getName()); + assertNull(retrievedPageTemplate.getDescription()); + assertEquals(0l, retrievedPageTemplate.getIllustrationId()); + + String frTitle = "testTitle"; + when(titleTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.FRENCH, frTitle)); + + retrievedPageTemplate = pageTemplateService.getPageTemplate(2l, Locale.FRENCH, true); + assertEquals(frTitle, retrievedPageTemplate.getName()); + + TranslationField descriptionTranslationField = mock(TranslationField.class); + when(translationService.getTranslationField(PageTemplateTranslationPlugin.OBJECT_TYPE, + template.getId(), + PageTemplateTranslationPlugin.DESCRIPTION_FIELD_NAME)).thenReturn(descriptionTranslationField); + String enDesc = "testDescription"; + when(descriptionTranslationField.getLabels()).thenReturn(Collections.singletonMap(Locale.ENGLISH, enDesc)); + + retrievedPageTemplate = pageTemplateService.getPageTemplate(2l, Locale.ENGLISH, true); + assertNotNull(retrievedPageTemplate); + assertEquals(enDesc, retrievedPageTemplate.getDescription()); + + when(attachmentService.getAttachmentFileIds(PageTemplateAttachmentPlugin.OBJECT_TYPE, + "2")).thenReturn(Collections.singletonList("32")); + retrievedPageTemplate = pageTemplateService.getPageTemplate(2l, Locale.GERMAN, true); + assertNotNull(retrievedPageTemplate); + assertEquals(32l, retrievedPageTemplate.getIllustrationId()); + } + @Test public void getPageTemplate() { when(pageTemplateStorage.getPageTemplate(2l)).thenReturn(pageTemplate); @@ -196,9 +259,17 @@ public void createPageTemplate() throws IllegalAccessException { @Test public void deletePageTemplate() throws ObjectNotFoundException, IllegalAccessException { assertThrows(IllegalAccessException.class, () -> pageTemplateService.deletePageTemplate(2l, testuser)); - + when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + assertThrows(ObjectNotFoundException.class, () -> pageTemplateService.deletePageTemplate(2l, testuser)); + + when(pageTemplateStorage.getPageTemplate(2l)).thenReturn(pageTemplate); + when(pageTemplate.isSystem()).thenReturn(true); + assertThrows(IllegalAccessException.class, () -> pageTemplateService.deletePageTemplate(2l, testuser)); + + when(pageTemplate.isSystem()).thenReturn(false); pageTemplateService.deletePageTemplate(2l, testuser); + verify(attachmentService, times(1)).deleteAttachments(PageTemplateAttachmentPlugin.OBJECT_TYPE, "2"); verify(translationService, times(1)).deleteTranslationLabels(PageTemplateTranslationPlugin.OBJECT_TYPE, 2l); verify(pageTemplateStorage, times(1)).deletePageTemplate(2l); @@ -209,6 +280,7 @@ public void deletePageTemplateWhenException() throws ObjectNotFoundException, Il assertThrows(IllegalAccessException.class, () -> pageTemplateService.deletePageTemplate(2l, testuser)); when(layoutAclService.isAdministrator(testuser)).thenReturn(true); + when(pageTemplateStorage.getPageTemplate(2l)).thenReturn(pageTemplate); doThrow(RuntimeException.class).when(attachmentService).deleteAttachments(anyString(), any()); doThrow(ObjectNotFoundException.class).when(translationService).deleteTranslationLabels(anyString(), anyLong()); pageTemplateService.deletePageTemplate(2l, testuser); diff --git a/layout-webapp/src/main/java/io/meeds/layout/LayoutApplication.java b/layout-webapp/src/main/java/io/meeds/layout/LayoutApplication.java index f20693629..7ba39cbbe 100644 --- a/layout-webapp/src/main/java/io/meeds/layout/LayoutApplication.java +++ b/layout-webapp/src/main/java/io/meeds/layout/LayoutApplication.java @@ -27,8 +27,7 @@ AvailableIntegration.KERNEL_MODULE, AvailableIntegration.JPA_MODULE, AvailableIntegration.LIQUIBASE_MODULE, - AvailableIntegration.WEB_SECURITY_MODULE, - AvailableIntegration.WEB_TRANSACTION_MODULE, + AvailableIntegration.WEB_MODULE, }) @EnableJpaRepositories(basePackages = LayoutApplication.MODULE_NAME) @PropertySource("classpath:application.properties") diff --git a/layout-webapp/src/main/resources/locale/portlet/Analytics_en.properties b/layout-webapp/src/main/resources/locale/portlet/Analytics_en.properties new file mode 100644 index 000000000..a8a372d30 --- /dev/null +++ b/layout-webapp/src/main/resources/locale/portlet/Analytics_en.properties @@ -0,0 +1,30 @@ +analytics.field.label.applicationName=Application Name + +analytics.field.label.pageTemplateId=Page Template Id +analytics.field.label.pageTemplateName=Page Template Name +analytics.field.label.pageTemplateCategory=Page Template Category +analytics.field.label.pageTemplateDisabled=Is Page Template Disabled +analytics.field.label.pageTemplateSystem=Is Page Template System +analytics.field.label.pageTemplateIllustrationId=Page Template illustration id +analytics.field.label.pageTemplatePage=Page Template originating page +analytics.field.label.pageReference=Page reference +analytics.field.label.siteName=Site name +analytics.field.label.siteType=Site type +analytics.field.label.pageType=Page type +analytics.field.label.pageName=Page name + +analytics.pageLayout=Page Layout +analytics.createPageLayout=Create Page Layout +analytics.createPageTemplate=Create Page Template +analytics.updatePageTemplate=Update Page Template +analytics.deletePageTemplate=Delete Page Template +analytics.enablePageTemplate=Enable Page Template +analytics.disablePageTemplate=Disable Page Template +analytics.siteNavigation=Site topbar navigation +analytics.siteManagement=Sites Management +analytics.pageTemplateManagement=Page Templates Management +analytics.pageLayoutEditor=Layout Editor + +analytics.customized=Customized +analytics.blank=Blank +analytics.analytics=Analytics \ No newline at end of file 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 a38c8aa34..ad1e9fcff 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -1,5 +1,6 @@ layout.publish=Publish layout.editPageName=Edit {0} +layout.editPageTemplate=Edit Template {0} layout.addSectionBefore=Add a section above layout.addSectionAfter=Add a section below layout.editSection=Edit section @@ -77,6 +78,8 @@ layout.listAppsInRows=List apps in rows layout.listAppsInColumns=List apps in columns layout.saveAsTemplate=Save as template layout.saveAsTemplateTitle=Save as template +layout.editTemplateTitle=Edit template characteristics +layout.duplicateTemplateTitle=Duplicate template layout.templateTitle=Name layout.templateDescription=Description layout.templatePreview=Preview @@ -111,3 +114,36 @@ layout.reminder.description.part1=From now on, in few clicks, design your page u layout.reminder.description.part2=- Organize Pages in Sections layout.reminder.description.part3=- Manage Apps Display layout.reminder.description.part4=- Preview Page Content + +pageTemplates.title=Page Templates +pageTemplates.add=Add +pageTemplates.filter.placeholder=Filter by name, description +pageTemplates.label.name=Name +pageTemplates.label.description=Description +pageTemplates.label.category=Category +pageTemplates.label.status=Status +pageTemplates.label.actions=Actions +pageTemplates.menu.open=Open Menu +pageTemplate.label.preview=Preview of {0} template +pageTemplate.status.update.success=Template status successfully updated +pageTemplate.status.update.error=An unknown error occurred while updating template status. Please contact the administrator or try agan later. +pageTemplate.layout.update.success=Template layout successfully updated +pageTemplate.layout.update.error=An unknown error occurred while updating template layout. Please contact the administrator or try agan later. +pageTemplate.delete.success=Template successfully deleted +pageTemplate.delete.error=An unknown error occurred while deleting template. Please contact the administrator or try agan later. +pageTemplate.label.delete=Delete +pageTemplate.label.editLayout=Edit Layout +pageTemplate.label.editProperties=Edit Properties +pageTemplate.label.duplicate=Duplicate +pageTemplate.label.templateMenu={0} +pageTemplate.label.closeMenu=Close Menu +pageTemplate.label.close=Close +pageTemplate.label.openIllustrationPreview=Open illustration Preview +pageTemplate.label.enableTemplate=Enable Template +pageTemplate.label.disableTemplate=Disable Template +pageTemplate.label.system.noDelete=This product template cannot be deleted +pageTemplate.label.system.noEditLayout=This product template's layout cannot be updated +pageTemplate.label.confirmDeleteTitle=Delete page template? +pageTemplate.label.confirmDeleteMessage=Would you like to delete page template: {0} +pageTemplate.label.confirm=Confirm +pageTemplate.label.cancel=Cancel 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 6e325efc8..3a7668f35 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -1,4 +1,4 @@ - + + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.exoplatform.org/xml/ns/gatein_resources_1_4 http://www.exoplatform.org/xml/ns/gatein_resources_1_4" + xmlns="http://www.exoplatform.org/xml/ns/gatein_resources_1_4"> layout @@ -214,10 +214,6 @@ imageCropper - - cropper - Cropper - commonLayoutComponents @@ -239,5 +235,36 @@ + + PageTemplatesManagement + + + + commonVueComponents + + + commonLayoutComponents + + + attachImage + + + translationField + + + applicationToolbarComponent + + + extensionRegistry + + + eXoVueI18n + + + + diff --git a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml index fb17c13d7..946a7ecdb 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/portlet.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/portlet.xml @@ -78,4 +78,23 @@ + + PageTemplatesManagement + Page Templates Management Portlet + org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet + + portlet-view-dispatched-file-path + /html/pageTemplatesManagement.html + + + text/html + + en + locale.portlet.LayoutEditor + + site navigation + site navigation Management + + + diff --git a/layout-webapp/src/main/webapp/html/pageTemplatesManagement.html b/layout-webapp/src/main/webapp/html/pageTemplatesManagement.html new file mode 100644 index 000000000..3a077d97b --- /dev/null +++ b/layout-webapp/src/main/webapp/html/pageTemplatesManagement.html @@ -0,0 +1,7 @@ +
+
+ +
+
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-layout-components/components/site-navigation/SiteNavigationElementDrawer.vue index 2f7b0cba6..e5777c582 100644 --- 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-layout-components/components/site-navigation/SiteNavigationElementDrawer.vue @@ -247,7 +247,8 @@ export default { this.navigationNode.siteKey.name, this.navigationNode.siteKey.type, this.elementType, this.elementType === 'LINK' && this.link || null, - this.elementType === 'PAGE' && this.pageTemplate?.id || null) + this.elementType === 'PAGE' && this.pageTemplate?.id || null + ) .then((createdPage) => { const pageRef = createdPage?.key?.ref || `${createdPage?.key.site.typeName}::${createdPage?.key.site.name}::${createdPage?.pageContext?.key.name}`; this.$root.$emit('save-node-with-page', { @@ -257,6 +258,9 @@ export default { 'createdPage': createdPage, 'openEditLayout': this.elementType === 'PAGE', }); + return createdPage; + }).then(page => { + this.$root.$emit('page-layout-created', page, this.pageTemplate); }).catch(() => { this.$root.$emit('alert-message', this.$t('siteNavigation.label.pageCreation.error'), 'error'); }).finally(() => this.loading = false); 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-layout-components/components/site-navigation/SiteNavigationNewPageElement.vue index f55176e8a..2915deffe 100644 --- 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-layout-components/components/site-navigation/SiteNavigationNewPageElement.vue @@ -44,13 +44,18 @@ export default { data: () => ({ pageTemplateId: null, + collator: new Intl.Collator(eXo.env.portal.language, {numeric: true, sensitivity: 'base'}), }), computed: { pageTemplates() { return this.$root.pageTemplates; }, items() { - return this.pageTemplates?.map?.(t => ({ + const items = this.pageTemplates.slice(); + items.sort((a, b) => this.collator.compare(a.name.toLowerCase(), b.name.toLowerCase())); + items.sort((a, b) => + ((b.category === 'blank' && 2) || (b.category === 'default' && 1) || 0) - ((a.category === 'blank' && 2) || (a.category === 'default' && 1) || 0)); + return items?.map?.(t => ({ name: this.$te(t.name) ? this.$t(t.name) : t.name, value: t.id, })); @@ -78,7 +83,7 @@ export default { immediate: true, handler() { if (this.items?.length) { - this.pageTemplateId = this.items[0].value; + this.pageTemplateId = this.pageTemplates[0].id; } }, }, 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-layout-components/components/site-navigation/SiteNavigationPageElement.vue index 873b51d67..5ea6b07f1 100644 --- 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-layout-components/components/site-navigation/SiteNavigationPageElement.vue @@ -56,7 +56,7 @@ export default { getPageTemplates() { if (!this.$root.pageTemplates) { return this.$pageTemplateService.getPageTemplates() - .then(pageTemplates => this.$root.pageTemplates = pageTemplates || []); + .then(pageTemplates => this.$root.pageTemplates = pageTemplates && pageTemplates.filter(t => !t.disabled) || []); } }, }, diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout-components/js/PageTemplateService.js b/layout-webapp/src/main/webapp/vue-app/common-layout-components/js/PageTemplateService.js index 427b30787..a2fc2f7d5 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout-components/js/PageTemplateService.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout-components/js/PageTemplateService.js @@ -30,7 +30,20 @@ export function getPageTemplates() { }); } -export function createPageTemplate(pageContent) { +export function getPageTemplate(id) { + return fetch(`/layout/rest/pageTemplates/${id}`, { + method: 'GET', + credentials: 'include', + }).then(resp => { + if (!resp?.ok) { + throw new Error('Error when retrieving page template'); + } else { + return resp.json(); + } + }); +} + +export function createPageTemplate(pageContent, disabled) { return fetch('/layout/rest/pageTemplates', { credentials: 'include', method: 'POST', @@ -38,7 +51,8 @@ export function createPageTemplate(pageContent) { 'Content-Type': 'application/json', }, body: JSON.stringify({ - content: JSON.stringify(pageContent), + content: pageContent, + disabled: disabled || false, }), }).then((resp) => { if (resp?.ok) { @@ -49,16 +63,25 @@ export function createPageTemplate(pageContent) { }); } -export function updatePageTemplate(pageContent, id) { - return fetch(`/layout/rest/pageTemplates/${id}`, { +export function updatePageTemplate(pageTemplate) { + return fetch(`/layout/rest/pageTemplates/${pageTemplate.id}`, { credentials: 'include', method: 'PUT', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - content: JSON.stringify(pageContent), - }), + body: JSON.stringify(pageTemplate), + }).then((resp) => { + if (!resp?.ok) { + throw new Error('Error when creating page template'); + } + }); +} + +export function deletePageTemplate(id) { + return fetch(`/layout/rest/pageTemplates/${id}`, { + credentials: 'include', + method: 'DELETE', }).then((resp) => { if (!resp?.ok) { throw new Error('Error when creating page template'); diff --git a/layout-webapp/src/main/webapp/vue-app/common/components/LayoutAnalytics.vue b/layout-webapp/src/main/webapp/vue-app/common/components/LayoutAnalytics.vue new file mode 100644 index 000000000..d7c5312b0 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common/components/LayoutAnalytics.vue @@ -0,0 +1,109 @@ + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common/initComponents.js b/layout-webapp/src/main/webapp/vue-app/common/initComponents.js new file mode 100644 index 000000000..aba8b87e3 --- /dev/null +++ b/layout-webapp/src/main/webapp/vue-app/common/initComponents.js @@ -0,0 +1,28 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import LayoutAnalytics from './components/LayoutAnalytics.vue'; + +const components = { + 'layout-analytics': LayoutAnalytics, +}; + +for (const key in components) { + Vue.component(key, components[key]); +} diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/LayoutEditor.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/LayoutEditor.vue index 8e12dab05..65dd5127e 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/LayoutEditor.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/LayoutEditor.vue @@ -36,6 +36,8 @@ @modified="modified = true" /> + \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/toolbar/Toolbar.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/toolbar/Toolbar.vue index a865a9598..10fab0a5a 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/toolbar/Toolbar.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/toolbar/Toolbar.vue @@ -26,12 +26,21 @@ width="100vw" flat> fa-pager - {{ $t('layout.editPageName', {0: pageName}) }} + {{ $t('layout.editPageTemplate', {0: pageTemplate.name}) }} + {{ $t('layout.editPageName', {0: pageName}) }} - - - + + + + \ No newline at end of file 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 0be595c59..6c29d36ad 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 @@ -22,6 +22,7 @@ import LayoutEditor from './components/LayoutEditor.vue'; import Toolbar from './components/toolbar/Toolbar.vue'; import SaveButton from './components/toolbar/actions/SaveButton.vue'; import SaveAsTemplateButton from './components/toolbar/actions/SaveAsTemplateButton.vue'; +import SaveTemplateButton from './components/toolbar/actions/SaveTemplateButton.vue'; import HistoryButtons from './components/toolbar/actions/HistoryButtons.vue'; import PreviewButton from './components/toolbar/actions/PreviewButton.vue'; @@ -64,7 +65,8 @@ const components = { 'layout-editor': LayoutEditor, 'layout-editor-toolbar': Toolbar, 'layout-editor-toolbar-save-button': SaveButton, - 'layout-editor-toolbar-save-template-button': SaveAsTemplateButton, + 'layout-editor-toolbar-save-as-template-button': SaveAsTemplateButton, + 'layout-editor-toolbar-save-template-button': SaveTemplateButton, 'layout-editor-toolbar-history-buttons': HistoryButtons, 'layout-editor-toolbar-preview-button': PreviewButton, 'layout-editor-content': Content, 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 771e7f369..645826c8a 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,6 +18,7 @@ */ import './initComponents.js'; +import '../common/initComponents.js'; import './extensions.js'; import './services.js'; @@ -59,6 +60,8 @@ export function init() { displayMode: 'desktop', layout: null, pageRef: null, + pageTemplate: null, + pageTemplateId: null, draftPageRef: null, draftNode: null, draftNodeId: null, diff --git a/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/DynamicSection.vue b/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/DynamicSection.vue index fbf5ca1c5..388448791 100644 --- a/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/DynamicSection.vue +++ b/layout-webapp/src/main/webapp/vue-app/page-layout/components/container/DynamicSection.vue @@ -19,7 +19,7 @@ --> 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 e67a8762b..37981cdd1 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,6 +18,7 @@ */ import './initComponents.js'; +import '../common/initComponents.js'; // get overridden components if exists if (extensionRegistry) { diff --git a/layout-webapp/src/main/webapp/vue-app/site-navigation/components/SiteNavigation.vue b/layout-webapp/src/main/webapp/vue-app/site-navigation/components/SiteNavigation.vue index 3828b0286..59047da0f 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-navigation/components/SiteNavigation.vue +++ b/layout-webapp/src/main/webapp/vue-app/site-navigation/components/SiteNavigation.vue @@ -22,6 +22,7 @@ icon-class="text-color" embedded /> +