From 8278b29c609274726c6a7ce0430bbb8fcbeab28f Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Tue, 29 Aug 2023 11:24:42 -0300 Subject: [PATCH 1/6] Fix #25917: Template Builder file containers were saving with Identifier instead of Path (#25920) * dev (gridstack utils): add getContainerReference * dev (template builder store): implement getContainerReference * fix (palette content type module): add DotMessagePipe import * feedback (template builder): moved getContainerReference from utils to box component * feedback (template builder box test): enhance testing * fix (containers mock): fix merge errors --- .../dot-palette-content-type.module.ts | 5 ++-- .../template-builder-box.component.spec.ts | 29 +++++++++++++++++-- .../template-builder-box.component.ts | 17 +++++++++-- .../store/template-builder.store.spec.ts | 7 +++-- .../src/lib/dot-containers.mock.ts | 16 ++++++---- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.module.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.module.ts index f4dd64ec9df6..a7b7eaa8d18c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.module.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.module.ts @@ -2,14 +2,15 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { DotPaletteContentTypeComponent } from '@dotcms/app/portlets/dot-edit-page/components/dot-palette/dot-palette-content-type/dot-palette-content-type.component'; -import { DotIconModule, DotSpinnerModule } from '@dotcms/ui'; -import { DotPipesModule } from '@pipes/dot-pipes.module'; +import { DotPipesModule } from '@dotcms/app/view/pipes/dot-pipes.module'; +import { DotIconModule, DotMessagePipe, DotSpinnerModule } from '@dotcms/ui'; import { DotPaletteInputFilterModule } from '../dot-palette-input-filter/dot-palette-input-filter.module'; @NgModule({ imports: [ CommonModule, + DotMessagePipe, DotPipesModule, DotIconModule, DotSpinnerModule, diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.spec.ts index a3b67241ee6c..d317eadb3f18 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.spec.ts @@ -11,7 +11,7 @@ import { ScrollPanelModule } from 'primeng/scrollpanel'; import { DotContainersService, DotMessageService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; -import { DotContainersServiceMock, mockMatchMedia } from '@dotcms/utils-testing'; +import { containersMock, DotContainersServiceMock, mockMatchMedia } from '@dotcms/utils-testing'; import { TemplateBuilderBoxComponent } from './template-builder-box.component'; @@ -127,7 +127,9 @@ describe('TemplateBuilderBoxComponent', () => { it('should trigger addContainer when click on plus button', () => { const addContainerMock = jest.spyOn(spectator.component.addContainer, 'emit'); - const addButton = spectator.debugElement.query(By.css('.p-dropdown')); + const addButton = spectator.debugElement.query( + By.css('[data-testId="btn-plus"]>.p-dropdown') // The parent element is not listening to the click event + ); spectator.click(addButton); const option = spectator.query('.p-dropdown-item'); @@ -135,6 +137,29 @@ describe('TemplateBuilderBoxComponent', () => { expect(addContainerMock).toHaveBeenCalled(); }); + it('should emit addContainer with a identifier as identifier when source is DB', () => { + const addContainerMock = jest.spyOn(spectator.component.addContainer, 'emit'); + + spectator.triggerEventHandler("[data-testId='btn-plus']", 'onChange', { + value: containersMock[0] + }); + + expect(addContainerMock).toHaveBeenCalledWith(containersMock[0]); + }); + + it('should emit addContainer with a path as identifier when source is FILE', () => { + const addContainerMock = jest.spyOn(spectator.component.addContainer, 'emit'); + + spectator.triggerEventHandler("[data-testId='btn-plus']", 'onChange', { + value: containersMock[2] + }); + + expect(addContainerMock).toHaveBeenCalledWith({ + ...containersMock[2], + identifier: containersMock[2].path + }); + }); + it('should trigger editClasses when click on palette button', () => { const editStyleMock = jest.spyOn(spectator.component.editClasses, 'emit'); const paletteButton = spectator.query(byTestId('box-style-class-button')); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.ts index cf532e9dbd4b..f430c3f91e5f 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-box/template-builder-box.component.ts @@ -18,7 +18,7 @@ import { DropdownModule } from 'primeng/dropdown'; import { ScrollPanelModule } from 'primeng/scrollpanel'; import { DotMessageService } from '@dotcms/data-access'; -import { DotContainer, DotContainerMap } from '@dotcms/dotcms-models'; +import { CONTAINER_SOURCE, DotContainer, DotContainerMap } from '@dotcms/dotcms-models'; import { DotContainerOptionsDirective, DotMessagePipe } from '@dotcms/ui'; import { DotTemplateBuilderContainer, TemplateBuilderBoxSize } from '../../models/models'; @@ -86,11 +86,24 @@ export class TemplateBuilderBoxComponent implements OnChanges { } onContainerSelect({ value }: { value: DotContainer }) { - this.addContainer.emit(value); + this.addContainer.emit({ ...value, identifier: this.getContainerReference(value) }); this.formControl.setValue(null); } requestColumnDelete() { this.deleteColumn.emit(); } + + /** + * Based on the container source, it returns the identifier that should be used as reference. + * + * @param dotContainer + * @returns string + * @memberof TemplateBuilderBoxComponent + */ + private getContainerReference(dotContainer: DotContainer): string { + return dotContainer.source === CONTAINER_SOURCE.FILE + ? dotContainer.path + : dotContainer.identifier; + } } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/store/template-builder.store.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/store/template-builder.store.spec.ts index f2b22aff36e4..262897dc9669 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/store/template-builder.store.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/store/template-builder.store.spec.ts @@ -39,14 +39,17 @@ describe('DotTemplateBuilderStore', () => { identifier: mockContainer.identifier }; - const addContainer = () => { + const addContainer = (container = mockContainer) => { const parentRow = initialState[0]; const columnToAddContainer: DotGridStackWidget = { ...parentRow.subGridOpts?.children[0], parentId: parentRow.id as string }; - service.addContainer({ affectedColumn: columnToAddContainer, container: mockContainer }); + service.addContainer({ + affectedColumn: columnToAddContainer, + container + }); }; beforeEach(() => { diff --git a/core-web/libs/utils-testing/src/lib/dot-containers.mock.ts b/core-web/libs/utils-testing/src/lib/dot-containers.mock.ts index ad6b155b29c5..b0c9ef351802 100644 --- a/core-web/libs/utils-testing/src/lib/dot-containers.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-containers.mock.ts @@ -152,12 +152,16 @@ export const containersMockArray = [ } ]; -export const containersMock: DotContainer[] = containersMockArray.map(({ name, identifier }) => ({ - friendlyName: name, - title: name, - parentPermissionable: { hostname: '' }, - identifier: identifier -})); +export const containersMock: DotContainer[] = containersMockArray.map( + ({ name, identifier, parentPermissionable, path, source }) => ({ + friendlyName: name, + title: name, + parentPermissionable: { hostname: parentPermissionable.hostname }, + identifier: identifier, + source, + path + }) +); export const containersMapMock: DotContainerMap = containersMock.reduce( (prev: DotContainerMap, curr) => ({ ...prev, [curr.identifier as string]: curr }), From 509f73de761db68035b8cf9a9e487e4462a9ccc9 Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Wed, 30 Aug 2023 14:58:20 -0300 Subject: [PATCH 2/6] Fix #25937: Template Builder Enhancing error and request handling (#25942) * fix (template save and publish): enhancing error and request handling * feedback (dot templates service): delete old save and publish * fix (gridstack utils): EMPTY_ROWS_VALUE was being modified by reference --- .../dot-templates.service.spec.ts | 2 + .../dot-templates/dot-templates.service.ts | 15 ++++---- .../store/dot-template.store.ts | 37 ++++++++++--------- .../template-builder/utils/gridstack-utils.ts | 2 +- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.spec.ts index e83683f911d1..b59be588fa63 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.spec.ts @@ -185,6 +185,7 @@ describe('DotTemplatesService', () => { } }); }); + it('should put to save and publish a template', () => { service .saveAndPublish({ @@ -211,6 +212,7 @@ describe('DotTemplatesService', () => { } }); }); + it('should delete a template', () => { service.delete(['testId01']).subscribe(); const req = httpMock.expectOne(TEMPLATE_API_URL); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.ts index 88cfa1706426..9ae76bb7fd0f 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-templates/dot-templates.service.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs'; -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { catchError, map, pluck, take } from 'rxjs/operators'; @@ -21,7 +21,8 @@ export const TEMPLATE_API_URL = '/api/v1/templates/'; export class DotTemplatesService { constructor( private coreWebService: CoreWebService, - private httpErrorManagerService: DotHttpErrorManagerService + private httpErrorManagerService: DotHttpErrorManagerService, + private http: HttpClient ) {} /** @@ -91,11 +92,11 @@ export class DotTemplatesService { * @memberof DotTemplatesService */ saveAndPublish(values: DotTemplate): Observable { - return this.request({ - method: 'PUT', - url: `${TEMPLATE_API_URL}_savepublish`, - body: values - }); + return this.http + .put(`${TEMPLATE_API_URL}_savepublish`, { + ...values + }) + .pipe(pluck('entity')); } /** diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.ts index a36cf7933417..a331fb9cc0ea 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.ts @@ -1,4 +1,4 @@ -import { ComponentStore } from '@ngrx/component-store'; +import { ComponentStore, tapResponse } from '@ngrx/component-store'; import * as _ from 'lodash'; import { Observable, of, zip } from 'rxjs'; @@ -142,22 +142,25 @@ export class DotTemplateStore extends ComponentStore { switchMap((template: DotTemplateItem) => { this.dotGlobalMessageService.loading(this.dotMessageService.get('publishing')); - return this.dotTemplateService.saveAndPublish(this.cleanTemplateItem(template)); - }), - tap((template: DotTemplate) => { - this.dotGlobalMessageService.success( - this.dotMessageService.get('message.template.published') - ); - this.dotRouterService.allowRouteDeactivation(); - this.updateTemplateState(template); - }), - catchError((err: HttpErrorResponse) => { - this.dotGlobalMessageService.error(err.statusText); - this.dotHttpErrorManagerService.handle(err).subscribe(() => { - this.dotRouterService.allowRouteDeactivation(); - }); - - return of(null); + return this.dotTemplateService + .saveAndPublish(this.cleanTemplateItem(template)) + .pipe( + tapResponse( + (template: DotTemplate) => { + this.dotGlobalMessageService.success( + this.dotMessageService.get('message.template.published') + ); + this.dotRouterService.allowRouteDeactivation(); + this.updateTemplateState(template); + }, + (err: HttpErrorResponse) => { + this.dotGlobalMessageService.error(err.statusText); + this.dotHttpErrorManagerService.handle(err).subscribe(() => { + this.dotRouterService.allowRouteDeactivation(); + }); + } + ) + ); }) ); }); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/utils/gridstack-utils.ts b/core-web/libs/template-builder/src/lib/components/template-builder/utils/gridstack-utils.ts index d0d76f2bcc7c..4a6843ca95bb 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/utils/gridstack-utils.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/utils/gridstack-utils.ts @@ -121,7 +121,7 @@ export function parseFromDotObjectToGridStack( body: DotLayoutBody | undefined ): DotGridStackWidget[] { if (!body || !body.rows?.length) { - return EMPTY_ROWS_VALUE; + return structuredClone(EMPTY_ROWS_VALUE); } return body.rows.map((row, i) => ({ From 3e02dbd654351ce14baf1ad037a3c57c216b75e4 Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Tue, 29 Aug 2023 14:19:00 -0300 Subject: [PATCH 3/6] Fix #25926: Template Builder fixing wrong classes deletion (#25928) * dev (add style classes module): refactor to support remove any class * fix (template builder story): console error due to missing import --- .../add-style-classes-dialog.component.html | 2 +- ...add-style-classes-dialog.component.spec.ts | 6 ++-- .../add-style-classes-dialog.component.ts | 4 +-- .../add-style-classes-dialog.store.spec.ts | 30 ++++++++++------ .../store/add-style-classes-dialog.store.ts | 35 +++++++++++++------ .../template-builder/models/models.ts | 1 + .../template-builder.component.stories.ts | 4 ++- 7 files changed, 54 insertions(+), 28 deletions(-) diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html index 3307322a48f3..007c68c281e5 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html @@ -9,7 +9,7 @@ [size]="446" [tabindex]="1" (onSelect)="onSelect($event)" - (onUnselect)="onUnselect()" + (onUnselect)="onUnselect($event)" (completeMethod)="filterClasses($event)" (onKeyUp)="onKeyUp($event)" dataKey="cssClass" diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts index 75510ce9c3bb..332d1a36f5fa 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts @@ -110,16 +110,16 @@ describe('AddStyleClassesDialogComponent', () => { expect(addClassMock).toHaveBeenCalled(); }); - it('should trigger removeLastClass when autocomplete emits onUnselect', () => { + it('should trigger removeClass when autocomplete emits onUnselect', () => { const autoComplete = spectator.query('p-autocomplete'); - const removeLastClassMock = jest.spyOn(store, 'removeLastClass'); + const removeClass = jest.spyOn(store, 'removeClass'); spectator.dispatchFakeEvent(autoComplete, 'onUnselect'); spectator.detectChanges(); - expect(removeLastClassMock).toHaveBeenCalled(); + expect(removeClass).toHaveBeenCalled(); }); it('should trigger saveClass when clicking on update-btn', () => { diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts index a5bac9cd0aa3..f06ae5b19ad4 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts @@ -100,8 +100,8 @@ export class AddStyleClassesDialogComponent implements OnInit, AfterViewInit, On * * @memberof AddStyleClassesDialogComponent */ - onUnselect(): void { - this.store.removeLastClass(); + onUnselect(deletedClass: StyleClassModel): void { + this.store.removeClass(deletedClass); } /** diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts index 0174784c3347..1b79198adcd0 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts @@ -7,6 +7,10 @@ import { DotAddStyleClassesDialogStore } from './add-style-classes-dialog.store' import { MOCK_STYLE_CLASSES_FILE } from '../../../utils/mocks'; +jest.mock('uuid', () => ({ + v4: () => 'test-id' +})); + describe('DotAddStyleClassesDialogStore', () => { let service: DotAddStyleClassesDialogStore; let httpTestingController: HttpTestingController; @@ -25,7 +29,10 @@ describe('DotAddStyleClassesDialogStore', () => { it('should have selected style classes on init when passed classes', (done) => { service.init({ selectedClasses: ['class1', 'class2'] }); service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'class1' }, { cssClass: 'class2' }]); + expect(state.selectedClasses).toEqual([ + { cssClass: 'class1', id: 'test-id' }, + { cssClass: 'class2', id: 'test-id' } + ]); done(); }); }); @@ -40,7 +47,7 @@ describe('DotAddStyleClassesDialogStore', () => { it('should add a class to selected ', (done) => { service.init({ selectedClasses: [] }); - service.addClass({ cssClass: 'class1' }); + service.addClass({ cssClass: 'class1', id: 'test-id' }); service.state$.subscribe((state) => { expect(state.selectedClasses.length).toBe(1); @@ -48,13 +55,16 @@ describe('DotAddStyleClassesDialogStore', () => { }); }); - it('should remove last class from selected ', (done) => { - service.init({ selectedClasses: ['class1', 'class2'] }); + it('should remove a class from selected that have the same id', (done) => { + service.init({ selectedClasses: ['class1'] }); - service.removeLastClass(); + service.removeClass({ + cssClass: 'class1', + id: 'test-id' + }); service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'class1' }]); + expect(state.selectedClasses).toEqual([]); done(); }); }); @@ -66,7 +76,7 @@ describe('DotAddStyleClassesDialogStore', () => { req.flush(MOCK_STYLE_CLASSES_FILE); service.state$.subscribe((state) => { expect(state.styleClasses).toEqual( - MOCK_STYLE_CLASSES_FILE.classes.map((cssClass) => ({ cssClass })) + MOCK_STYLE_CLASSES_FILE.classes.map((cssClass) => ({ cssClass, id: 'test-id' })) ); done(); }); @@ -106,7 +116,7 @@ describe('DotAddStyleClassesDialogStore', () => { service.filterClasses(query); service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'align' }]); + expect(state.selectedClasses).toEqual([{ cssClass: 'align', id: 'test-id' }]); done(); }); }); @@ -117,7 +127,7 @@ describe('DotAddStyleClassesDialogStore', () => { service.filterClasses(query); service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'align' }]); + expect(state.selectedClasses).toEqual([{ cssClass: 'align', id: 'test-id' }]); done(); }); }); @@ -128,7 +138,7 @@ describe('DotAddStyleClassesDialogStore', () => { service.filterClasses(query); service.state$.subscribe((state) => { - expect(state.filteredClasses).toEqual([{ cssClass: query }]); + expect(state.filteredClasses).toEqual([{ cssClass: query, id: 'test-id' }]); done(); }); }); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts index 72e4f98b4d5d..66330b88774f 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts @@ -1,5 +1,6 @@ import { ComponentStore, tapResponse } from '@ngrx/component-store'; import { Observable } from 'rxjs'; +import { v4 as uuid } from 'uuid'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @@ -7,7 +8,6 @@ import { Injectable } from '@angular/core'; import { switchMap } from 'rxjs/operators'; import { DotAddStyleClassesDialogState, StyleClassModel } from '../../../models/models'; - export const STYLE_CLASSES_FILE_URL = '/application/templates/classes.json'; const COMMA_SPACES_REGEX = /(,|\s)(.*)/; @@ -34,9 +34,7 @@ export class DotAddStyleClassesDialogStore extends ComponentStore { return { ...state, - selectedClasses: selectedClasses.map((cssClass) => ({ - cssClass - })) + selectedClasses: selectedClasses.map((cssClass) => this.buildStyleClass(cssClass)) }; }); @@ -55,9 +53,9 @@ export class DotAddStyleClassesDialogStore extends ComponentStore { this.patchState({ - styleClasses: classes.map((cssClass) => ({ - cssClass - })) + styleClasses: classes.map((cssClass) => + this.buildStyleClass(cssClass) + ) }); }, // Here is the error, if it fails for any reason I just fill the state with an empty array @@ -92,7 +90,7 @@ export class DotAddStyleClassesDialogStore extends ComponentStore { + readonly removeClass = this.updater((state, { id }: StyleClassModel) => { return { ...state, - selectedClasses: state.selectedClasses.slice(0, -1) + selectedClasses: state.selectedClasses.filter((classObj) => classObj.id !== id) }; }); @@ -185,7 +183,7 @@ export class DotAddStyleClassesDialogStore extends ComponentStore cssClass === classObj.cssClass); } + + /** + * @description This method builds a StyleClassModel + * + * @private + * @param {string} cssClass + * @return {*} {StyleClassModel} + * @memberof DotAddStyleClassesDialogStore + */ + private buildStyleClass(cssClass: string): StyleClassModel { + return { + cssClass, + id: uuid() + }; + } } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts b/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts index 2e691a9460cd..e21c83827127 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts @@ -117,6 +117,7 @@ export interface DotTemplateLayoutProperties { */ export interface StyleClassModel { cssClass: string; + id: string; } /** diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts index 9c7639e5953a..63204f077512 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts @@ -3,6 +3,7 @@ import { of } from 'rxjs'; import { AsyncPipe, NgClass, NgFor, NgIf } from '@angular/common'; import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ButtonModule } from 'primeng/button'; @@ -54,7 +55,8 @@ export default { ButtonModule, ToolbarModule, DividerModule, - DropdownModule + DropdownModule, + FormsModule ], providers: [ DotTemplateBuilderStore, From fb9371877de69ec38cce26bd1ab1bf432c56e58e Mon Sep 17 00:00:00 2001 From: Freddy Montes <751424+fmontes@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:48:04 -0600 Subject: [PATCH 4/6] Fix #25926 Reimplement the autocomplete component (#25935) * Reimplement the autocomplete component * dev (add styles autocomplete): enhance functionality * Fix filtering and styling. * Fix the filtering and language messages * Add tests for JsonClassesService * Test are not running * fix (add styles dialog): now tests are running * Fix tests * Update mocks --------- Co-authored-by: Jalinson Diaz --- .prettierrc | 1 + .../components/form/_autocomplete.scss | 10 +- .../add-style-classes-dialog.component.html | 74 ++-- .../add-style-classes-dialog.component.scss | 41 ++- ...add-style-classes-dialog.component.spec.ts | 335 +++++++++++++----- ...-style-classes-dialog.component.stories.ts | 4 +- .../add-style-classes-dialog.component.ts | 120 +++---- .../services/json-classes.service.spec.ts | 38 ++ .../services/json-classes.service.ts | 15 + .../add-style-classes-dialog.store.spec.ts | 164 --------- .../store/add-style-classes-dialog.store.ts | 233 ------------ .../template-builder-row.component.spec.ts | 4 +- .../template-builder/models/models.ts | 23 -- .../template-builder.component.spec.ts | 2 - .../template-builder.component.stories.ts | 4 +- .../template-builder/utils/mocks.ts | 7 +- .../src/lib/template-builder.module.ts | 8 +- .../WEB-INF/messages/Language.properties | 3 + 18 files changed, 437 insertions(+), 649 deletions(-) create mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.spec.ts create mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts delete mode 100644 core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts diff --git a/.prettierrc b/.prettierrc index a7fecf74b1ff..50f70c5eab84 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,6 +6,7 @@ "trailingComma": "none", "jsxBracketSameLine": false, "arrowParens": "always", + "bracketSameLine": true, "overrides": [ { "files": "*.scss", diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss index f99d3c8cc79c..98a3a8893978 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_autocomplete.scss @@ -3,7 +3,6 @@ .p-autocomplete { @extend #form-field-extend; - min-height: $field-height-md; height: auto; padding-right: 0; @@ -38,6 +37,8 @@ align-items: center; max-height: 13rem; // 208px overflow: auto; // Make it scrollable + gap: $spacing-0; + padding: 7px 0 6px; // Specific padding for the tokens in multiple lines &:hover, &:active, @@ -45,23 +46,18 @@ border: none; } - &:has(> .p-autocomplete-token) { - padding: $spacing-1 0; - } - .p-autocomplete-token { @extend #field-chip; } .p-autocomplete-input-token { - padding: 0.375rem 0; - input { font-family: $font-default; font-size: $font-size-md; color: $black; padding: 0; margin: 0; + height: 1.5rem; } } } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html index 007c68c281e5..ad3b6d2e2645 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.html @@ -1,30 +1,44 @@ - -
-
- - -
-
-
- - {{ 'dot.template.builder.classes.dialog.update.button' | dm }} -
-
+
+ + + +
    +
  • + + {{ 'dot.template.builder.autocomplete.has.suggestions' | dm }} +
  • +
