From 4b4b2da6ef48a2d9e2aea548f679e2de74b921a5 Mon Sep 17 00:00:00 2001 From: Neehakethi-dotcms <139247809+Neehakethi@users.noreply.github.com> Date: Thu, 26 Dec 2024 23:14:49 +0530 Subject: [PATCH] issue-29621: Excluding Hidden content types from the Widgets displayed (#30517) This pull request introduces several changes to the `DotPaletteStore` and related test files to enhance the handling of content types and widgets, including the filtering of hidden content types. The most important changes include adding the `DotPropertiesService` for configuration management and refactoring the content type loading logic. Enhancements to `DotPaletteStore`: * Added `DotPropertiesService` to manage configuration settings and retrieve hidden content types (`CONTENT_PALETTE_HIDDEN_CONTENT_TYPES`). (`[[1]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556R9)`, `[[2]](diffhunk://#diff-5e9e57d14e6a9185a0988e2062be4028f862924c1b068ac9eddab73da058e94fR13-R20)`, `[[3]](diffhunk://#diff-5e9e57d14e6a9185a0988e2062be4028f862924c1b068ac9eddab73da058e94fL166-R169)`) * Refactored the `getContenttypesData` method to use `forkJoin` with multiple observables, including hidden content types, and improved the filtering logic to exclude hidden content types. (`[core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/store/dot-palette.store.tsL252-L282](diffhunk://#diff-5e9e57d14e6a9185a0988e2062be4028f862924c1b068ac9eddab73da058e94fL252-L282)`) Updates to tests: * Updated test cases to include `DotPropertiesService` and verify the filtering of hidden content types in `dot-palette.store.spec.ts` and `edit-ema-palette.store.spec.ts`. (`[[1]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556R114-R136)`, `[[2]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556R147)`, `[[3]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556L185-R233)`, `[[4]](diffhunk://#diff-1440c725f71c6ba67d7a4847e983e63b1e7a3a4fe17042aafe9553a89ad19f22L46-R82)`, `[[5]](diffhunk://#diff-1440c725f71c6ba67d7a4847e983e63b1e7a3a4fe17042aafe9553a89ad19f22R119-R131)`, `[[6]](diffhunk://#diff-1440c725f71c6ba67d7a4847e983e63b1e7a3a4fe17042aafe9553a89ad19f22R141-R160)`) Refactoring: * Replaced inline sorting and filtering logic with pre-defined constants and helper methods to improve code readability and maintainability. (`[[1]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556R114-R136)`, `[[2]](diffhunk://#diff-8262303c46e34e159efdc68dec99a47ddf453302794be94483b1abee94c13556L185-R233)`, `[[3]](diffhunk://#diff-e60190c318aa61ddbc495b1c20398bb42e7baa8429b8241aa6964267f3ddd445L235-L259)`) These changes improve the overall functionality and maintainability of the `DotPaletteStore` by incorporating configuration management and enhancing the content type filtering logic. ### Video https://github.com/user-attachments/assets/a376dd9d-861f-4250-a67d-1dd0dd5474da --------- Co-authored-by: Rafael Velazco --- .../store/dot-palette.store.spec.ts | 42 ++++++++-- .../dot-palette/store/dot-palette.store.ts | 74 +++++++++-------- .../content/dot-edit-content.component.ts | 4 +- .../dotcms-models/src/lib/shared-models.ts | 5 ++ .../store/edit-ema-palette.store.spec.ts | 81 ++++++++++++------- .../store/edit-ema-palette.store.ts | 41 +++++----- 6 files changed, 159 insertions(+), 88 deletions(-) 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 82d10925dd45..5ce65f84733d 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 @@ -6,6 +6,7 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { DotContentTypeService, DotESContentService, + DotPropertiesService, DotSessionStorageService, PaginatorService } from '@dotcms/data-access'; @@ -110,18 +111,29 @@ class MockContentTypeService { } } +const SORTED_CONTENT_TYPE_MOCK = contentTypeDataMock.sort((a, b) => + a.name.localeCompare(b.name) +) as DotCMSContentType[]; + describe('DotPaletteStore', () => { let dotPaletteStore: DotPaletteStore; let paginatorService: PaginatorService; let dotContentTypeService: DotContentTypeService; let dotESContentService: DotESContentService; let dotSessionStorageService: DotSessionStorageService; + let dotPropertiesService: DotPropertiesService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ DotPaletteStore, DotSessionStorageService, + { + provide: DotPropertiesService, + useValue: { + getKeyAsList: () => of([]) + } + }, { provide: PaginatorService, useClass: MockPaginatorService }, { provide: DotContentTypeService, useClass: MockContentTypeService }, { provide: DotESContentService, useClass: MockESPaginatorService } @@ -132,6 +144,7 @@ describe('DotPaletteStore', () => { dotContentTypeService = TestBed.inject(DotContentTypeService); dotESContentService = TestBed.inject(DotESContentService); dotSessionStorageService = TestBed.inject(DotSessionStorageService); + dotPropertiesService = TestBed.inject(DotPropertiesService); }); // Updaters @@ -182,27 +195,42 @@ describe('DotPaletteStore', () => { // Effects it('should load contentTypes to store', (done) => { - const sortedDataMock = contentTypeDataMock.sort((a, b) => a.name.localeCompare(b.name)); spyOn(dotContentTypeService, 'filterContentTypes').and.returnValues( - of(sortedDataMock as DotCMSContentType[]) + of(SORTED_CONTENT_TYPE_MOCK) + ); + spyOn(dotContentTypeService, 'getContentTypes').and.returnValues(of([])); + + dotPaletteStore.loadContentTypes(['blog', 'banner']); + dotPaletteStore.vm$.subscribe((data) => { + expect(data.contentTypes).toEqual(SORTED_CONTENT_TYPE_MOCK); + done(); + }); + }); + + it("should load contentTypes and remove the hidden is the CONTENT_PALETTE_HIDDEN_CONTENT_TYPES is setted'", (done) => { + spyOn(dotContentTypeService, 'filterContentTypes').and.returnValues( + of(SORTED_CONTENT_TYPE_MOCK) ); spyOn(dotContentTypeService, 'getContentTypes').and.returnValues(of([])); + spyOn(dotPropertiesService, 'getKeyAsList').and.returnValue(of(['Form'])); + + const expectedData = SORTED_CONTENT_TYPE_MOCK.filter((item) => item.variable !== 'Form'); + dotPaletteStore.loadContentTypes(['blog', 'banner']); dotPaletteStore.vm$.subscribe((data) => { - expect(data.contentTypes).toEqual(sortedDataMock as DotCMSContentType[]); + expect(data.contentTypes).toEqual(expectedData); done(); }); }); - it('should load inly widgets to store if allowedContent is empty', (done) => { - const sortedDataMock = contentTypeDataMock.sort((a, b) => a.name.localeCompare(b.name)); + it('should load only widgets to store if allowedContent is empty', (done) => { spyOn(dotContentTypeService, 'filterContentTypes').and.returnValues(of([])); spyOn(dotContentTypeService, 'getContentTypes').and.returnValues( - of(sortedDataMock as DotCMSContentType[]) + of(SORTED_CONTENT_TYPE_MOCK) ); dotPaletteStore.loadContentTypes([]); dotPaletteStore.vm$.subscribe((data) => { - expect(data.contentTypes).toEqual(sortedDataMock as DotCMSContentType[]); + expect(data.contentTypes).toEqual(SORTED_CONTENT_TYPE_MOCK); 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 b1dcbb95ffcc..a7db59398fac 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 @@ -1,5 +1,5 @@ import { ComponentStore } from '@ngrx/component-store'; -import { forkJoin, Observable } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { Injectable } from '@angular/core'; @@ -10,14 +10,17 @@ import { debounceTime, map, take } from 'rxjs/operators'; import { DotContentTypeService, DotESContentService, + DotPropertiesService, DotSessionStorageService, PaginatorService } from '@dotcms/data-access'; import { ComponentStatus, DEFAULT_VARIANT_ID, + DotCMSBaseTypesContentTypes, DotCMSContentlet, DotCMSContentType, + DotConfigurationVariables, ESContent } from '@dotcms/dotcms-models'; @@ -163,7 +166,8 @@ export class DotPaletteStore extends ComponentStore { private dotContentTypeService: DotContentTypeService, private paginatorESService: DotESContentService, private paginationService: PaginatorService, - private dotSessionStorageService: DotSessionStorageService + private dotSessionStorageService: DotSessionStorageService, + private dotConfigurationService: DotPropertiesService ) { super({ contentlets: null, @@ -249,37 +253,41 @@ export class DotPaletteStore extends ComponentStore { */ getContenttypesData(): void { this.setLoading(); - this.state$.pipe(take(1)).subscribe(({ filter, allowedContent }) => { - if (allowedContent && allowedContent.length) { - forkJoin([ - this.dotContentTypeService.filterContentTypes(filter, allowedContent.join(',')), - this.dotContentTypeService.getContentTypes({ filter, page: 40, type: 'WIDGET' }) - ]) - .pipe(take(1)) - .subscribe((results: DotCMSContentType[][]) => { - const [allowContent, widgets] = results; - - // Some pages bring widgets in the CONTENT_PALETTE_HIDDEN_CONTENT_TYPES, and others don't. - // However, all pages allow widgets, so we make a request just to get them. - // Full comment here: https://github.com/dotCMS/core/pull/22573#discussion_r921263060 - // This filter is used to prevent widgets from being repeated. - const contentLets = allowContent.filter( - (item) => item.baseType !== 'WIDGET' - ); - - // Merge both array and order them by name - const contentTypes = [...contentLets, ...widgets] - .sort((a, b) => a.name.localeCompare(b.name)) - .slice(0, 40); - - this.loadContentypes(contentTypes); - }); - } else { - this.dotContentTypeService - .getContentTypes({ filter, page: 40, type: 'WIDGET' }) - .pipe(take(1)) - .subscribe((data: DotCMSContentType[]) => this.loadContentypes(data)); - } + this.state$.pipe(take(1)).subscribe(({ filter, allowedContent = [] }) => { + // Note: This store needs to be refactored + const hasAllowedContent = allowedContent && allowedContent.length > 0; + const contentTypes$ = hasAllowedContent + ? this.dotContentTypeService.filterContentTypes(filter, allowedContent.join(',')) + : of([]); + + forkJoin({ + contentTypes: contentTypes$, + widgets: this.dotContentTypeService.getContentTypes({ + filter, + page: 40, + type: 'WIDGET' + }), + hiddenContentTypes: this.dotConfigurationService.getKeyAsList( + DotConfigurationVariables.CONTENT_PALETTE_HIDDEN_CONTENT_TYPES + ) + }) + .pipe(take(1)) + .subscribe(({ contentTypes, widgets, hiddenContentTypes }) => { + /** + * This filter is used to prevent widgets from being repeated. + * More information here: https://github.com/dotCMS/core/pull/22573#discussion_r921263060 + */ + const filteredContentTypes = contentTypes.filter( + (item) => item.baseType !== DotCMSBaseTypesContentTypes.WIDGET + ); + const mergedContentAndWidgets = [...filteredContentTypes, ...widgets]; + const data = mergedContentAndWidgets + .filter((item) => !hiddenContentTypes.includes(item.variable)) + .sort((a, b) => a.name.localeCompare(b.name)) + .slice(0, 40); + + this.loadContentypes(data); + }); }); } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.ts index 4484bebc6876..7da30585801d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.ts @@ -31,6 +31,7 @@ import { DEFAULT_VARIANT_NAME, DotCMSContentlet, DotCMSContentType, + DotConfigurationVariables, DotContainerStructure, DotContentletEventAddContentType, DotExperiment, @@ -377,9 +378,8 @@ browse from the page internal links } private setAllowedContent(pageState: DotPageRenderState): void { - const CONTENT_HIDDEN_KEY = 'CONTENT_PALETTE_HIDDEN_CONTENT_TYPES'; this.dotConfigurationService - .getKeyAsList(CONTENT_HIDDEN_KEY) + .getKeyAsList(DotConfigurationVariables.CONTENT_PALETTE_HIDDEN_CONTENT_TYPES) .pipe(take(1)) .subscribe((results) => { this.allowedContent = this.filterAllowedContentTypes(results, pageState) || []; diff --git a/core-web/libs/dotcms-models/src/lib/shared-models.ts b/core-web/libs/dotcms-models/src/lib/shared-models.ts index d085547b5bfd..845d253cf2f5 100644 --- a/core-web/libs/dotcms-models/src/lib/shared-models.ts +++ b/core-web/libs/dotcms-models/src/lib/shared-models.ts @@ -31,6 +31,11 @@ export const enum FeaturedFlags { FEATURE_FLAG_UVE_PREVIEW_MODE = 'FEATURE_FLAG_UVE_PREVIEW_MODE' } +export const enum DotConfigurationVariables { + CONTENT_PALETTE_HIDDEN_CONTENT_TYPES = 'CONTENT_PALETTE_HIDDEN_CONTENT_TYPES', + WYSIWYG_IMAGE_URL_PATTERN = 'WYSIWYG_IMAGE_URL_PATTERN' +} + export const FEATURE_FLAG_NOT_FOUND = 'NOT_FOUND'; export type DotDropdownGroupSelectOption = { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.spec.ts index bfbb9a45e479..0c14aa9516b2 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.spec.ts @@ -8,7 +8,7 @@ import { DotESContentService, DotPropertiesService } from '@dotcms/data-access'; -import { DotCMSContentType } from '@dotcms/dotcms-models'; +import { DotConfigurationVariables } from '@dotcms/dotcms-models'; import { DotPaletteStore, @@ -17,8 +17,23 @@ import { PALETTE_TYPES } from './edit-ema-palette.store'; +const WIDGET_MOCK = { + baseType: 'WIDGET', + id: 'widgetTest1', + name: 'widgetTest1', + variable: 'widgetTest1' +}; + +const VALID_CONTENT_TYPE_MOCK = { + baseType: 'someContent', + id: 'contentTypeTest1', + name: 'contentTypeTest1', + variable: 'contentTypeTest1' +}; + describe('EditEmaPaletteStore', () => { let spectator: SpectatorService; + let dotPropertiesService: DotPropertiesService; const createService = createServiceFactory({ service: DotPaletteStore, providers: [ @@ -43,25 +58,21 @@ describe('EditEmaPaletteStore', () => { useValue: { filterContentTypes: () => of([ - { - baseType: 'someContent', - id: 'contentTypeTest1', - name: 'contentTypeTest1' - }, + VALID_CONTENT_TYPE_MOCK, { baseType: 'WIDGET', id: 'contentTypeTest2', - name: 'contentTypeTest2' + name: 'contentTypeTest2', + variable: 'contentTypeTest2' } ]), - getContentTypes: () => - of([ - { - baseType: 'WIDGET', - id: 'widgetTest1', - name: 'widgetTest1' - } - ]) + getContentTypes: () => of([WIDGET_MOCK]) + } + }, + { + provide: DotPropertiesService, + useValue: { + getKeyAsList: () => of([]) } } ] @@ -69,6 +80,7 @@ describe('EditEmaPaletteStore', () => { beforeEach(() => { spectator = createService(); + dotPropertiesService = spectator.inject(DotPropertiesService); }); describe('updaters', () => { @@ -105,26 +117,19 @@ describe('EditEmaPaletteStore', () => { spectator.inject(DotContentTypeService), 'getContentTypes' ); + + const getKeyAsListSpy = jest.spyOn(dotPropertiesService, 'getKeyAsList'); spectator.service.loadContentTypes({ filter: '', allowedContent: ['contentTypeTest1'] }); - const expected = [ - { - baseType: 'someContent', - id: 'contentTypeTest1', - name: 'contentTypeTest1' - }, - { - baseType: 'WIDGET', - id: 'widgetTest1', - name: 'widgetTest1' - } - ]; spectator.service.vm$.subscribe((state) => { - expect(state.contenttypes.items).toEqual(expected as DotCMSContentType[]); + expect(state.contenttypes.items).toEqual([VALID_CONTENT_TYPE_MOCK, WIDGET_MOCK]); expect(patchStateSpy).toHaveBeenCalled(); expect(filterContentTypesSpy).toHaveBeenCalledWith('', 'contentTypeTest1'); + expect(getKeyAsListSpy).toHaveBeenCalledWith( + DotConfigurationVariables.CONTENT_PALETTE_HIDDEN_CONTENT_TYPES + ); expect(getContentTypesSpy).toHaveBeenCalledWith({ filter: '', page: 40, @@ -134,6 +139,26 @@ describe('EditEmaPaletteStore', () => { }); }); + it('should load content types with filter hidden content types', (done) => { + const getKeyAsListSpy = jest + .spyOn(dotPropertiesService, 'getKeyAsList') + .mockReturnValue(of(['contentTypeTest1'])); + + const payload = { + filter: '', + allowedContent: ['contentTypeTest1'] + }; + + spectator.service.loadContentTypes(payload); + spectator.service.vm$.subscribe((state) => { + expect(state.contenttypes.items).toEqual([WIDGET_MOCK]); + expect(getKeyAsListSpy).toHaveBeenCalledWith( + DotConfigurationVariables.CONTENT_PALETTE_HIDDEN_CONTENT_TYPES + ); + done(); + }); + }); + it('should load contentlets', (done) => { const contentServiceSpy = jest.spyOn(spectator.inject(DotESContentService), 'get'); const patchStateSpy = jest.spyOn(spectator.service, 'patchState'); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.ts index 5196b2f6696f..eb77d25bd8e9 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-palette/store/edit-ema-palette.store.ts @@ -13,8 +13,10 @@ import { } from '@dotcms/data-access'; import { DEFAULT_VARIANT_ID, + DotCMSBaseTypesContentTypes, DotCMSContentlet, DotCMSContentType, + DotConfigurationVariables, DotContainerStructure, DotPageContainerStructure } from '@dotcms/dotcms-models'; @@ -232,31 +234,34 @@ export class DotPaletteStore extends ComponentStore { * @returns An Observable that emits the filtered content types. */ private getFilteredContentTypes(filter: string, allowedContent: string[]) { - return forkJoin([ - this.dotContentTypeService.filterContentTypes(filter, allowedContent.join(',')), - this.dotContentTypeService.getContentTypes({ + return forkJoin({ + contentTypes: this.dotContentTypeService.filterContentTypes( + filter, + allowedContent.join(',') + ), + widgets: this.dotContentTypeService.getContentTypes({ filter, page: 40, type: 'WIDGET' - }) - ]).pipe( - map((results) => { - const [filteredContentTypes, widgets] = results; - - // Some pages bring widgets in the CONTENT_PALETTE_HIDDEN_CONTENT_TYPES, and others don't. - // However, all pages allow widgets, so we make a request just to get them. - // Full comment here: https://github.com/dotCMS/core/pull/22573#discussion_r921263060 - // This filter is used to prevent widgets from being repeated. - const contentLets = filteredContentTypes.filter( - (item) => item.baseType !== 'WIDGET' + }), + hiddenContentTypes: this.dotConfigurationService.getKeyAsList( + DotConfigurationVariables.CONTENT_PALETTE_HIDDEN_CONTENT_TYPES + ) + }).pipe( + map(({ contentTypes, widgets, hiddenContentTypes }) => { + /** + * This filter is used to prevent widgets from being repeated. + * More information here: https://github.com/dotCMS/core/pull/22573#discussion_r921263060 + */ + const filteredContentTypes = contentTypes.filter( + (item) => item.baseType !== DotCMSBaseTypesContentTypes.WIDGET ); + const mergedContentAndWidgets = [...filteredContentTypes, ...widgets]; - // Merge both array and order them by name - const contentTypes = [...contentLets, ...widgets] + return mergedContentAndWidgets + .filter((item) => !hiddenContentTypes.includes(item.variable)) .sort((a, b) => a.name.localeCompare(b.name)) .slice(0, 40); - - return contentTypes; }), catchError(() => EMPTY) );