diff --git a/core-web/apps/dotcms-ui/proxy-dev.conf.mjs b/core-web/apps/dotcms-ui/proxy-dev.conf.mjs index 8684ee53a24d..f9f65b0e35ae 100644 --- a/core-web/apps/dotcms-ui/proxy-dev.conf.mjs +++ b/core-web/apps/dotcms-ui/proxy-dev.conf.mjs @@ -10,10 +10,13 @@ export default [ '/DotAjaxDirector', '/contentAsset', '/application', - '/assets/seo/page-tools.json' + '/assets' ], target: 'http://localhost:8080', secure: false, - logLevel: 'debug' + logLevel: 'debug', + pathRewrite: { + '^/assets': '/dotAdmin/assets' + } } ]; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette.component.html index a14de012e6dd..1cc687326b72 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette.component.html @@ -2,13 +2,11 @@
+ (selected)="switchView($event)"> + (paginate)="paginateContentlets($event)">
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/mocks/contentlets.mock.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/mocks/contentlets.mock.ts new file mode 100644 index 000000000000..88a39507b8ff --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/mocks/contentlets.mock.ts @@ -0,0 +1,134 @@ +import { DEFAULT_VARIANT_ID, DotCMSContentlet } from '@dotcms/dotcms-models'; + +const IDENTIFIER = '0af1efad-6f3c-480e-bb91-fe786a4b6dfe'; +export const VARIANT_ID_MOCK = 'dotexperiment-3759acc113-variant-1'; + +export const ContentletWithDuplicatedMock: Array = [ + { + hostName: 'demo.dotcms.com', + modDate: '2023-10-03 17:47:16.198', + publishDate: '2023-10-03 17:47:16.198', + title: 'Travel Blog Header [MODIFIED]', + body: '

Travel Blog MODIFIED

', + baseType: 'CONTENT', + inode: 'cf2af7da-6968-48ef-97fa-d638ba7def01', + archived: false, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '2a3e91e4-fbbf-4876-8c5b-2233c1739b05', + contentType: 'webPageContent', + live: true, + owner: 'dotcms.org.1', + identifier: IDENTIFIER, + languageId: 1, + url: '/content.5f3ba352-0139-425e-880f-c8bbfafcea7d', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + modUserName: 'Admin User', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: false, + sortOrder: 0, + modUser: 'dotcms.org.1', + __icon__: 'contentIcon', + contentTypeIcon: 'wysiwyg', + variant: VARIANT_ID_MOCK + }, + + { + hostName: 'demo.dotcms.com', + modDate: '2020-09-02 16:45:50.663', + publishDate: '2020-09-02 16:45:50.663', + title: 'Travel Blog Header [Original]', + body: '

Travel Blog ORIGINAL

', + baseType: 'CONTENT', + inode: '782c7e2c-5c95-41fd-83aa-d3ff8cb143d3', + archived: false, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '2a3e91e4-fbbf-4876-8c5b-2233c1739b05', + contentType: 'webPageContent', + live: true, + owner: 'dotcms.org.1', + identifier: IDENTIFIER, + languageId: 1, + url: '/content.5f3ba352-0139-425e-880f-c8bbfafcea7d', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + modUserName: 'Admin User', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: false, + sortOrder: 0, + modUser: 'dotcms.org.1', + __icon__: 'contentIcon', + contentTypeIcon: 'wysiwyg', + variant: DEFAULT_VARIANT_ID + } +]; + +export const NotDuplicatedContentletMock: Array = [ + { + hostName: 'demo.dotcms.com', + modDate: '2020-09-02 16:45:53.832', + publishDate: '2020-09-02 16:45:53.832', + title: 'Thank You [ORIGINAL]', + body: '

Thank You

\n

Thank you for your interest in TravelLux, the industry leader in luxury adventure travel. We have received your information and are reviewing it. One of our team members will reach out to you shortly. If you need immediate assistance please call us at 1-800-LUX-TRAV.

\n\n

', + baseType: 'CONTENT', + inode: 'b614f0a1-02fd-4a09-b62c-81e7973eeb40', + archived: false, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '2a3e91e4-fbbf-4876-8c5b-2233c1739b05', + contentType: 'webPageContent', + live: true, + owner: 'dotcms.org.1', + identifier: 'e3988576-cf62-437b-ac93-6237baf519c5', + languageId: 1, + url: '/content.c1831446-8eb9-4b15-b7ea-43d6301f51b5', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + modUserName: 'Admin User', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: false, + sortOrder: 0, + modUser: 'dotcms.org.1', + __icon__: 'contentIcon', + contentTypeIcon: 'wysiwyg', + variant: DEFAULT_VARIANT_ID + } +]; + +export const NewVariantContentletMock: Array = [ + { + hostName: 'demo.dotcms.com', + modDate: '2023-10-04 17:14:25.153', + publishDate: '2023-10-04 17:14:25.153', + title: 'New Variant Contentlet', + body: '

TEST Rich 2

', + baseType: 'CONTENT', + inode: 'd1fdadf0-2782-4680-986c-caf63b40a787', + archived: false, + host: '48190c8c-42c4-46af-8d1a-0cd5db894797', + working: true, + locked: false, + stInode: '2a3e91e4-fbbf-4876-8c5b-2233c1739b05', + contentType: 'webPageContent', + live: true, + owner: 'dotcms.org.1', + identifier: '7de976503e17c7f51f6b24433187365c', + languageId: 1, + url: '/content.3dd13f81-68a7-4690-ae05-9ce03b830cf2', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + modUserName: 'Admin User', + hasLiveVersion: true, + folder: 'SYSTEM_FOLDER', + hasTitleImage: false, + sortOrder: 0, + modUser: 'dotcms.org.1', + __icon__: 'contentIcon', + contentTypeIcon: 'wysiwyg', + variant: VARIANT_ID_MOCK + } +]; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.spec.ts index 82a60972be2e..a8d8332e1306 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.spec.ts @@ -3,8 +3,19 @@ import { Observable, of } from 'rxjs'; import { Injectable } from '@angular/core'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { DotContentTypeService, DotESContentService, PaginatorService } from '@dotcms/data-access'; +import { + DotContentTypeService, + DotESContentService, + DotSessionStorageService, + PaginatorService +} from '@dotcms/data-access'; import { DotCMSContentlet, DotCMSContentType, ESContent } from '@dotcms/dotcms-models'; +import { + ContentletWithDuplicatedMock, + NewVariantContentletMock, + NotDuplicatedContentletMock, + VARIANT_ID_MOCK +} from '@portlets/dot-edit-page/components/dot-palette/mocks/contentlets.mock'; import { DotPaletteStore } from './dot-palette.store'; @@ -104,11 +115,13 @@ describe('DotPaletteStore', () => { let paginatorService: PaginatorService; let dotContentTypeService: DotContentTypeService; let dotESContentService: DotESContentService; + let dotSessionStorageService: DotSessionStorageService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ DotPaletteStore, + DotSessionStorageService, { provide: PaginatorService, useClass: MockPaginatorService }, { provide: DotContentTypeService, useClass: MockContentTypeService }, { provide: DotESContentService, useClass: MockESPaginatorService } @@ -118,6 +131,7 @@ describe('DotPaletteStore', () => { paginatorService = TestBed.inject(PaginatorService); dotContentTypeService = TestBed.inject(DotContentTypeService); dotESContentService = TestBed.inject(DotESContentService); + dotSessionStorageService = TestBed.inject(DotSessionStorageService); }); // Updaters @@ -240,7 +254,7 @@ describe('DotPaletteStore', () => { ] as unknown as DotCMSContentlet[]); expect(data.filter).toEqual(''); expect(data.loading).toEqual(false); - expect(data.totalRecords).toEqual(20); + expect(data.totalRecords).toEqual(1); // changed due a filter the data in the store and the totalRecords now have the real amount of the array done(); }); }); @@ -303,4 +317,87 @@ describe('DotPaletteStore', () => { expect(dotContentTypeService.filterContentTypes).not.toHaveBeenCalled(); expect(dotContentTypeService.getContentTypes).not.toHaveBeenCalled(); })); + + describe('handle variant contentlets', () => { + beforeEach(() => { + spyOn(dotSessionStorageService, 'getVariationId').and.returnValue(VARIANT_ID_MOCK); + }); + it('should remove the `DEFAULT` Contentlets and leave the copied', (done) => { + spyOn(dotESContentService, 'get').and.returnValue( + of({ + contentTook: 0, + jsonObjectView: { + contentlets: [ + ...ContentletWithDuplicatedMock, + ...NotDuplicatedContentletMock + ] + }, + queryTook: 1, + resultsSize: 20 + }) + ); + + dotPaletteStore.loadContentlets(''); + + dotPaletteStore.vm$.subscribe(({ contentlets }) => { + expect(contentlets.length).toEqual(2); + + const contentlet = contentlets[0] as DotCMSContentlet; + expect(ContentletWithDuplicatedMock[0].inode).toEqual(contentlet.inode); + done(); + }); + }); + it('should leave the created contentled in the variant', (done) => { + spyOn(dotESContentService, 'get').and.returnValue( + of({ + contentTook: 0, + jsonObjectView: { + contentlets: [...NotDuplicatedContentletMock, ...NewVariantContentletMock] + }, + queryTook: 1, + resultsSize: 20 + }) + ); + + dotPaletteStore.loadContentlets(''); + + dotPaletteStore.vm$.subscribe(({ contentlets }) => { + expect(contentlets.length).toEqual(2); + + const contentletsStore = contentlets as DotCMSContentlet[]; + expect(NotDuplicatedContentletMock[0].inode).toEqual(contentletsStore[0].inode); + expect(NewVariantContentletMock[0].inode).toEqual(contentletsStore[1].inode); + done(); + }); + }); + + it('should leave the created variant contentled and delete the `DEFAULT` Contentlets modified ', (done) => { + spyOn(dotESContentService, 'get').and.returnValue( + of({ + contentTook: 0, + jsonObjectView: { + contentlets: [ + ...ContentletWithDuplicatedMock, + ...NotDuplicatedContentletMock, + ...NewVariantContentletMock + ] + }, + queryTook: 1, + resultsSize: 20 + }) + ); + + dotPaletteStore.loadContentlets(''); + + dotPaletteStore.vm$.subscribe(({ contentlets }) => { + expect(contentlets.length).toEqual(3); + + const contentletsStore = contentlets as DotCMSContentlet[]; + expect(ContentletWithDuplicatedMock[0].inode).toEqual(contentletsStore[0].inode); + expect(NotDuplicatedContentletMock[0].inode).toEqual(contentletsStore[1].inode); + expect(NewVariantContentletMock[0].inode).toEqual(contentletsStore[2].inode); + done(); + }); + }); + }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.ts index a8a77c087fa4..1d0be3725447 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.ts @@ -7,9 +7,15 @@ import { LazyLoadEvent } from 'primeng/api'; import { debounceTime, map, take } from 'rxjs/operators'; -import { DotContentTypeService, DotESContentService, PaginatorService } from '@dotcms/data-access'; +import { + DotContentTypeService, + DotESContentService, + DotSessionStorageService, + PaginatorService +} from '@dotcms/data-access'; import { ComponentStatus, + DEFAULT_VARIANT_ID, DotCMSContentlet, DotCMSContentType, ESContent @@ -113,7 +119,7 @@ export class DotPaletteStore extends ComponentStore { // UPDATERS private readonly setContentlets = this.updater( (state: DotPaletteState, data: DotCMSContentlet[] | DotCMSContentType[]) => { - return { ...state, contentlets: data }; + return { ...state, contentlets: data, totalRecords: data.length }; } ); @@ -151,8 +157,9 @@ export class DotPaletteStore extends ComponentStore { constructor( private dotContentTypeService: DotContentTypeService, - public paginatorESService: DotESContentService, - public paginationService: PaginatorService + private paginatorESService: DotESContentService, + private paginationService: PaginatorService, + private dotSessionStorageService: DotSessionStorageService ) { super({ contentlets: null, @@ -183,7 +190,6 @@ export class DotPaletteStore extends ComponentStore { .getWithOffset((event && event.first) || 0) .pipe(take(1)) .subscribe((data: DotCMSContentlet[] | DotCMSContentType[]) => { - data.forEach((item) => (item.contentType = item.variable = 'FORM')); this.setLoaded(); this.setContentlets(data); this.setTotalRecords(this.paginationService.totalRecords); @@ -195,13 +201,27 @@ export class DotPaletteStore extends ComponentStore { lang: languageId || '1', filter: filter || '', offset: (event && event.first.toString()) || '0', - query: `+contentType: ${this.contentTypeVarName} +deleted: false` + query: `+contentType: ${ + this.contentTypeVarName + } +deleted: false ${this.getExperimentVariantQueryField()}`.trim() }) .pipe(take(1)) .subscribe((response: ESContent) => { this.setLoaded(); - this.setTotalRecords(response.resultsSize); - this.setContentlets(response.jsonObjectView.contentlets); + if (this.dotSessionStorageService.getVariationId() !== DEFAULT_VARIANT_ID) { + // GH issue: https://github.com/dotCMS/core/issues/26363 + // This is a workaround to remove the original (variant: DEFAULT) when exist a modified contentlet inside a + // variant (it make a copy of the original) the endpoint return the original and the derivated/duplicated. + // We need to discus about create or not a new endpoint to get the contentlets taking + // in consideration the variant contentlets, if you remove this, the contentlets will show the duplicated and the original contentlet + const contentlets = this.removeOriginalContentletsDuplicated( + response.jsonObjectView.contentlets + ); + + this.setContentlets(contentlets); + } else { + this.setContentlets(response.jsonObjectView.contentlets); + } }); } }); @@ -275,4 +295,48 @@ export class DotPaletteStore extends ComponentStore { this.loadContentlets(variableName); this.setContentTypes(this.initialContent); } + + /** + * Retrieves the experiment variant query field. + * + * @private + * + * @returns {string} The query field for the experiment variant. + */ + private getExperimentVariantQueryField(): string { + return this.dotSessionStorageService.getVariationId() !== DEFAULT_VARIANT_ID + ? `+(variant:default OR variant:${this.dotSessionStorageService.getVariationId()})` + : ''; + } + + /** + * If the contentlets have a derivated/duplicated contentlet, remove the original (variant: DEFAULT). + * + * @param {DotCMSContentlet[]} contentlets - The array of contentlets to remove derived contentlets from. + * @return {DotCMSContentlet[]} - The modified array of contentlets without the original contentlets. + */ + private removeOriginalContentletsDuplicated(contentlets: DotCMSContentlet[]) { + const currentVariationId = this.dotSessionStorageService.getVariationId(); + const uniqueIdentifiersFromVariantContentlet = new Set(); + const iNodesOfOriginalContentletToDelete = []; + + contentlets.reduce((acc, item) => { + if (item.variant === currentVariationId) { + uniqueIdentifiersFromVariantContentlet.add(item.identifier); + } + + if ( + uniqueIdentifiersFromVariantContentlet.has(item.identifier) && + item.variant !== currentVariationId + ) { + iNodesOfOriginalContentletToDelete.push(item.inode); + } + + return acc; + }, {}); + + return contentlets.filter( + (item) => !iNodesOfOriginalContentletToDelete.includes(item.inode) + ); + } } diff --git a/core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts b/core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts index f5624f518a50..d48c2d46c53e 100644 --- a/core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts +++ b/core-web/libs/dotcms-models/src/lib/dot-contentlet.model.ts @@ -31,6 +31,10 @@ export interface DotCMSContentlet { text?: string; url: string; working: boolean; + body?: string; + contentTypeIcon?: string; + variant?: string; + __icon__?: string; [key: string]: any; } diff --git a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java index 46e1679e4145..effca7c6e4f3 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/ContentResource.java @@ -198,14 +198,14 @@ public Response search(@Context HttpServletRequest request, if (UtilMethods.isSet(query)) { - final String queryDefaultVariant = query + " +variant:default"; + final String realQuery = query.indexOf("variant:") != -1 ? query : query + " +variant:default"; startAPISearchPull = Calendar.getInstance().getTimeInMillis(); - resultsSize = APILocator.getContentletAPI().indexCount(queryDefaultVariant, userForPull, pageMode.respectAnonPerms); + resultsSize = APILocator.getContentletAPI().indexCount(realQuery, userForPull, pageMode.respectAnonPerms); afterAPISearchPull = Calendar.getInstance().getTimeInMillis(); startAPIPull = Calendar.getInstance().getTimeInMillis(); - contentlets = ContentUtils.pull(processQuery(queryDefaultVariant), offset, limit, sort, userForPull, tmDate, pageMode.respectAnonPerms); + contentlets = ContentUtils.pull(processQuery(realQuery), offset, limit, sort, userForPull, tmDate, pageMode.respectAnonPerms); resultJson = getJSONObject(contentlets, request, response, render, user, depth, pageMode.respectAnonPerms, language, pageMode.showLive, allCategoriesInfo); @@ -1463,6 +1463,7 @@ public static JSONObject contentletToJSON(Contentlet contentlet, final HttpServl jsonObject.put("__icon__", UtilHTML.getIconClass(contentlet)); jsonObject.put("contentTypeIcon", type.icon()); + jsonObject.put("variant", contentlet.getVariantId()); return jsonObject; }