+
+ + +
    +
  • + + {{ 'dot.template.builder.autocomplete.no.suggestions' | dm }} +
  • +
  • + + +
  • +
+
+
+
+ + {{ 'dot.template.builder.classes.dialog.update.button' | dm }} +
diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.scss b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.scss index 063265b1ecb3..362a035f264a 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.scss +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.scss @@ -1,30 +1,37 @@ @use "variables" as *; + :host { width: 33.875rem; display: block; } -.dialog__auto-complete-container { - .auto-complete-container__label { - margin: 0; - color: $black; - } -} - .dialog__actions-container { display: flex; justify-content: end; - padding: 1.25rem $spacing-5; } -::ng-deep { - .p-autocomplete.p-autocomplete-multiple - .p-autocomplete-multiple-container - .p-autocomplete-token { - margin-right: 0; - } +p-autoComplete { + margin-bottom: $spacing-3; + display: block; +} + +ul { + color: $color-palette-gray-700; + display: flex; + flex-direction: column; + font-size: $font-size-sm; + gap: $spacing-1; + list-style: none; + margin: 0; + padding: 0; +} + +li { + display: flex; + align-items: center; + gap: $spacing-0; +} - .p-autocomplete-multiple-container { - gap: 0.25rem; - } +.pi { + color: $color-palette-gray; } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts index 332d1a36f5fa..d024ae5e1dbc 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.spec.ts @@ -1,147 +1,298 @@ -import { byTestId, createHostFactory, SpectatorHost } from '@ngneat/spectator'; -import { of } from 'rxjs'; +import { expect, it } from '@jest/globals'; +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; +import { of, throwError } from 'rxjs'; -import { AsyncPipe, NgIf } from '@angular/common'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { NgIf, AsyncPipe } from '@angular/common'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { FormsModule } from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { AutoCompleteModule } from 'primeng/autocomplete'; +import { AutoComplete, AutoCompleteModule } from 'primeng/autocomplete'; import { ButtonModule } from 'primeng/button'; -import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { DynamicDialogConfig, DynamicDialogRef, DynamicDialogModule } from 'primeng/dynamicdialog'; import { DotMessageService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; +import { MockDotMessageService } from '@dotcms/utils-testing'; import { AddStyleClassesDialogComponent } from './add-style-classes-dialog.component'; -import { DotAddStyleClassesDialogStore } from './store/add-style-classes-dialog.store'; - -import { - CLASS_NAME_MOCK, - DOT_MESSAGE_SERVICE_TB_MOCK, - MOCK_SELECTED_STYLE_CLASSES, - MOCK_STYLE_CLASSES_FILE, - mockMatchMedia -} from '../../utils/mocks'; +import { JsonClassesService } from './services/json-classes.service'; + +const DOT_MESSAGES = { + 'dot.template.builder.autocomplete.has.suggestions': 'has suggestions', + 'dot.template.builder.autocomplete.no.suggestions': 'no suggestions', + 'dot.template.builder.autocomplete.setup.suggestions': 'setup suggestions', + 'dot.template.builder.classes.dialog.update.button': 'update button' +}; + +const providers = [ + { + provide: DotMessageService, + useValue: new MockDotMessageService(DOT_MESSAGES) + }, + { + provide: DynamicDialogRef, + useValue: { + close: jest.fn() + } + } +]; describe('AddStyleClassesDialogComponent', () => { - let spectator: SpectatorHost; - let input: HTMLInputElement; - let ref: DynamicDialogRef; - let store: DotAddStyleClassesDialogStore; + let spectator: Spectator; + let service: JsonClassesService; + let dialogRef: DynamicDialogRef; + let autocomplete: AutoComplete; - const createHost = createHostFactory({ - component: AddStyleClassesDialogComponent, + const createComponent = createComponentFactory({ imports: [ AutoCompleteModule, + HttpClientTestingModule, + DynamicDialogModule, FormsModule, ButtonModule, DotMessagePipe, NgIf, - AsyncPipe, - HttpClientModule, - NoopAnimationsModule + AsyncPipe ], - providers: [ - { - provide: DynamicDialogConfig, - useValue: { - data: { - selectedClasses: MOCK_SELECTED_STYLE_CLASSES - } - } - }, - { - provide: HttpClient, - useValue: { - get: () => of(MOCK_STYLE_CLASSES_FILE) - } - }, - { - provide: DotMessageService, - useValue: DOT_MESSAGE_SERVICE_TB_MOCK - }, - DotAddStyleClassesDialogStore, - DynamicDialogRef - ] + component: AddStyleClassesDialogComponent, + providers: [JsonClassesService, DynamicDialogRef, DynamicDialogConfig, DotMessageService], + detectChanges: false }); - beforeEach(() => { - spectator = createHost( - '' - ); + describe('with classes', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + ...providers, + { + provide: DynamicDialogConfig, + useValue: { + data: { + selectedClasses: ['backend-class'] + } + } + }, + { + provide: JsonClassesService, + useValue: { + getClasses() { + return of({ classes: ['class1', 'class2'] }); + } + } + } + ] + }); + + service = spectator.inject(JsonClassesService); + dialogRef = spectator.inject(DynamicDialogRef); + autocomplete = spectator.query(AutoComplete); + }); - ref = spectator.inject(DynamicDialogRef); - store = spectator.inject(DotAddStyleClassesDialogStore); + it('should set attributes to autocomplete', () => { + spectator.detectChanges(); + expect(autocomplete.unique).toBe(true); + expect(autocomplete.autofocus).toBe(true); + expect(autocomplete.multiple).toBe(true); + expect(autocomplete.size).toBe(446); + expect(autocomplete.inputId).toBe('auto-complete-input'); + expect(autocomplete.appendTo).toBe('body'); + expect(autocomplete.dropdown).toBe(true); + expect(autocomplete.el.nativeElement.className).toContain('p-fluid'); + expect(autocomplete.suggestions).toBe(null); + }); - spectator.detectChanges(); + it('should call jsonClassesService.getClasses on init', () => { + const getClassesMock = jest.spyOn(service, 'getClasses'); + spectator.detectChanges(); - input = spectator.query('#auto-complete-input'); + expect(getClassesMock).toHaveBeenCalledTimes(1); + }); - mockMatchMedia(); - }); + it('should set classes property on init', () => { + spectator.detectChanges(); - it('should have an update button', () => { - expect(spectator.query(byTestId('update-btn'))).toBeTruthy(); - }); + expect(spectator.component.classes).toEqual(['class1', 'class2']); + }); - it('should trigger filterClasses when focusing on the input', () => { - const filterMock = jest.spyOn(store, 'filterClasses'); - const query = CLASS_NAME_MOCK; + it('should initialize selectedClasses from DynamicDialogConfig data', () => { + spectator.detectChanges(); - input.value = query; + expect(spectator.component.selectedClasses).toEqual(['backend-class']); + }); - spectator.click(input); + it('should filter suggestions and pass to autocomplete on completeMethod', () => { + spectator.detectChanges(); + spectator.triggerEventHandler(AutoComplete, 'completeMethod', { query: 'class1' }); - spectator.detectChanges(); + expect(autocomplete.suggestions).toEqual(['class1']); + }); - expect(filterMock).toHaveBeenCalledWith(query); - }); + it('should add class on keyup.enter', () => { + const selectItemSpy = jest.spyOn(autocomplete, 'selectItem'); + spectator.detectChanges(); + + const input = document.createElement('input'); + input.value = 'class1'; - it('should trigger addClass when autocomplete emits onSelect', () => { - const autoComplete = spectator.query('p-autocomplete'); + spectator.triggerEventHandler(AutoComplete, 'onKeyUp', { key: 'Enter', target: input }); - const addClassMock = jest.spyOn(store, 'addClass'); + expect(selectItemSpy).toBeCalledWith('class1'); + }); - spectator.dispatchFakeEvent(autoComplete, 'onSelect'); + it('should save selected classes and close the dialog', () => { + spectator.component.selectedClasses = ['class1']; + spectator.component.save(); + spectator.detectChanges(); - spectator.detectChanges(); + expect(dialogRef.close).toHaveBeenCalledWith(['class1']); + }); - expect(addClassMock).toHaveBeenCalled(); + it('should have help message', () => { + spectator.detectChanges(); + const list = spectator.query(byTestId('list')); + + expect(list.textContent).toContain('has suggestions'); + }); }); - it('should trigger removeClass when autocomplete emits onUnselect', () => { - const autoComplete = spectator.query('p-autocomplete'); + describe('no classes', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + ...providers, + { + provide: DynamicDialogConfig, + useValue: { + data: { + selectedClasses: [] + } + } + }, + { + provide: JsonClassesService, + useValue: { + getClasses() { + return of({ classes: [] }); + } + } + } + ] + }); + + service = spectator.inject(JsonClassesService); + dialogRef = spectator.inject(DynamicDialogRef); + autocomplete = spectator.query(AutoComplete); + }); - const removeClass = jest.spyOn(store, 'removeClass'); + it('should set dropdown to false in autocomplete', () => { + spectator.detectChanges(); + expect(autocomplete.dropdown).toBe(false); + }); - spectator.dispatchFakeEvent(autoComplete, 'onUnselect'); + it('should set component.classes empty', () => { + spectator.detectChanges(); - spectator.detectChanges(); + expect(spectator.component.classes).toEqual([]); + }); - expect(removeClass).toHaveBeenCalled(); + it('should have multiples help message', () => { + spectator.detectChanges(); + const list = spectator.query(byTestId('list')); + + expect(list.textContent).toContain('no suggestions setup suggestions'); + }); }); - it('should trigger saveClass when clicking on update-btn', () => { - const closeMock = jest.spyOn(ref, 'close'); + describe('bad format json', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + ...providers, + { + provide: DynamicDialogConfig, + useValue: { + data: { + selectedClasses: [] + } + } + }, + { + provide: JsonClassesService, + useValue: { + getClasses() { + return of({ badFormat: ['class1'] }); + } + } + } + ] + }); + + service = spectator.inject(JsonClassesService); + dialogRef = spectator.inject(DynamicDialogRef); + autocomplete = spectator.query(AutoComplete); + }); + + it('should set dropdown to false in autocomplete', () => { + spectator.detectChanges(); + expect(autocomplete.dropdown).toBe(false); + }); - const updateBtn = spectator.query(byTestId('update-btn')); + it('should set component.classes empty', () => { + spectator.detectChanges(); - spectator.dispatchFakeEvent(updateBtn, 'onClick'); + expect(spectator.component.classes).toEqual([]); + }); - spectator.detectChanges(); + it('should have multiples help message', () => { + spectator.detectChanges(); + const list = spectator.query(byTestId('list')); - expect(closeMock).toHaveBeenCalled(); + expect(list.textContent).toContain('no suggestions setup suggestions'); + }); }); - it('should trigger addClass when enter is pressed', () => { - const addClassMock = jest.spyOn(store, 'addClass'); + describe('error', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + ...providers, + { + provide: DynamicDialogConfig, + useValue: { + data: { + selectedClasses: [] + } + } + }, + { + provide: JsonClassesService, + useValue: { + getClasses() { + return throwError( + new Error('An error occurred while fetching classes') + ); + } + } + } + ] + }); - spectator.typeInElement(CLASS_NAME_MOCK, input); - spectator.keyboard.pressEnter(input); + service = spectator.inject(JsonClassesService); + dialogRef = spectator.inject(DynamicDialogRef); + autocomplete = spectator.query(AutoComplete); + }); - spectator.detectChanges(); + it('should set dropdown to false in autocomplete', () => { + spectator.detectChanges(); + expect(autocomplete.dropdown).toBe(false); + }); - expect(addClassMock).toHaveBeenCalledWith({ cssClass: CLASS_NAME_MOCK }); + it('should set component.classes empty', () => { + spectator.detectChanges(); + + expect(spectator.component.classes).toEqual([]); + }); }); + + // More tests can be added as needed... }); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts index 6788a95f1183..919af74942fe 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.stories.ts @@ -14,7 +14,7 @@ import { DotMessageService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; import { AddStyleClassesDialogComponent } from './add-style-classes-dialog.component'; -import { DotAddStyleClassesDialogStore } from './store/add-style-classes-dialog.store'; +import { JsonClassesService } from './services/json-classes.service'; import { DOT_MESSAGE_SERVICE_TB_MOCK, @@ -57,7 +57,7 @@ export default { useValue: DOT_MESSAGE_SERVICE_TB_MOCK }, DynamicDialogRef, - DotAddStyleClassesDialogStore + JsonClassesService ] }) ] diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts index f06ae5b19ad4..47bd706b52c4 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/add-style-classes-dialog.component.ts @@ -1,25 +1,18 @@ -import { Subject } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { AsyncPipe, NgIf } from '@angular/common'; -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - OnDestroy, - OnInit, - ViewChild -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AutoComplete, AutoCompleteModule } from 'primeng/autocomplete'; import { ButtonModule } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { DotMessagePipe } from '@dotcms/ui'; +import { catchError, map, shareReplay, tap } from 'rxjs/operators'; -import { DotAddStyleClassesDialogStore } from './store/add-style-classes-dialog.store'; +import { DotMessagePipe } from '@dotcms/ui'; -import { StyleClassModel } from '../../models/models'; +import { JsonClassesService } from './services/json-classes.service'; @Component({ selector: 'dotcms-add-style-classes-dialog', @@ -27,92 +20,87 @@ import { StyleClassModel } from '../../models/models'; imports: [AutoCompleteModule, FormsModule, ButtonModule, DotMessagePipe, NgIf, AsyncPipe], templateUrl: './add-style-classes-dialog.component.html', styleUrls: ['./add-style-classes-dialog.component.scss'], + providers: [JsonClassesService], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AddStyleClassesDialogComponent implements OnInit, AfterViewInit, OnDestroy { - public vm$ = this.store.vm$; - @ViewChild(AutoComplete) - autoComplete: AutoComplete; - private autoCompleteInput: HTMLInputElement; - private destroy$: Subject = new Subject(); +export class AddStyleClassesDialogComponent implements OnInit { + @ViewChild(AutoComplete) autoComplete: AutoComplete; + filteredSuggestions = null; + selectedClasses: string[] = []; + + isJsonClasses$: Observable; + classes: string[]; constructor( - private ref: DynamicDialogRef, - private store: DotAddStyleClassesDialogStore, + private jsonClassesService: JsonClassesService, public dynamicDialogConfig: DynamicDialogConfig<{ selectedClasses: string[]; - }> + }>, + private ref: DynamicDialogRef ) {} ngOnInit() { const { selectedClasses } = this.dynamicDialogConfig.data; - - this.store.init({ selectedClasses }); - - this.store.fetchStyleClasses(); - } - - ngAfterViewInit() { - this.autoCompleteInput = document.getElementById('auto-complete-input') as HTMLInputElement; - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + this.selectedClasses = selectedClasses; + + this.isJsonClasses$ = this.jsonClassesService.getClasses().pipe( + tap(({ classes }) => { + if (classes?.length) { + this.classes = classes; + } else { + this.classes = []; + } + }), + map(({ classes }) => { + return !!classes?.length; + }), + catchError(() => { + this.classes = []; + + return of(false); + }), + shareReplay(1) + ); } /** - * @description Filter the classes based on the query + * Filter the suggestions based on the query * * @param {{ query: string }} { query } + * @return {*} * @memberof AddStyleClassesDialogComponent */ - filterClasses({ query }: { query: string }) { - this.store.filterClasses(query); - - // To reset the input when the user types a comma or space - if (query.includes(',') || query.includes(' ')) { - this.autoCompleteInput.value = ''; - } - } + filterClasses({ query }: { query: string }): void { + /* + https://github.com/primefaces/primeng/blob/master/src/app/components/autocomplete/autocomplete.ts#L739 - /** - * @description Closes the dialog and returns the selected classes - * - * @memberof AddStyleClassesDialogComponent - */ - saveClass(selectedClasses: StyleClassModel[]): void { - this.ref.close(selectedClasses.map((styleClass) => styleClass.cssClass)); - } + Sadly we need to pass suggestions all the time, even if they are empty because on the set is where the primeng remove the loading icon + */ - /** - * @description Selects a class and adds it to the selected classes - * - * @param {StyleClassModel} newClass - * @memberof AddStyleClassesDialogComponent - */ - onSelect(newClass: StyleClassModel): void { - this.store.addClass(newClass); + // PrimeNG autocomplete doesn't support async pipe in the suggestions + this.filteredSuggestions = this.classes.filter((item) => item.includes(query)); } /** - * @description Removes the last class from the selected classes + * Save the selected classes * * @memberof AddStyleClassesDialogComponent */ - onUnselect(deletedClass: StyleClassModel): void { - this.store.removeClass(deletedClass); + save() { + this.ref.close(this.selectedClasses); } /** - * @description Used to listen for enter presses + * Remove a class from the selected classes * * @param {KeyboardEvent} event * @memberof AddStyleClassesDialogComponent */ - onKeyUp(event: KeyboardEvent): void { - if (event.key === 'Enter' && this.autoCompleteInput.value) { - this.autoComplete.selectItem({ cssClass: this.autoCompleteInput.value }); + onKeyUp(event: KeyboardEvent) { + const target: HTMLInputElement = event.target as unknown as HTMLInputElement; + + if (event.key === 'Enter' && !!target.value) { + this.autoComplete.selectItem(target.value); } } } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.spec.ts new file mode 100644 index 000000000000..7f9b685d157f --- /dev/null +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed, getTestBed } from '@angular/core/testing'; + +import { JsonClassesService, STYLE_CLASSES_FILE_URL } from './json-classes.service'; + +describe('JsonClassesService', () => { + let injector: TestBed; + let service: JsonClassesService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [JsonClassesService] + }); + + injector = getTestBed(); + service = injector.inject(JsonClassesService); + httpMock = injector.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should return an Observable with classes', () => { + const expectedClasses = { classes: ['class1', 'class2', 'class3'] }; + // httpClientSpy.get.and.returnValue(of(expectedClasses)); + + service.getClasses().subscribe((classes) => { + expect(classes).toEqual(expectedClasses); + }); + + const req = httpMock.expectOne(STYLE_CLASSES_FILE_URL); + expect(req.request.method).toBe('GET'); + req.flush(expectedClasses); + }); +}); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.ts new file mode 100644 index 000000000000..eb0323d271f5 --- /dev/null +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/services/json-classes.service.ts @@ -0,0 +1,15 @@ +import { Observable } from 'rxjs'; + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +export const STYLE_CLASSES_FILE_URL = '/application/templates/classes.json'; + +@Injectable() +export class JsonClassesService { + constructor(private http: HttpClient) {} + + getClasses(): Observable<{ classes: string[] }> { + return this.http.get<{ classes: string[] }>(STYLE_CLASSES_FILE_URL); + } +} diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts deleted file mode 100644 index 1b79198adcd0..000000000000 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { expect, describe } from '@jest/globals'; - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { TestBed } from '@angular/core/testing'; - -import { DotAddStyleClassesDialogStore } from './add-style-classes-dialog.store'; - -import { MOCK_STYLE_CLASSES_FILE } from '../../../utils/mocks'; - -jest.mock('uuid', () => ({ - v4: () => 'test-id' -})); - -describe('DotAddStyleClassesDialogStore', () => { - let service: DotAddStyleClassesDialogStore; - let httpTestingController: HttpTestingController; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [DotAddStyleClassesDialogStore] - }); - service = TestBed.inject(DotAddStyleClassesDialogStore); - httpTestingController = TestBed.inject(HttpTestingController); - }); - - afterEach(() => httpTestingController.verify()); - - it('should have selected style classes on init when passed classes', (done) => { - service.init({ selectedClasses: ['class1', 'class2'] }); - service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([ - { cssClass: 'class1', id: 'test-id' }, - { cssClass: 'class2', id: 'test-id' } - ]); - done(); - }); - }); - - it('should not have selected style classes on init', (done) => { - service.state$.subscribe((state) => { - expect(state.selectedClasses.length).toBe(0); - done(); - }); - }); - - it('should add a class to selected ', (done) => { - service.init({ selectedClasses: [] }); - - service.addClass({ cssClass: 'class1', id: 'test-id' }); - - service.state$.subscribe((state) => { - expect(state.selectedClasses.length).toBe(1); - done(); - }); - }); - - it('should remove a class from selected that have the same id', (done) => { - service.init({ selectedClasses: ['class1'] }); - - service.removeClass({ - cssClass: 'class1', - id: 'test-id' - }); - - service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([]); - done(); - }); - }); - - it('should fetch style classes file', (done) => { - service.fetchStyleClasses(); - const req = httpTestingController.expectOne('/application/templates/classes.json'); - expect(req.request.method).toEqual('GET'); - req.flush(MOCK_STYLE_CLASSES_FILE); - service.state$.subscribe((state) => { - expect(state.styleClasses).toEqual( - MOCK_STYLE_CLASSES_FILE.classes.map((cssClass) => ({ cssClass, id: 'test-id' })) - ); - done(); - }); - }); - - it('should set styleClasses to empty array if fetch style classes fails', (done) => { - service.fetchStyleClasses(); - const req = httpTestingController.expectOne('/application/templates/classes.json'); - expect(req.request.method).toEqual('GET'); - req.flush("This file doesn't exist", { status: 404, statusText: 'Not Found' }); - service.state$.subscribe((state) => { - expect(state.styleClasses).toEqual([]); - done(); - }); - }); - - it('should filter style classes by a query', (done) => { - const query = 'align'; - - service.fetchStyleClasses(); - const req = httpTestingController.expectOne('/application/templates/classes.json'); - req.flush(MOCK_STYLE_CLASSES_FILE); - - service.filterClasses(query); - - service.state$.subscribe((state) => { - expect( - state.filteredClasses.every(({ cssClass }) => cssClass.startsWith(query)) - ).toBeTruthy(); - done(); - }); - }); - - it('should add class to selectedClasses when found a comma on query', (done) => { - const query = 'align,'; - - service.filterClasses(query); - - service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'align', id: 'test-id' }]); - done(); - }); - }); - - it('should add class to selectedClasses when found a comma on query', (done) => { - const query = 'align '; - - service.filterClasses(query); - - service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'align', id: 'test-id' }]); - done(); - }); - }); - - it('should show query on filtered is nothing is found', (done) => { - const query = 'align-custom-class'; - - service.filterClasses(query); - - service.state$.subscribe((state) => { - expect(state.filteredClasses).toEqual([{ cssClass: query, id: 'test-id' }]); - done(); - }); - }); - - it('should filter selected classes from filteredClass', (done) => { - const query = 'd-'; - const selectedClasses = ['d-flex']; - service.init({ selectedClasses }); - - service.fetchStyleClasses(); - const req = httpTestingController.expectOne('/application/templates/classes.json'); - req.flush(MOCK_STYLE_CLASSES_FILE); - - service.filterClasses(query); - - service.state$.subscribe((state) => { - expect( - state.filteredClasses.find(({ cssClass }) => cssClass == selectedClasses[0]) - ).toBeFalsy(); - done(); - }); - }); -}); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts deleted file mode 100644 index 66330b88774f..000000000000 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { Observable } from 'rxjs'; -import { v4 as uuid } from 'uuid'; - -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import { switchMap } from 'rxjs/operators'; - -import { DotAddStyleClassesDialogState, StyleClassModel } from '../../../models/models'; -export const STYLE_CLASSES_FILE_URL = '/application/templates/classes.json'; - -const COMMA_SPACES_REGEX = /(,|\s)(.*)/; - -/** - * - * - * @export - * @class DotAddStyleClassesDialogStore - * @extends {ComponentStore} - */ -@Injectable() -export class DotAddStyleClassesDialogStore extends ComponentStore { - public vm$ = this.select((state) => ({ - filteredClasses: state.filteredClasses, - selectedClasses: state.selectedClasses, - styleClasses: state.styleClasses - })); - - constructor(private http: HttpClient) { - super({ styleClasses: [], selectedClasses: [], filteredClasses: [] }); - } - - readonly init = this.updater((state, { selectedClasses }: { selectedClasses: string[] }) => { - return { - ...state, - selectedClasses: selectedClasses.map((cssClass) => this.buildStyleClass(cssClass)) - }; - }); - - // Effects - /** - * @description This effect fetchs the style classes from the file only once - * - * @memberof DotAddStyleClassesDialogStore - */ - readonly fetchStyleClasses = this.effect((trigger$) => { - return trigger$.pipe( - switchMap(() => - this.getStyleClassesFromFile().pipe( - // This operator is used to handle the error - tapResponse( - // 200 response - ({ classes = [] }: { classes: string[] }) => { - this.patchState({ - styleClasses: classes.map((cssClass) => - this.buildStyleClass(cssClass) - ) - }); - }, - // Here is the error, if it fails for any reason I just fill the state with an empty array - (_) => { - this.patchState({ styleClasses: [] }); - } - ) - ) - ) - ); - }); - - // Updaters - - /** - * @description Filters the classes based on the query - * - * @param { query: string } { query } - * @return {*} - * @memberof DotAddStyleClassesDialogStore - */ - readonly filterClasses = this.updater((state, query: string) => { - const { styleClasses, selectedClasses } = state; - const queryIsNotEmpty = query.trim().length > 0; - - // To select the text if it has "," or space - const queryContainsDelimiter = query.includes(',') || query.includes(' '); - - if (queryIsNotEmpty && queryContainsDelimiter) - return { - ...state, - filteredClasses: [], // I need to reset the filter, because I'm doing the selection manually - selectedClasses: [ - ...state.selectedClasses, - this.buildStyleClass(query.replace(COMMA_SPACES_REGEX, '')) - ] - }; - - return { - ...state, - filteredClasses: this.getFilteredClasses({ - query, - queryIsNotEmpty, - styleClasses, - selectedClasses - }) - }; - }); - - /** - * @description This method removes the last class from the selected classes - * - * @memberof DotAddStyleClassesDialogStore - */ - readonly removeClass = this.updater((state, { id }: StyleClassModel) => { - return { - ...state, - selectedClasses: state.selectedClasses.filter((classObj) => classObj.id !== id) - }; - }); - - /** - * @description This method adds a class to the selected classes - * - * @memberof DotAddStyleClassesDialogStore - */ - readonly addClass = this.updater((state, classToAdd: StyleClassModel) => { - return { - ...state, - selectedClasses: [...state.selectedClasses, classToAdd] - }; - }); - - // Util methods - - /** - * @description This method fetchs the style classes from "/application/templates/classes.json" - * - * @return {*} {Observable} - * @memberof DotAddStyleClassesDialogStore - */ - private getStyleClassesFromFile(): Observable { - return this.http.get(STYLE_CLASSES_FILE_URL); - } - - /** - * @description This method filters the classes based on the query and if the class is already selected - * - * @private - * @param {{ - * query: string; - * queryIsNotEmpty: boolean; - * styleClasses: StyleClassModel[]; - * selectedClasses: StyleClassModel[]; - * }} { - * query, - * queryIsNotEmpty, - * styleClasses, - * selectedClasses - * } - * @return {*} {StyleClassModel[]} - * @memberof DotAddStyleClassesDialogStore - */ - private getFilteredClasses({ - query, - queryIsNotEmpty, - styleClasses, - selectedClasses - }: { - query: string; - queryIsNotEmpty: boolean; - styleClasses: StyleClassModel[]; - selectedClasses: StyleClassModel[]; - }): StyleClassModel[] { - const filtered: StyleClassModel[] = []; - - styleClasses.forEach((classObj) => { - if ( - this.classMatchesQuery(query, classObj) && - !this.classAlreadySelected(classObj, selectedClasses) - ) { - filtered.push(classObj); - } - }); - - // If no classes were found and query is not empty, create a new class based on the query - if (queryIsNotEmpty && filtered.length === 0) { - filtered.push(this.buildStyleClass(query.trim())); - } - - return filtered; - } - - /** - * Checks if a class matches the query - * - * @param {string} query - * @param {StyleClassModel} classObj - * @return {boolean} - */ - private classMatchesQuery(query: string, classObj: StyleClassModel): boolean { - const queryLowerCased = query.toLowerCase(); - const cssClassLowerCased = classObj.cssClass.toLowerCase(); - - return cssClassLowerCased.startsWith(queryLowerCased); - } - - /** - * Checks if a class is already selected - * - * @param {StyleClassModel} classObj - * @return {boolean} - */ - private classAlreadySelected( - classObj: StyleClassModel, - selectedClasses: StyleClassModel[] - ): boolean { - return selectedClasses.some(({ cssClass }) => cssClass === classObj.cssClass); - } - - /** - * @description This method builds a StyleClassModel - * - * @private - * @param {string} cssClass - * @return {*} {StyleClassModel} - * @memberof DotAddStyleClassesDialogStore - */ - private buildStyleClass(cssClass: string): StyleClassModel { - return { - cssClass, - id: uuid() - }; - } -} diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-row/template-builder-row.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-row/template-builder-row.component.spec.ts index 405b94d4ab05..fd964828dc0b 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-row/template-builder-row.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/components/template-builder-row/template-builder-row.component.spec.ts @@ -17,7 +17,6 @@ import { TemplateBuilderRowComponent } from './template-builder-row.component'; import { DotTemplateBuilderStore } from '../../store/template-builder.store'; import { DOT_MESSAGE_SERVICE_TB_MOCK } from '../../utils/mocks'; -import { DotAddStyleClassesDialogStore } from '../add-style-classes-dialog/store/add-style-classes-dialog.store'; import { RemoveConfirmDialogComponent } from '../remove-confirm-dialog/remove-confirm-dialog.component'; import { TemplateBuilderBackgroundColumnsComponent } from '../template-builder-background-columns/template-builder-background-columns.component'; @@ -59,8 +58,7 @@ describe('TemplateBuilderRowComponent', () => { useValue: DOT_MESSAGE_SERVICE_TB_MOCK }, DotTemplateBuilderStore, - DialogService, - DotAddStyleClassesDialogStore + DialogService ], teardown: { destroyAfterEach: false } }).compileComponents(); diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts b/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts index e21c83827127..dee56e5641bf 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/models/models.ts @@ -71,18 +71,6 @@ export interface DotTemplateBuilderState { resizingRowID: string; } -/** - * @description This is the model for the DotAddStyleClassesDialogStore - * - * @export - * @interface DotAddStyleClassesDialogState - */ -export interface DotAddStyleClassesDialogState { - styleClasses: StyleClassModel[]; - selectedClasses: StyleClassModel[]; - filteredClasses: StyleClassModel[]; -} - export type WidgetType = 'col' | 'row'; /** @@ -109,17 +97,6 @@ export interface DotTemplateLayoutProperties { sidebar: DotLayoutSideBar; } -/** - * @description This it the model for Autocomplete StyleClasses - * - * @export - * @interface StyleClassModel - */ -export interface StyleClassModel { - cssClass: string; - id: string; -} - /** * @description This it the model for the Scroll Direction of the GridStack * diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts index 5291b04951ea..91aa315bd2b1 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.spec.ts @@ -15,7 +15,6 @@ import { DotContainersService, DotMessageService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; import { containersMock, DotContainersServiceMock } from '@dotcms/utils-testing'; -import { DotAddStyleClassesDialogStore } from './components/add-style-classes-dialog/store/add-style-classes-dialog.store'; import { TemplateBuilderComponentsModule } from './components/template-builder-components.module'; import { DotGridStackWidget, SCROLL_DIRECTION } from './models/models'; import { DotTemplateBuilderStore } from './store/template-builder.store'; @@ -71,7 +70,6 @@ describe('TemplateBuilderComponent', () => { DotTemplateBuilderStore, DialogService, DynamicDialogRef, - DotAddStyleClassesDialogStore, { provide: DotMessageService, useValue: DOT_MESSAGE_SERVICE_TB_MOCK diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts index 63204f077512..977820d5883b 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.stories.ts @@ -26,7 +26,7 @@ import { SiteServiceMock } from '@dotcms/utils-testing'; -import { DotAddStyleClassesDialogStore } from './components/add-style-classes-dialog/store/add-style-classes-dialog.store'; +import { JsonClassesService } from './components/add-style-classes-dialog/services/json-classes.service'; import { TemplateBuilderComponentsModule } from './components/template-builder-components.module'; import { DotTemplateBuilderStore } from './store/template-builder.store'; import { TemplateBuilderComponent } from './template-builder.component'; @@ -62,7 +62,7 @@ export default { DotTemplateBuilderStore, DialogService, DynamicDialogRef, - DotAddStyleClassesDialogStore, + JsonClassesService, { provide: DotMessageService, useValue: DOT_MESSAGE_SERVICE_TB_MOCK diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/utils/mocks.ts b/core-web/libs/template-builder/src/lib/components/template-builder/utils/mocks.ts index f7e488d7b925..ad30d80e76e0 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/utils/mocks.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/utils/mocks.ts @@ -288,7 +288,12 @@ export const MESSAGES_MOCK = { 'editpage.layout.theme.search': 'Search', 'dot.template.builder.classes.dialog.update.button': 'Update', 'dot.template.builder.sidebar.header.title': 'Sidebar', - 'dot.template.builder.row.box.wont.fit': 'Minimum 1 column needed for box drop.' + 'dot.template.builder.row.box.wont.fit': 'Minimum 1 column needed for box drop.', + 'dot.template.builder.autocomplete.has.suggestions': + 'Type and hit enter or select from suggestions to add a class', + 'dot.template.builder.autocomplete.no.suggestions': 'Type and hit enter to add a class', + 'dot.template.builder.autocomplete.setup.suggestions': + 'You can set up predefined class suggestions. Get the setup guide' }; export const DOT_MESSAGE_SERVICE_TB_MOCK = new MockDotMessageService(MESSAGES_MOCK); diff --git a/core-web/libs/template-builder/src/lib/template-builder.module.ts b/core-web/libs/template-builder/src/lib/template-builder.module.ts index ad5b40e65770..d9a28db37755 100644 --- a/core-web/libs/template-builder/src/lib/template-builder.module.ts +++ b/core-web/libs/template-builder/src/lib/template-builder.module.ts @@ -8,7 +8,6 @@ import { ToolbarModule } from 'primeng/toolbar'; import { DotContainersService } from '@dotcms/data-access'; import { DotMessagePipe } from '@dotcms/ui'; -import { DotAddStyleClassesDialogStore } from './components/template-builder/components/add-style-classes-dialog/store/add-style-classes-dialog.store'; import { DotLayoutPropertiesComponent } from './components/template-builder/components/dot-layout-properties/dot-layout-properties.component'; import { TemplateBuilderComponentsModule } from './components/template-builder/components/template-builder-components.module'; import { TemplateBuilderComponent } from './components/template-builder/template-builder.component'; @@ -27,12 +26,7 @@ import { TemplateBuilderComponent } from './components/template-builder/template TemplateBuilderComponentsModule ], declarations: [TemplateBuilderComponent], - providers: [ - DialogService, - DynamicDialogRef, - DotAddStyleClassesDialogStore, - DotContainersService - ], + providers: [DialogService, DynamicDialogRef, DotContainersService], exports: [TemplateBuilderComponent, DotLayoutPropertiesComponent] }) export class TemplateBuilderModule {} diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index ab7a9e5ae867..1643654c1c7e 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1192,6 +1192,9 @@ dot.template.builder.toolbar.button.layout.label=Layout dot.template.builder.toolbar.button.theme.label=Theme dot.template.builder.sidebar.header.title=Sidebar dot.template.builder.row.box.wont.fit=Minimum 1 column needed for box drop. +dot.template.builder.autocomplete.has.suggestions=Type and hit enter or select from suggestions to add a class +dot.template.builder.autocomplete.no.suggestions=Type and hit enter to add a class +dot.template.builder.autocomplete.setup.suggestions=You can set up predefined class suggestions. Get the setup guide dotCMS-Enterprise-comes-with-an-advanced-Image-Editor-tool=Advanced Image Tools is a dotCMS Enterprise only feature. dotCMS-Image-Clipboard=dotCMS Image Clipboard dotCMS-is-dedicated-to-quality-assurance=dotCMS is dedicated to delivering high quality and extremely reliable enterprise software, and we make extensive efforts to perform thorough quality assurance and resolve all issues before release. However software is complex, and if you feel you have found a problem with dotCMS, we would very much like you to report it so we can resolve it in a future release; please click the link below to report it. From a4fec3b273419929f6c517c9e943b5e72114dc16 Mon Sep 17 00:00:00 2001 From: Rafael Velazco Date: Thu, 31 Aug 2023 14:00:00 -0400 Subject: [PATCH 5/6] Fix #25926 Templater Builder: Fixing link target --- dotCMS/src/main/webapp/WEB-INF/messages/Language.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 1643654c1c7e..814d6d6865e6 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1194,7 +1194,7 @@ dot.template.builder.sidebar.header.title=Sidebar dot.template.builder.row.box.wont.fit=Minimum 1 column needed for box drop. dot.template.builder.autocomplete.has.suggestions=Type and hit enter or select from suggestions to add a class dot.template.builder.autocomplete.no.suggestions=Type and hit enter to add a class -dot.template.builder.autocomplete.setup.suggestions=You can set up predefined class suggestions. Get the setup guide +dot.template.builder.autocomplete.setup.suggestions=You can set up predefined class suggestions. Get the setup guide dotCMS-Enterprise-comes-with-an-advanced-Image-Editor-tool=Advanced Image Tools is a dotCMS Enterprise only feature. dotCMS-Image-Clipboard=dotCMS Image Clipboard dotCMS-is-dedicated-to-quality-assurance=dotCMS is dedicated to delivering high quality and extremely reliable enterprise software, and we make extensive efforts to perform thorough quality assurance and resolve all issues before release. However software is complex, and if you feel you have found a problem with dotCMS, we would very much like you to report it so we can resolve it in a future release; please click the link below to report it. From 75b9826a3548f078521b286d2129e2ef1efdd5da Mon Sep 17 00:00:00 2001 From: Manuel Rojas Date: Mon, 28 Aug 2023 09:30:16 -0600 Subject: [PATCH 6/6] Fixing test (#25911) --- .../dot-edit-page-state-controller-seo.component.spec.ts | 2 +- .../libs/utils-testing/src/lib/dot-page-state.service.mock.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts index 46e8b59fd3c4..8ae9bfa5a245 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts @@ -489,7 +489,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { expect(dotPageStateService.setSeoMedia).toHaveBeenCalledWith('Google'); }); - it('should call changeSeoMedia event', async () => { + it('should call selected event', async () => { spyOn(dotPageStateService, 'setDevice'); const dotSelector = de.query(By.css('[data-testId="dot-device-selector"]')); const event = { diff --git a/core-web/libs/utils-testing/src/lib/dot-page-state.service.mock.ts b/core-web/libs/utils-testing/src/lib/dot-page-state.service.mock.ts index 965bb9c21b75..d7794bf9fbb4 100644 --- a/core-web/libs/utils-testing/src/lib/dot-page-state.service.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-page-state.service.mock.ts @@ -32,4 +32,8 @@ export class DotPageStateServiceMock { setPersona(_persona: DotPersona): void { /* */ } + + setSeoMedia(_seoMedia: string): void { + /* */ + } }