From ef325bf194025b0ee740cec07fc65799b1db3728 Mon Sep 17 00:00:00 2001 From: Nollymar Longa Date: Tue, 14 Jan 2025 17:14:55 -0500 Subject: [PATCH] fix(core): Adding support for listing templates across multiple sites (#31086) ### Proposed Changes * Wildcard host support was included in the endpoint that lists templates to be able to get all the templates across multiple sites ### Checklist - [x] Tests This pull request includes several changes to improve the handling of template selection and filtering in both the backend and frontend of the dotCMS application. The most important changes include updates to the `TemplateResource` class, improvements to the template selection logic in the frontend, and the addition of a new test case. ### Backend changes: * [`dotCMS/src/main/java/com/dotcms/rest/api/v1/template/TemplateResource.java`](diffhunk://#diff-448b3630547f743e0d556752c2e68b797b03e9c4b9d7e238c84889274435635eL139-R156): Refactored the method to handle template listing by incorporating logic to support wildcard host IDs, which allows fetching templates across multiple sites. ### Frontend changes: * [`dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/template_custom_field.vtl`](diffhunk://#diff-27bf5cec90b9630400c776d8f3fd37151da487ffbebbef2a630946b559bf0dd6L96-R122): Added a new function `resetTemplateSelection` to reset the template selection, and modified the template fetch logic to handle the wildcard host ID. [[1]](diffhunk://#diff-27bf5cec90b9630400c776d8f3fd37151da487ffbebbef2a630946b559bf0dd6L96-R122) [[2]](diffhunk://#diff-27bf5cec90b9630400c776d8f3fd37151da487ffbebbef2a630946b559bf0dd6L154-R158) [[3]](diffhunk://#diff-27bf5cec90b9630400c776d8f3fd37151da487ffbebbef2a630946b559bf0dd6L173-R177) [[4]](diffhunk://#diff-27bf5cec90b9630400c776d8f3fd37151da487ffbebbef2a630946b559bf0dd6L185-R189) * [`dotCMS/src/main/webapp/html/js/dotcms/dojo/data/TemplateReadStore.js`](diffhunk://#diff-5453f9a344625845fd35818fe8b7853f0b5ef2428b53b0e775a6f5cd13bf7c22R14-L17): Added a new constant `ALL_SITE_TEMPLATE` to emulate the old backend behavior and updated the fetch logic to include this template when the wildcard host ID is used. [[1]](diffhunk://#diff-5453f9a344625845fd35818fe8b7853f0b5ef2428b53b0e775a6f5cd13bf7c22R14-L17) [[2]](diffhunk://#diff-5453f9a344625845fd35818fe8b7853f0b5ef2428b53b0e775a6f5cd13bf7c22R111-R130) ### Test changes: * [`dotcms-integration/src/test/java/com/dotcms/rest/api/v1/template/TemplateResourceTest.java`](diffhunk://#diff-745f7eccb02b77ba5b0bc21bba208f1fa5113e2e1b93261173f2091a579936b5R1072-R1114): Added a new test case `test_listTemplate_filterByHost_usingWildcardHost` to verify the correct behavior when listing templates with a wildcard host ID. ### Video https://github.com/user-attachments/assets/0fbb9f5a-3341-4760-a4d0-d477a584192e --------- Co-authored-by: Rafael Velazco --- .../api/v1/template/TemplateResource.java | 21 +++-- .../htmlpage_assets/template_custom_field.vtl | 78 +++++++++++-------- .../js/dotcms/dojo/data/TemplateReadStore.js | 33 ++++++-- .../api/v1/template/TemplateResourceTest.java | 48 +++++++++++- 4 files changed, 133 insertions(+), 47 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/template/TemplateResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/template/TemplateResource.java index c7206a9d5659..e85e380bd77f 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/template/TemplateResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/template/TemplateResource.java @@ -8,7 +8,6 @@ import com.dotcms.rest.api.BulkResultView; import com.dotcms.rest.api.FailedResultView; import com.dotcms.util.PaginationUtil; -import com.dotcms.util.pagination.ContainerPaginator; import com.dotcms.util.pagination.OrderDirection; import com.dotcms.util.pagination.TemplatePaginator; import com.dotmarketing.beans.Host; @@ -26,7 +25,6 @@ import com.dotmarketing.portlets.templates.business.TemplateSaveParameters; import com.dotmarketing.portlets.templates.design.bean.TemplateLayout; import com.dotmarketing.portlets.templates.design.util.DesignTemplateUtil; -import com.dotmarketing.portlets.templates.factories.TemplateFactory; import com.dotmarketing.portlets.templates.model.Template; import com.dotmarketing.util.ActivityLogger; import com.dotmarketing.util.InodeUtils; @@ -136,15 +134,26 @@ public final Response list(@Context final HttpServletRequest httpRequest, final InitDataObject initData = new WebResource.InitBuilder(webResource) .requestAndResponse(httpRequest, httpResponse).rejectWhenNoUser(true).init(); final User user = initData.getUser(); - final Lazy lazyCurrentHost = Lazy.of(() -> Try.of(() -> Host.class.cast(httpRequest.getSession().getAttribute(WebKeys.CURRENT_HOST)).getIdentifier()).getOrNull()); - final Optional checkedHostId = Optional.ofNullable(Try.of(()-> APILocator.getHostAPI() - .find(hostId, user, false).getIdentifier()).getOrElse(lazyCurrentHost.get())); + Logger.debug(this, ()-> "Getting the List of templates"); final Map extraParams = new HashMap<>(); extraParams.put(ARCHIVE_PARAM, archive); - checkedHostId.ifPresent(checkedHostIdentifier -> extraParams.put(ContainerPaginator.HOST_PARAMETER_ID, checkedHostIdentifier)); + + //In case we need to get the list of templates across multiple sites, we don't set the TemplatePaginator.HOST_PARAMETER_ID + if (null == hostId || !StringPool.STAR.equals(hostId)) { + final Lazy lazyCurrentHost = Lazy.of(() -> Try.of(() -> Host.class.cast( + httpRequest.getSession().getAttribute(WebKeys.CURRENT_HOST)).getIdentifier()) + .getOrNull()); + final Optional checkedHostId = Optional.ofNullable( + Try.of(() -> APILocator.getHostAPI() + .find(hostId, user, false).getIdentifier()) + .getOrElse(lazyCurrentHost.get())); + checkedHostId.ifPresent( + checkedHostIdentifier -> extraParams.put(TemplatePaginator.HOST_PARAMETER_ID, + checkedHostIdentifier)); + } return this.paginationUtil.getPage(httpRequest, user, filter, page, perPage, orderBy, OrderDirection.valueOf(direction), extraParams); } diff --git a/dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/template_custom_field.vtl b/dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/template_custom_field.vtl index e906b927dca2..4d9f4dc74355 100644 --- a/dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/template_custom_field.vtl +++ b/dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/template_custom_field.vtl @@ -74,12 +74,10 @@ function fetchTemplateImage(templateId) { * Callback function to handle the template fetch * */ -const onTemplateFetchComplete = function(templates, currentRequest){ +const onTemplateFetchComplete = function(templates, currentRequest) { const templateId = dojo.byId("template").value; const isTemplateValid = templateId && templateId != "0"; - console.log("templateId", templateId); - if(!templates || templates.length === 0){ return; } @@ -93,36 +91,26 @@ const onTemplateFetchComplete = function(templates, currentRequest){ } const { identifier, fullTitle } = template; - // We set the values directly into the components because setting it direcrtly into`templateSel` fires another load operation. + + // We set the values directly into the components because setting it directly into`templateSelect` fires another load operation. dojo.byId("currentTemplateId").value = identifier; dojo.byId("template").value = identifier; dijit.byId('templateSel').set("displayedValue", fullTitle); fetchTemplateImage(identifier); }; -/** -* Handles the template change event -* -*/ -function templateChanged() { - const templateSel=dijit.byId("templateSel"); - const value=templateSel.get('value'); - - if(!value) { - return; - } - - if(value == "0") { - templateSel.set("value",""); - templateSel.filter(); - - return; - } - - dojo.byId("template").value=value; - fetchTemplateImage(value); +function resetTemplateSelection() { + const templateSel = dijit.byId("templateSel"); + templateSel.set("value",""); + templateSel.filter(); + dojo.byId("template").value= ''; + dojo.byId("templateSel").value = ""; + dojo.byId("currentTemplateId").value = ""; + dojo.byId("templateThumbnailHolder").src = "/html/images/shim.gif"; + dojo.byId("templateThumbnailHolder").style.border = '0px'; } + /** * Get the template callback * @@ -151,11 +139,12 @@ dojo.ready(function(){ currentTemplateIdElement.value = templateId; const templateStore = new dotcms.dojo.data.TemplateReadStore({ - hostId: hostId, - templateSelected: templateId + hostId: '', + templateSelected: templateId, + allSiteLabel: true }); - const templateSelect=new dijit.form.FilteringSelect({ + const templateSelect = new dijit.form.FilteringSelect({ id:"templateSel", name:"templateSel", style:"width:350px;", @@ -170,7 +159,7 @@ dojo.ready(function(){ labelAttr: "htmlTitle", searchAttr: "fullTitle", value: templateId, - invalidMessage: '$text.get("Invalid-option-selected")' + invalidMessage: '$text.get("Invalid-option-selected")', },"templateHolder"); if (isTemplateValid){ @@ -182,8 +171,7 @@ dojo.ready(function(){ const templateFetchParams = { query: { - fullTitle: '*', - hostId: hostId + fullTitle: '*' }, queryOptions: {}, start: 0, @@ -192,6 +180,34 @@ dojo.ready(function(){ onComplete: onTemplateFetchComplete }; + function handleAllSiteClick() { + templateStore.hostId = "*"; + templateStore.allSiteLabel=false; + resetTemplateSelection(); + } + + /** + * Handles the template change event + * + */ + function templateChanged() { + const templateSel = dijit.byId("templateSel"); + const value = templateSel?.get('value'); + + if(!value) { + resetTemplateSelection(); + return; + } + + if(value == "0") { + handleAllSiteClick(); + return; + } + + dojo.byId("template").value=value; + fetchTemplateImage(value); + } + templateStore.fetch(templateFetchParams); }); diff --git a/dotCMS/src/main/webapp/html/js/dotcms/dojo/data/TemplateReadStore.js b/dotCMS/src/main/webapp/html/js/dotcms/dojo/data/TemplateReadStore.js index 4e7dbe29fbbd..c43bcf677da4 100644 --- a/dotCMS/src/main/webapp/html/js/dotcms/dojo/data/TemplateReadStore.js +++ b/dotCMS/src/main/webapp/html/js/dotcms/dojo/data/TemplateReadStore.js @@ -10,11 +10,26 @@ dojo.declare("dotcms.dojo.data.TemplateReadStore", null, { includeArchived: false, includeTemplate: null, templateSelected: '', + allSiteLabel: false, + + /** + * To Emulate this old Backend Behavion + * https://github.com/dotCMS/core/blob/7bc05d335b98ffb30d909de9ec82dd4557b37078/dotCMS/src/main/java/com/dotmarketing/portlets/templates/ajax/TemplateAjax.java#L72-L76 + * + * @type {*} + * */ + ALL_SITE_TEMPLATE: { + title: "All Sites", + fullTitle: "All Sites", + htmlTitle: '
-- All Sites ---
', + identifier: "0", + inode: "0" + }, constructor: function (options) { this.hostId = options.hostId; + this.allSiteLabel = options.allSiteLabel; this.templateSelected = options.templateSelected; - window.top._dotTemplateStore = this; }, getValue: function (item, attribute, defaultValue) { @@ -95,20 +110,25 @@ dojo.declare("dotcms.dojo.data.TemplateReadStore", null, { } else { + const hostId = keywordArgs.query.hostId; + let url = "/api/v1/templates/?filter=" + keywordArgs.query.fullTitle.replace('*','') + "&page=" + keywordArgs.start + "&per_page=" + keywordArgs.count + (keywordArgs.sort == undefined || keywordArgs.sort != ""? "": "&orderby=" + keywordArgs.sort); - if (keywordArgs.query.hostId != undefined && keywordArgs.query.hostId != "") { - url += "&host=" + keywordArgs.query.hostId; + if (hostId != undefined && hostId != "") { + url += "&host=" + hostId; } fetch(url) .then((fetchResp) => fetchResp.json()) .then(responseEntity => { - this.fetchTemplatesCallback(keywordArgs, responseEntity); - } - ); + if(this.allSiteLabel) { + responseEntity.entity.unshift(this.ALL_SITE_TEMPLATE); + }; + + this.fetchTemplatesCallback(keywordArgs, responseEntity); + }); this.currentRequest = keywordArgs; this.currentRequest.abort = function () { }; @@ -117,7 +137,6 @@ dojo.declare("dotcms.dojo.data.TemplateReadStore", null, { }, fetchTemplatesCallback: function (keywordArgs, templatesEntity) { - var scope = keywordArgs.scope; if(keywordArgs.onBegin) { diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/template/TemplateResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/template/TemplateResourceTest.java index 9af89f0e24bf..71a7a182a0bd 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/template/TemplateResourceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/template/TemplateResourceTest.java @@ -1,6 +1,5 @@ package com.dotcms.rest.api.v1.template; -import static com.dotcms.rendering.velocity.directive.ParseContainer.PARSE_CONTAINER_UUID_PREFIX; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -32,7 +31,6 @@ import com.dotmarketing.beans.MultiTree; import com.dotmarketing.beans.Permission; import com.dotmarketing.business.APILocator; -import com.dotmarketing.business.ApiProvider; import com.dotmarketing.business.PermissionAPI; import com.dotmarketing.exception.DoesNotExistException; import com.dotmarketing.exception.DotDataException; @@ -46,17 +44,18 @@ import com.dotmarketing.portlets.templates.model.Template; import com.dotmarketing.util.PageMode; import com.dotmarketing.util.PaginatedArrayList; +import com.dotmarketing.util.StringUtils; import com.dotmarketing.util.UUIDGenerator; import com.dotmarketing.util.WebKeys; import com.liferay.portal.model.User; import com.liferay.util.Base64; +import com.liferay.util.StringPool; import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.junit.Assert; @@ -1070,6 +1069,49 @@ public void test_listTemplate_filterByHost_usingCurrentHost() APILocator.getIdentifierAPI().find(((TemplateView) paginatedArrayListWihoutSystemTemplate.get(0)).getIdentifier()).getHostId()); } + /** + * Method to test: list in the TemplateResource + * Given Scenario: Create a template on hostA, and a template on hostB. List templates using the + * wildcard host as the query param. + * ExpectedResult: The endpoint should return 200, and templateA and templateB should be returned + */ + @Test + public void test_listTemplate_filterByHost_usingWildcardHost() + throws DotSecurityException, DotDataException { + final String title = "Template" + System.currentTimeMillis(); + final Host newHostA = new SiteDataGen().nextPersisted(); + final Host newHostB = new SiteDataGen().nextPersisted(); + + //Create templates in two different sites + final Template templateA = APILocator.getTemplateAPI() + .saveTemplate(new TemplateDataGen().title(title).next(), newHostA, adminUser, + false); + final Template templateB = APILocator.getTemplateAPI() + .saveTemplate(new TemplateDataGen().title(title).next(), newHostB, adminUser, + false); + + //Call Resource + final Response responseResource = resource.list( + getHttpRequest(adminUser.getEmailAddress(), "admin"), response, title, 0, 40, + "mod_date", "DESC", + StringPool.STAR, false); + //Check that the response is 200, OK + Assert.assertEquals(Status.OK.getStatusCode(), responseResource.getStatus()); + final ResponseEntityView responseEntityView = ResponseEntityView.class.cast( + responseResource.getEntity()); + final PaginatedArrayList paginatedArrayList = PaginatedArrayList.class.cast( + responseEntityView.getEntity()); + final PaginatedArrayList paginatedArrayListWihoutSystemTemplate = removeSystemTemplate( + paginatedArrayList); + Assert.assertEquals(2, paginatedArrayListWihoutSystemTemplate.size()); + paginatedArrayListWihoutSystemTemplate.stream().anyMatch( + templateView -> ((TemplateView) templateView).getIdentifier() + .equals(templateA.getIdentifier())); + paginatedArrayListWihoutSystemTemplate.stream().anyMatch( + templateView -> ((TemplateView) templateView).getIdentifier() + .equals(templateB.getIdentifier())); + } + /** * Method to test: list in the TemplateResource * Given Scenario: Create 2 templates, and a limited user. Give READ Permissions to one template to