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/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-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/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/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/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 3307322a48f3..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 75510ce9c3bb..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 removeLastClass 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 removeLastClassMock = jest.spyOn(store, 'removeLastClass'); + 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(removeLastClassMock).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 a5bac9cd0aa3..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(): void { - this.store.removeLastClass(); + 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 0174784c3347..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,154 +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'; - -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' }, { cssClass: 'class2' }]); - 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' }); - - service.state$.subscribe((state) => { - expect(state.selectedClasses.length).toBe(1); - done(); - }); - }); - - it('should remove last class from selected ', (done) => { - service.init({ selectedClasses: ['class1', 'class2'] }); - - service.removeLastClass(); - - service.state$.subscribe((state) => { - expect(state.selectedClasses).toEqual([{ cssClass: 'class1' }]); - 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 })) - ); - 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' }]); - 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' }]); - 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 }]); - 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 72e4f98b4d5d..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,220 +0,0 @@ -import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { Observable } from 'rxjs'; - -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) => ({ - 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) => ({ - 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, - { cssClass: 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 removeLastClass = this.updater((state) => { - return { - ...state, - selectedClasses: state.selectedClasses.slice(0, -1) - }; - }); - - /** - * @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({ cssClass: 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); - } -} 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/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 2e691a9460cd..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,16 +97,6 @@ export interface DotTemplateLayoutProperties { sidebar: DotLayoutSideBar; } -/** - * @description This it the model for Autocomplete StyleClasses - * - * @export - * @interface StyleClassModel - */ -export interface StyleClassModel { - cssClass: 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/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/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 9c7639e5953a..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 @@ -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'; @@ -25,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'; @@ -54,13 +55,14 @@ export default { ButtonModule, ToolbarModule, DividerModule, - DropdownModule + DropdownModule, + FormsModule ], providers: [ 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/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) => ({ 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/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 }), 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 { + /* */ + } } diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index ab7a9e5ae867..814d6d6865e6 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.