From f74fdb1b1aaa2b47f90b13f46ce1a1895cf31af6 Mon Sep 17 00:00:00 2001 From: ddariod Date: Fri, 9 Jan 2026 22:38:13 -0500 Subject: [PATCH 1/8] preserve styleProperties when FF is disabled --- .../factories/MultiTreeAPIImpl.java | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java index f85d86af6496..44fe94da1a39 100644 --- a/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java @@ -126,6 +126,8 @@ public class MultiTreeAPIImpl implements MultiTreeAPI { private static final String SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT_LANGUAGE = SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT + " AND child IN (SELECT DISTINCT identifier FROM contentlet, multi_tree " + "WHERE multi_tree.child = contentlet.identifier AND multi_tree.parent1 = ? AND language_id = ?)"; + private static final String SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PARENTS_AND_RELATIONS = + SELECT_ALL + "WHERE parent1 = ? AND style_properties IS NOT NULL"; @WrapInTransaction @Override @@ -661,7 +663,7 @@ public void overridesMultitreesByPersonalization(final String pageId, * @param multiTrees {@link List} of {@link MultiTree} to safe * @param languageIdOpt {@link Optional} {@link Long} optional language, if present will deletes only the contentlets that have a version on this language. * Since it is by identifier, when deleting for instance in spanish, will remove the english and any other lang version too. - * @throws DotDataException + * @throws DotDataException If there is an issue retrieving data from the DB. */ @Override @WrapInTransaction @@ -683,6 +685,13 @@ public void overridesMultitreesByPersonalization(final String pageId, Logger.debug(MultiTreeAPIImpl.class, ()->String.format("Saving page's content: %s", multiTrees)); Set originalContentletIds = new HashSet<>(); final DotConnect db = new DotConnect(); + + // preserve style properties if feature flag is disabled and there are style properties to preserve + final boolean isStyleEditorEnabled = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false); + if (!isStyleEditorEnabled) { + preserveStylesBeforeSaving(pageId, multiTrees); + } + if (languageIdOpt.isPresent()) { if (DbConnectionFactory.isMySql()) { deleteMultiTreeToMySQL(pageId, personalization, languageIdOpt, variantId); @@ -1570,6 +1579,55 @@ private Set getOriginalContentlets(final String sqlQuery, final List dataMap.get("child").toString()).collect(Collectors.toSet()); } + /** + * Restores the style properties for a list of MultiTree objects by looking up + * their existing values in the database. + * * This prevents style data loss when overwriting existing records. It matches + * records based on the composite key of Container + Contentlet. + * + * @param pageId The identifier of the page being processed. + * @param multiTrees The list of MultiTree objects to be saved. + * NOTE: This list is modified in-place. + * The same list of multiTrees will be enriched with their original styles. + * @throws DotDataException If there is an issue retrieving data from the DB. + */ + private void preserveStylesBeforeSaving(final String pageId, List multiTrees) throws DotDataException { + // Gets existing multiTrees by Page from DB + final List multiTreesFromDB = fetchStylesFromDB(pageId); + + if (multiTreesFromDB.isEmpty()) { + return; + } + + // Create a "Lookup Map" from the DB multiTrees. + // Key: Unique combination of Container + Contentlet + // Value: Contentlet styleProperties of the Contentlet + final Map> dbStyleMap = multiTreesFromDB.stream() + .collect(Collectors.toMap( + mt -> mt.getContainer() + "_" + mt.getContentlet(), + MultiTree::getStyleProperties, + // In case of duplicates, keep last entrance value (shouldn't happen) + (existing, replacement) -> replacement + )); + + // Update the multiTrees list to preserve existing styleProperties + for (MultiTree multiTree : multiTrees) { + String key = multiTree.getContainer() + "_" + multiTree.getContentlet(); + + // If this relationship already existed in DB, preserves the old styles + if (dbStyleMap.containsKey(key)) { + multiTree.setStyleProperties(dbStyleMap.get(key)); + } + } + } + + protected List fetchStylesFromDB(String pageId) throws DotDataException { + final DotConnect db = new DotConnect() + .setSQL(SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PARENTS_AND_RELATIONS) + .addParam(pageId); + return TransformerLocator.createMultiTreeTransformer(db.loadObjectResults()).asList(); + } + /** * Takes the list of Contentlet IDs present in an HTML Page before any change is made, and the list of * {@link MultiTree} objects representing the updated Contentlets. Then, compares both lists and determines what From 06a8f191933136e8a75f75a1e75cf2f8e4e9ad77 Mon Sep 17 00:00:00 2001 From: ddariod Date: Fri, 9 Jan 2026 23:32:17 -0500 Subject: [PATCH 2/8] reset incoming styles when Style Editor FF is disabled --- .../factories/MultiTreeAPIImpl.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java index 44fe94da1a39..1ff3df005b8d 100644 --- a/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/factories/MultiTreeAPIImpl.java @@ -126,7 +126,7 @@ public class MultiTreeAPIImpl implements MultiTreeAPI { private static final String SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT_LANGUAGE = SELECT_CHILD_BY_PARENT_RELATION_PERSONALIZATION_VARIANT + " AND child IN (SELECT DISTINCT identifier FROM contentlet, multi_tree " + "WHERE multi_tree.child = contentlet.identifier AND multi_tree.parent1 = ? AND language_id = ?)"; - private static final String SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PARENTS_AND_RELATIONS = + private static final String SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PAGE = SELECT_ALL + "WHERE parent1 = ? AND style_properties IS NOT NULL"; @WrapInTransaction @@ -686,9 +686,12 @@ public void overridesMultitreesByPersonalization(final String pageId, Set originalContentletIds = new HashSet<>(); final DotConnect db = new DotConnect(); - // preserve style properties if feature flag is disabled and there are style properties to preserve + // Preserves already existing styles if Style Editor is disabled final boolean isStyleEditorEnabled = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false); if (!isStyleEditorEnabled) { + // Delete incoming styles (DB is the only source of truth) + multiTrees.forEach(mTree -> mTree.setStyleProperties(null)); + // Restore existing styles from DB preserveStylesBeforeSaving(pageId, multiTrees); } @@ -1582,7 +1585,7 @@ private Set getOriginalContentlets(final String sqlQuery, final List mul // Create a "Lookup Map" from the DB multiTrees. // Key: Unique combination of Container + Contentlet - // Value: Contentlet styleProperties of the Contentlet + // Value: Contentlet styleProperties final Map> dbStyleMap = multiTreesFromDB.stream() .collect(Collectors.toMap( mt -> mt.getContainer() + "_" + mt.getContentlet(), @@ -1621,9 +1624,16 @@ private void preserveStylesBeforeSaving(final String pageId, List mul } } + /** + * Fetches the style properties from the database for a given page. + * + * @param pageId The ID of the page to fetch the style properties from. + * @return The list of MultiTree objects with the style properties. + * @throws DotDataException If there is an issue retrieving data from the DB. + */ protected List fetchStylesFromDB(String pageId) throws DotDataException { final DotConnect db = new DotConnect() - .setSQL(SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PARENTS_AND_RELATIONS) + .setSQL(SELECT_NOT_EMPTY_CONTENTLET_STYLES_BY_PAGE) .addParam(pageId); return TransformerLocator.createMultiTreeTransformer(db.loadObjectResults()).asList(); } From 4dec43f7d7b6f330246984574458bb3d6a4f09c1 Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 05:52:50 -0500 Subject: [PATCH 3/8] show style properties in response only if FF is enabled --- .../api/v1/page/AbstractContentletStylingView.java | 7 ++++++- .../com/dotcms/rest/api/v1/page/PageResource.java | 2 +- .../dotcms/rest/api/v1/page/PageResourceHelper.java | 12 +++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java index 6089a1e92404..bd20ea294413 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java @@ -1,9 +1,12 @@ package com.dotcms.rest.api.v1.page; +import com.dotcms.annotations.Nullable; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import java.util.Map; import org.immutables.value.Value; @@ -37,10 +40,12 @@ public interface AbstractContentletStylingView { String contentletId(); @JsonProperty("styleProperties") + @Nullable + @JsonInclude(JsonInclude.Include.NON_NULL) @Schema( description = "Styles defined for the Contentlet", example = "{\"color\": \"#FF0000\", \"margin\": \"10px\"}", - requiredMode = Schema.RequiredMode.REQUIRED + requiredMode = RequiredMode.NOT_REQUIRED ) Map styleProperties(); } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java index 3e6f05115645..48e60937b14f 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java @@ -891,7 +891,7 @@ class ContainerData { data.contentIds.addAll(containerEntry.getContentIds()); // Merge styles. Duplicated keys overwrite previous ones (last one wins) - final Map> incomingStyles = Optional.ofNullable( + final Map> incomingStyles = Optional.of( containerEntry.getStylePropertiesMap()).orElse(Collections.emptyMap()); data.stylePropertiesMap.putAll(incomingStyles); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java index 7d033bd94324..c13278801c95 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java @@ -55,6 +55,7 @@ import com.dotmarketing.portlets.templates.design.bean.ContainerUUID; import com.dotmarketing.portlets.templates.design.bean.TemplateLayout; import com.dotmarketing.portlets.templates.model.Template; +import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UtilMethods; @@ -232,7 +233,7 @@ public void run() { } /** - * Validates the style properties during the saving process. + * Validates the style properties for a given contentlet and container during the saving process. * @param stylePropertiesMap The map of style properties. * @param contentIds The list of contentlet ids. * @param containerId The id of the container. @@ -279,10 +280,11 @@ private List buildSaveContentResponse(List sav .contentletId(multiTree.getContentlet()) .uuid(multiTree.getRelationType()); - // Add Style properties if present - final Map styleProperties = multiTree.getStyleProperties(); - if (styleProperties != null && !styleProperties.isEmpty()) { - builder.putAllStyleProperties(styleProperties); + // Include style properties in the response if Style Editor FF is enabled + final boolean isStyleEditorEnabled = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false); + if (isStyleEditorEnabled) { + final Map styleProperties = multiTree.getStyleProperties(); + builder.putAllStyleProperties(styleProperties != null ? styleProperties : new HashMap<>()); } return builder.build(); From a854db26ee4f9dbcec9506126e8ae9d312d95b66 Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 07:25:24 -0500 Subject: [PATCH 4/8] add test when defining styles with FF --- ...ts_StyleProperties.postman_collection.json | 781 ++++++++++++++++++ 1 file changed, 781 insertions(+) diff --git a/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json b/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json index 6093710323fd..ce360ac57dfe 100644 --- a/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json @@ -422,6 +422,56 @@ { "name": "Defining Contentlet Styles", "item": [ + { + "name": "Turn ON Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is enabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR saved/updated');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"key\": \"FEATURE_FLAG_UVE_STYLE_EDITOR\",\n \"value\": \"true\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "" + ] + } + }, + "response": [] + }, { "name": "Save Content With Basic Styles", "event": [ @@ -769,6 +819,56 @@ "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." }, "response": [] + }, + { + "name": "Turn OFF Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is disabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR Deleted');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\"key\":\"FEATURE_FLAG_UVE_STYLE_EDITOR\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/_delete", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "_delete" + ] + } + }, + "response": [] } ], "description": "### Defining Contentlet Styles:\n\nTest the Contentlet Style definition in the `testPageStyles_1`.\n\n- Save Contentlets with Basic Styles\n \n - `Contentlet 1` with basic Style Properties\n \n - `Contentlet 2` without Styles\n \n- Save Contentlet with Complex Styles\n \n - `Contentlet 1` with deep object definition Style Properties\n \n- BadRequest trying to set Styles for a contentlet that does not exists, should fail.\n \n- Save Styles for the same Contentlet but in different Containers, each of them does not affect the other one.\n \n - `Contentlet 1` saved with \"width = 100px\" style value in the `SYSTEM_CONTAINER`\n \n - `Contentlet 1` saved with \"color = \"#FF0000\" style in the custom Container `Styles_Container`" @@ -1186,6 +1286,687 @@ } ], "description": "### Retrieve Contentlet Styles:\n\nTest that the Contentlets have specific Styles defined when the feature flag `FEATURE_FLAG_UVE_STYLE_EDITOR` is enabled.\n\n- Turn ON `FEATURE_FLAG_UVE_STYLE_EDITOR` to allow the visualization of the Style Properties.\n \n- Save the **same** **Contentlet** in the **same** **Container** but in **different** **Pages**\n \n - `Contentlet 1` in the `SYSTEM_CONTAINER` **Container**, inside the **Page**`testPageStyles_1`\n \n - `Contentlet 1` in the `SYSTEM_CONTAINER` **Container**, inside the **Page**`testPageStyles_2`\n \n- Validate that the **Contentlet** `Contentlet 1` within the `testPageStyles_1` **Page** have the \"color = #FF0000\" Style.\n \n- Validate that the **Contentlet** `Contentlet 1` within the `testPageStyles_2` **Page** have the \"width = 100px\" Style.\n \n- Turn OFF `FEATURE_FLAG_UVE_STYLE_EDITOR`\n \n - Get **Contentlet** `Contentlet 1` does not contain `styleProperties` field" + }, + { + "name": "Persist Contentlet Styles", + "item": [ + { + "name": "Turn ON Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is enabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR saved/updated');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"key\": \"FEATURE_FLAG_UVE_STYLE_EDITOR\",\n \"value\": \"true\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "" + ] + } + }, + "response": [] + }, + { + "name": "Save Content With Styles FF On", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "const contentlet2 = pm.collectionVariables.get(\"Contentlet_2\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// Parse JSON", + "const json = pm.response.json();", + "", + "pm.test(\"Entity array exists\", function () {", + " pm.expect(json.entity).to.be.an(\"array\").that.is.not.empty;", + "});", + "", + "pm.test(\"No errors returned\", function () {", + " pm.expect(json.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "// ============================", + "// Validate first merged entry", + "// ============================", + "pm.test(\"First entry has correct styleProperties\", function () {", + " const first = json.entity.find(item => item.contentletId === contentlet1);", + " pm.expect(first).to.exist;", + " pm.expect(first.styleProperties).to.be.an(\"object\");", + " pm.expect(first.styleProperties.width).to.eql(\"100px\");", + " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", + "});", + "", + "// ============================", + "// Validate second merged entry", + "// ============================", + "pm.test(\"Second entry has no style properties\", function () {", + " const second = json.entity.find(item => item.contentletId === contentlet2);", + " pm.expect(second).to.exist;", + " pm.expect(second.styleProperties).to.eql({});", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"identifier\": \"SYSTEM_CONTAINER\",\n \"uuid\": \"1\",\n \"contentletsId\": [\n \"{{Contentlet_1}}\",\n \"{{Contentlet_2}}\"\n ],\n \"styleProperties\": {\n \"{{Contentlet_1}}\": {\n \"width\": \"100px\",\n \"color\": \"#FF0000\",\n \"margin\": \"10px\"\n }\n }\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/page/{{firstPageIdentifier}}/content", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "{{firstPageIdentifier}}", + "content" + ] + }, + "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." + }, + "response": [] + }, + { + "name": "Turn OFF Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is disabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR Deleted');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\"key\":\"FEATURE_FLAG_UVE_STYLE_EDITOR\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/_delete", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "_delete" + ] + } + }, + "response": [] + }, + { + "name": "Save Content Styles FF Off", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "const contentlet2 = pm.collectionVariables.get(\"Contentlet_2\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// Parse JSON", + "const json = pm.response.json();", + "", + "pm.test(\"Entity array exists\", function () {", + " pm.expect(json.entity).to.be.an(\"array\").that.is.not.empty;", + "});", + "", + "pm.test(\"No errors returned\", function () {", + " pm.expect(json.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "// ============================", + "// Validate first merged entry", + "// ============================", + "pm.test(\"Contentlet_1 entry has correct styleProperties\", function () {", + " const first = json.entity.find(item => item.contentletId === contentlet1);", + " pm.expect(first).to.exist;", + " pm.expect(first.styleProperties).to.be.undefined;", + "});", + "", + "// ============================", + "// Validate second merged entry", + "// ============================", + "pm.test(\"Second entry has no style properties\", function () {", + " const second = json.entity.find(item => item.contentletId === contentlet2);", + " pm.expect(second).to.exist;", + " pm.expect(second.styleProperties).to.be.undefined;", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"identifier\": \"SYSTEM_CONTAINER\",\n \"uuid\": \"1\",\n \"contentletsId\": [\n \"{{Contentlet_1}}\",\n \"{{Contentlet_2}}\"\n ],\n \"styleProperties\": {\n \"{{Contentlet_1}}\": {\n \"width\": \"100px\",\n \"color\": \"#FF0000\",\n \"margin\": \"10px\"\n }\n }\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/page/{{firstPageIdentifier}}/content", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "{{firstPageIdentifier}}", + "content" + ] + }, + "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." + }, + "response": [] + }, + { + "name": "Styles NOT Present FF Off", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "const contentlet2 = pm.collectionVariables.get(\"Contentlet_2\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Validate Contentlet_1 has NOT styleProperties\", function () {", + " // filter by container", + " const entity = pm.response.json().entity;", + " const contentletList = entity.containers.SYSTEM_CONTAINER.contentlets[\"uuid-1\"];", + "", + " // filter by contentlet", + " const contentlet = contentletList.find(c => c.identifier === contentlet1);", + " pm.expect(contentlet).to.exist;", + " pm.expect(contentlet.styleProperties).to.be.undefined;", + "});", + "", + "pm.test(\"Validate Contentlet_2 has NOT styleProperties\", function () {", + " // filter by container", + " const entity = pm.response.json().entity;", + " const contentletList = entity.containers.SYSTEM_CONTAINER.contentlets[\"uuid-1\"];", + "", + " // filter by contentlet", + " const contentlet = contentletList.find(c => c.identifier === contentlet2);", + " pm.expect(contentlet).to.exist;", + " pm.expect(contentlet.styleProperties).to.be.undefined;", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/page/render/{{firstPageUrl}}?language_id=1&mode=EDIT_MODE&host_id={{hostIdentifier}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "render", + "{{firstPageUrl}}" + ], + "query": [ + { + "key": "language_id", + "value": "1" + }, + { + "key": "mode", + "value": "EDIT_MODE" + }, + { + "key": "host_id", + "value": "{{hostIdentifier}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Turn ON Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is enabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR saved/updated');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"key\": \"FEATURE_FLAG_UVE_STYLE_EDITOR\",\n \"value\": \"true\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "" + ] + } + }, + "response": [] + }, + { + "name": "Persisted Styles FF On", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Validate Contentlet_1 persists its styleProperties\", function () {", + " // filter by container", + " const entity = pm.response.json().entity;", + " const contentletList = entity.containers.SYSTEM_CONTAINER.contentlets[\"uuid-1\"];", + "", + " // filter by contentlet", + " const contentlet = contentletList.find(c => c.identifier === contentlet1);", + " pm.expect(contentlet).to.exist;", + " // Validate that styles defined with FF on are present", + " pm.expect(contentlet.styleProperties.width).to.eql(\"100px\");", + " pm.expect(contentlet.styleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(contentlet.styleProperties.margin).to.eql(\"10px\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/page/render/{{firstPageUrl}}?language_id=1&mode=EDIT_MODE&host_id={{hostIdentifier}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "render", + "{{firstPageUrl}}" + ], + "query": [ + { + "key": "language_id", + "value": "1" + }, + { + "key": "mode", + "value": "EDIT_MODE" + }, + { + "key": "host_id", + "value": "{{hostIdentifier}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete Styles FF On (empty)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "const contentlet2 = pm.collectionVariables.get(\"Contentlet_2\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "// Parse JSON", + "const json = pm.response.json();", + "", + "pm.test(\"Entity array exists\", function () {", + " pm.expect(json.entity).to.be.an(\"array\").that.is.not.empty;", + "});", + "", + "pm.test(\"No errors returned\", function () {", + " pm.expect(json.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "// ============================", + "// Validate first merged entry", + "// ============================", + "pm.test(\"First entry has no styleProperties\", function () {", + " const first = json.entity.find(item => item.contentletId === contentlet1);", + " pm.expect(first).to.exist;", + " pm.expect(first.styleProperties).to.eql({});", + "});", + "", + "// ============================", + "// Validate second merged entry", + "// ============================", + "pm.test(\"Second entry has no style properties\", function () {", + " const second = json.entity.find(item => item.contentletId === contentlet2);", + " pm.expect(second).to.exist;", + " pm.expect(second.styleProperties).to.eql({});", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "[\n {\n \"identifier\": \"SYSTEM_CONTAINER\",\n \"uuid\": \"1\",\n \"contentletsId\": [\n \"{{Contentlet_1}}\",\n \"{{Contentlet_2}}\"\n ]\n }\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/page/{{firstPageIdentifier}}/content", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "{{firstPageIdentifier}}", + "content" + ] + }, + "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." + }, + "response": [] + }, + { + "name": "Styles NOT Present FF On", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Validate Contentlet_1 styleProperties were deleted\", function () {", + " // filter by container", + " const entity = pm.response.json().entity;", + " const contentletList = entity.containers.SYSTEM_CONTAINER.contentlets[\"uuid-1\"];", + "", + " // filter by contentlet", + " const contentlet = contentletList.find(c => c.identifier === contentlet1);", + " pm.expect(contentlet).to.exist;", + " // Validate that styles defined with FF on are NOT present", + " pm.expect(contentlet.styleProperties).to.be.undefined;", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/page/render/{{firstPageUrl}}?language_id=1&mode=EDIT_MODE&host_id={{hostIdentifier}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "page", + "render", + "{{firstPageUrl}}" + ], + "query": [ + { + "key": "language_id", + "value": "1" + }, + { + "key": "mode", + "value": "EDIT_MODE" + }, + { + "key": "host_id", + "value": "{{hostIdentifier}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Turn OFF Style Editor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"FEATURE_FLAG_UVE_STYLE_EDITOR is disabled\", function () {", + " const entity = pm.response.json().entity;", + " pm.expect(entity).to.exist;", + " pm.expect(entity).to.eql('FEATURE_FLAG_UVE_STYLE_EDITOR Deleted');", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\"key\":\"FEATURE_FLAG_UVE_STYLE_EDITOR\"}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/system-table/_delete", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "system-table", + "_delete" + ] + } + }, + "response": [] + } + ], + "description": "### Persist Contentlet Styles:\n\nTest that ensures Contentlets have the Styles defined when the feature flag `FEATURE_FLAG_UVE_STYLE_EDITOR` is enabled.\n\nIf the **FF is OFF** even if I **send** or not **styles** in the content definition it **won't affect** the **styles** that I **defined** when the FF **was ON**\n\n- Turn **ON** `FEATURE_FLAG_UVE_STYLE_EDITOR` to allow the visualization of the Style Properties.\n \n - Save Styles with **FF ON**\n \n - `Contentlet 1` has Styles `{width, color, margin}`.\n \n - `Contentlet 2` doesn't have Styles defined.\n \n- Turn **OFF** `FEATURE_FLAG_UVE_STYLE_EDITOR`\n \n - Save Styles with **FF OFF**\n \n - `Contentlet 1` send Styles in the request, but `styleProperties` field is not present in the response.\n \n - `Contentlet 2` doesn't define Styles in the request, and`styleProperties` field is not present in the response.\n \n - Get **Contentlet** `Contentlet 1` does not contain `styleProperties` field.\n \n- Turn **ON** `FEATURE_FLAG_UVE_STYLE_EDITOR.`\n \n - Get **Contentlet** `Contentlet 1` have `styleProperties = {width, color, margin}`.\n \n - Delete the Styles for the `Contentlet 1` and `Contentlet 2` by sending `styleProperties = {}`\n \n - Get **Contentlet** `Contentlet 1` does not contain `styleProperties` field.\n \n- Turn **OFF** `FEATURE_FLAG_UVE_STYLE_EDITOR`" } ], "auth": { From 2a6cc5870fcd51a3718542a636116f516ca860d7 Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 08:36:46 -0500 Subject: [PATCH 5/8] fix GraphQLTests.json test --- .../main/resources/postman/GraphQLTests.json | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/dotcms-postman/src/main/resources/postman/GraphQLTests.json b/dotcms-postman/src/main/resources/postman/GraphQLTests.json index 5e2d799380d8..3abb92702871 100644 --- a/dotcms-postman/src/main/resources/postman/GraphQLTests.json +++ b/dotcms-postman/src/main/resources/postman/GraphQLTests.json @@ -4576,43 +4576,15 @@ "response": [] }, { - "name": "Save Content With Basic Styles Copy", + "name": "Turn ON Style Editor", "event": [ { "listen": "test", "script": { "exec": [ - "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", - "", "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", - "", - "// Parse JSON", - "const json = pm.response.json();", - "", - "pm.test(\"No errors returned\", function () {", - " pm.expect(json.errors).to.be.an(\"array\").that.is.empty;", - "});", - "", - "pm.test(\"Contentlet_1 has correct styleProperties\", function () {", - " const first = json.entity.find(item => item.contentletId === contentlet1);", - " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.be.an(\"object\");", - " pm.expect(first.styleProperties.width).to.eql(\"100px\");", - " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", - " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", - "});" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ "" ], "type": "text/javascript", @@ -4626,7 +4598,7 @@ "header": [], "body": { "mode": "raw", - "raw": "[\n {\n \"identifier\": \"SYSTEM_CONTAINER\",\n \"uuid\": \"1\",\n \"contentletsId\": [\n \"{{Contentlet_1}}\"\n ],\n \"styleProperties\": {\n \"{{Contentlet_1}}\": {\n \"width\": \"100px\",\n \"color\": \"#FF0000\",\n \"margin\": \"10px\"\n }\n }\n }\n]", + "raw": "{\n \"key\": \"FEATURE_FLAG_UVE_STYLE_EDITOR\",\n \"value\": \"true\"\n}", "options": { "raw": { "language": "json" @@ -4634,32 +4606,58 @@ } }, "url": { - "raw": "{{serverURL}}/api/v1/page/{{pageIdentifier}}/content", + "raw": "{{serverURL}}/api/v1/system-table", "host": [ "{{serverURL}}" ], "path": [ "api", "v1", - "page", - "{{pageIdentifier}}", - "content" + "system-table", + "" ] - }, - "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." + } }, "response": [] }, { - "name": "Turn ON Style Editor", + "name": "Save Content With Basic Styles", "event": [ { "listen": "test", "script": { "exec": [ + "const contentlet1 = pm.collectionVariables.get(\"Contentlet_1\");", + "", "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", + "", + "// Parse JSON", + "const json = pm.response.json();", + "", + "pm.test(\"No errors returned\", function () {", + " pm.expect(json.errors).to.be.an(\"array\").that.is.empty;", + "});", + "", + "pm.test(\"Contentlet_1 has correct styleProperties\", function () {", + " const first = json.entity.find(item => item.contentletId === contentlet1);", + " pm.expect(first).to.exist;", + " pm.expect(first.styleProperties).to.be.an(\"object\");", + " pm.expect(first.styleProperties.width).to.eql(\"100px\");", + " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ "" ], "type": "text/javascript", @@ -4673,7 +4671,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"key\": \"FEATURE_FLAG_UVE_STYLE_EDITOR\",\n \"value\": \"true\"\n}", + "raw": "[\n {\n \"identifier\": \"SYSTEM_CONTAINER\",\n \"uuid\": \"1\",\n \"contentletsId\": [\n \"{{Contentlet_1}}\"\n ],\n \"styleProperties\": {\n \"{{Contentlet_1}}\": {\n \"width\": \"100px\",\n \"color\": \"#FF0000\",\n \"margin\": \"10px\"\n }\n }\n }\n]", "options": { "raw": { "language": "json" @@ -4681,17 +4679,19 @@ } }, "url": { - "raw": "{{serverURL}}/api/v1/system-table", + "raw": "{{serverURL}}/api/v1/page/{{pageIdentifier}}/content", "host": [ "{{serverURL}}" ], "path": [ "api", "v1", - "system-table", - "" + "page", + "{{pageIdentifier}}", + "content" ] - } + }, + "description": "Test when users make a request sending the style properties for some contentlets.\nThis will save the style properties for the specified contentlets, in case style properties are not send the contentlet style properties will be null." }, "response": [] }, @@ -6103,7 +6103,7 @@ "response": [] }, { - "name": "RequestVanityURLBaseType_ReturnsAllFields Copy", + "name": "RequestVanityURLBaseType_ReturnsAllFields", "event": [ { "listen": "test", @@ -9442,7 +9442,7 @@ "name": "Cats", "item": [ { - "name": "No value for cats field should return empty list Copy", + "name": "No value for cats field should return empty list", "item": [ { "name": "pre_ImportBundleWithContext", From 4d9a66d8cde0a0a43feed70282c79b6b256ef4ec Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 09:02:33 -0500 Subject: [PATCH 6/8] rename ContentView --- ...ntentletStylingView.java => AbstractContentView.java} | 9 +++++---- .../java/com/dotcms/rest/api/v1/page/PageResource.java | 4 ++-- .../com/dotcms/rest/api/v1/page/PageResourceHelper.java | 6 +++--- ...etStylingView.java => ResponseEntityContentView.java} | 4 ++-- .../portlets/contentlet/model/Contentlet.java | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) rename dotCMS/src/main/java/com/dotcms/rest/api/v1/page/{AbstractContentletStylingView.java => AbstractContentView.java} (87%) rename dotCMS/src/main/java/com/dotcms/rest/api/v1/page/{ResponseEntityContentletStylingView.java => ResponseEntityContentView.java} (69%) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentView.java similarity index 87% rename from dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java rename to dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentView.java index bd20ea294413..55bdcbb1926e 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentletStylingView.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/AbstractContentView.java @@ -1,6 +1,7 @@ package com.dotcms.rest.api.v1.page; import com.dotcms.annotations.Nullable; +import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -12,10 +13,10 @@ @Value.Style(typeImmutable = "*", typeAbstract = "Abstract*") @Value.Immutable -@JsonSerialize(as = ContentletStylingView.class) -@JsonDeserialize(as = ContentletStylingView.class) +@JsonSerialize(as = ContentView.class) +@JsonDeserialize(as = ContentView.class) @Schema(description = "Contentlet with Styles info") -public interface AbstractContentletStylingView { +public interface AbstractContentView { @JsonProperty("containerId") @Schema( @@ -39,7 +40,7 @@ public interface AbstractContentletStylingView { ) String contentletId(); - @JsonProperty("styleProperties") + @JsonProperty(Contentlet.STYLE_PROPERTIES_KEY) @Nullable @JsonInclude(JsonInclude.Include.NON_NULL) @Schema( diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java index 48e60937b14f..c306c2eeafed 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResource.java @@ -813,10 +813,10 @@ public final Response addContent(@Context final HttpServletRequest request, this.validateContainerEntries(pageContainerForm.getContainerEntries()); // Save content and Get the saved contentlets - final List savedContent = pageResourceHelper.saveContent( + final List savedContent = pageResourceHelper.saveContent( pageId, this.reduce(pageContainerForm.getContainerEntries()), language, variantName); - return Response.ok(new ResponseEntityContentletStylingView(savedContent)).build(); + return Response.ok(new ResponseEntityContentView(savedContent)).build(); } catch(HTMLPageAssetNotFoundException e) { final String errorMsg = String.format("HTMLPageAssetNotFoundException on PageResource.addContent, pageId: %s: ", pageId); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java index c13278801c95..d5c2bf19490c 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/PageResourceHelper.java @@ -154,7 +154,7 @@ public HttpServletRequest decorateRequest(final HttpServletRequest request) { * @throws BadRequestException if style validation fails */ @WrapInTransaction - public List saveContent(final String pageId, + public List saveContent(final String pageId, final List containerEntries, final Language language, String variantName) throws DotDataException { @@ -272,10 +272,10 @@ private void stylePropertiesValidation( * @return A list of the saved Contentlets with the containerId, uuid, contentletId and * styleProperties. */ - private List buildSaveContentResponse(List savedMultiTrees) { + private List buildSaveContentResponse(List savedMultiTrees) { return savedMultiTrees.stream() .map(multiTree -> { - ContentletStylingView.Builder builder = ContentletStylingView.builder() + ContentView.Builder builder = ContentView.builder() .containerId(multiTree.getContainer()) .contentletId(multiTree.getContentlet()) .uuid(multiTree.getRelationType()); diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentletStylingView.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentView.java similarity index 69% rename from dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentletStylingView.java rename to dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentView.java index 4f1c3043ce87..b7874bbf2e70 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentletStylingView.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/page/ResponseEntityContentView.java @@ -6,14 +6,14 @@ /** * Response wrapper for contentlets with its container, page and styles information. */ -public class ResponseEntityContentletStylingView extends ResponseEntityView> { +public class ResponseEntityContentView extends ResponseEntityView> { /** * Constructor for contentlets with its container, page and styles information. * * @param contentletStylingList The list of ContentletStylingView objects to be included in the response. */ - public ResponseEntityContentletStylingView(List contentletStylingList) { + public ResponseEntityContentView(List contentletStylingList) { super(contentletStylingList); } } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/model/Contentlet.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/model/Contentlet.java index 853c1322fefa..b52396c66d60 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/model/Contentlet.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/model/Contentlet.java @@ -155,7 +155,7 @@ public class Contentlet implements Serializable, Permissionable, Categorizable, public static final String DONT_VALIDATE_ME = "_dont_validate_me"; public static final String DISABLE_WORKFLOW = "__disable_workflow__"; public static final String VALIDATE_EMPTY_FILE = "_validateEmptyFile_"; - public static final String STYLE_PROPERTIES_KEY = "styleProperties"; + public static final String STYLE_PROPERTIES_KEY = "dotStyleProperties"; // means the contentlet is being used on unit test mode. // this is only for unit test. do not use on production. From 98cdb51772ab1d11bb4ac9456a65372fc4be5386 Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 10:17:41 -0500 Subject: [PATCH 7/8] fix postman test to retrieve the dotStyleProperties in the PageAPI and GraphQL --- ...ts_StyleProperties.postman_collection.json | 58 +++++++++---------- .../main/resources/postman/GraphQLTests.json | 22 +++---- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json b/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json index ce360ac57dfe..0a78c5c7ea12 100644 --- a/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/Define_Contentlets_StyleProperties.postman_collection.json @@ -503,10 +503,10 @@ "pm.test(\"First entry has correct styleProperties\", function () {", " const first = json.entity.find(item => item.contentletId === contentlet1);", " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.be.an(\"object\");", - " pm.expect(first.styleProperties.width).to.eql(\"100px\");", - " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", - " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", + " pm.expect(first.dotStyleProperties).to.be.an(\"object\");", + " pm.expect(first.dotStyleProperties.width).to.eql(\"100px\");", + " pm.expect(first.dotStyleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(first.dotStyleProperties.margin).to.eql(\"10px\");", "});", "", "// ============================", @@ -515,7 +515,7 @@ "pm.test(\"Second entry has no style properties\", function () {", " const second = json.entity.find(item => item.contentletId === contentlet2);", " pm.expect(second).to.exist;", - " pm.expect(second.styleProperties).to.eql({});", + " pm.expect(second.dotStyleProperties).to.eql({});", "});" ], "type": "text/javascript", @@ -587,12 +587,12 @@ "pm.test(`Validate ${contentlet1} has correct styleProperties`, function () {", " const contentlet = entity.find(item => item.contentletId === contentlet1);", " pm.expect(contentlet).to.exist;", - " pm.expect(contentlet.styleProperties).to.be.an(\"object\");", - " pm.expect(contentlet.styleProperties.height).to.eql(300);", - " pm.expect(contentlet.styleProperties.visible).to.be.true;", + " pm.expect(contentlet.dotStyleProperties).to.be.an(\"object\");", + " pm.expect(contentlet.dotStyleProperties.height).to.eql(300);", + " pm.expect(contentlet.dotStyleProperties.visible).to.be.true;", "", " // Deep styleProperties object", - " const styleLayout = contentlet.styleProperties.layout;", + " const styleLayout = contentlet.dotStyleProperties.layout;", " pm.expect(styleLayout).to.be.an(\"object\");", " pm.expect(styleLayout.display).to.eql(\"flex\");", " pm.expect(styleLayout.gap).to.eql(16);", @@ -761,7 +761,7 @@ " const systemContainer = json.entity.find(item => item.containerId === \"SYSTEM_CONTAINER\");", " pm.expect(systemContainer).to.exist;", " pm.expect(systemContainer.contentletId).to.eql(contentlet1);", - " pm.expect(systemContainer.styleProperties.width).to.eql(\"100px\");", + " pm.expect(systemContainer.dotStyleProperties.width).to.eql(\"100px\");", "});", "", "// ============================", @@ -771,7 +771,7 @@ " const customContainer = json.entity.find(item => item.containerId === CUSTOM_CONTAINER);", " pm.expect(customContainer).to.exist;", " pm.expect(customContainer.contentletId).to.eql(contentlet1);", - " pm.expect(customContainer.styleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(customContainer.dotStyleProperties.color).to.eql(\"#FF0000\");", "});" ], "type": "text/javascript", @@ -1064,7 +1064,7 @@ " // filter by contentlet", " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", - " pm.expect(contentlet.styleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(contentlet.dotStyleProperties.color).to.eql(\"#FF0000\");", "});" ], "type": "text/javascript", @@ -1128,7 +1128,7 @@ " // filter by contentlet", " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", - " pm.expect(contentlet.styleProperties.width).to.eql(\"100px\");", + " pm.expect(contentlet.dotStyleProperties.width).to.eql(\"100px\");", "});" ], "type": "text/javascript", @@ -1242,7 +1242,7 @@ " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", " // Validate that styles are not present", - " pm.expect(contentlet.styleProperties).to.be.undefined;", + " pm.expect(contentlet.dotStyleProperties).to.be.undefined;", "});" ], "type": "text/javascript", @@ -1371,10 +1371,10 @@ "pm.test(\"First entry has correct styleProperties\", function () {", " const first = json.entity.find(item => item.contentletId === contentlet1);", " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.be.an(\"object\");", - " pm.expect(first.styleProperties.width).to.eql(\"100px\");", - " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", - " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", + " pm.expect(first.dotStyleProperties).to.be.an(\"object\");", + " pm.expect(first.dotStyleProperties.width).to.eql(\"100px\");", + " pm.expect(first.dotStyleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(first.dotStyleProperties.margin).to.eql(\"10px\");", "});", "", "// ============================", @@ -1383,7 +1383,7 @@ "pm.test(\"Second entry has no style properties\", function () {", " const second = json.entity.find(item => item.contentletId === contentlet2);", " pm.expect(second).to.exist;", - " pm.expect(second.styleProperties).to.eql({});", + " pm.expect(second.dotStyleProperties).to.eql({});", "});" ], "type": "text/javascript", @@ -1513,7 +1513,7 @@ "pm.test(\"Contentlet_1 entry has correct styleProperties\", function () {", " const first = json.entity.find(item => item.contentletId === contentlet1);", " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.be.undefined;", + " pm.expect(first.dotStyleProperties).to.be.undefined;", "});", "", "// ============================", @@ -1522,7 +1522,7 @@ "pm.test(\"Second entry has no style properties\", function () {", " const second = json.entity.find(item => item.contentletId === contentlet2);", " pm.expect(second).to.exist;", - " pm.expect(second.styleProperties).to.be.undefined;", + " pm.expect(second.dotStyleProperties).to.be.undefined;", "});" ], "type": "text/javascript", @@ -1593,7 +1593,7 @@ " // filter by contentlet", " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", - " pm.expect(contentlet.styleProperties).to.be.undefined;", + " pm.expect(contentlet.dotStyleProperties).to.be.undefined;", "});", "", "pm.test(\"Validate Contentlet_2 has NOT styleProperties\", function () {", @@ -1604,7 +1604,7 @@ " // filter by contentlet", " const contentlet = contentletList.find(c => c.identifier === contentlet2);", " pm.expect(contentlet).to.exist;", - " pm.expect(contentlet.styleProperties).to.be.undefined;", + " pm.expect(contentlet.dotStyleProperties).to.be.undefined;", "});" ], "type": "text/javascript", @@ -1718,9 +1718,9 @@ " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", " // Validate that styles defined with FF on are present", - " pm.expect(contentlet.styleProperties.width).to.eql(\"100px\");", - " pm.expect(contentlet.styleProperties.color).to.eql(\"#FF0000\");", - " pm.expect(contentlet.styleProperties.margin).to.eql(\"10px\");", + " pm.expect(contentlet.dotStyleProperties.width).to.eql(\"100px\");", + " pm.expect(contentlet.dotStyleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(contentlet.dotStyleProperties.margin).to.eql(\"10px\");", "});" ], "type": "text/javascript", @@ -1793,7 +1793,7 @@ "pm.test(\"First entry has no styleProperties\", function () {", " const first = json.entity.find(item => item.contentletId === contentlet1);", " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.eql({});", + " pm.expect(first.dotStyleProperties).to.eql({});", "});", "", "// ============================", @@ -1802,7 +1802,7 @@ "pm.test(\"Second entry has no style properties\", function () {", " const second = json.entity.find(item => item.contentletId === contentlet2);", " pm.expect(second).to.exist;", - " pm.expect(second.styleProperties).to.eql({});", + " pm.expect(second.dotStyleProperties).to.eql({});", "});" ], "type": "text/javascript", @@ -1873,7 +1873,7 @@ " const contentlet = contentletList.find(c => c.identifier === contentlet1);", " pm.expect(contentlet).to.exist;", " // Validate that styles defined with FF on are NOT present", - " pm.expect(contentlet.styleProperties).to.be.undefined;", + " pm.expect(contentlet.dotStyleProperties).to.be.undefined;", "});" ], "type": "text/javascript", diff --git a/dotcms-postman/src/main/resources/postman/GraphQLTests.json b/dotcms-postman/src/main/resources/postman/GraphQLTests.json index 3abb92702871..c53820d23149 100644 --- a/dotcms-postman/src/main/resources/postman/GraphQLTests.json +++ b/dotcms-postman/src/main/resources/postman/GraphQLTests.json @@ -4585,7 +4585,9 @@ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", - "" + "", + "// Wait for cache to invalidate", + "setTimeout(function(){}, 5000);" ], "type": "text/javascript", "packages": {}, @@ -4643,10 +4645,10 @@ "pm.test(\"Contentlet_1 has correct styleProperties\", function () {", " const first = json.entity.find(item => item.contentletId === contentlet1);", " pm.expect(first).to.exist;", - " pm.expect(first.styleProperties).to.be.an(\"object\");", - " pm.expect(first.styleProperties.width).to.eql(\"100px\");", - " pm.expect(first.styleProperties.color).to.eql(\"#FF0000\");", - " pm.expect(first.styleProperties.margin).to.eql(\"10px\");", + " pm.expect(first.dotStyleProperties).to.be.an(\"object\");", + " pm.expect(first.dotStyleProperties.width).to.eql(\"100px\");", + " pm.expect(first.dotStyleProperties.color).to.eql(\"#FF0000\");", + " pm.expect(first.dotStyleProperties.margin).to.eql(\"10px\");", "});" ], "type": "text/javascript", @@ -4757,10 +4759,10 @@ "", "// Checking Style Values as a top level field", "pm.test(\"Top level Styles have correct width\", function () {", - " pm.expect(targetContentlet.styleProperties).to.not.be.null;", - " pm.expect(targetContentlet.styleProperties.width).to.eql('100px');", - " pm.expect(targetContentlet.styleProperties.color).to.eql('#FF0000');", - " pm.expect(targetContentlet.styleProperties.margin).to.eql('10px');", + " pm.expect(targetContentlet.dotStyleProperties).to.not.be.null;", + " pm.expect(targetContentlet.dotStyleProperties.width).to.eql('100px');", + " pm.expect(targetContentlet.dotStyleProperties.color).to.eql('#FF0000');", + " pm.expect(targetContentlet.dotStyleProperties.margin).to.eql('10px');", "});" ], "type": "text/javascript", @@ -4780,7 +4782,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query getPageData($url: String!) {\n page(url: $url) {\n containers {\n containerContentlets {\n uuid\n contentlets {\n identifier\n title\n styles: _map(key: \"styleProperties\")\n styleProperties\n }\n }\n }\n }\n}\n", + "query": "query getPageData($url: String!) {\n page(url: $url) {\n containers {\n containerContentlets {\n uuid\n contentlets {\n identifier\n title\n styles: _map(key: \"dotStyleProperties\")\n dotStyleProperties\n }\n }\n }\n }\n}\n", "variables": "{\n \"url\": \"{{pageUrl}}\"\n}" } }, From 44612133721b7bdbaacee84f038dcbedef902962 Mon Sep 17 00:00:00 2001 From: ddariod Date: Mon, 12 Jan 2026 12:12:50 -0500 Subject: [PATCH 8/8] fix integration test --- .../rest/api/v1/page/PageResourceTest.java | 172 +++++++++--------- 1 file changed, 91 insertions(+), 81 deletions(-) diff --git a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java index 32591dbce540..63c47e5e72ae 100644 --- a/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/rest/api/v1/page/PageResourceTest.java @@ -93,6 +93,7 @@ import com.dotmarketing.portlets.structure.model.Structure; import com.dotmarketing.portlets.templates.design.bean.TemplateLayout; import com.dotmarketing.portlets.templates.model.Template; +import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; import com.dotmarketing.util.PaginatedArrayList; @@ -329,89 +330,98 @@ public void test_addContent_invalid_content_type() throws Exception { */ @Test public void test_addContent_with_styleProperties() throws Exception { - // Create a page with container - final PageRenderTestUtil.PageRenderTest pageRenderTest = PageRenderTestUtil.createPage(1, host); - final HTMLPageAsset testPage = pageRenderTest.getPage(); - final Container container = pageRenderTest.getFirstContainer(); - - // Create contentlet - final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(APILocator.systemUser()); - final ContentType contentGenericType = contentTypeAPI.find("webPageContent"); - final Contentlet contentlet = new ContentletDataGen(contentGenericType.id()) - .languageId(1) - .folder(APILocator.getFolderAPI().findSystemFolder()) - .host(host) - .setProperty("title", "Test Content with Style Properties") - .setProperty("body", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT) - .nextPersisted(); - - contentlet.setIndexPolicy(IndexPolicy.WAIT_FOR); - contentlet.setIndexPolicyDependencies(IndexPolicy.WAIT_FOR); - contentlet.setBoolProperty(Contentlet.IS_TEST_MODE, true); - APILocator.getContentletAPI().publish(contentlet, user, false); - - // Prepare styleProperties - final Map styleProperties = new HashMap<>(); - styleProperties.put("backgroundColor", "red"); - styleProperties.put("fontSize", "16px"); - styleProperties.put("padding", "10px"); - - // Create ContainerEntry with styleProperties - final List entries = new ArrayList<>(); - final String containerUUID = UUIDGenerator.generateUuid(); - final Map> stylePropertiesMap = new HashMap<>(); - stylePropertiesMap.put(contentlet.getIdentifier(), styleProperties); - - final PageContainerForm.ContainerEntry containerEntry = - new PageContainerForm.ContainerEntry( - null, - container.getIdentifier(), - containerUUID, - list(contentlet.getIdentifier()), - stylePropertiesMap - ); + // Save the original feature flag value + final boolean originalFeatureFlagValue = Config.getBooleanProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", false); - entries.add(containerEntry); - final PageContainerForm pageContainerForm = new PageContainerForm(entries, null); - - // Save content with styleProperties - final Response addContentResponse = this.pageResourceWithHelper.addContent( - request, - response, - testPage.getIdentifier(), - VariantAPI.DEFAULT_VARIANT.name(), - pageContainerForm - ); - - // Verify response is successful - assertNotNull(addContentResponse); - assertEquals(200, addContentResponse.getStatus()); - - // Retrieve MultiTree and verify styleProperties are saved - final MultiTreeAPI multiTreeAPI = APILocator.getMultiTreeAPI(); - final List multiTrees = multiTreeAPI.getMultiTrees(testPage.getIdentifier()); - - assertNotNull("MultiTrees should not be null", multiTrees); - assertFalse("MultiTrees should not be empty", multiTrees.isEmpty()); - - // Find the MultiTree for our contentlet - final Optional multiTreeOpt = multiTrees.stream() - .filter(mt -> mt.getContentlet().equals(contentlet.getIdentifier())) - .findFirst(); - - assertTrue("MultiTree for the contentlet should exist", multiTreeOpt.isPresent()); - - final MultiTree multiTree = multiTreeOpt.get(); - final Map savedStyleProperties = multiTree.getStyleProperties(); - - // Verify styleProperties were saved correctly - assertNotNull("StyleProperties should not be null", savedStyleProperties); - assertFalse("StyleProperties should not be empty", savedStyleProperties.isEmpty()); - assertEquals("backgroundColor should match", "red", savedStyleProperties.get("backgroundColor")); - assertEquals("fontSize should match", "16px", savedStyleProperties.get("fontSize")); - assertEquals("padding should match", "10px", savedStyleProperties.get("padding")); + try { + // Enable the Style Editor feature flag + Config.setProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", true); + final PageRenderTestUtil.PageRenderTest pageRenderTest = PageRenderTestUtil.createPage(1, host); + final HTMLPageAsset testPage = pageRenderTest.getPage(); + final Container container = pageRenderTest.getFirstContainer(); - Logger.info(this, "StyleProperties saved successfully: " + savedStyleProperties); + // Create contentlet + final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(APILocator.systemUser()); + final ContentType contentGenericType = contentTypeAPI.find("webPageContent"); + final Contentlet contentlet = new ContentletDataGen(contentGenericType.id()) + .languageId(1) + .folder(APILocator.getFolderAPI().findSystemFolder()) + .host(host) + .setProperty("title", "Test Content with Style Properties") + .setProperty("body", TestDataUtils.BLOCK_EDITOR_DUMMY_CONTENT) + .nextPersisted(); + + contentlet.setIndexPolicy(IndexPolicy.WAIT_FOR); + contentlet.setIndexPolicyDependencies(IndexPolicy.WAIT_FOR); + contentlet.setBoolProperty(Contentlet.IS_TEST_MODE, true); + APILocator.getContentletAPI().publish(contentlet, user, false); + + // Prepare styleProperties + final Map styleProperties = new HashMap<>(); + styleProperties.put("backgroundColor", "red"); + styleProperties.put("fontSize", "16px"); + styleProperties.put("padding", "10px"); + + // Create ContainerEntry with styleProperties + final List entries = new ArrayList<>(); + final String containerUUID = UUIDGenerator.generateUuid(); + final Map> stylePropertiesMap = new HashMap<>(); + stylePropertiesMap.put(contentlet.getIdentifier(), styleProperties); + + final PageContainerForm.ContainerEntry containerEntry = + new PageContainerForm.ContainerEntry( + null, + container.getIdentifier(), + containerUUID, + list(contentlet.getIdentifier()), + stylePropertiesMap + ); + + entries.add(containerEntry); + final PageContainerForm pageContainerForm = new PageContainerForm(entries, null); + + // Save content with styleProperties + final Response addContentResponse = this.pageResourceWithHelper.addContent( + request, + response, + testPage.getIdentifier(), + VariantAPI.DEFAULT_VARIANT.name(), + pageContainerForm + ); + + // Verify response is successful + assertNotNull(addContentResponse); + assertEquals(200, addContentResponse.getStatus()); + + // Retrieve MultiTree and verify styleProperties are saved + final MultiTreeAPI multiTreeAPI = APILocator.getMultiTreeAPI(); + final List multiTrees = multiTreeAPI.getMultiTrees(testPage.getIdentifier()); + + assertNotNull("MultiTrees should not be null", multiTrees); + assertFalse("MultiTrees should not be empty", multiTrees.isEmpty()); + + // Find the MultiTree for our contentlet + final Optional multiTreeOpt = multiTrees.stream() + .filter(mt -> mt.getContentlet().equals(contentlet.getIdentifier())) + .findFirst(); + + assertTrue("MultiTree for the contentlet should exist", multiTreeOpt.isPresent()); + + final MultiTree multiTree = multiTreeOpt.get(); + final Map savedStyleProperties = multiTree.getStyleProperties(); + + // Verify styleProperties were saved correctly + assertNotNull("StyleProperties should not be null", savedStyleProperties); + assertFalse("StyleProperties should not be empty", savedStyleProperties.isEmpty()); + assertEquals("backgroundColor should match", "red", savedStyleProperties.get("backgroundColor")); + assertEquals("fontSize should match", "16px", savedStyleProperties.get("fontSize")); + assertEquals("padding should match", "10px", savedStyleProperties.get("padding")); + + Logger.info(this, "StyleProperties saved successfully: " + savedStyleProperties); + } finally { + // Restore the original feature flag value + Config.setProperty("FEATURE_FLAG_UVE_STYLE_EDITOR", originalFeatureFlagValue); + } } /**