diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.spec.ts deleted file mode 100644 index 727fda72771f..000000000000 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.spec.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { DotRouterService } from '@dotcms/data-access'; -import { CONTAINER_SOURCE, DotLayoutBody } from '@dotcms/dotcms-models'; -import { dotContainerMapMock, mockDotContainers, processedContainers } from '@dotcms/utils-testing'; -import { - DotContainerColumnBox, - DotLayoutGrid, - DotLayoutGridBox -} from '@models/dot-edit-layout-designer'; -import { DotTemplateContainersCacheService } from '@services/dot-template-containers-cache/dot-template-containers-cache.service'; - -import { DotEditLayoutService } from './dot-edit-layout.service'; - -describe('DotEditLayoutService', () => { - const containers = dotContainerMapMock(); - - let dotEditLayoutService: DotEditLayoutService; - let templateContainersCacheService: DotTemplateContainersCacheService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DotEditLayoutService, DotTemplateContainersCacheService, DotRouterService] - }); - dotEditLayoutService = TestBed.inject(DotEditLayoutService); - templateContainersCacheService = TestBed.inject(DotTemplateContainersCacheService); - - templateContainersCacheService.set(containers); - }); - - it('should transform the data from the service to the grid format ', () => { - const dotLayoutBody: DotLayoutBody = { - rows: [ - { - columns: [ - { - containers: [ - { - identifier: '/container/path', - uuid: '1' - } - ], - leftOffset: 1, - width: 8 - }, - { - containers: [ - { - identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', - uuid: '2' - } - ], - leftOffset: 9, - width: 4 - } - ] - }, - { - columns: [ - { - containers: [ - { - identifier: 'd71d56b4-0a8b-4bb2-be15-ffa5a23366ea', - uuid: '3' - }, - { - identifier: 'a6e9652b-8183-4c09-b775-26196b09a300', - uuid: '4' - }, - { - identifier: 'UNKNOWN ID', - uuid: 'UNKNOWN' - } - ], - leftOffset: 1, - width: 3 - }, - { - containers: [ - { - identifier: '6a12bbda-0ae2-4121-a98b-ad8069eaff3a', - uuid: '5' - } - ], - leftOffset: 4, - width: 3 - } - ] - } - ] - }; - - const grid: DotLayoutGrid = dotEditLayoutService.getDotLayoutGridBox(dotLayoutBody); - - expect(grid.boxes.length).toEqual(4); - expect(grid.boxes[2].containers.length).toEqual(2); - expect(grid.boxes[3].config).toEqual({ - col: 4, - row: 2, - sizex: 3, - fixed: true, - maxCols: 12, - maxRows: 1, - payload: { - containers: [{ identifier: '6a12bbda-0ae2-4121-a98b-ad8069eaff3a', uuid: '5' }], - leftOffset: 4, - width: 3 - } - }); - expect(grid.boxes[0].containers.length).toEqual(1, 'map FILE type containers'); - }); - - it('should transform the grid data to LayoutBody to export the data', () => { - const gridBoxes: DotLayoutGridBox[] = [ - { - containers: [ - { - container: { - type: 'containers', - identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc3', - name: 'Large Column (lg-1)', - categoryId: 'dde0b865-6cea-4ff0-8582-85e5974cf94f', - source: CONTAINER_SOURCE.FILE, - path: 'container/path', - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '2' - } - ], - config: { - fixed: true, - sizex: 8, - maxCols: 12, - maxRows: 1, - col: 1, - row: 1, - sizey: 1, - dragHandle: null, - resizeHandle: null, - draggable: true, - resizable: true, - borderSize: 25, - payload: { - styleClass: 'test_column_class' - } - } - }, - { - containers: [ - { - container: { - type: 'containers', - identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', - name: 'Medium Column (md-1)', - categoryId: '9ab97328-e72f-4d7e-8be6-232f53218a93', - source: CONTAINER_SOURCE.DB, - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '1' - }, - { - container: { - type: 'containers', - identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc3', - name: 'Large Column (lg-1)', - categoryId: 'dde0b865-6cea-4ff0-8582-85e5974cf94f', - source: CONTAINER_SOURCE.FILE, - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '2' - }, - { - container: null, - uuid: '1234' - } - ], - config: { - fixed: true, - sizex: 4, - maxCols: 12, - maxRows: 1, - col: 9, - row: 1, - sizey: 1, - dragHandle: null, - resizeHandle: null, - draggable: true, - resizable: true, - borderSize: 25, - payload: { - styleClass: '' - } - } - } - ]; - const grid: DotLayoutGrid = new DotLayoutGrid(gridBoxes, ['test_row_class']); - const layoutBody: DotLayoutBody = dotEditLayoutService.getDotLayoutBody(grid); - - expect(layoutBody.rows.length).toEqual(1); - - expect(layoutBody.rows[0].styleClass).toEqual('test_row_class'); - expect(layoutBody.rows[0].columns[0].styleClass).toEqual('test_column_class'); - expect(layoutBody.rows[0].columns[1].styleClass).toEqual(''); - expect(layoutBody.rows[0].columns.length).toEqual(2, 'create two columns'); - expect(layoutBody.rows[0].columns[1].containers.length).toEqual(2, 'create two containers'); - expect(layoutBody.rows[0].columns[1].leftOffset).toEqual(9, 'set leftOffset to 9'); - expect(layoutBody.rows[0].columns[1].width).toEqual( - 4, - 'set container box to 4 in the second column' - ); - }); - - it('should transform the Sidebar data to ContainerColumnBox (ignore UNKNOWN ids) to export the data', () => { - const mockContainers = mockDotContainers(); - const rawContainers = [ - { - identifier: mockContainers[Object.keys(mockContainers)[0]].container.identifier, - uuid: '1234567890' - }, - { - identifier: mockContainers[Object.keys(mockContainers)[1]].container.path, - uuid: '1234567891' - }, - { - identifier: 'UNKNOWN ID', - uuid: 'INVALID' - } - ]; - const containerColumnBox: DotContainerColumnBox[] = - dotEditLayoutService.getDotLayoutSidebar(rawContainers); - delete containerColumnBox[0].uuid; - delete containerColumnBox[1].uuid; - - expect(containerColumnBox).toEqual(processedContainers); - }); - - it('should emit add box event', (done) => { - dotEditLayoutService.getBoxes().subscribe((box: boolean) => { - expect(box).toBe(true); - done(); - }); - - dotEditLayoutService.addBox(); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.ts deleted file mode 100644 index 14ada734a4d4..000000000000 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-edit-layout/dot-edit-layout.service.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Observable, Subject } from 'rxjs'; - -import { Injectable } from '@angular/core'; - -import { - DotLayoutBody, - DotLayoutColumn, - DotLayoutRow, - DotPageContainer -} from '@dotcms/dotcms-models'; -import { DotTemplateContainersCacheService } from '@services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { - DotContainerColumnBox, - DotLayoutGrid, - DotLayoutGridBox, - DotLayoutGridRow -} from '@shared/models/dot-edit-layout-designer'; - -/** - * Provide methods to transform NgGrid model into PageView model and viceversa. - * - * @class DotEditLayoutService - */ -@Injectable({ - providedIn: 'root' -}) -export class DotEditLayoutService { - private _addGridBox: Subject = new Subject(); - - constructor(private templateContainersCacheService: DotTemplateContainersCacheService) {} - - /** - * Take an DotPageView and return an array of DotLayoutGridBox - * - * @param DotLayoutBody dotLayoutBody - * @returns DotLayoutGridBox[] - */ - getDotLayoutGridBox(dotLayoutBody: DotLayoutBody): DotLayoutGrid { - const grid: DotLayoutGridBox[] = []; - - dotLayoutBody.rows.forEach((row: DotLayoutRow, rowIndex) => { - row.columns.forEach((column: DotLayoutColumn) => { - grid.push({ - containers: this.getDotContainerColumnBoxFromDotPageContainer( - column.containers - ), - config: Object.assign(DotLayoutGrid.getDefaultConfig(), { - sizex: column.width, - col: column.leftOffset, - row: rowIndex + 1, - payload: column - }) - }); - }); - }); - - return new DotLayoutGrid( - grid, - dotLayoutBody.rows.map((row: DotLayoutRow) => row.styleClass) - ); - } - - /** - * Take an array of DotLayoutGridBox and return a DotLayoutBody. - * - * @param DotLayoutGridBox[] grid - * @returns DotLayoutBody - */ - getDotLayoutBody(grid: DotLayoutGrid): DotLayoutBody { - return { - rows: grid - .getRows() - .map((row: DotLayoutGridRow) => this.getLayoutRowFromLayoutGridBoxes(row)) - }; - } - - /** - * Take an array of DotPageContainer and return a DotContainerColumnBox. - * - * @param DotPageContainer[] containers - * @returns DotContainerColumnBox[] - */ - getDotLayoutSidebar(containers: DotPageContainer[]): DotContainerColumnBox[] { - return this.getDotContainerColumnBoxFromDotPageContainer(containers); - } - - /** - * Add box to the grid system - * - * @memberof DotEditLayoutService - */ - addBox(): void { - this._addGridBox.next(true); - } - - /** - * Get notified when a box is added to the grid system - * - * @returns {Observable} - * @memberof DotEditLayoutService - */ - getBoxes(): Observable { - return this._addGridBox.asObservable(); - } - - private getDotContainerColumnBoxFromDotPageContainer( - containers: DotPageContainer[] - ): DotContainerColumnBox[] { - return containers - .filter((dotPageContainer: DotPageContainer) => - this.templateContainersCacheService.get(dotPageContainer.identifier) - ) - .map((dotPageContainer: DotPageContainer) => { - return { - container: this.templateContainersCacheService.get(dotPageContainer.identifier), - uuid: dotPageContainer.uuid ? dotPageContainer.uuid : '' - }; - }); - } - - private getLayoutRowFromLayoutGridBoxes(dotLayoutGridRow: DotLayoutGridRow): DotLayoutRow { - return { - styleClass: dotLayoutGridRow.styleClass, - columns: dotLayoutGridRow.boxes.map(this.getColumn.bind(this)) - }; - } - - private getColumn(layoutGridBox: DotLayoutGridBox): DotLayoutColumn { - return { - styleClass: layoutGridBox.config.payload.styleClass, - leftOffset: layoutGridBox.config.col, - width: layoutGridBox.config.sizex, - containers: layoutGridBox.containers - .filter((dotContainersColumnBox) => dotContainersColumnBox.container) - .map(this.getContainer.bind(this)) - }; - } - - private getContainer(dotContainersColumnBox: DotContainerColumnBox): DotPageContainer { - return { - identifier: this.templateContainersCacheService.getContainerReference( - dotContainersColumnBox.container - ), - uuid: dotContainersColumnBox.uuid - }; - } -} diff --git a/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.spec.ts b/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.spec.ts deleted file mode 100644 index 4da5ef260b8d..000000000000 --- a/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { StringPixels } from './string-pixels-util'; - -describe('StringPixelsUtil', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [StringPixels] - }); - }); - - it('should return max width --> 140px', () => { - const textValues = ['demo text', 'demo longer test', 'the longest text of all times']; - const textWidth = StringPixels.getDropdownWidth(textValues); - expect(textWidth).toBe('140px'); - }); - - it('should return max widht --> 67 (taking 7 as the size of each character)', () => { - const textValues = ['text', 'demo', 'hello']; - const textWidth = StringPixels.getDropdownWidth(textValues); - expect(textWidth).toBe('67px'); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.ts b/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.ts deleted file mode 100644 index 9ac6636bdb6c..000000000000 --- a/core-web/apps/dotcms-ui/src/app/api/util/string-pixels-util.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as _ from 'lodash'; - -import { Injectable } from '@angular/core'; -@Injectable() -export class StringPixels { - private static readonly characterSize = 7; - private static readonly arrowDropdownComponentSize = 32; - - /** - * Returns an estimate of the width in pixels that may have the longer - * text from a collection, based on a character constant - * @param Array textValues The text to be measure. - * - */ - public static getDropdownWidth(textValues: Array): string { - const maxText = _.maxBy(textValues, (text: string) => text.length).length; - const maxWidth = - StringPixels.characterSize * maxText > 108 ? 108 : StringPixels.characterSize * maxText; - - return `${maxWidth + StringPixels.arrowDropdownComponentSize}px`; - } -} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail.component.spec.ts index 4a7051f221bc..ee2fe355e3d0 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-configuration-detail/dot-apps-configuration-detail.component.spec.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { MarkdownModule } from 'ngx-markdown'; import { Observable, of } from 'rxjs'; @@ -303,7 +302,7 @@ describe('DotAppsConfigurationDetailComponent', () => { describe('With dynamic variables', () => { beforeEach(() => { - const sitesDynamic = _.cloneDeep(sites); + const sitesDynamic = structuredClone(sites); sitesDynamic[0].secrets = [ ...sites[0].secrets, { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-list.component.ts index 2530d503cd61..c4bfc13dd499 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/dot-apps-list/dot-apps-list.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { fromEvent as observableFromEvent, Subject } from 'rxjs'; import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; @@ -106,7 +105,7 @@ export class DotAppsListComponent implements OnInit, OnDestroy { private getApps(apps: DotApp[]): void { this.apps = apps; - this.appsCopy = _.cloneDeep(apps); + this.appsCopy = structuredClone(apps); setTimeout(() => { this.attachFilterEvents(); }, 0); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/store/dot-container-properties.store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/store/dot-container-properties.store.ts index 65579d829391..871915a036fb 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/store/dot-container-properties.store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/dot-container-create/dot-container-properties/store/dot-container-properties.store.ts @@ -1,5 +1,4 @@ import { ComponentStore } from '@ngrx/component-store'; -import * as _ from 'lodash'; import { Observable, of, pipe } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; @@ -22,6 +21,7 @@ import { DotContainerPayload, DotContainerStructure } from '@dotcms/dotcms-models'; +import { isEqual } from '@dotcms/utils'; import { DotContainersService } from '@services/dot-containers/dot-containers.service'; export interface DotContainerPropertiesState { @@ -148,7 +148,7 @@ export class DotContainerPropertiesStore extends ComponentStore 0, - invalidForm: _.isEqual(state.originalForm, container) || invalidForm + invalidForm: isEqual(state.originalForm, container) || invalidForm }; }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts index a086333bb336..73a239b6aae9 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as _ from 'lodash'; import { of } from 'rxjs'; import { CommonModule } from '@angular/common'; @@ -78,10 +76,8 @@ const dotVariantDataMock: DotVariantData = { mode: DotPageMode.PREVIEW }; -const pageRenderStateMock: DotPageRenderState = new DotPageRenderState( - mockUser(), - new DotPageRender(mockDotRenderedPage()) -); +const getPageRenderStateMock = () => + new DotPageRenderState(mockUser(), new DotPageRender(mockDotRenderedPage())); @Component({ selector: 'dot-test-host-component', @@ -92,7 +88,7 @@ const pageRenderStateMock: DotPageRenderState = new DotPageRenderState( ` }) class TestHostComponent { - pageState: DotPageRenderState = _.cloneDeep(pageRenderStateMock); + pageState: DotPageRenderState = getPageRenderStateMock(); variant: DotVariantData; } @@ -223,7 +219,7 @@ describe('DotEditPageStateControllerComponent', () => { { ...mockUser(), userId: '456' }, new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; componentHost.variant = null; fixtureHost.detectChanges(); const lockerDe = de.query(By.css('p-inputSwitch')); @@ -242,7 +238,7 @@ describe('DotEditPageStateControllerComponent', () => { it('should have lock info', () => { fixtureHost.detectChanges(); const message = de.query(By.css('[data-testId="lockInfo"]')).componentInstance; - expect(message.pageState).toEqual(pageRenderStateMock); + expect(message.pageState).toEqual(getPageRenderStateMock()); }); }); @@ -430,7 +426,7 @@ describe('DotEditPageStateControllerComponent', () => { new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; }); it('should update pageState service when confirmation dialog Success', async () => { @@ -495,7 +491,7 @@ describe('DotEditPageStateControllerComponent', () => { }) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; spyOn(dialogService, 'confirm').and.callFake((conf) => { conf.accept(); }); @@ -530,7 +526,7 @@ describe('DotEditPageStateControllerComponent', () => { EXPERIMENT_MOCK ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; }); it('should update pageState service when confirmation dialog Success', async () => { @@ -568,7 +564,7 @@ describe('DotEditPageStateControllerComponent', () => { } } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); }); @@ -615,7 +611,7 @@ describe('DotEditPageStateControllerComponent', () => { ...mockDotRenderedPage() } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); await fixtureHost.whenStable(); @@ -632,7 +628,7 @@ describe('DotEditPageStateControllerComponent', () => { } } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); await fixtureHost.whenStable(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout.module.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout.module.ts index 373101933c84..51d0c1e02686 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout.module.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout.module.ts @@ -3,8 +3,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DotGlobalMessageModule } from '@components/_common/dot-global-message/dot-global-message.module'; -import { DotEditLayoutDesignerModule } from '@components/dot-edit-layout-designer/dot-edit-layout-designer.module'; -import { DotShowHideFeatureDirective } from '@dotcms/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive'; import { TemplateBuilderModule } from '@dotcms/template-builder'; import { DotEditLayoutComponent } from './dot-edit-layout/dot-edit-layout.component'; @@ -21,8 +19,6 @@ const routes: Routes = [ imports: [ CommonModule, RouterModule.forChild(routes), - DotEditLayoutDesignerModule, - DotShowHideFeatureDirective, TemplateBuilderModule, DotGlobalMessageModule ], diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.html index 2e11a1e5a066..4919cbd2d1c3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.html @@ -1,26 +1,8 @@ - - - - - - - - - - - - + + + diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts index 8d6e61634bf2..5818bf35e645 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts @@ -1,13 +1,11 @@ -import { of, throwError } from 'rxjs'; +import { of } from 'rxjs'; -import { HttpResponse } from '@angular/common/http'; import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; import { EMPTY_TEMPLATE_DESIGN } from '@dotcms/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store'; import { DotShowHideFeatureDirective } from '@dotcms/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive'; @@ -15,22 +13,15 @@ import { DotHttpErrorManagerService, DotMessageService, DotPageLayoutService, - DotPropertiesService, DotRouterService, DotSessionStorageService, DotGlobalMessageService, DotPageStateService } from '@dotcms/data-access'; -import { DotCMSResponse, HttpCode, ResponseView } from '@dotcms/dotcms-js'; import { DotLayout, DotPageRender, DotTemplateDesigner } from '@dotcms/dotcms-models'; -import { - MockDotMessageService, - mockDotRenderedPage, - mockResponseView, - processedContainers -} from '@dotcms/utils-testing'; +import { MockDotMessageService, mockDotRenderedPage } from '@dotcms/utils-testing'; -import { DEBOUNCE_TIME, DotEditLayoutComponent } from './dot-edit-layout.component'; +import { DotEditLayoutComponent } from './dot-edit-layout.component'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -48,27 +39,6 @@ export class MockTemplateBuilderComponent { templateChange: EventEmitter = new EventEmitter(); } -@Component({ - selector: 'dot-edit-layout-designer', - template: '' -}) -export class MockDotEditLayoutDesignerComponent { - @Input() - layout: DotLayout; - - @Input() - title: string; - - @Input() - theme: string; - - @Input() - apiLink: string; - - @Input() - url: string; -} - const PAGE_STATE = new DotPageRender(mockDotRenderedPage()); let fixture: ComponentFixture; @@ -80,30 +50,14 @@ const messageServiceMock = new MockDotMessageService({ describe('DotEditLayoutComponent', () => { let component: DotEditLayoutComponent; - let layoutDesignerDe: DebugElement; - let layoutDesigner: MockDotEditLayoutDesignerComponent; - let dotPageLayoutService: DotPageLayoutService; - let dotGlobalMessageService: DotGlobalMessageService; - let dotTemplateContainersCacheService: DotTemplateContainersCacheService; - let fakeLayout: DotLayout; - let dotHttpErrorManagerService: DotHttpErrorManagerService; - let dotSessionStorageService: DotSessionStorageService; - let dotPropertiesService: DotPropertiesService; - let dotRouterService: DotRouterService; - let router: Router; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - MockDotEditLayoutDesignerComponent, - DotEditLayoutComponent, - MockTemplateBuilderComponent - ], + declarations: [DotEditLayoutComponent, MockTemplateBuilderComponent], imports: [DotShowHideFeatureDirective, RouterTestingModule], providers: [ RouterTestingModule, DotSessionStorageService, - DotEditLayoutService, DotRouterService, { provide: DotPageStateService, @@ -154,12 +108,6 @@ describe('DotEditLayoutComponent', () => { } } } - }, - { - provide: DotPropertiesService, - useValue: { - getFeatureFlag: () => of(false) - } } ] }); @@ -168,224 +116,8 @@ describe('DotEditLayoutComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DotEditLayoutComponent); component = fixture.componentInstance; - - dotPageLayoutService = TestBed.inject(DotPageLayoutService); - dotGlobalMessageService = TestBed.inject(DotGlobalMessageService); - dotTemplateContainersCacheService = TestBed.inject(DotTemplateContainersCacheService); - dotHttpErrorManagerService = TestBed.inject(DotHttpErrorManagerService); - dotSessionStorageService = TestBed.inject(DotSessionStorageService); - dotPropertiesService = TestBed.inject(DotPropertiesService); - dotRouterService = TestBed.inject(DotRouterService); - router = TestBed.inject(Router); }); - - describe('with data', () => { - beforeEach(() => { - fixture.detectChanges(); - layoutDesignerDe = fixture.debugElement.query(By.css('dot-edit-layout-designer')); - layoutDesigner = layoutDesignerDe.componentInstance; - fakeLayout = { - body: null, - footer: false, - header: true, - sidebar: null, - themeId: '123', - title: 'Title', - width: '100' - }; - }); - - it('should be 100% min-width in the host', () => { - // https://github.com/dotCMS/core/issues/19540 - expect(fixture.debugElement.nativeElement.style.minWidth).toBe('100%'); - }); - - it('should pass attr to the dot-edit-layout-designer', () => { - fixture.detectChanges(); - const state = mockDotRenderedPage(); - expect(layoutDesigner.layout).toEqual(state.layout); - expect(layoutDesigner.theme).toEqual(state.template.theme); - expect(layoutDesigner.title).toEqual(state.page.title); - expect(layoutDesigner.apiLink).toEqual('api/v1/page/render/an/url/test?language_id=1'); - expect(layoutDesigner.url).toEqual(state.page.pageURI); - }); - - describe('save', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('should change pageState', () => { - const res: DotPageRender = new DotPageRender({ - ...mockDotRenderedPage(), - layout: fakeLayout - }); - - spyOn(dotPageLayoutService, 'save').and.returnValue(of(res)); - - layoutDesignerDe.triggerEventHandler('save', { - themeId: '123', - layout: fakeLayout, - title: null - }); - - // The initial value - expect(component.pageState).not.toEqual(PAGE_STATE); - }); - - it('should save the layout', () => { - const res: DotPageRender = new DotPageRender({ - ...mockDotRenderedPage(), - layout: fakeLayout - }); - spyOn(dotPageLayoutService, 'save').and.returnValue(of(res)); - - layoutDesignerDe.triggerEventHandler('save', { - themeId: '123', - layout: fakeLayout, - title: null - }); - - expect(dotGlobalMessageService.loading).toHaveBeenCalledWith('Saving'); - expect(dotGlobalMessageService.success).toHaveBeenCalledWith('Saved'); - expect(dotGlobalMessageService.error).not.toHaveBeenCalled(); - - expect(dotPageLayoutService.save).toHaveBeenCalledWith('123', { - themeId: '123', - layout: fakeLayout, - title: null - }); - expect(dotTemplateContainersCacheService.set).toHaveBeenCalledWith({ - '/default/': processedContainers[0].container, - '/banner/': processedContainers[1].container - }); - - expect(component.pageState).toEqual(res); - }); - - it(`should save the layout after ${DEBOUNCE_TIME}`, fakeAsync(() => { - const res: DotPageRender = new DotPageRender({ - ...mockDotRenderedPage(), - layout: fakeLayout - }); - spyOn(dotPageLayoutService, 'save').and.returnValue(of(res)); - - layoutDesignerDe.triggerEventHandler('updateTemplate', { - themeId: '123', - layout: fakeLayout, - title: null - }); - - tick(DEBOUNCE_TIME); - expect(dotGlobalMessageService.loading).toHaveBeenCalledWith('Saving'); - expect(dotGlobalMessageService.success).toHaveBeenCalledWith('Saved'); - expect(dotGlobalMessageService.error).not.toHaveBeenCalled(); - - expect(dotPageLayoutService.save).toHaveBeenCalledWith('123', { - themeId: '123', - layout: fakeLayout, - title: null - }); - expect(dotTemplateContainersCacheService.set).toHaveBeenCalledWith({ - '/default/': processedContainers[0].container, - '/banner/': processedContainers[1].container - }); - expect(component.pageState).toEqual(res); - })); - - it('should save the layout instantly when closeEditLayout is true', () => { - const res: DotPageRender = new DotPageRender({ - ...mockDotRenderedPage(), - layout: fakeLayout - }); - spyOn(dotPageLayoutService, 'save').and.returnValue(of(res)); - - layoutDesignerDe.triggerEventHandler('updateTemplate', { - themeId: '123', - layout: fakeLayout, - title: null - }); - - dotRouterService.requestPageLeave(); - - expect(dotGlobalMessageService.loading).toHaveBeenCalledWith('Saving'); - expect(dotGlobalMessageService.success).toHaveBeenCalledWith('Saved'); - expect(dotGlobalMessageService.error).not.toHaveBeenCalled(); - - expect(dotPageLayoutService.save).toHaveBeenCalledWith('123', { - themeId: '123', - layout: fakeLayout, - title: null - }); - expect(dotTemplateContainersCacheService.set).toHaveBeenCalledWith({ - '/default/': processedContainers[0].container, - '/banner/': processedContainers[1].container - }); - expect(component.pageState).toEqual(res); - }); - - it('should not save the layout when observable is destroy', fakeAsync(() => { - const res: DotPageRender = new DotPageRender({ - ...mockDotRenderedPage(), - layout: fakeLayout - }); - spyOn(dotPageLayoutService, 'save').and.returnValue(of(res)); - - // Destroy should be true. - component.destroy$.subscribe((value) => expect(value).toBeTruthy()); - - // Trigger the observable - layoutDesignerDe.triggerEventHandler('updateTemplate', fakeLayout); - - // Destroy the observable - component.destroy$.next(true); - component.destroy$.complete(); - tick(DEBOUNCE_TIME); - - expect(dotPageLayoutService.save).not.toHaveBeenCalled(); - })); - - it('should handle error when save fail', (done) => { - spyOn(dotPageLayoutService, 'save').and.returnValue( - throwError( - new ResponseView( - new HttpResponse>( - mockResponseView(HttpCode.BAD_REQUEST) - ) - ) - ) - ); - - layoutDesignerDe.triggerEventHandler('save', fakeLayout); - expect(dotGlobalMessageService.error).toHaveBeenCalledWith('Unknown Error'); - expect(dotHttpErrorManagerService.handle).toHaveBeenCalledTimes(1); - dotRouterService.canDeactivateRoute$.subscribe((resp) => { - expect(resp).toBeTruthy(); - done(); - }); - }); - - it('should remove variant key from session storage on destoy', () => { - spyOn(dotSessionStorageService, 'removeVariantId'); - component.ngOnDestroy(); - expect(dotSessionStorageService.removeVariantId).toHaveBeenCalledTimes(1); - }); - - it('should keep variant key from session storage if going to Edit content portlet', () => { - router.routerState.snapshot.url = '/edit-page/content'; - spyOn(dotSessionStorageService, 'removeVariantId'); - component.ngOnDestroy(); - expect(dotSessionStorageService.removeVariantId).toHaveBeenCalledTimes(0); - }); - }); - }); - describe('New Template Builder', () => { - beforeEach(() => { - spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); - fixture.detectChanges(); - }); - it('should show new template builder component', () => { fixture.detectChanges(); const component: DebugElement = fixture.debugElement.query( 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 a8307f416ff4..6eebc18be85c 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 @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as _ from 'lodash'; import { of } from 'rxjs'; import { CommonModule, DecimalPipe } from '@angular/common'; @@ -86,10 +85,8 @@ export const dotVariantDataMock: DotVariantData = { mode: DotPageMode.PREVIEW }; -const pageRenderStateMock: DotPageRenderState = new DotPageRenderState( - mockUser(), - new DotPageRender(mockDotRenderedPage()) -); +const getPageRenderStateMock = () => + new DotPageRenderState(mockUser(), new DotPageRender(mockDotRenderedPage())); @Component({ selector: 'dot-test-host-component', @@ -100,7 +97,7 @@ const pageRenderStateMock: DotPageRenderState = new DotPageRenderState( ` }) class TestHostComponent { - pageState: DotPageRenderState = _.cloneDeep(pageRenderStateMock); + pageState: DotPageRenderState = getPageRenderStateMock(); variant: DotVariantData; } @@ -231,7 +228,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { { ...mockUser(), userId: '456' }, new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; componentHost.variant = null; fixtureHost.detectChanges(); const lockerDe = de.query(By.css('p-inputSwitch')); @@ -255,7 +252,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { { ...mockUser(), userId: '456' }, new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; componentHost.variant = null; componentHost.pageState.page.locked = true; fixtureHost.detectChanges(); @@ -273,7 +270,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { { ...mockUser(), userId: '456' }, new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; componentHost.variant = null; componentHost.pageState.state.locked = false; fixtureHost.detectChanges(); @@ -289,7 +286,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { it('should have lock info', () => { fixtureHost.detectChanges(); const message = de.query(By.css('[data-testId="lockInfo"]')).componentInstance; - expect(message.pageState).toEqual(pageRenderStateMock); + expect(message.pageState).toEqual(getPageRenderStateMock()); }); }); @@ -436,7 +433,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { new DotPageRender(mockDotRenderedPage()) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; }); it('should update pageState service when confirmation dialog Success', async () => { @@ -501,7 +498,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { }) ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; spyOn(dialogService, 'confirm').and.callFake((conf) => { conf.accept(); }); @@ -536,7 +533,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { EXPERIMENT_MOCK ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; }); it('should update pageState service when confirmation dialog Success', async () => { @@ -598,7 +595,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { { ...mockUser(), userId: '486' }, mockDotRenderedPage() ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); }); @@ -623,7 +620,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { } } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); }); @@ -676,7 +673,8 @@ describe('DotEditPageStateControllerSeoComponent', () => { ...mockDotRenderedPage() } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); await fixtureHost.whenStable(); @@ -693,7 +691,8 @@ describe('DotEditPageStateControllerSeoComponent', () => { } } ); - fixtureHost.componentInstance.pageState = _.cloneDeep(pageRenderStateMocked); + + fixtureHost.componentInstance.pageState = pageRenderStateMocked; fixtureHost.detectChanges(); await fixtureHost.whenStable(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.html index de518a4b0eac..ee818209d48b 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.html @@ -1,39 +1,31 @@ - + - - - - - - - - - - - - - + + @switch (item.type) { + @case ('advanced') { + + } + @case ('design') { + + + + } + } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts index 2920af40aad3..9b404b0cb7b2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts @@ -1,5 +1,3 @@ -import { of } from 'rxjs'; - import { AfterContentInit, Component, @@ -9,33 +7,24 @@ import { EventEmitter, Input, Output, - TemplateRef, ViewChild } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; +import { PrimeTemplate } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { DotGlobalMessageComponent } from '@components/_common/dot-global-message/dot-global-message.component'; -import { IframeComponent } from '@components/_common/iframe/iframe-component'; import { DotPortletBoxModule } from '@components/dot-portlet-base/components/dot-portlet-box/dot-portlet-box.module'; import { DotShowHideFeatureDirective } from '@dotcms/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive'; -import { - DotEventsService, - DotMessageService, - DotPropertiesService, - DotRouterService -} from '@dotcms/data-access'; +import { DotEventsService, DotMessageService, DotRouterService } from '@dotcms/data-access'; import { DotLayout, DotTemplate, DotTemplateDesigner } from '@dotcms/dotcms-models'; import { DotIconModule, DotMessagePipe } from '@dotcms/ui'; import { MockDotMessageService, MockDotRouterService } from '@dotcms/utils-testing'; -import { - AUTOSAVE_DEBOUNCE_TIME, - DotTemplateBuilderComponent -} from './dot-template-builder.component'; +import { DotTemplateBuilderComponent } from './dot-template-builder.component'; import { DotTemplateItem, @@ -57,26 +46,6 @@ class TemplateBuilderMockComponent { @Output() templateChange: EventEmitter = new EventEmitter(); } -@Component({ - selector: 'dot-edit-layout-designer', - template: `` -}) -class DotEditLayoutDesignerMockComponent { - @Input() theme: string; - - @Input() layout; - - @Input() disablePublish: boolean; - - @Output() cancel: EventEmitter = new EventEmitter(); - - @Output() save: EventEmitter = new EventEmitter(); - - @Output() updateTemplate: EventEmitter = new EventEmitter(); - - @Output() saveAndPublish: EventEmitter = new EventEmitter(); -} - @Component({ selector: 'dot-template-advanced', template: `` @@ -122,12 +91,12 @@ export class TabViewMockComponent { }) export class TabPanelMockComponent implements AfterContentInit { @Input() header: string; - @ContentChild(TemplateRef) container; + @ContentChild(PrimeTemplate) container; contentTemplate; ngAfterContentInit() { - if (this.container.elementRef.nativeElement.textContent === 'container') { - this.contentTemplate = this.container; + if (this.container.name === 'content') { + this.contentTemplate = this.container.template; } } } @@ -151,15 +120,11 @@ describe('DotTemplateBuilderComponent', () => { let component: DotTemplateBuilderComponent; let fixture: ComponentFixture; let de: DebugElement; - let dotTestHostComponent: DotTestHostComponent; - let hostFixture: ComponentFixture; - let dotPropertiesService: DotPropertiesService; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ DotTemplateBuilderComponent, - DotEditLayoutDesignerMockComponent, DotTemplateAdvancedMockComponent, IframeMockComponent, TabViewMockComponent, @@ -184,12 +149,6 @@ describe('DotTemplateBuilderComponent', () => { code: 'Code' }) }, - { - provide: DotPropertiesService, - useValue: { - getFeatureFlag: () => of(false) - } - }, DotEventsService, { provide: DotRouterService, @@ -204,7 +163,6 @@ describe('DotTemplateBuilderComponent', () => { de = fixture.debugElement; component = fixture.componentInstance; - dotPropertiesService = TestBed.inject(DotPropertiesService); spyOn(component.save, 'emit'); spyOn(component.updateTemplate, 'emit'); spyOn(component.cancel, 'emit'); @@ -221,44 +179,10 @@ describe('DotTemplateBuilderComponent', () => { expect(panel.componentInstance.header).toBe('Design'); }); - it('should show dot-edit-layout-designer and pass attr', () => { - const builder = de.query(By.css('dot-edit-layout-designer')).componentInstance; - expect(builder.theme).toBe('123'); - expect(builder.disablePublish).toBe(true); - expect(builder.layout).toEqual({ - header: true, - footer: true, - body: { - rows: [] - }, - sidebar: null, - title: '', - width: null - }); - }); - it('should not show ', () => { const advanced = de.query(By.css('dot-template-advanced')); expect(advanced).toBeNull(); }); - - it('should emit save events from dot-edit-layout-designer', () => { - const builder = de.query(By.css('dot-edit-layout-designer')); - - builder.triggerEventHandler('save', EMPTY_TEMPLATE_DESIGN); - - expect(component.save.emit).toHaveBeenCalledWith(EMPTY_TEMPLATE_DESIGN); - }); - - it('should emit save event from dot-edit-layout-designer automatically on template updates', () => { - fakeAsync(() => { - spyOn(component.save, 'emit'); - const builder = de.query(By.css('dot-edit-layout-designer')); - builder.triggerEventHandler('save', EMPTY_TEMPLATE_DESIGN); - tick(AUTOSAVE_DEBOUNCE_TIME); - expect(component.save.emit).toHaveBeenCalledWith(EMPTY_TEMPLATE_DESIGN); - }); - }); }); describe('New template design', () => { @@ -268,7 +192,6 @@ describe('DotTemplateBuilderComponent', () => { theme: '123', live: true }; - spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); fixture.detectChanges(); }); @@ -321,7 +244,7 @@ describe('DotTemplateBuilderComponent', () => { fixture.detectChanges(); }); - it('should have tab title "Design"', () => { + it('should have tab title "Code"', () => { const panel = de.query(By.css('[data-testId="builder"]')); expect(panel.componentInstance.header).toBe('Code'); }); @@ -332,11 +255,6 @@ describe('DotTemplateBuilderComponent', () => { expect(builder.didTemplateChanged).toBe(false); }); - it('should not show ', () => { - const designer = de.query(By.css('dot-edit-layout-designer')); - expect(designer).toBeNull(); - }); - it('should emit events from dot-template-advanced', () => { const builder = de.query(By.css('dot-template-advanced')); @@ -377,42 +295,6 @@ describe('DotTemplateBuilderComponent', () => { }); }); - it('should reload iframe when changes in the template happens', () => { - hostFixture = TestBed.createComponent(DotTestHostComponent); - dotTestHostComponent = hostFixture.componentInstance; - dotTestHostComponent.item = { - ...EMPTY_TEMPLATE_DESIGN, - theme: '123' - }; - hostFixture.detectChanges(); - const builder = hostFixture.debugElement.query( - By.css('dot-edit-layout-designer') - ).componentInstance; - dotTestHostComponent.builder.historyIframe = { - iframeElement: { - nativeElement: { - contentWindow: { - location: { - reload: jasmine.createSpy('reload') - } - } - } - } - } as IframeComponent; - dotTestHostComponent.item = { - ...EMPTY_TEMPLATE_DESIGN, - theme: 'dotcms-123' - }; - - builder.updateTemplate.emit(dotTestHostComponent.item); - - hostFixture.detectChanges(); - expect( - dotTestHostComponent.builder.historyIframe.iframeElement.nativeElement.contentWindow - .location.reload - ).toHaveBeenCalledTimes(1); - }); - it('should handle custom event', () => { spyOn(component.custom, 'emit'); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts index 518df9f644e6..57bb77614bd5 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts @@ -14,8 +14,7 @@ import { import { debounceTime, takeUntil } from 'rxjs/operators'; import { IframeComponent } from '@components/_common/iframe/iframe-component'; -import { DotPropertiesService, DotRouterService } from '@dotcms/data-access'; -import { FeaturedFlags } from '@dotcms/dotcms-models'; +import { DotRouterService } from '@dotcms/data-access'; import { DotTemplateItem } from '../store/dot-template.store'; @@ -27,7 +26,6 @@ export const AUTOSAVE_DEBOUNCE_TIME = 5000; styleUrls: ['./dot-template-builder.component.scss'] }) export class DotTemplateBuilderComponent implements OnInit, OnDestroy { - readonly #propertiesService = inject(DotPropertiesService); readonly #dotRouterService = inject(DotRouterService); @Input() item: DotTemplateItem; @@ -40,8 +38,6 @@ export class DotTemplateBuilderComponent implements OnInit, OnDestroy { @ViewChild('historyIframe') historyIframe: IframeComponent; permissionsUrl = ''; historyUrl = ''; - readonly featureFlag = FeaturedFlags.FEATURE_FLAG_TEMPLATE_BUILDER; - featureFlagIsOn$ = this.#propertiesService.getFeatureFlag(this.featureFlag); templateUpdate$ = new Subject(); destroy$: Subject = new Subject(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.module.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.module.ts index 9dfb7150f063..c74f8a610ca3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.module.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.module.ts @@ -6,9 +6,7 @@ import { TabViewModule } from 'primeng/tabview'; import { DotGlobalMessageModule } from '@components/_common/dot-global-message/dot-global-message.module'; import { IFrameModule } from '@components/_common/iframe'; -import { DotEditLayoutDesignerModule } from '@components/dot-edit-layout-designer/dot-edit-layout-designer.module'; import { DotPortletBoxModule } from '@components/dot-portlet-base/components/dot-portlet-box/dot-portlet-box.module'; -import { DotShowHideFeatureDirective } from '@dotcms/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive'; import { TemplateBuilderModule } from '@dotcms/template-builder'; import { DotMessagePipe } from '@dotcms/ui'; @@ -19,13 +17,11 @@ import { DotTemplateAdvancedModule } from '../dot-template-advanced/dot-template @NgModule({ imports: [ CommonModule, - DotEditLayoutDesignerModule, DotMessagePipe, DotTemplateAdvancedModule, TabViewModule, IFrameModule, DotPortletBoxModule, - DotShowHideFeatureDirective, TemplateBuilderModule, ButtonModule, DotGlobalMessageModule diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.html index 807c55a9b5b9..537ac56b6218 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.html @@ -12,16 +12,13 @@ - - - - + diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts index e5d05ea61b27..69602c96eca4 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-create-edit.component.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; @@ -6,14 +6,14 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { DialogService } from 'primeng/dynamicdialog'; import { DynamicDialogRef } from 'primeng/dynamicdialog/dynamicdialog-ref'; -import { filter, takeUntil } from 'rxjs/operators'; +import { filter, takeUntil, tap } from 'rxjs/operators'; import { DotMessageService } from '@dotcms/data-access'; import { Site, SiteService } from '@dotcms/dotcms-js'; import { DotLayout, DotTemplate } from '@dotcms/dotcms-models'; import { DotTemplatePropsComponent } from './dot-template-props/dot-template-props.component'; -import { DotTemplateItem, DotTemplateState, DotTemplateStore } from './store/dot-template.store'; +import { DotTemplateItem, DotTemplateStore, VM } from './store/dot-template.store'; @Component({ selector: 'dot-template-create-edit', @@ -24,8 +24,7 @@ import { DotTemplateItem, DotTemplateState, DotTemplateStore } from './store/dot export class DotTemplateCreateEditComponent implements OnInit, OnDestroy { readonly #store = inject(DotTemplateStore); - vm$ = this.#store.vm$; - didTemplateChanged$ = this.#store.didTemplateChanged$; + vm$: Observable; form: UntypedFormGroup; private destroy$: Subject = new Subject(); @@ -38,9 +37,9 @@ export class DotTemplateCreateEditComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.vm$ - .pipe(takeUntil(this.destroy$)) - .subscribe(({ original, working }: DotTemplateState) => { + this.vm$ = this.#store.vm$.pipe( + takeUntil(this.destroy$), + tap(({ original, working }: VM) => { const template = original.type === 'design' ? working : original; if (this.form) { const value = this.getFormValue(template); @@ -53,7 +52,9 @@ export class DotTemplateCreateEditComponent implements OnInit, OnDestroy { if (!template.identifier) { this.createTemplate(); } - }); + }) + ); + this.setSwitchSiteListener(); } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.spec.ts index b13b61ac1374..3df637df3348 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/store/dot-template.store.spec.ts @@ -6,7 +6,6 @@ import { HttpErrorResponse } from '@angular/common/http'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; import { DotTemplatesService } from '@dotcms/app/api/services/dot-templates/dot-templates.service'; import { @@ -73,7 +72,6 @@ const cacheSetSpy = jasmine.createSpy(); const BASIC_PROVIDERS = [ DotTemplateStore, - DotEditLayoutService, { provide: DotHttpErrorManagerService, useValue: { @@ -206,7 +204,8 @@ describe('DotTemplateStore', () => { const state = { original: template, working: template, - apiLink: '' + apiLink: '', + didTemplateChanged: false }; service.vm$.subscribe((res) => { @@ -298,7 +297,8 @@ describe('DotTemplateStore', () => { const state = { original: template, working: template, - apiLink: '/api/v1/templates/2d87af36-a935-4689-b427-dea75e9d84cf/working' + apiLink: '/api/v1/templates/2d87af36-a935-4689-b427-dea75e9d84cf/working', + didTemplateChanged: false }; service.vm$.subscribe((res) => { 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 455934f6166f..1cf906afc0c0 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,5 +1,4 @@ import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import * as _ from 'lodash'; import { Observable, of, zip } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; @@ -18,7 +17,6 @@ import { withLatestFrom } from 'rxjs/operators'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; import { DotTemplatesService } from '@dotcms/app/api/services/dot-templates/dot-templates.service'; import { @@ -28,10 +26,11 @@ import { DotGlobalMessageService } from '@dotcms/data-access'; import { DotContainerMap, DotLayout, DotTemplate } from '@dotcms/dotcms-models'; +import { isEqual } from '@dotcms/utils'; type DotTemplateType = 'design' | 'advanced'; -interface DotTemplateItemDesign { +export interface DotTemplateItemDesign { containers?: DotContainerMap; drawed?: boolean; friendlyName: string; @@ -63,6 +62,10 @@ export interface DotTemplateState { apiLink: string; } +export interface VM extends DotTemplateState { + didTemplateChanged: boolean; +} + const EMPTY_TEMPLATE = { identifier: '', title: '', @@ -97,16 +100,17 @@ export const EMPTY_TEMPLATE_ADVANCED: DotTemplateItemadvanced = { @Injectable() export class DotTemplateStore extends ComponentStore { - readonly vm$ = this.select(({ working, original, apiLink }: DotTemplateState) => { + readonly vm$ = this.select(({ working, original, apiLink }: DotTemplateState): VM => { return { working, original, - apiLink + apiLink, + didTemplateChanged: !isEqual(working, original) }; }); readonly didTemplateChanged$: Observable = this.select( - ({ original, working }: DotTemplateState) => !_.isEqual(original, working) + ({ original, working }: DotTemplateState) => !isEqual(original, working) ); readonly updateBody = this.updater((state: DotTemplateState, body: string) => ({ @@ -265,7 +269,6 @@ export class DotTemplateStore extends ComponentStore { private dotRouterService: DotRouterService, private activatedRoute: ActivatedRoute, private dotHttpErrorManagerService: DotHttpErrorManagerService, - private dotEditLayoutService: DotEditLayoutService, private templateContainersCacheService: DotTemplateContainersCacheService, private dotGlobalMessageService: DotGlobalMessageService, private dotMessageService: DotMessageService diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts index 4d6d91d07638..b370c4b75ce5 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as _ from 'lodash'; import { DragulaModule, DragulaService } from 'ng2-dragula'; import { Observable, of, Subject } from 'rxjs'; @@ -818,9 +817,9 @@ describe('Load fields and drag and drop', () => { fixture.detectChanges(); const fieldMoved = [ - _.cloneDeep(comp.fieldRows[1]), - _.cloneDeep(comp.fieldRows[0]), - _.cloneDeep(comp.fieldRows[2]) + structuredClone(comp.fieldRows[1]), + structuredClone(comp.fieldRows[0]), + structuredClone(comp.fieldRows[2]) ]; comp.saveFields.subscribe((data) => { @@ -852,9 +851,9 @@ describe('Load fields and drag and drop', () => { fixture.detectChanges(); const fieldMoved = [ - _.cloneDeep(comp.fieldRows[1]), - _.cloneDeep(comp.fieldRows[0]), - _.cloneDeep(comp.fieldRows[2]) + structuredClone(comp.fieldRows[1]), + structuredClone(comp.fieldRows[0]), + structuredClone(comp.fieldRows[2]) ]; comp.saveFields.subscribe(() => { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.ts index 39cc2c9f2db1..dbf12005d1fc 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-drop-zone/content-type-fields-drop-zone.component.ts @@ -1,5 +1,4 @@ import autoScroll from 'dom-autoscroller'; -import * as _ from 'lodash'; import { DragulaService } from 'ng2-dragula'; import { Subject } from 'rxjs'; @@ -224,7 +223,7 @@ export class ContentTypeFieldsDropZoneComponent implements OnInit, OnChanges, On ngOnChanges(changes: SimpleChanges): void { if (changes.layout && changes.layout.currentValue) { - this.fieldRows = _.cloneDeep(changes.layout.currentValue); + this.fieldRows = structuredClone(changes.layout.currentValue); } } @@ -369,7 +368,7 @@ export class ContentTypeFieldsDropZoneComponent implements OnInit, OnChanges, On * @memberof ContentTypeFieldsDropZoneComponent */ cancelLastDragAndDrop(): void { - this.fieldRows = _.cloneDeep(this.layout); + this.fieldRows = structuredClone(this.layout); } /** diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/content-type-fields-properties-form.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/content-type-fields-properties-form.component.ts index 4e0050991b4a..7cbbd61d8bef 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/content-type-fields-properties-form.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/content-type-fields-properties-form.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { @@ -17,6 +16,7 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/ import { takeUntil } from 'rxjs/operators'; import { DotCMSContentType, DotCMSContentTypeField } from '@dotcms/dotcms-models'; +import { isEqual } from '@dotcms/utils'; import { FieldPropertyService } from '../service'; @@ -142,7 +142,7 @@ export class ContentTypeFieldsPropertiesFormComponent implements OnChanges, OnIn } private isFormValueUpdated(): boolean { - return !_.isEqual(this.form.value, this.originalValue); + return !isEqual(this.form.value, this.originalValue); } private isPropertyDisabled(property: string): boolean { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-relationships-property.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-relationships-property.component.ts index 39cfd5e98dd2..67e25f9034e2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-relationships-property.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/dot-relationships-property.component.ts @@ -1,5 +1,3 @@ -import * as _ from 'lodash'; - import { Component, OnInit } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; @@ -37,7 +35,7 @@ export class DotRelationshipsPropertyComponent implements OnInit { constructor(private dotMessageService: DotMessageService) {} ngOnInit() { - this.beforeValue = _.cloneDeep(this.group.get(this.property.name).value); + this.beforeValue = structuredClone(this.group.get(this.property.name).value); this.editing = !!this.group.get(this.property.name).value.velocityVar; } @@ -57,7 +55,7 @@ export class DotRelationshipsPropertyComponent implements OnInit { * @memberof DotRelationshipsPropertyComponent */ clean(): void { - this.group.get(this.property.name).setValue(_.cloneDeep(this.beforeValue)); + this.group.get(this.property.name).setValue(structuredClone(this.beforeValue)); } /** diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/services/dot-edit-content-type-cache.service.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/services/dot-edit-content-type-cache.service.ts index 36dc72bbd26f..6412ba5814f6 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/services/dot-edit-content-type-cache.service.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/content-type-fields-properties-form/field-properties/dot-relationships-property/services/dot-edit-content-type-cache.service.ts @@ -1,5 +1,3 @@ -import * as _ from 'lodash'; - import { Injectable } from '@angular/core'; import { DotCMSContentType } from '@dotcms/dotcms-models'; @@ -31,6 +29,6 @@ export class DotEditContentTypeCacheService { * @memberof DotEditContentTypeCacheService */ get(): DotCMSContentType { - return _.cloneDeep(this.currentContentType); + return structuredClone(this.currentContentType); } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-drag-drop.service.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-drag-drop.service.ts index 4aa219886275..b996010d15f0 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-drag-drop.service.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/fields/service/field-drag-drop.service.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { DragulaService } from 'ng2-dragula'; import { merge, Observable } from 'rxjs'; @@ -184,7 +183,7 @@ export class FieldDragDropService { copy: this.shouldCopy.bind(this), accepts: this.shouldAccepts.bind(this), moves: this.shouldMovesField, - copyItem: (item) => _.cloneDeep(item) + copyItem: (item) => structuredClone(item) }); } } @@ -200,7 +199,7 @@ export class FieldDragDropService { copy: this.shouldCopy.bind(this), accepts: this.shouldAccepts.bind(this), moves: this.shouldMoveRow.bind(this), - copyItem: (item) => _.cloneDeep(item) + copyItem: (item) => structuredClone(item) }); } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts index a75b2cccd4fd..6595fdf1eeef 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { Observable, Subject } from 'rxjs'; import { @@ -34,6 +33,7 @@ import { DotCMSWorkflow, FeaturedFlags } from '@dotcms/dotcms-models'; +import { isEqual } from '@dotcms/utils'; import { FieldUtil } from '@dotcms/utils-testing'; /** @@ -269,7 +269,7 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { } private isFormValueUpdated(): boolean { - return !_.isEqual(this.form.value, this.originalValue); + return !isEqual(this.form.value, this.originalValue); } private isNewDateVarFields(newOptions: SelectItem[]): boolean { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts index 1c2a0060a1a2..4dc67ec2d627 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit.component.spec.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-explicit-any */ - -import * as _ from 'lodash'; import { of, throwError } from 'rxjs'; import { Location } from '@angular/common'; @@ -535,7 +533,7 @@ describe('DotContentTypesEditComponent', () => { }); it('should update fields attribute when a field is edit', () => { - const layout: DotCMSContentTypeLayoutRow[] = _.cloneDeep(currentLayoutInServer); + const layout: DotCMSContentTypeLayoutRow[] = structuredClone(currentLayoutInServer); const fieldToUpdate: DotCMSContentTypeField = layout[0].columns[0].fields[0]; fieldToUpdate.name = 'Updated field'; @@ -551,7 +549,7 @@ describe('DotContentTypesEditComponent', () => { }); it('should update fields on dropzone event', () => { - const layout: DotCMSContentTypeLayoutRow[] = _.cloneDeep(currentLayoutInServer); + const layout: DotCMSContentTypeLayoutRow[] = structuredClone(currentLayoutInServer); const fieldToUpdate: DotCMSContentTypeField = layout[0].columns[0].fields[0]; spyOn(fieldService, 'updateField').and.returnValue(of(layout)); @@ -643,7 +641,7 @@ describe('DotContentTypesEditComponent', () => { ]; const fieldsReturnByServer: DotCMSContentTypeLayoutRow[] = - _.cloneDeep(currentLayoutInServer); + structuredClone(currentLayoutInServer); newFieldsAdded.concat(fieldsReturnByServer[0].columns[0].fields); fieldsReturnByServer[0].columns[0].fields = newFieldsAdded; @@ -670,7 +668,7 @@ describe('DotContentTypesEditComponent', () => { } ); - const layout: DotCMSContentTypeLayoutRow[] = _.cloneDeep(currentLayoutInServer); + const layout: DotCMSContentTypeLayoutRow[] = structuredClone(currentLayoutInServer); layout[0].columns[0].fields = fieldsReturnByServer; layout[0].divider.id = new Date().getMilliseconds().toString(); layout[0].columns[0].columnDivider.id = new Date().getMilliseconds().toString(); @@ -730,7 +728,7 @@ describe('DotContentTypesEditComponent', () => { }); it('should remove fields on dropzone event', () => { - const layout: DotCMSContentTypeLayoutRow[] = _.cloneDeep(currentLayoutInServer); + const layout: DotCMSContentTypeLayoutRow[] = structuredClone(currentLayoutInServer); layout[0].columns[0].fields = layout[0].columns[0].fields.slice(-1); spyOn(fieldService, 'deleteFields').and.returnValue(of({ fields: layout })); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.spec.ts index 489cc08f9bb3..9a71238354a3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; +import { Observable, of, throwError as observableThrowError } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, DebugElement, EventEmitter, Injectable, Input, Output } from '@angular/core'; @@ -82,14 +82,14 @@ class MockDotBaseTypeSelectorComponent { @Injectable() class MockDotLicenseService { isEnterprise(): Observable { - return observableOf(true); + return of(true); } } @Injectable() class MockDotHttpErrorManagerService { handle(_err: ResponseView): Observable { - return observableOf({ + return of({ redirected: false, status: HttpCode.BAD_REQUEST }); @@ -193,7 +193,7 @@ describe('DotContentTypesPortletComponent', () => { ); spyOn(dotContentletService, 'getAllContentTypes').and.returnValue( - observableOf([ + of([ { name: 'CONTENT', label: 'Content', types: [] }, { name: 'WIDGET', label: 'Widget', types: [] }, { name: 'FORM', label: 'Form', types: [] } @@ -254,7 +254,7 @@ describe('DotContentTypesPortletComponent', () => { conf.accept(); }); - spyOn(crudService, 'delete').and.returnValue(observableOf(mockContentType)); + spyOn(crudService, 'delete').and.returnValue(of(mockContentType)); comp.rowActions[DELETE_MENU_ITEM_INDEX].menuItem.command(mockContentType); fixture.detectChanges(); @@ -275,7 +275,7 @@ describe('DotContentTypesPortletComponent', () => { }); it('should have ONLY remove action because is community license', () => { - spyOn(dotLicenseService, 'isEnterprise').and.returnValue(observableOf(false)); + spyOn(dotLicenseService, 'isEnterprise').and.returnValue(of(false)); fixture.detectChanges(); expect( @@ -294,7 +294,7 @@ describe('DotContentTypesPortletComponent', () => { }); it('should have remove and add to bundle actions if is not community license and no publish environments are created', () => { - spyOn(pushPublishService, 'getEnvironments').and.returnValue(observableOf([])); + spyOn(pushPublishService, 'getEnvironments').and.returnValue(of([])); fixture.detectChanges(); expect(comp.rowActions.map((action) => action.menuItem.label)).toEqual([ @@ -477,9 +477,10 @@ describe('DotContentTypesPortletComponent', () => { describe('filterBy', () => { beforeEach(() => { - router.data = observableOf({ + router.queryParams = of({ filterBy: 'FORM' }); + fixture.detectChanges(); }); @@ -489,7 +490,6 @@ describe('DotContentTypesPortletComponent', () => { }); it('should set filterBy params', () => { - fixture.detectChanges(); expect(comp.filterBy).toBe('Form'); expect(comp.listing.paginatorService.extraParams.get('type')).toBe('Form'); expect(comp.actionHeaderOptions.primary.model).toBe(null); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts index e09ab97cf690..72dd6815e8c1 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-listing/dot-content-types.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { forkJoin, Subject } from 'rxjs'; import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; @@ -92,7 +91,7 @@ export class DotContentTypesPortletComponent implements OnInit, OnDestroy { map((environments: DotEnvironment[]) => !!environments.length), take(1) ), - this.route.data.pipe(pluck('filterBy'), take(1)) + this.route.queryParams.pipe(pluck('filterBy'), take(1)) ).subscribe(([contentTypes, isEnterprise, environments, filterBy]) => { const baseTypes: StructureTypeView[] = contentTypes; @@ -159,7 +158,10 @@ export class DotContentTypesPortletComponent implements OnInit, OnDestroy { } private setFilterByContentType(contentType: string) { - this.filterBy = _.startCase(_.toLower(contentType)); + const lowerCased = contentType.toLowerCase(); + + this.filterBy = lowerCased.charAt(0).toUpperCase() + lowerCased.slice(1); + this.paginatorExtraParams = { type: this.filterBy }; this.actionHeaderOptions.primary.command = ($event) => { this.createContentType(null, $event); diff --git a/core-web/apps/dotcms-ui/src/app/providers.ts b/core-web/apps/dotcms-ui/src/app/providers.ts index 25bee8b1feba..2e9f4d1a73f2 100644 --- a/core-web/apps/dotcms-ui/src/app/providers.ts +++ b/core-web/apps/dotcms-ui/src/app/providers.ts @@ -39,7 +39,6 @@ import { PagesGuardService } from './api/services/guards/pages-guard.service'; import { PublicAuthGuardService } from './api/services/guards/public-auth-guard.service'; import { NotificationsService } from './api/services/notifications-service'; import { ColorUtil } from './api/util/ColorUtil'; -import { StringPixels } from './api/util/string-pixels-util'; import { StringFormat } from './api/util/stringFormat'; import { DotSaveOnDeactivateService } from './shared/dot-save-on-deactivate-service/dot-save-on-deactivate.service'; import { IframeOverlayService } from './view/components/_common/iframe/service/iframe-overlay.service'; @@ -75,7 +74,6 @@ const PROVIDERS: Provider[] = [ PagesGuardService, PublicAuthGuardService, StringFormat, - StringPixels, DotLoginPageResolver, DotLoginPageStateService, DotPushPublishDialogService, diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-container-column-box.model.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-container-column-box.model.ts deleted file mode 100644 index a0231aa87b42..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-container-column-box.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DotContainer } from '@dotcms/dotcms-models'; - -/** - * It is a Container linked into a DotLayoutGridBox - */ -export interface DotContainerColumnBox { - container: DotContainer; - uuid?: string; -} diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-box.model.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-box.model.ts deleted file mode 100644 index 90be8c750187..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-box.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgGridItemConfig } from '@dotcms/dot-layout-grid'; - -import { DotContainerColumnBox } from './dot-container-column-box.model'; - -/** - * It is NgGrid box - * - * for more information see: https://github.com/BTMorton/angular2-grid - */ -export interface DotLayoutGridBox { - config: NgGridItemConfig; - containers: DotContainerColumnBox[]; -} diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-row.model.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-row.model.ts deleted file mode 100644 index 4a1ae7b42b19..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid-row.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DotLayoutGridBox } from './dot-layout-grid-box.model'; - -export interface DotLayoutGridRow { - boxes: DotLayoutGridBox[]; - styleClass?: string; -} diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.spec.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.spec.ts deleted file mode 100644 index e1208ae1e678..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { CONTAINER_SOURCE } from '@dotcms/dotcms-models'; - -import { DotLayoutGridBox } from './dot-layout-grid-box.model'; -import { DotLayoutGrid } from './dot-layout-grid.model'; - -describe('DotLayoutGridRow', () => { - let dotLayoutGrid: DotLayoutGrid; - - const gridBoxes: DotLayoutGridBox[] = [ - { - containers: [ - { - container: { - type: 'containers', - identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc3', - name: 'Large Column (lg-1)', - categoryId: 'dde0b865-6cea-4ff0-8582-85e5974cf94f', - source: CONTAINER_SOURCE.FILE, - path: 'container/path', - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '0' - } - ], - config: { - fixed: true, - sizex: 8, - maxCols: 12, - maxRows: 1, - col: 1, - row: 1, - sizey: 1, - dragHandle: null, - resizeHandle: null, - draggable: true, - resizable: true, - borderSize: 25, - payload: { - styleClass: 'test_column_class' - } - } - }, - { - containers: [ - { - container: { - type: 'containers', - identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', - name: 'Medium Column (md-1)', - categoryId: '9ab97328-e72f-4d7e-8be6-232f53218a93', - source: CONTAINER_SOURCE.DB, - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '1' - }, - { - container: { - type: 'containers', - identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc3', - name: 'Large Column (lg-1)', - categoryId: 'dde0b865-6cea-4ff0-8582-85e5974cf94f', - source: CONTAINER_SOURCE.FILE, - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '2' - }, - { - container: null, - uuid: '1234' - } - ], - config: { - fixed: true, - sizex: 4, - maxCols: 12, - maxRows: 1, - col: 9, - row: 1, - sizey: 1, - dragHandle: null, - resizeHandle: null, - draggable: true, - resizable: true, - borderSize: 25 - } - }, - { - containers: [ - { - container: { - type: 'containers', - identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc4', - name: 'Large Column (lg-1)', - categoryId: 'dde0b865-6cea-4ff0-8582-85e5974cf94g', - source: CONTAINER_SOURCE.FILE, - path: 'container/path', - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - uuid: '3' - } - ], - config: { - fixed: true, - sizex: 8, - maxCols: 12, - maxRows: 1, - col: 1, - row: 2, - sizey: 1, - dragHandle: null, - resizeHandle: null, - draggable: true, - resizable: true, - borderSize: 25 - } - } - ]; - - const rowClasses: string[] = [ - 'test_column_class_1', - 'test_column_class_2', - 'test_column_class_3' - ]; - - beforeEach(() => { - dotLayoutGrid = new DotLayoutGrid([...gridBoxes], [...rowClasses]); - }); - - it('should get default box config object', () => { - expect(DotLayoutGrid.getDefaultConfig()).toEqual({ - fixed: true, - sizex: 3, - maxCols: 12, - maxRows: 1, - payload: { - styleClass: '' - } - }); - }); - - it('should get default grid layout object', () => { - const defaultGrid = DotLayoutGrid.getDefaultGrid(); - - expect(defaultGrid.boxes).toEqual([ - { - config: { - fixed: true, - sizex: 12, - maxCols: 12, - maxRows: 1, - col: 1, - row: 1, - payload: { - styleClass: '' - } - }, - containers: [] - } - ]); - - expect(defaultGrid.getAllRowClass()).toEqual(['']); - }); - - it('should return all boxes', () => { - expect(dotLayoutGrid.boxes).toEqual(gridBoxes); - }); - - it('should return all DotLayoutGridRow', () => { - const copy: any = JSON.parse(JSON.stringify(gridBoxes)); - const row1: any = JSON.parse(JSON.stringify(gridBoxes)); - - row1.splice(1, 1); - row1[0].config = copy[0].config; - row1[1].config = copy[1].config; - row1[0].containers = [...copy[0].containers]; - row1[1].containers = [...copy[1].containers]; - - expect(dotLayoutGrid.getRows()).toEqual([ - { - boxes: row1, - styleClass: 'test_column_class_1' - }, - { - boxes: [gridBoxes[2]], - styleClass: 'test_column_class_2' - } - ]); - }); - - it('should the second box', () => { - expect(dotLayoutGrid.box(1)).toEqual(gridBoxes[1]); - }); - - it('should add a new box', () => { - dotLayoutGrid.addBox(); - expect(dotLayoutGrid.boxes.length).toEqual(4); - - expect(dotLayoutGrid.boxes[3]).toEqual({ - config: { - fixed: true, - sizex: 3, - maxCols: 12, - maxRows: 1, - row: 2, - col: 9, - payload: { - styleClass: '' - } - }, - containers: [] - }); - }); - - it('should remove a container 1 and Css class asociated to that row', () => { - dotLayoutGrid.removeContainer(0); - expect(dotLayoutGrid.boxes.length).toEqual(2); - expect(dotLayoutGrid.getAllRowClass()).toEqual([ - 'test_column_class_2', - 'test_column_class_3' - ]); - }); - - it('should get all row class', () => { - expect(dotLayoutGrid.getAllRowClass()).toEqual(rowClasses); - }); - - it('should get row class 1', () => { - expect(dotLayoutGrid.getRowClass(1)).toEqual('test_column_class_2'); - }); - - it('should set row class', () => { - dotLayoutGrid.setRowClass('test_column_class_3', 1); - expect(dotLayoutGrid.getRowClass(1)).toEqual('test_column_class_3'); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.ts deleted file mode 100644 index 1a937b637164..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/dot-layout-grid.model.ts +++ /dev/null @@ -1,179 +0,0 @@ -import * as _ from 'lodash'; - -import { NgGridItemConfig } from '@dotcms/dot-layout-grid'; - -import { DotContainerColumnBox } from './dot-container-column-box.model'; -import { DotLayoutGridBox } from './dot-layout-grid-box.model'; -import { DotLayoutGridRow } from './dot-layout-grid-row.model'; - -export const DOT_LAYOUT_GRID_MAX_COLUMNS = 12; - -const DOT_LAYOUT_GRID_DEFAULT_EMPTY_GRID_ROWS: Record< - string, - boolean | number | { [key: string]: string } -> = { - fixed: true, - sizex: 12, - maxCols: 12, - maxRows: 1, - col: 1, - row: 1, - payload: { - styleClass: '' - } -}; -const DEFAULT_CONFIG_FOR_NOT_EMPTY_GRID_TEMPLATE: Record = { - fixed: true, - sizex: 3, - maxCols: 12, - maxRows: 1 -}; - -/** - * Layout using NgGrid box and DotLayoutGridBox - * - */ - -export class DotLayoutGrid { - constructor( - private dotLayoutGridBoxs: DotLayoutGridBox[], - private rowClasses: string[] - ) {} - - static getDefaultConfig(): NgGridItemConfig { - return { - ...DEFAULT_CONFIG_FOR_NOT_EMPTY_GRID_TEMPLATE, - ...{ - payload: { - styleClass: '' - } - } - }; - } - - static getDefaultGrid(): DotLayoutGrid { - const defaultBox: DotLayoutGridBox[] = [ - { - config: ( - Object.assign({}, DOT_LAYOUT_GRID_DEFAULT_EMPTY_GRID_ROWS) - ), - containers: [] - } - ]; - - return new DotLayoutGrid(defaultBox, ['']); - } - - get boxes() { - return this.dotLayoutGridBoxs; - } - - getRows(): DotLayoutGridRow[] { - return _.chain(this.boxes) - .sortBy('config.row') - .sortBy('config.col') - .groupBy('config.row') - .values() - .map((dotLayoutGridBox: DotLayoutGridBox[], index: number) => { - return { - boxes: dotLayoutGridBox, - styleClass: this.rowClasses[index] - }; - }) - .value(); - } - - box(index: number): DotLayoutGridBox { - return this.dotLayoutGridBoxs[index]; - } - - addBox(): void { - const conf: NgGridItemConfig = this.getConfigOfNewBox(); - this.dotLayoutGridBoxs.push({ config: conf, containers: [] }); - this.setRowClases(); - } - - deleteEmptyRows(): void { - this.dotLayoutGridBoxs = _.chain(this.dotLayoutGridBoxs) - .sortBy('config.row') - .groupBy('config.row') - .values() - .map(this.updateContainerIndex) - .flatten() - .value(); - } - - getRowClass(index: number): string { - return this.rowClasses[index]; - } - - getAllRowClass(): string[] { - return this.rowClasses; - } - - setRowClass(value: string, index: number): void { - this.rowClasses[index] = value; - } - - removeContainer(index: number): void { - const classIndex = this.dotLayoutGridBoxs[index].config.row - 1; - this.dotLayoutGridBoxs.splice(index, 1); - this.deleteEmptyRows(); - this.setRowClases(classIndex); - } - - private setRowClases(classIndex?: number): void { - const newNRows = this.dotLayoutGridBoxs - .map((box: DotLayoutGridBox) => box.config.row) - .reduce((before: number, current: number) => { - return before > current ? before : current; - }, 0); - - if (this.rowClasses.length > newNRows) { - this.rowClasses.splice(classIndex, 1); - } else { - this.rowClasses = [ - ...this.rowClasses, - ...Array(newNRows - this.rowClasses.length).fill(null) - ]; - } - } - - private getConfigOfNewBox(): NgGridItemConfig { - const newRow: NgGridItemConfig = DotLayoutGrid.getDefaultConfig(); - - if (this.dotLayoutGridBoxs.length) { - const lastContainer = _.chain(this.dotLayoutGridBoxs) - .groupBy('config.row') - .values() - .last() - .maxBy('config.col') - .value(); - - let busyColumns: number = DEFAULT_CONFIG_FOR_NOT_EMPTY_GRID_TEMPLATE.sizex as number; - busyColumns += lastContainer.config.col - 1 + lastContainer.config.sizex; - - if (busyColumns <= DOT_LAYOUT_GRID_MAX_COLUMNS) { - newRow.row = lastContainer.config.row; - newRow.col = lastContainer.config.col + lastContainer.config.sizex; - } else { - newRow.row = lastContainer.config.row + 1; - newRow.col = 1; - } - } - - return newRow; - } - - private updateContainerIndex(rowArray, index) { - if (rowArray[0].row !== index + 1) { - return rowArray.map((container) => { - container.config.row = index + 1; - - return container; - }); - } - - return rowArray; - } -} diff --git a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/index.ts b/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/index.ts deleted file mode 100644 index 7faaa93cb897..000000000000 --- a/core-web/apps/dotcms-ui/src/app/shared/models/dot-edit-layout-designer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './dot-container-column-box.model'; -export * from './dot-layout-grid-box.model'; -export * from './dot-layout-grid-row.model'; -export * from './dot-layout-grid.model'; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts index ea1e4d6126bb..ec804eb416fc 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.spec.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as _ from 'lodash'; - import { Component, DebugElement, Input } from '@angular/core'; import { ComponentFixture, @@ -304,7 +302,7 @@ describe('SearchableDropdownComponent', () => { hostFixture.detectChanges(); items = de.queryAll(By.css('.searchable-dropdown__data-list-item')); - dataExpected = _.cloneDeep(data[0]); + dataExpected = structuredClone(data[0]); dataExpected.label = dataExpected.name; }); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts index 83bd07d9873f..9e8473977218 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/searchable-dropdown/component/searchable-dropdown.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { fromEvent } from 'rxjs'; import { @@ -399,7 +398,7 @@ export class SearchableDropdownComponent private setOptions(change: SimpleChanges): void { if (change.data && change.data.currentValue) { - this.options = _.cloneDeep(change.data.currentValue).map((item) => { + this.options = structuredClone(change.data.currentValue).map((item) => { item.label = this.getItemLabel(item); return item; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.ts index 693e3fb12c3d..5cbea7724b1b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-add-persona-dialog/dot-create-persona-form/dot-create-persona-form.component.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; @@ -8,6 +7,7 @@ import { takeUntil } from 'rxjs/operators'; import { SiteService } from '@dotcms/dotcms-js'; import { DotCMSTempFile } from '@dotcms/dotcms-models'; +import { camelCase } from '@dotcms/utils'; import { DotFileUpload } from '@models/dot-file-upload/dot-file-upload.model'; @Component({ @@ -66,7 +66,7 @@ export class DotCreatePersonaFormComponent implements OnInit, OnDestroy { * @memberof DotCreatePersonaFormComponent */ setKeyTag(): void { - this.form.get('keyTag').setValue(_.camelCase(this.form.get('name').value)); + this.form.get('keyTag').setValue(camelCase(this.form.get('name').value)); } /** diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.html deleted file mode 100644 index 9c2f6d9efe85..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.html +++ /dev/null @@ -1,18 +0,0 @@ - -
    -
  • - - - {{ dotContainerColumnBox.container?.name }} ({{ - dotContainerColumnBox.container?.parentPermissionable.hostname - }}) - -
  • -
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.scss deleted file mode 100644 index e12b8c9ec377..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use "variables" as *; -@import "mixins"; - -:host { - display: flex; - height: 100%; - flex-direction: column; - width: 100%; -} - -.container-selector__list { - @include naked-list; - margin: 0; - overflow-x: hidden; - overflow-y: auto; - padding: 0; -} - -.container-selector__list-item { - display: flex; - align-items: center; - border: 1px solid $color-palette-gray-300; - color: $black; - font-size: $font-size-md; - overflow: hidden; - padding: 0; - margin: $spacing-1; - border-radius: $border-radius-xs; - - p-button { - margin-right: $spacing-1; - } -} - -.container-selector__list-item-text { - @include truncate-text; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.spec.ts deleted file mode 100644 index 71c1e8885b3d..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { of as observableOf } from 'rxjs'; - -import { CommonModule } from '@angular/common'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule } from 'primeng/button'; - -import { DotContainerSelectorModule } from '@components/dot-container-selector/dot-container-selector.module'; -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; -import { DotMessageService, PaginatorService } from '@dotcms/data-access'; -import { - ApiRoot, - BrowserUtil, - CoreWebService, - LoggerService, - StringUtils, - UserModel -} from '@dotcms/dotcms-js'; -import { CONTAINER_SOURCE, DotContainer } from '@dotcms/dotcms-models'; -import { DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; -import { CoreWebServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; - -import { DotContainerSelectorLayoutComponent } from './dot-container-selector-layout.component'; - -import { IframeOverlayService } from '../_common/iframe/service/iframe-overlay.service'; -import { SearchableDropDownModule } from '../_common/searchable-dropdown/searchable-dropdown.module'; - -describe('DotContainerSelectorLayoutComponent', () => { - let comp: DotContainerSelectorLayoutComponent; - let fixture: ComponentFixture; - let de: DebugElement; - let dotContainerSelector; - let containers: DotContainer[]; - - beforeEach(() => { - const messageServiceMock = new MockDotMessageService({ - addcontainer: 'Add a Container' - }); - - TestBed.configureTestingModule({ - declarations: [DotContainerSelectorLayoutComponent], - imports: [ - SearchableDropDownModule, - BrowserAnimationsModule, - CommonModule, - FormsModule, - ButtonModule, - DotSafeHtmlPipe, - DotMessagePipe, - HttpClientTestingModule, - DotContainerSelectorModule - ], - providers: [ - { provide: DotMessageService, useValue: messageServiceMock }, - BrowserUtil, - IframeOverlayService, - PaginatorService, - DotTemplateContainersCacheService, - { provide: CoreWebService, useClass: CoreWebServiceMock }, - ApiRoot, - UserModel, - LoggerService, - StringUtils - ] - }).compileComponents(); - - fixture = DOTTestBed.createComponent(DotContainerSelectorLayoutComponent); - comp = fixture.componentInstance; - de = fixture.debugElement; - - dotContainerSelector = de.query(By.css('dot-container-selector')).componentInstance; - - containers = [ - { - categoryId: '427c47a4-c380-439f-a6d0-97d81deed57e', - deleted: false, - friendlyName: 'Friendly Container name', - identifier: '427c47a4-c380-439f', - name: 'Container 1', - type: 'Container', - source: CONTAINER_SOURCE.DB, - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - }, - { - categoryId: '40204d-c380-439f-a6d0-97d8sdeed57e', - deleted: false, - friendlyName: 'Friendly Container2 name', - identifier: '427c47a4-c380-439f', - name: 'Container 2', - type: 'Container', - source: CONTAINER_SOURCE.FILE, - path: 'container/path', - parentPermissionable: { - hostname: 'demo.dotcms.com' - } - } - ]; - }); - - it('should show the hots name and container name', async () => { - comp.data = [ - { - container: containers[0], - uuid: '1' - } - ]; - - fixture.detectChanges(); - await fixture.whenStable(); - - const dataItem = de.query(By.css('.container-selector__list-item-text')); - expect(dataItem.nativeNode.textContent.trim()).toEqual('Container 1 (demo.dotcms.com)'); - }); - - it('should pass the innerClass', () => { - expect(dotContainerSelector.innerClass).toBe('d-secondary'); - }); - - it('should add containers to containers list and emit a change event', () => { - comp.currentContainers = containers; - - dotContainerSelector.swap.emit(containers[0]); - - expect(comp.data[0].container).toEqual(containers[0]); - expect(comp.data[0].uuid).not.toBeNull(); - expect(comp.data.length).toEqual(1); - }); - - it('should remove containers after click on trash icon', () => { - const bodySelectorList = de.query(By.css('.container-selector__list')); - - const bodySelectorListItems = bodySelectorList.nativeElement.children; - - comp.currentContainers = containers; - - dotContainerSelector.swap.emit(containers[0]); - - fixture.detectChanges(); - - bodySelectorListItems[0].children[0].click(); - expect(comp.data.length).toEqual(0); - }); - - it('should not add duplicated containers to the list when multiple false', () => { - comp.currentContainers = containers; - - dotContainerSelector.swap.emit(containers[0]); - fixture.detectChanges(); - - expect(comp.data.length).toEqual(1); - - dotContainerSelector.swap.emit(containers[0]); - fixture.detectChanges(); - - expect(comp.data.length).toEqual(1); - }); - - it('should add duplicated containers to the list when multiple true', () => { - comp.currentContainers = containers; - comp.multiple = true; - - dotContainerSelector.swap.emit(containers[0]); - dotContainerSelector.swap.emit(containers[0]); - fixture.detectChanges(); - - expect(comp.data.length).toEqual(2); - }); - - it('should set container list replacing the identifier for the path, if needed', () => { - fixture.detectChanges(); - const paginatorService: PaginatorService = de.injector.get(PaginatorService); - spyOn(paginatorService, 'getWithOffset').and.returnValue(observableOf(containers)); - comp.handleFilterChange(''); - - expect(comp.currentContainers[0].identifier).toEqual('427c47a4-c380-439f'); - expect(comp.currentContainers[1].identifier).toEqual('container/path'); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.ts deleted file mode 100644 index f30020dd0ce0..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; - -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { PaginatorService } from '@dotcms/data-access'; -import { DotContainer } from '@dotcms/dotcms-models'; -import { DotContainerColumnBox } from '@shared/models/dot-edit-layout-designer/dot-container-column-box.model'; - -@Component({ - selector: 'dot-container-selector-layout', - templateUrl: './dot-container-selector-layout.component.html', - styleUrls: ['./dot-container-selector-layout.component.scss'] -}) -export class DotContainerSelectorLayoutComponent implements OnInit { - @Input() data: DotContainerColumnBox[] = []; - @Input() multiple: boolean; - @Output() switch: EventEmitter = new EventEmitter(); - - totalRecords: number; - currentContainers: DotContainer[] = []; - - constructor( - public paginationService: PaginatorService, - private templateContainersCacheService: DotTemplateContainersCacheService - ) {} - - ngOnInit(): void { - this.paginationService.url = 'v1/containers'; - } - - /** - * Called when the selected site changed and the change event is emmited - * - * @param DotContainer container - * @memberof DotContainerSelectorLayoutComponent - */ - containerChange(container: DotContainer): void { - if (this.multiple || !this.isContainerSelected(container)) { - this.data.push({ - container: container - }); - this.switch.emit(this.data); - } - } - - /** - * Call to handle filter containers from list - * - * @param string filter - * @memberof DotContainerSelectorLayoutComponent - */ - handleFilterChange(filter: string): void { - this.getContainersList(filter); - } - - /** - * Call when the current page changed - * @param any event - * @memberof DotContainerSelectorLayoutComponent - */ - handlePageChange(event: { filter: string; first: number }): void { - this.getContainersList(event.filter, event.first); - } - - /** - * Remove container item from selected containers and emit selected containers - * @param number i - * @memberof DotContainerSelectorLayoutComponent - */ - removeContainerItem(i: number): void { - this.data.splice(i, 1); - this.switch.emit(this.data); - } - - /** - * Check if a container was already added to the list - * - * @param DotContainer container - * @returns boolean - * @memberof DotContainerSelectorLayoutComponent - */ - isContainerSelected(dotContainer: DotContainer): boolean { - return this.data.some( - (containerItem) => containerItem.container.identifier === dotContainer.identifier - ); - } - - private getContainersList(filter = '', offset = 0): void { - this.paginationService.filter = filter; - this.paginationService - .getWithOffset(offset) - .subscribe((items: DotContainer[]) => { - this.currentContainers = this.setIdentifierReference(items.splice(0)); - this.totalRecords = this.totalRecords || this.paginationService.totalRecords; - }); - } - - private setIdentifierReference(items: DotContainer[]): DotContainer[] { - return items.map((dotContainer) => { - dotContainer.identifier = - this.templateContainersCacheService.getContainerReference(dotContainer); - - return dotContainer; - }); - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.module.ts deleted file mode 100644 index 730b7feb3eca..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector-layout/dot-container-selector-layout.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; - -import { DotContainerSelectorModule } from '@components/dot-container-selector/dot-container-selector.module'; -import { PaginatorService } from '@dotcms/data-access'; -import { DotSafeHtmlPipe } from '@dotcms/ui'; - -import { DotContainerSelectorLayoutComponent } from './dot-container-selector-layout.component'; - -import { SearchableDropDownModule } from '../_common/searchable-dropdown/searchable-dropdown.module'; - -@NgModule({ - declarations: [DotContainerSelectorLayoutComponent], - exports: [DotContainerSelectorLayoutComponent], - imports: [ - CommonModule, - FormsModule, - ButtonModule, - SearchableDropDownModule, - DotSafeHtmlPipe, - DotContainerSelectorModule - ], - providers: [PaginatorService] -}) -export class DotContainerSelectorLayoutModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.ts index 83447ce4cb48..b02e1fb75f11 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-container-selector/dot-container-selector.component.ts @@ -8,7 +8,6 @@ import { PaginationEvent } from '@components/_common/searchable-dropdown/compone import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; import { PaginatorService } from '@dotcms/data-access'; import { DotContainer } from '@dotcms/dotcms-models'; -import { DotContainerColumnBox } from '@models/dot-edit-layout-designer'; @Component({ providers: [PaginatorService], @@ -19,7 +18,6 @@ import { DotContainerColumnBox } from '@models/dot-edit-layout-designer'; export class DotContainerSelectorComponent implements OnInit { @Output() swap: EventEmitter = new EventEmitter(); - @Input() data: DotContainerColumnBox[] = []; @Input() innerClass = ''; totalRecords: number; diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.html deleted file mode 100644 index 55280579b596..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
- -
- -
- -
- - -
-
-
- - -
-
- - -
-
-
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.scss deleted file mode 100644 index 16b9adfe5230..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.scss +++ /dev/null @@ -1,77 +0,0 @@ -@use "variables" as *; -@import "mixins"; - -:host { - display: block; - width: 100%; - - ::ng-deep { - dot-container-selector { - height: auto; - margin-left: $spacing-3; - margin-right: $spacing-3; - - dot-searchable-dropdown button { - margin: $spacing-1 0; - } - - dot-searchable-dropdown button, - dot-searchable-dropdown button:hover { - box-shadow: none; - } - - .container-selector__list { - height: 140px; - margin-top: $spacing-1; - } - } - } -} - -$box-header-height: 52px; -$box-height: 206px; - -.box { - background: linear-gradient( - to bottom, - $color-palette-gray-200 0, - $color-palette-gray-200 $box-header-height, - $white $box-header-height, - $white 100% - ); - border-radius: $border-radius-xs; - border: 1px solid $color-palette-gray-300; - display: flex; - height: $box-height; - justify-content: space-between; -} - -.box__actions { - align-items: center; - display: flex; - height: $box-header-height; - position: absolute; - right: $spacing-1; -} - -.box__add-row-class-button { - align-items: center; - border: 0; - cursor: pointer; - display: flex; - height: $box-height; - padding: 0; - position: absolute; - right: -$spacing-8; - width: $spacing-8; - z-index: 1; - - &::ng-deep .dot-icon-button { - background-color: $color-palette-gray-200; - - &:hover { - background-color: $color-palette-primary-op-20; - color: $color-palette-primary; - } - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.spec.ts deleted file mode 100644 index 93103c69a5ec..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.spec.ts +++ /dev/null @@ -1,546 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { Component, DebugElement, EventEmitter, HostBinding, Input, Output } from '@angular/core'; -import { ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule } from 'primeng/button'; -import { TooltipModule } from 'primeng/tooltip'; - -import { DotContainerSelectorLayoutModule } from '@components/dot-container-selector-layout/dot-container-selector-layout.module'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; -import { - DotAlertConfirmService, - DotEventsService, - DotMessageService, - PaginatorService -} from '@dotcms/data-access'; -import { NgGridModule } from '@dotcms/dot-layout-grid'; -import { DotDialogActions, DotLayoutBody } from '@dotcms/dotcms-models'; -import { DotAutofocusDirective, DotMessagePipe } from '@dotcms/ui'; -import { MockDotMessageService } from '@dotcms/utils-testing'; - -import { DotEditLayoutGridComponent } from './dot-edit-layout-grid.component'; - -let fakeValue: DotLayoutBody; - -@Component({ - selector: 'dot-test-host-component', - template: ` -
- -
- ` -}) -class TestHostComponent { - form: UntypedFormGroup; - - constructor() { - this.createForm(); - } - - createForm(): void { - this.form = new UntypedFormGroup({ - body: new UntypedFormControl(fakeValue) - }); - } -} - -@Component({ - selector: 'dot-dialog', - template: '' -}) -class DotDialogMockComponent { - @Input() - actions: DotDialogActions; - - @Input() - @HostBinding('class.active') - visible: boolean; - - @Input() - header = ''; - - @Input() - width: string; - - @Output() - hide: EventEmitter = new EventEmitter(); - - close(): void {} -} - -describe('DotEditLayoutGridComponent', () => { - let component: DotEditLayoutGridComponent; - let de: DebugElement; - let hostComponentfixture: ComponentFixture; - let dotEditLayoutService: DotEditLayoutService; - - beforeEach(() => { - fakeValue = { - rows: [ - { - styleClass: '', - columns: [ - { - styleClass: '', - containers: [], - leftOffset: 1, - width: 2 - } - ] - } - ] - }; - - const messageServiceMock = new MockDotMessageService({ - cancel: 'Cancel', - 'dot.common.dialog.accept': 'Accept', - 'dot.common.dialog.reject': 'Cancel', - 'editpage.action.cancel': 'Cancel', - 'editpage.action.delete': 'Delete', - 'editpage.action.save': 'Save', - 'editpage.confirm.header': 'Header', - 'editpage.confirm.message.delete': 'Delete', - 'editpage.confirm.message.delete.warning': 'Warning', - 'editpage.layout.css.class.add.to.box': 'Add class to box', - 'editpage.layout.css.class.add.to.row': 'Add class to row' - }); - - DOTTestBed.configureTestingModule({ - declarations: [DotEditLayoutGridComponent, TestHostComponent, DotDialogMockComponent], - imports: [ - NgGridModule, - DotContainerSelectorLayoutModule, - BrowserAnimationsModule, - DotAutofocusDirective, - ButtonModule, - TooltipModule, - DotMessagePipe - ], - providers: [ - DotAlertConfirmService, - DotEditLayoutService, - DotTemplateContainersCacheService, - PaginatorService, - { provide: DotMessageService, useValue: messageServiceMock } - ] - }); - - hostComponentfixture = DOTTestBed.createComponent(TestHostComponent); - de = hostComponentfixture.debugElement.query(By.css('dot-edit-layout-grid')); - component = de.componentInstance; - - dotEditLayoutService = de.injector.get(DotEditLayoutService); - - hostComponentfixture.detectChanges(); - }); - - it('should show set one element in the grid of 12 columns', () => { - hostComponentfixture.componentInstance.form = new UntypedFormGroup({ - body: new UntypedFormControl({}) - }); - - hostComponentfixture.detectChanges(); - - expect(component.grid.boxes.length).toEqual(1); - expect(component.grid.boxes[0].config.sizex).toEqual(12); - }); - - it('should subscribe to layour service and call addBox', () => { - spyOn(component, 'addBox'); - dotEditLayoutService.addBox(); - expect(component.addBox).toHaveBeenCalledTimes(1); - }); - - it('should add one Container to the grid of 3 columns', () => { - component.addBox(); - expect(component.grid.boxes.length).toEqual(2); - expect(component.grid.boxes[1].config.sizex).toEqual(3); - }); - - it('should add a new Container in the same row', () => { - component.addBox(); - component.addBox(); - - expect(component.grid.boxes.length).toEqual(3); - expect(component.grid.boxes[1].config.row).toEqual(1); - expect(component.grid.boxes[2].config.row).toEqual(1); - }); - - it('should each box has its own payload object', () => { - component.addBox(); - component.addBox(); - - const length = component.grid.boxes.length; - - expect(component.grid.boxes[length - 1].config.payload).not.toBe( - component.grid.boxes[length - 2].config.payload - ); - }); - - it('should add a new add class button', () => { - fakeValue.rows[0].columns[0].width = 12; - hostComponentfixture.componentInstance.form = new UntypedFormGroup({ - body: new UntypedFormControl(fakeValue) - }); - - hostComponentfixture.detectChanges(); - - component.addBox(); - - hostComponentfixture.detectChanges(); - - const addRowClassButtons = hostComponentfixture.debugElement.queryAll( - By.css('.box__add-row-class-button') - ); - - expect(addRowClassButtons.length).toBe(2); - }); - - it('should add a new Container in a new row, when there is no space in the last row', () => { - fakeValue.rows[0].columns[0].width = 12; - hostComponentfixture.componentInstance.form = new UntypedFormGroup({ - body: new UntypedFormControl(fakeValue) - }); - - hostComponentfixture.detectChanges(); - - component.addBox(); - expect(component.grid.boxes.length).toEqual(2); - expect(component.grid.boxes[1].config.row).toEqual(2); - }); - - it('should remove one Container from the Grid', () => { - component.addBox(); - const dotDialogService = - hostComponentfixture.debugElement.injector.get(DotAlertConfirmService); - spyOn(dotDialogService, 'confirm').and.callFake((conf) => { - conf.accept(); - }); - component.onRemoveContainer(1); - expect(component.grid.boxes.length).toEqual(1); - }); - - it('should create a new row with a basic configuration object', () => { - fakeValue.rows[0].columns[0].width = 12; - hostComponentfixture.componentInstance.form = new UntypedFormGroup({ - body: new UntypedFormControl(fakeValue) - }); - - hostComponentfixture.detectChanges(); - - component.addBox(); - expect(component.grid.boxes[1].config).toEqual({ - row: 2, - sizex: 3, - col: 1, - fixed: true, - maxCols: 12, - maxRows: 1, - payload: { - styleClass: '' - } - }); - }); - - it('should remove the empty rows in the grid', fakeAsync(() => { - component.addBox(); - component.addBox(); - component.grid.boxes[0].config.row = 5; - component.grid.boxes[0].config.sizex = 5; - component.grid.boxes[1].config.row = 2; - component.grid.boxes[2].config.row = 4; - component.grid.boxes[2].config.sizex = 1; - component.updateModel(); - tick(); - expect(component.grid.boxes[0].config.sizex).toEqual(3); - expect(component.grid.boxes[1].config.sizex).toEqual(1); - expect(component.grid.boxes[2].config.sizex).toEqual(5); - })); - - it('should Propagate Change after a grid box is deleted', () => { - component.addBox(); - const dotDialogService = - hostComponentfixture.debugElement.injector.get(DotAlertConfirmService); - spyOn(dotDialogService, 'confirm').and.callFake((conf) => { - conf.accept(); - }); - spyOn(component, 'propagateChange'); - component.onRemoveContainer(1); - expect(component.propagateChange).toHaveBeenCalledWith(fakeValue); - }); - - it('should Propagate Change after a grid box is moved', () => { - spyOn(component, 'propagateChange'); - component.updateModel(); - expect(component.propagateChange).toHaveBeenCalledWith(fakeValue); - }); - - it('should Propagate Change after a grid box is added', () => { - fakeValue.rows[0].columns.push({ - containers: [], - leftOffset: 3, - width: 3 - }); - spyOn(component, 'propagateChange'); - component.addBox(); - expect(component.propagateChange).toHaveBeenCalled(); - }); - - it('should resize the grid when the left menu is toggle', fakeAsync(() => { - const dotEventsService = hostComponentfixture.debugElement.injector.get(DotEventsService); - spyOn(component.ngGrid, 'triggerResize'); - dotEventsService.notify('dot-side-nav-toggle'); - tick(210); - expect(component.ngGrid.triggerResize).toHaveBeenCalled(); - })); - - it('should resize the grid when the layout sidebar change', fakeAsync(() => { - const dotEventsService = hostComponentfixture.debugElement.injector.get(DotEventsService); - spyOn(component.ngGrid, 'triggerResize'); - dotEventsService.notify('layout-sidebar-change'); - tick(0); - expect(component.ngGrid.triggerResize).toHaveBeenCalled(); - })); - - it('should call writeValue to define the initial value of grid', () => { - hostComponentfixture.detectChanges(); - expect(component.value).toEqual(fakeValue); - }); - - it('should be multiple true on dot-container-selector', () => { - const containerSelector = de.query(By.css('dot-container-selector-layout')); - expect(containerSelector.attributes['ng-reflect-multiple']).toBeTruthy(); - }); - - it('should have a dot-dialog but not form', () => { - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - const form = hostComponentfixture.debugElement.query(By.css('dot-dialog form')); - - expect(dotDialog).not.toBeNull(); - expect(form).toBeNull(); - }); - - describe('show dialog for add class', () => { - let dotDialog; - let dotText; - let dotDialogForm; - - function showDialog(type) { - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css( - type === 'box' - ? `.box__add-box-class-button` - : `.box__add-${type}-class-button p-button` - ) - ); - - addRowClassButtons.triggerEventHandler('click', null); - hostComponentfixture.detectChanges(); - - dotDialog = hostComponentfixture.debugElement.query( - By.css('dot-dialog') - ).componentInstance; - dotDialogForm = hostComponentfixture.debugElement.query(By.css('dot-dialog form')); - dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - } - - it('should show dot-dialog when click any add class to row button', () => { - showDialog('row'); - expect(dotDialog.visible).toBe(true); - expect(dotDialog.header).toBe('Add class to row'); - expect(dotText.nativeElement.value).toBe(''); - expect(dotDialogForm).toBeDefined(); - }); - - it('should show dot-dialog when click any add class to box button', () => { - showDialog('box'); - expect(dotDialog.visible).toBe(true); - expect(dotDialog.header).toBe('Add class to box'); - expect(dotText.nativeElement.value).toBe(''); - expect(dotDialogForm).toBeDefined(); - }); - }); - - it('should set row class as text value', () => { - fakeValue.rows[0].styleClass = 'test_row_class'; - hostComponentfixture.componentInstance.createForm(); - hostComponentfixture.detectChanges(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-row-class-button p-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_row_class'); - }); - - it('should trigger change when row class is added', () => { - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - spyOn(dotDialog.componentInstance, 'close').and.callThrough(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-row-class-button p-button') - ); - - addRowClassButtons.triggerEventHandler('click', null); - - component.form.setValue({ classToAdd: 'test_row_class' }); - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_row_class'); - - dotDialog.componentInstance.actions.accept.action(dotDialog.componentInstance); - - expect(hostComponentfixture.componentInstance.form.value.body.rows[0].styleClass).toBe( - 'test_row_class' - ); - expect(dotDialog.componentInstance.close).toHaveBeenCalled(); - }); - - it('should trigger change when row class is add', () => { - fakeValue.rows[0].styleClass = 'test_row_class'; - hostComponentfixture.componentInstance.createForm(); - hostComponentfixture.detectChanges(); - - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - spyOn(dotDialog.componentInstance, 'close').and.callThrough(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-row-class-button p-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - component.form.setValue({ classToAdd: 'test_row_class_2' }); - - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_row_class_2'); - - dotDialog.componentInstance.actions.accept.action(dotDialog.componentInstance); - - expect(hostComponentfixture.componentInstance.form.value.body.rows[0].styleClass).toBe( - 'test_row_class_2' - ); - expect(dotDialog.componentInstance.close).toHaveBeenCalled(); - }); - - it('should show dot-dialog when click any add class to column button', () => { - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - hostComponentfixture.detectChanges(); - - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - expect(dotDialog.componentInstance.visible).toBe(true); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe(''); - }); - - it('should disabled accept add class ok button when class is undefined', () => { - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - hostComponentfixture.detectChanges(); - - expect(component.addClassDialogActions.accept.disabled).toBe(true); - }); - - it('should enabled accept add class ok button when class is not undefined', () => { - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - component.form.setValue({ classToAdd: 'test_class' }); - - hostComponentfixture.detectChanges(); - - expect(component.addClassDialogActions.accept.disabled).toBe(false); - }); - - it('should set column class as text value', () => { - fakeValue.rows[0].columns[0].styleClass = 'test_column_class'; - hostComponentfixture.componentInstance.createForm(); - hostComponentfixture.detectChanges(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_column_class'); - }); - - it('should trigger change when column class is added', () => { - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - spyOn(dotDialog.componentInstance, 'close').and.callThrough(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - component.form.setValue({ classToAdd: 'test_column_class' }); - - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_column_class'); - - dotDialog.componentInstance.actions.accept.action(dotDialog.componentInstance); - - expect( - hostComponentfixture.componentInstance.form.value.body.rows[0].columns[0].styleClass - ).toBe('test_column_class'); - expect(dotDialog.componentInstance.close).toHaveBeenCalled(); - }); - - it('should trigger change when column class is edit', () => { - hostComponentfixture.componentInstance.form.value.body.rows[0].columns[0].styleClass = - 'test_column_class'; - hostComponentfixture.componentInstance.createForm(); - hostComponentfixture.detectChanges(); - - const dotDialog = hostComponentfixture.debugElement.query(By.css('dot-dialog')); - spyOn(dotDialog.componentInstance, 'close').and.callThrough(); - - const addRowClassButtons = hostComponentfixture.debugElement.query( - By.css('.box__add-box-class-button') - ); - addRowClassButtons.triggerEventHandler('click', null); - - component.form.setValue({ classToAdd: 'test_column_class_2' }); - - hostComponentfixture.detectChanges(); - - const dotText = hostComponentfixture.debugElement.query(By.css('.box__add-class-text')); - expect(dotText.nativeElement.value).toBe('test_column_class_2'); - - dotDialog.componentInstance.actions.accept.action(dotDialog.componentInstance); - - expect( - hostComponentfixture.componentInstance.form.value.body.rows[0].columns[0].styleClass - ).toBe('test_column_class_2'); - expect(dotDialog.componentInstance.close).toHaveBeenCalled(); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.ts deleted file mode 100644 index 580bce1d5fdc..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.component.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* eslint-disable @angular-eslint/component-selector */ - -import { Subject } from 'rxjs'; - -import { AfterViewInit, Component, forwardRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { - ControlValueAccessor, - NG_VALUE_ACCESSOR, - UntypedFormBuilder, - UntypedFormGroup -} from '@angular/forms'; - -import { takeUntil } from 'rxjs/operators'; - -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { DotAlertConfirmService, DotEventsService, DotMessageService } from '@dotcms/data-access'; -import { NgGrid, NgGridConfig } from '@dotcms/dot-layout-grid'; -import { DotDialogActions, DotLayoutBody } from '@dotcms/dotcms-models'; -import { DOT_LAYOUT_GRID_MAX_COLUMNS, DotLayoutGrid } from '@models/dot-edit-layout-designer'; - -interface DotAddClass { - setter: (string) => void; - getter: () => void; - title: string; -} - -/** - * Component in charge of update the model that will be used be the NgGrid to display containers - * - * @implements {OnInit} - */ -@Component({ - selector: 'dot-edit-layout-grid', - templateUrl: './dot-edit-layout-grid.component.html', - styleUrls: ['./dot-edit-layout-grid.component.scss'], - providers: [ - { - multi: true, - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DotEditLayoutGridComponent) - } - ] -}) -export class DotEditLayoutGridComponent - implements OnInit, OnDestroy, ControlValueAccessor, AfterViewInit -{ - @ViewChild(NgGrid, { static: true }) ngGrid: NgGrid; - - form: UntypedFormGroup; - value: DotLayoutBody; - grid: DotLayoutGrid; - - addClassDialogShow = false; - addClassDialogActions: DotDialogActions; - addClassDialogHeader: string; - - gridConfig: NgGridConfig = { - margins: [0, 8, 8, 0], - draggable: true, - resizable: true, - max_cols: DOT_LAYOUT_GRID_MAX_COLUMNS, - max_rows: 0, - visible_cols: DOT_LAYOUT_GRID_MAX_COLUMNS, - min_cols: 1, - min_rows: 1, - col_width: 90, - row_height: 206, - cascade: 'up', - min_width: 40, - min_height: 206, - fix_to_grid: true, - auto_style: true, - auto_resize: true, - maintain_ratio: false, - prefer_new: false, - zoom_on_drag: false, - limit_to_screen: true - }; - - rowClass: string[] = []; - private destroy$: Subject = new Subject(); - - constructor( - private dotDialogService: DotAlertConfirmService, - private dotEditLayoutService: DotEditLayoutService, - private dotMessageService: DotMessageService, - private dotEventsService: DotEventsService, - public fb: UntypedFormBuilder - ) {} - - ngAfterViewInit(): void { - // needed it because the transition between content & layout. - this.resizeGrid(); - } - - ngOnInit() { - this.dotEventsService - .listen('dot-side-nav-toggle') - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.resizeGrid(200); - }); - - this.dotEventsService - .listen('layout-sidebar-change') - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.resizeGrid(); - }); - - this.form = this.fb.group({ - classToAdd: '' - }); - - this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.addClassDialogActions = { - cancel: { - ...this.addClassDialogActions.cancel - }, - accept: { - ...this.addClassDialogActions.accept, - disabled: !this.form.valid - } - }; - }); - - this.dotEditLayoutService - .getBoxes() - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.addBox(); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - } - - /** - * Add new Box to the gridBoxes Arrray. - * - * @memberof DotEditLayoutGridComponent - */ - addBox(): void { - this.grid.addBox(); - this.propagateGridLayoutChange(); - } - - /** - * Return ng-grid model. - * - * @returns DotLayoutBody - * @memberof DotEditLayoutGridComponent - */ - getModel(): DotLayoutBody { - return this.dotEditLayoutService.getDotLayoutBody(this.grid); - } - - /** - * Event fired when the drag or resize of a container ends, remove empty rows if any. - * - * @memberof DotEditLayoutGridComponent - */ - updateModel(): void { - this.deleteEmptyRows(); - this.propagateGridLayoutChange(); - } - - /** - * Removes the given index to the gridBoxes Array after the user confirms. - * - * @param number index - * @memberof DotEditLayoutGridComponent - */ - onRemoveContainer(index: number): void { - if (this.grid.boxes[index].containers.length) { - this.dotDialogService.confirm({ - accept: () => { - this.removeContainer(index); - }, - header: this.dotMessageService.get('editpage.confirm.header'), - message: `${this.dotMessageService.get( - 'editpage.confirm.message.delete' - )} ${this.dotMessageService.get( - 'editpage.confirm.message.delete.warning' - )}`, - footerLabel: { - accept: this.dotMessageService.get('editpage.action.delete'), - reject: this.dotMessageService.get('editpage.action.cancel') - } - }); - } else { - this.removeContainer(index); - } - } - - propagateChange = (_: unknown) => { - /**/ - }; - - /** - * Set the function to be called when the control receives a change event. - * - * @param * fn - * @memberof DotEditLayoutGridComponent - */ - registerOnChange( - fn: () => { - /**/ - } - ): void { - this.propagateChange = fn; - } - - registerOnTouched(): void { - /**/ - } - - /** - * Update the model when the grid is changed - * - * @memberof DotEditLayoutGridComponent - */ - propagateGridLayoutChange(): void { - this.propagateChange(this.getModel()); - this.rowClass = this.grid.getAllRowClass(); - } - - /** - * Write a new value to the element - * - * @param DotLayoutBody value - * @memberof DotEditLayoutGridComponent - */ - writeValue(value: DotLayoutBody): void { - if (value) { - this.value = value || null; - this.setGridValue(); - } - } - - /** - * Add style class to a column - * - * @param {number} index - * @param {string} title - * @memberof DotEditLayoutGridComponent - */ - addColumnClass(index: number): void { - this.addClass({ - getter: () => { - return this.grid.boxes[index].config.payload - ? this.grid.boxes[index].config.payload.styleClass || null - : null; - }, - setter: (value: string) => { - if (!this.grid.boxes[index].config.payload) { - this.grid.boxes[index].config.payload = { - styleClass: value - }; - } else { - this.grid.boxes[index].config.payload.styleClass = value; - } - }, - title: this.dotMessageService.get('editpage.layout.css.class.add.to.box') - }); - } - - /** - * Add style class to a row - * - * @param {number} index - * @param {string} title - * @memberof DotEditLayoutGridComponent - */ - addRowClass(index: number): void { - this.addClass({ - getter: () => this.grid.getRowClass(index) || '', - setter: (value) => this.grid.setRowClass(value, index), - title: this.dotMessageService.get('editpage.layout.css.class.add.to.row') - }); - } - - /** - * Handle hide event from add class dialog - * - * @memberof DotEditLayoutGridComponent - */ - onAddClassDialogHide(): void { - this.addClassDialogActions = null; - this.addClassDialogShow = false; - this.addClassDialogHeader = ''; - } - - private addClass(params: DotAddClass): void { - this.form.setValue( - { - classToAdd: params.getter.bind(this)() - }, - { - emitEvent: false - } - ); - - this.addClassDialogActions = { - accept: { - action: (dialog?: { - close: () => { - /**/ - }; - }) => { - params.setter.bind(this)(this.form.get('classToAdd').value); - this.propagateGridLayoutChange(); - dialog.close(); - }, - label: 'Ok', - disabled: true - }, - cancel: { - label: 'Cancel' - } - }; - - this.addClassDialogShow = true; - this.addClassDialogHeader = params.title; - } - - private setGridValue(): void { - this.grid = this.isHaveRows() - ? this.dotEditLayoutService.getDotLayoutGridBox(this.value) - : DotLayoutGrid.getDefaultGrid(); - - this.rowClass = this.grid.getAllRowClass(); - } - - private removeContainer(index: number): void { - if (this.grid.boxes[index]) { - this.grid.removeContainer(index); - this.propagateGridLayoutChange(); - } - } - - private deleteEmptyRows(): void { - // TODO: Find a solution to remove setTimeout - setTimeout(() => { - this.grid.deleteEmptyRows(); - }, 0); - } - - private isHaveRows(): boolean { - return !!(this.value && this.value.rows && this.value.rows.length); - } - - private resizeGrid(timeOut = 0): void { - setTimeout(() => { - this.ngGrid.triggerResize(); - }, timeOut); - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.module.ts deleted file mode 100644 index 6a7b71b12b48..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { InputTextModule } from 'primeng/inputtext'; -import { TooltipModule } from 'primeng/tooltip'; - -import { DotActionButtonModule } from '@components/_common/dot-action-button/dot-action-button.module'; -import { DotContainerSelectorLayoutModule } from '@components/dot-container-selector-layout/dot-container-selector-layout.module'; -import { NgGridModule } from '@dotcms/dot-layout-grid'; -import { - DotAutofocusDirective, - DotDialogModule, - DotFieldRequiredDirective, - DotMessagePipe, - DotSafeHtmlPipe -} from '@dotcms/ui'; - -import { DotEditLayoutGridComponent } from './dot-edit-layout-grid.component'; - -@NgModule({ - declarations: [DotEditLayoutGridComponent], - imports: [ - CommonModule, - NgGridModule, - DotActionButtonModule, - DotContainerSelectorLayoutModule, - ButtonModule, - DotDialogModule, - InputTextModule, - FormsModule, - ReactiveFormsModule, - TooltipModule, - DotAutofocusDirective, - DotSafeHtmlPipe, - DotFieldRequiredDirective, - DotMessagePipe - ], - exports: [DotEditLayoutGridComponent], - providers: [] -}) -export class DotEditLayoutGridModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.html deleted file mode 100644 index 20fb1e23f4c7..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
{{ 'editpage.layout.designer.sidebar' | dm }}
- - diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.scss deleted file mode 100644 index 272fd0415e6a..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use "variables" as *; - -:host { - background-color: white; - position: relative; - width: 100%; - - ::ng-deep { - dot-container-selector { - height: auto; - margin-left: $spacing-3; - margin-right: $spacing-3; - - dot-searchable-dropdown button { - margin: $spacing-1 0; - } - .container-selector__list { - height: 100%; - } - } - - .dot-layout-designer__sidebar-properties { - position: absolute; - } - } -} - -h6 { - align-items: center; - background-color: $color-palette-gray-200; - display: flex; - font-size: $font-size-lmd; - font-weight: normal; - height: 48px; - margin: 0; - padding-left: $spacing-3; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.spec.ts deleted file mode 100644 index 63c477dfb270..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Component, DebugElement } from '@angular/core'; -import { ComponentFixture } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { DotContainerSelectorLayoutModule } from '@components/dot-container-selector-layout/dot-container-selector-layout.module'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; -import { DotMessageService } from '@dotcms/data-access'; -import { DotLayoutSideBar } from '@dotcms/dotcms-models'; -import { DotMessagePipe } from '@dotcms/ui'; -import { - mockDotContainers, - MockDotMessageService, - processedContainers -} from '@dotcms/utils-testing'; - -import { DotEditLayoutSidebarComponent } from './dot-edit-layout-sidebar.component'; - -import { DotSidebarPropertiesModule } from '../dot-sidebar-properties/dot-sidebar-properties.module'; - -let fakeValue: DotLayoutSideBar; - -@Component({ - selector: 'dot-test-host-component', - template: ` -
- -
- ` -}) -class TestHostComponent { - form: UntypedFormGroup; - - constructor() { - this.form = new UntypedFormGroup({ - sidebar: new UntypedFormControl(fakeValue) - }); - } -} - -describe('DotEditLayoutSidebarComponent', () => { - let component: DotEditLayoutSidebarComponent; - let de: DebugElement; - let hostComponentfixture: ComponentFixture; - let dotEditLayoutService: DotEditLayoutService; - - beforeEach(() => { - fakeValue = { - containers: [], - location: 'left', - width: 'small' - }; - - const messageServiceMock = new MockDotMessageService({ - 'editpage.layout.designer.sidebar': 'Sidebar' - }); - - DOTTestBed.configureTestingModule({ - declarations: [DotEditLayoutSidebarComponent, TestHostComponent], - imports: [ - DotContainerSelectorLayoutModule, - BrowserAnimationsModule, - DotSidebarPropertiesModule, - DotMessagePipe - ], - providers: [ - DotEditLayoutService, - DotTemplateContainersCacheService, - { provide: DotMessageService, useValue: messageServiceMock } - ] - }); - - hostComponentfixture = DOTTestBed.createComponent(TestHostComponent); - de = hostComponentfixture.debugElement.query(By.css('dot-edit-layout-sidebar')); - component = hostComponentfixture.debugElement.query( - By.css('dot-edit-layout-sidebar') - ).componentInstance; - hostComponentfixture.detectChanges(); - }); - - it('should have the right header for the Sidebar Header', () => { - const headerSelector = de.query(By.css('h6')); - expect(headerSelector.nativeElement.outerText).toBe('Sidebar'); - }); - - it('should call the write value and transform the containers data', () => { - hostComponentfixture.componentInstance.form = new UntypedFormGroup({ - sidebar: new UntypedFormControl({ - containers: [ - { identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', uuid: '' }, - { identifier: '56bd55ea-b04b-480d-9e37-5d6f9217dcc3', uuid: '' } - ], - location: 'left', - width: 'small' - }) - }); - - dotEditLayoutService = hostComponentfixture.debugElement - .query(By.css('dot-edit-layout-sidebar')) - .injector.get(DotEditLayoutService); - - spyOn(dotEditLayoutService, 'getDotLayoutSidebar').and.returnValue(processedContainers); - - hostComponentfixture.detectChanges(); - expect(dotEditLayoutService.getDotLayoutSidebar).toHaveBeenCalled(); - expect(component.containers).toBe(processedContainers); - }); - - it('should transform containers raw data from component "dot-container-selector" into proper data to be saved in the BE', () => { - const containerSelector: DebugElement = hostComponentfixture.debugElement.query( - By.css('dot-container-selector') - ); - const mockContainers = mockDotContainers(); - const transformedValue = { - containers: [ - { - identifier: mockContainers[Object.keys(mockContainers)[0]].container.identifier, - uuid: undefined - }, - { - identifier: mockContainers[Object.keys(mockContainers)[1]].container.path, - uuid: undefined - } - ], - location: 'left', - width: 'small' - }; - spyOn(component, 'updateAndPropagate').and.callThrough(); - spyOn(component, 'propagateChange'); - containerSelector.triggerEventHandler('change', processedContainers); - component.updateAndPropagate(processedContainers); - expect(component.updateAndPropagate).toHaveBeenCalled(); - expect(component.propagateChange).toHaveBeenCalledWith(transformedValue); - }); - - it('should propagate call from component "dot-sidebar-properties" into parent container', () => { - const sidebarProperties: DebugElement = hostComponentfixture.debugElement.query( - By.css('dot-sidebar-properties') - ); - spyOn(component, 'updateAndPropagate').and.callThrough(); - spyOn(component, 'propagateChange'); - sidebarProperties.triggerEventHandler('change', ''); - component.updateAndPropagate(); - expect(component.updateAndPropagate).toHaveBeenCalled(); - expect(component.propagateChange).toHaveBeenCalled(); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.ts deleted file mode 100644 index dbc81ff495f7..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Component, forwardRef } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { DotContainerColumnBox } from '@dotcms/app/shared/models/dot-edit-layout-designer'; -import { DotLayoutSideBar } from '@dotcms/dotcms-models'; - -/** - * Component in charge of update the model that will be used in the sidebar display containers - * - * @implements {OnInit} - */ -@Component({ - selector: 'dot-edit-layout-sidebar', - templateUrl: './dot-edit-layout-sidebar.component.html', - styleUrls: ['./dot-edit-layout-sidebar.component.scss'], - providers: [ - { - multi: true, - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DotEditLayoutSidebarComponent) - } - ] -}) -export class DotEditLayoutSidebarComponent implements ControlValueAccessor { - containers: DotContainerColumnBox[]; - value: DotLayoutSideBar; - - constructor( - private dotEditLayoutService: DotEditLayoutService, - private templateContainersCacheService: DotTemplateContainersCacheService - ) {} - - /** - * Returns DotContainerColumnBox model. - * - * @param DotContainerColumnBox[] containers - * @returns DotLayoutSideBar - * @memberof DotEditLayoutSidebarComponent - */ - getModel(containers: DotContainerColumnBox[]): DotLayoutSideBar { - if (containers) { - this.value.containers = containers.map((item) => { - return { - identifier: this.templateContainersCacheService.getContainerReference( - item.container - ), - uuid: item.uuid - }; - }); - } - - return this.value; - } - - propagateChange = (_: unknown) => { - /**/ - }; - - /** - * Set the function to be called when the control receives a change event. - * - * @param ()=>{} fn - * @memberof DotEditLayoutSidebarComponent - */ - registerOnChange( - fn: () => { - /**/ - } - ): void { - this.propagateChange = fn; - } - - registerOnTouched(): void { - /**/ - } - - /** - * Update model and propagate changes - * - * @param DotContainerColumnBox[] containers - * @memberof DotEditLayoutSidebarComponent - */ - updateAndPropagate(containers?: DotContainerColumnBox[]): void { - this.propagateChange(containers ? this.getModel(containers) : this.value); - } - - /** - * Write a new value to the element - * - * @param DotLayoutSideBar value - * @memberof DotEditLayoutSidebarComponent - */ - writeValue(value: DotLayoutSideBar): void { - if (value) { - this.value = value || null; - this.setContainersValue(); - } - } - - private setContainersValue(): void { - this.containers = this.dotEditLayoutService.getDotLayoutSidebar(this.value.containers); - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.module.ts deleted file mode 100644 index def0544fdbee..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; - -import { DotActionButtonModule } from '@components/_common/dot-action-button/dot-action-button.module'; -import { DotContainerSelectorLayoutModule } from '@components/dot-container-selector-layout/dot-container-selector-layout.module'; -import { DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; - -import { DotEditLayoutSidebarComponent } from './dot-edit-layout-sidebar.component'; - -import { DotSidebarPropertiesModule } from '../dot-sidebar-properties/dot-sidebar-properties.module'; - -@NgModule({ - declarations: [DotEditLayoutSidebarComponent], - imports: [ - CommonModule, - DotActionButtonModule, - FormsModule, - DotContainerSelectorLayoutModule, - ButtonModule, - DotSidebarPropertiesModule, - DotSafeHtmlPipe, - DotMessagePipe - ], - exports: [DotEditLayoutSidebarComponent], - providers: [] -}) -export class DotEditLayoutSidebarModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.html deleted file mode 100644 index fa29d8a8faa1..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.html +++ /dev/null @@ -1,31 +0,0 @@ -
-
- {{ 'editpage.layout.designer.header' | dm }} -
-
-
- -
-
-
- -
-
-
- -
-
- -
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.scss deleted file mode 100644 index 9cf6cf07c14c..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.scss +++ /dev/null @@ -1,110 +0,0 @@ -@use "variables" as *; -@import "mixins"; - -:host { - background-color: $white; - box-shadow: $shadow-xs; - display: flex; - flex-grow: 1; - margin: $spacing-3; - overflow-x: hidden; - overflow-y: auto; - padding: $spacing-2 $spacing-8 $spacing-2 $spacing-2; -} - -.dot-layout-designer { - display: flex; - flex-direction: column; - flex-grow: 1; -} - -.dot-layout-designer__main { - display: flex; - flex-grow: 1; -} - -.dot-layout-designer__footer, -.dot-layout-designer__header, -[class^="dot-layout-designer__sidebar"] { - align-items: center; - background: $color-palette-gray-200; - color: $color-palette-gray-700; - display: flex; - flex-shrink: 0; - font-size: $font-size-xl; - justify-content: center; -} - -[class^="dot-layout-designer__sidebar"] { - align-items: normal; - - dot-container-selector { - width: 100%; - } -} - -.dot-layout-designer__sidebar--left, -.dot-layout-designer__sidebar--right { - border: 1px solid $color-palette-gray-500; -} - -.dot-layout-designer__sidebar { - &--small { - width: 200px; - } - - &--medium { - width: 250px; - } - - &--large { - width: 300px; - } - - &--left { - margin-right: $spacing-3; - } - - &--right { - margin-left: $spacing-9; - } -} - -.dot-layout-designer__footer, -.dot-layout-designer__header { - height: 80px; -} - -.dot-layout-designer__header, -.dot-layout-designer__main { - margin-bottom: $spacing-3; -} - -.dot-layout-designer__body { - display: flex; - flex-grow: 1; - flex-direction: column; - - &::ng-deep .action-button--no-label .p-button.p-button-icon-only { - height: $field-height-md; - width: $field-height-md; - font-size: $font-size-sm; - } -} - -.dot-layout-designer__toolbar-add { - align-self: flex-end; - margin-bottom: $spacing-1; -} - -.dot-layout-designer__grid-guides { - background: repeating-linear-gradient( - -90deg, - transparent, - transparent $spacing-1, - $color-palette-gray-200 $spacing-1, - $color-palette-gray-200 calc(100% / 12) - ); - flex-grow: 1; - margin-right: -$spacing-1; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.spec.ts deleted file mode 100644 index 5df502de5de4..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.spec.ts +++ /dev/null @@ -1,259 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { Component, DebugElement, forwardRef, Input, OnInit } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { - ControlValueAccessor, - FormsModule, - NG_VALUE_ACCESSOR, - ReactiveFormsModule, - UntypedFormBuilder, - UntypedFormGroup -} from '@angular/forms'; -import { By } from '@angular/platform-browser'; - -import { DotMessageService } from '@dotcms/data-access'; -import { DotLayout } from '@dotcms/dotcms-models'; -import { DotMessagePipe } from '@dotcms/ui'; -import { mockDotLayout } from '@dotcms/utils-testing'; - -import { DotLayoutDesignerComponent } from './dot-layout-designer.component'; - -@Component({ - selector: 'dot-edit-layout-grid', - template: '', - providers: [ - { - multi: true, - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MockDotEditLayoutGridComponent) - } - ] -}) -export class MockDotEditLayoutGridComponent implements ControlValueAccessor { - propagateChange = (_: any) => {}; - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(): void {} - - writeValue(): void {} -} - -@Component({ - selector: 'dot-edit-layout-sidebar', - template: '', - providers: [ - { - multi: true, - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MockDotEditLayoutSidebarComponent) - } - ] -}) -export class MockDotEditLayoutSidebarComponent implements ControlValueAccessor { - propagateChange = (_: any) => {}; - - registerOnChange(fn: any): void { - this.propagateChange = fn; - } - - registerOnTouched(): void {} - - writeValue(): void {} -} - -@Component({ - template: ` -
- -
- ` -}) -class TestHostComponent implements OnInit { - @Input() - layout: DotLayout; - - form: UntypedFormGroup; - - constructor(private fb: UntypedFormBuilder) {} - - ngOnInit() { - this.form = this.fb.group({ - layout: this.fb.group(this.layout) - }); - } -} - -describe('DotLayoutDesignerComponent', () => { - let hostFixture: ComponentFixture; - let hostComponent: TestHostComponent; - let hostDe: DebugElement; - let component: DotLayoutDesignerComponent; - let de: DebugElement; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ - TestHostComponent, - DotLayoutDesignerComponent, - MockDotEditLayoutGridComponent, - MockDotEditLayoutSidebarComponent - ], - imports: [DotMessagePipe, FormsModule, ReactiveFormsModule], - providers: [ - { - provide: DotMessageService, - useValue: { - get(value) { - const map = { - 'editpage.layout.designer.header': 'HEADER', - 'editpage.layout.designer.footer': 'FOOTER' - }; - - return map[value]; - } - } - } - ] - }).compileComponents(); - })); - - beforeEach(() => { - hostFixture = TestBed.createComponent(TestHostComponent); - hostComponent = hostFixture.componentInstance; - hostDe = hostFixture.debugElement; - - de = hostDe.query(By.css('dot-layout-designer')); - component = de.componentInstance; - }); - - describe('default', () => { - beforeEach(() => { - hostComponent.layout = { - ...mockDotLayout(), - sidebar: { - location: '', - containers: [], - width: 'small' - } - }; - hostFixture.detectChanges(); - }); - - it('should NOT show header in the template', () => { - const headerElem: DebugElement = de.query(By.css('.dot-layout-designer__header')); - expect(headerElem).toBe(null); - }); - - it('should NOT show footer in the template', () => { - const footerElem: DebugElement = de.query(By.css('.dot-layout-designer__footer')); - expect(footerElem).toBe(null); - }); - - it('should NOT show a sidebar', () => { - const sidebar: DebugElement = de.query( - By.css('[class^="dot-layout-designer__sidebar"]') - ); - expect(sidebar).toBe(null); - }); - - describe('dot-edit-layout-grid', () => { - let gridLayout: DebugElement; - - beforeEach(() => { - gridLayout = de.query(By.css('dot-edit-layout-grid')); - }); - - it('should show dot-edit-layout-grid', () => { - expect(gridLayout).toBeDefined(); - }); - - it('should pass body as form control', () => { - expect(gridLayout.attributes.formControlName).toBe('body'); - }); - }); - }); - - describe('filled', () => { - describe('header and footer', () => { - beforeEach(() => { - hostComponent.layout = { - width: '', - title: '', - header: true, - footer: true, - body: mockDotLayout().body, - sidebar: { - location: '', - containers: [], - width: 'small' - } - }; - hostFixture.detectChanges(); - }); - - it('should show header in the template', () => { - const headerElem: DebugElement = de.query(By.css('.dot-layout-designer__header')); - expect(headerElem).toBeTruthy(); - }); - - it('should show footer in the template', () => { - const footerElem: DebugElement = de.query(By.css('.dot-layout-designer__footer')); - expect(footerElem).toBeTruthy(); - }); - - it('should have the right label for the Header', () => { - const headerSelector = de.query(By.css('.dot-layout-designer__header')); - expect(headerSelector.nativeElement.outerText).toBe('HEADER'); - }); - - it('should have the right label for the Footer', () => { - const headerSelector = de.query(By.css('.dot-layout-designer__footer')); - expect(headerSelector.nativeElement.outerText).toBe('FOOTER'); - }); - }); - - describe('sidebar size and position', () => { - beforeEach(() => { - hostComponent.layout = mockDotLayout(); - hostFixture.detectChanges(); - }); - - it('should show', () => { - const sidebar: DebugElement = de.query( - By.css('.dot-layout-designer__sidebar--left') - ); - expect(sidebar).toBeTruthy(); - }); - - it('should show sidebar position correctly', () => { - const positions = ['left', 'right']; - positions.forEach((position) => { - component.group.control.get('sidebar').value.location = position; - hostFixture.detectChanges(); - const sidebar: DebugElement = de.query( - By.css(`.dot-layout-designer__sidebar--${position}`) - ); - expect(sidebar).toBeTruthy(position); - }); - }); - - it('it should set sidebar size correctly', () => { - const sizes = ['small', 'medium', 'large']; - - sizes.forEach((size) => { - component.group.control.get('sidebar').value.width = size; - hostFixture.detectChanges(); - const sidebar: DebugElement = de.query( - By.css(`.dot-layout-designer__sidebar--${size}`) - ); - expect(sidebar).toBeDefined(); - }); - }); - }); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.ts deleted file mode 100644 index 19dc310a21d6..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; -import { ControlContainer } from '@angular/forms'; - -@Component({ - selector: 'dot-layout-designer', - templateUrl: './dot-layout-designer.component.html', - styleUrls: ['./dot-layout-designer.component.scss'] -}) -export class DotLayoutDesignerComponent { - constructor(public group: ControlContainer) {} -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.module.ts deleted file mode 100644 index df3297d4eadf..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-layout-designer/dot-layout-designer.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { DotEditLayoutGridModule } from '@components/dot-edit-layout-designer/components/dot-edit-layout-grid/dot-edit-layout-grid.module'; -import { DotEditLayoutSidebarModule } from '@components/dot-edit-layout-designer/components/dot-edit-layout-sidebar/dot-edit-layout-sidebar.module'; -import { DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; - -import { DotLayoutDesignerComponent } from './dot-layout-designer.component'; - -@NgModule({ - imports: [ - CommonModule, - DotEditLayoutSidebarModule, - DotEditLayoutGridModule, - DotSafeHtmlPipe, - FormsModule, - ReactiveFormsModule, - DotMessagePipe - ], - declarations: [DotLayoutDesignerComponent], - exports: [DotLayoutDesignerComponent] -}) -export class DotLayoutDesignerModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.html deleted file mode 100644 index 4c6cbb823851..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.html +++ /dev/null @@ -1,36 +0,0 @@ - -
-
- -
-
- -
-
- -
-
-
- - diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.scss deleted file mode 100644 index fe4cd8ef811e..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@use "variables" as *; - -:host { - right: $spacing-1; - top: $spacing-1; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.spec.ts deleted file mode 100644 index b4b3626f8b7f..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { DebugElement } from '@angular/core'; -import { ComponentFixture } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { OverlayPanelModule } from 'primeng/overlaypanel'; - -import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; -import { DotEventsService, DotMessageService } from '@dotcms/data-access'; -import { DotMessagePipe } from '@dotcms/ui'; -import { MockDotMessageService } from '@dotcms/utils-testing'; - -import { DotSidebarPropertiesComponent } from './dot-sidebar-properties.component'; - -describe('DotSidebarPropertiesComponent', () => { - let component: DotSidebarPropertiesComponent; - let fixture: ComponentFixture; - let de: DebugElement; - let dotEventsService: DotEventsService; - let mainButton: HTMLElement; - - beforeEach(() => { - const messageServiceMock = new MockDotMessageService({ - 'editpage.layout.sidebar.width.small': 'Small', - 'editpage.layout.sidebar.width.medium': 'Medium', - 'editpage.layout.sidebar.width.large': 'Large', - 'editpage.layout.sidebar.width.open': 'Open' - }); - - DOTTestBed.configureTestingModule({ - declarations: [DotSidebarPropertiesComponent], - imports: [OverlayPanelModule, BrowserAnimationsModule, DotMessagePipe], - providers: [{ provide: DotMessageService, useValue: messageServiceMock }] - }); - - fixture = DOTTestBed.createComponent(DotSidebarPropertiesComponent); - de = fixture.debugElement; - component = fixture.componentInstance; - dotEventsService = de.injector.get(DotEventsService); - - mainButton = de.query(By.css('button')).nativeElement; - mainButton.dispatchEvent(new MouseEvent('click')); - fixture.detectChanges(); - }); - - it('should has an overlay panel', () => { - const pOverlayPanel = fixture.debugElement.query(By.css('p-overlaypanel')); - expect(pOverlayPanel).toBeDefined(); - }); - - it('should has 3 radio buttons', () => { - const radioButtons = fixture.debugElement.queryAll( - By.css('.dot-sidebar-properties__radio-buttons-container p-radioButton') - ); - - expect(radioButtons.length).toEqual(3); - expect(radioButtons[0].attributes.value).toEqual('small'); - expect(radioButtons[1].attributes.value).toEqual('medium'); - expect(radioButtons[2].attributes.value).toEqual('large'); - }); - - it('should toggle overlay panel', () => { - const button = fixture.debugElement.query(By.css('button')); - spyOn(component.overlay, 'toggle'); - - button.nativeElement.click(); - expect(component.overlay.toggle).toHaveBeenCalledTimes(1); - }); - - it('should hide overlay panel when a sidebar size property is clicked', () => { - spyOn(component.overlay, 'hide'); - const radioButtons = fixture.debugElement.queryAll( - By.css('.dot-sidebar-properties__radio-buttons-container p-radioButton') - ); - radioButtons[0].nativeElement.click(); - expect(component.overlay.hide).toHaveBeenCalledTimes(1); - }); - - it('should send a layout-sidebar-change notification when a sidebar size property is updated', () => { - spyOn(component.switch, 'emit'); - spyOn(dotEventsService, 'notify'); - const radioButtons = fixture.debugElement.queryAll( - By.css('.dot-sidebar-properties__radio-buttons-container p-radioButton') - ); - radioButtons[0].nativeElement.click(); - expect(dotEventsService.notify).toHaveBeenCalledWith('layout-sidebar-change'); - expect(component.switch.emit).toHaveBeenCalled(); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.ts deleted file mode 100644 index 1cf97a055d18..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Component, EventEmitter, forwardRef, OnInit, Output, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { OverlayPanel } from 'primeng/overlaypanel'; - -import { DotEventsService } from '@dotcms/data-access'; -import { DotLayoutSideBar } from '@dotcms/dotcms-models'; - -@Component({ - selector: 'dot-sidebar-properties', - templateUrl: './dot-sidebar-properties.component.html', - styleUrls: ['./dot-sidebar-properties.component.scss'], - providers: [ - { - multi: true, - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DotSidebarPropertiesComponent) - } - ] -}) -export class DotSidebarPropertiesComponent implements OnInit, ControlValueAccessor { - value: DotLayoutSideBar; - @ViewChild('overlay', { static: true }) overlay: OverlayPanel; - @Output() switch: EventEmitter = new EventEmitter(); - - constructor(private dotEventsService: DotEventsService) {} - - propagateChange = (_: unknown) => { - /**/ - }; - - ngOnInit() { - this.value = { - containers: [], - location: '', - width: '' - }; - } - - /** - * Hides overlay panel and emits a notification to repainted the Grid - * - * @memberof DotSidebarPropertiesComponent - */ - changeSidebarSize(): void { - this.overlay.hide(); - this.dotEventsService.notify('layout-sidebar-change'); - this.switch.emit(); - } - - /** - * Write a new value to the property item - * @param DotLayoutSideBar value - * @memberof DotSidebarPropertiesComponent - */ - writeValue(value: DotLayoutSideBar): void { - if (value) { - this.value = value; - } - } - - /** - * Set the function to be called when the control receives a change event - * @param () => {} fn - * @memberof DotSidebarPropertiesComponent - */ - registerOnChange( - fn: () => { - /* */ - } - ): void { - this.propagateChange = fn; - } - - registerOnTouched(): void { - /* */ - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.module.ts deleted file mode 100644 index d7f54738bf61..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-sidebar-properties/dot-sidebar-properties.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { OverlayPanelModule } from 'primeng/overlaypanel'; -import { RadioButtonModule } from 'primeng/radiobutton'; - -import { DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; - -import { DotSidebarPropertiesComponent } from './dot-sidebar-properties.component'; - -@NgModule({ - declarations: [DotSidebarPropertiesComponent], - imports: [ - ButtonModule, - CommonModule, - FormsModule, - RadioButtonModule, - OverlayPanelModule, - ReactiveFormsModule, - DotSafeHtmlPipe, - DotMessagePipe - ], - exports: [DotSidebarPropertiesComponent] -}) -export class DotSidebarPropertiesModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.html deleted file mode 100644 index 0ed3d9403afc..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.html +++ /dev/null @@ -1,73 +0,0 @@ - -
- - -
-
- - -
- - - - -
{{ theme.name }}
-
-
-
-
-
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.scss deleted file mode 100644 index 586cfbadb71d..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.scss +++ /dev/null @@ -1,108 +0,0 @@ -@use "variables" as *; -@import "dotcms-theme/utils/theme-variables"; - -$theme-item-dimension: 240px; - -:host ::ng-deep { - dot-site-selector { - margin-left: $spacing-2; - - dot-searchable-dropdown .p-button, - .site-selector__title { - font-size: $font-size-lmd; - min-width: 200px; - } - } - - .dialog__content { - min-width: 77.85rem; - padding: $spacing-3 $spacing-4 $spacing-6; - } - - @media (max-width: 1100px) { - .dialog__content { - min-width: calc(100vw - 5vw) !important; - } - } - - .p-dataview-emptymessage { - text-align: center; - padding-top: $spacing-7 * 2; - } - - .p-dataview-content { - min-height: $theme-item-dimension * 2; - .p-g { - margin: 0; - } - } - - .p-dataview-content .p-widget-content.p-g-12 { - text-align: center; - font-size: $font-size-lmd; - margin-top: $spacing-7; - } - - .paginator .dot-theme-container { - .p-dataView { - padding-bottom: 56px; - position: relative; - } - } -} - -.dot-theme__header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.dot-theme-container { - margin-top: $spacing-1; -} - -.dot-theme-search-box { - position: relative; - - input { - font-size: $font-size-lmd; - } - - dot-icon { - position: absolute; - top: 50%; - transform: translateY(-50%); - right: $spacing-1; - } -} - -.dot-theme-item { - align-items: center; - background: $color-palette-gray-200; - border-radius: 4px; - cursor: pointer; - display: flex; - flex-direction: column; - height: $theme-item-dimension; - margin: $spacing-3 $spacing-1 0; - padding: $spacing-4 $spacing-3; - transition: background-color $field-animation-speed ease-in; - width: $theme-item-dimension; - - svg { - fill: $color-palette-gray-700; - border-radius: 6px; - } - - &:hover, - &.active { - box-shadow: $shadow-s; - background: $white; - } -} - -.dot-theme-iteme__image { - border-radius: 6px; - width: 9.28rem; - height: 9.28rem; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.spec.ts deleted file mode 100644 index d70056e82565..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { of } from 'rxjs'; - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, DebugElement, Input } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { DataViewModule } from 'primeng/dataview'; - -import { - DotEventsService, - DotMessageService, - DotThemesService, - PaginatorService -} from '@dotcms/data-access'; -import { CoreWebService, Site, SiteService } from '@dotcms/dotcms-js'; -import { DotDialogModule, DotIconModule, DotMessagePipe } from '@dotcms/ui'; -import { - CoreWebServiceMock, - DotThemesServiceMock, - MockDotMessageService, - mockDotThemes, - mockSites, - SiteServiceMock -} from '@dotcms/utils-testing'; - -import { DotThemeSelectorComponent } from './dot-theme-selector.component'; - -@Component({ - selector: 'dot-site-selector', - template: ` - - ` -}) -class MockDotSiteSelectorComponent { - @Input() system; - @Input() archive; - searchableDropdown = { - handleClick: () => { - // - } - }; -} - -describe('DotThemeSelectorComponent', () => { - let component: DotThemeSelectorComponent; - let fixture: ComponentFixture; - let de: DebugElement; - const messageServiceMock = new MockDotMessageService({ - 'editpage.layout.theme.header': 'Header', - 'editpage.layout.theme.search': 'Search', - 'dot.common.apply': 'Apply', - 'dot.common.cancel': 'Cancel' - }); - const siteServiceMock = new SiteServiceMock(); - let dialog; - let paginatorService: PaginatorService; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [DotThemeSelectorComponent, MockDotSiteSelectorComponent], - imports: [ - DataViewModule, - BrowserAnimationsModule, - DotDialogModule, - DotIconModule, - DotMessagePipe, - HttpClientTestingModule - ], - providers: [ - { - provide: DotThemesService, - useClass: DotThemesServiceMock - }, - { - provide: DotMessageService, - useValue: messageServiceMock - }, - { provide: SiteService, useValue: siteServiceMock }, - { provide: CoreWebService, useClass: CoreWebServiceMock }, - PaginatorService, - DotEventsService - ] - }); - - fixture = TestBed.createComponent(DotThemeSelectorComponent); - component = fixture.componentInstance; - de = fixture.debugElement; - dialog = de.query(By.css('dot-dialog')).componentInstance; - component.value = { ...mockDotThemes[0] }; - paginatorService = de.injector.get(PaginatorService); - }); - - afterEach(() => { - component.visible = false; - fixture.detectChanges(); - }); - - describe('Dialog', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - describe('header', () => { - it('should have site-selector', () => { - const siteSelector = de.query( - By.css('[data-testId="header"] [data-testId="siteSelector"]') - ); - expect(siteSelector).not.toBeNull(); - expect(siteSelector.componentInstance.system).toEqual(true); - expect(siteSelector.componentInstance.archive).toEqual(false); - }); - - it('should have dot-icon', () => { - const icon = de.query( - By.css( - '[data-testId="header"] .dot-theme-search-box [data-testId="searchIcon"]' - ) - ); - expect(icon.attributes.name).toBe('search'); - }); - - it('should have input', () => { - const input = de.query( - By.css('[data-testId="header"] [data-testId="searchInput"]') - ); - expect(input.attributes.pInputText).toBeDefined(); - expect(input.attributes.placeholder).toBe('Search'); - }); - }); - - it('should be visible on init', () => { - expect(dialog.visible).toBeTruthy(); - }); - - it('should have set dialog actions', () => { - expect(component.dialogActions).toEqual({ - accept: { - label: 'Apply', - disabled: true, - action: jasmine.any(Function) - }, - cancel: { - label: 'Cancel' - } - }); - }); - }); - - describe('On Init', () => { - it('should set url, the page size and hostid for the pagination service', () => { - paginatorService.searchParam = 'test'; - spyOn(paginatorService, 'setExtraParams'); - spyOn(paginatorService, 'deleteExtraParams'); - fixture.detectChanges(); - expect(paginatorService.paginationPerPage).toBe(8); - expect(paginatorService.url).toBe('v1/themes'); - expect(paginatorService.setExtraParams).toHaveBeenCalledWith( - 'hostId', - mockDotThemes[0].hostId - ); - expect(paginatorService.deleteExtraParams).toHaveBeenCalledWith('searchParam'); - }); - - it('should set the current theme variable based on the Input value', () => { - const value = { ...mockDotThemes[0] }; - component.value = value; - fixture.detectChanges(); - expect(component.current).toBe(value); - }); - - it('should call pagination service with offset of 0 ', () => { - spyOn(component.cd, 'detectChanges').and.callThrough(); - spyOn(paginatorService, 'getWithOffset').and.returnValue(of([...mockDotThemes])); - fixture.detectChanges(); - - expect(paginatorService.getWithOffset).toHaveBeenCalledWith(0); - expect(component.cd.detectChanges).toHaveBeenCalledTimes(1); - }); - - it('should disable the apply button', () => { - fixture.detectChanges(); - expect(component.dialogActions.accept.disabled).toBe(true); - }); - - it('should show theme image when available', () => { - const systemTheme = { - name: 'system Theme', - title: 'Theme tittle', - inode: '1', - themeThumbnail: '/system/theme/url', - identifier: 'SYSTEM_THEME', - hostId: '1', - host: { - hostName: 'Test', - inode: '3', - identifier: '345' - } - }; - - spyOn(paginatorService, 'getWithOffset').and.returnValue( - of([...mockDotThemes, systemTheme]) - ); - component.siteChange(mockSites[0]); - fixture.detectChanges(); - const themeImages = de.queryAll(By.css('[data-testId="themeImage"]')); - expect(themeImages[0].nativeElement.src).toContain( - `/dA/${mockDotThemes[2].themeThumbnail}/130w/130h/thumbnail.png` - ); - expect(themeImages[1].nativeElement.src).toContain(systemTheme.themeThumbnail); - }); - }); - - describe('User interaction', () => { - beforeEach(() => { - spyOn(paginatorService, 'getWithOffset').and.returnValue(of(mockDotThemes)); - }); - - it('should set pagination, call endpoint and clear search field on site change ', () => { - spyOn(component, 'paginate'); - spyOn(paginatorService, 'setExtraParams'); - component.siteChange(mockSites[0]); - fixture.detectChanges(); - - expect(component.searchInput.nativeElement.value).toBe(''); - expect(paginatorService.setExtraParams).toHaveBeenCalledWith( - 'hostId', - mockSites[0].identifier - ); - expect(paginatorService.setExtraParams).toHaveBeenCalledWith('searchParam', ''); - expect(component.paginate).toHaveBeenCalledWith({ first: 0 }); - }); - - it('should set the current value when the user click a specific theme', () => { - spyOn(component, 'selectTheme').and.callThrough(); - component.paginate({ first: 0 }); - fixture.detectChanges(); - const themes: DebugElement[] = fixture.debugElement.queryAll(By.css('.dot-theme-item')); - themes[1].nativeElement.click(); - - expect(component.current).toBe(mockDotThemes[1]); - expect(component.selectTheme).toHaveBeenCalled(); - }); - - it('should active the apply button and set active when user select a different theme than the one in value', () => { - component.paginate({ first: 0 }); - fixture.detectChanges(); - const themes: DebugElement[] = fixture.debugElement.queryAll(By.css('.dot-theme-item')); - themes[1].nativeElement.click(); - fixture.detectChanges(); - - expect(component.dialogActions.accept.disabled).toBe(false); - }); - - it('should call theme enpoint on search', fakeAsync(() => { - spyOn(component, 'paginate'); - fixture.detectChanges(); - component.searchInput.nativeElement.value = 'test'; - component.searchInput.nativeElement.dispatchEvent(new Event('keyup')); - tick(550); - - expect(paginatorService.extraParams.get('searchParam')).toBe('test'); - expect(component.paginate).toHaveBeenCalled(); - })); - }); - - describe('User interaction empty', () => { - let siteService: SiteService; - - beforeEach(() => { - siteService = TestBed.inject(SiteService); - spyOn(paginatorService, 'getWithOffset').and.returnValue(of([])); - }); - - it(' should set system host ', () => { - spyOn(siteService, 'getSiteById').and.returnValue(of({} as Site)); - fixture.detectChanges(); - expect(siteService.getSiteById).toHaveBeenCalledOnceWith('SYSTEM_HOST'); - }); - - it(' should set system host just once ', () => { - spyOn(siteService, 'getSiteById').and.returnValue(of({} as Site)); - fixture.detectChanges(); - setTimeout(() => component.siteChange({ identifier: '123' } as Site), 0); // simulate user site change. - expect(siteService.getSiteById).toHaveBeenCalledOnceWith('SYSTEM_HOST'); - }); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.ts deleted file mode 100644 index 16aa5a89b0a9..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.component.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { fromEvent as observableFromEvent, Subject } from 'rxjs'; - -import { - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, - ViewChild -} from '@angular/core'; - -import { LazyLoadEvent } from 'primeng/api'; -import { DataView } from 'primeng/dataview'; - -import { debounceTime, take, takeUntil } from 'rxjs/operators'; - -import { DotSiteSelectorComponent } from '@components/_common/dot-site-selector/dot-site-selector.component'; -import { DotMessageService, PaginatorService } from '@dotcms/data-access'; -import { Site, SiteService } from '@dotcms/dotcms-js'; -import { DotDialogActions, DotTheme } from '@dotcms/dotcms-models'; - -/** - * The DotThemeSelectorComponent is modal that - * show the themes of the available hosts - * @export - * @class DotThemeSelectorComponent - */ -@Component({ - providers: [PaginatorService], - selector: 'dot-theme-selector', - templateUrl: './dot-theme-selector.component.html', - styleUrls: ['./dot-theme-selector.component.scss'] -}) -export class DotThemeSelectorComponent implements OnInit, OnDestroy { - themes: DotTheme[] = []; - - @Input() - value: DotTheme; - - @Output() - selected = new EventEmitter(); - - @Output() - shutdown = new EventEmitter(); - - @ViewChild('searchInput', { static: true }) - searchInput: ElementRef; - - @ViewChild('dataView', { static: true }) - dataView: DataView; - - @ViewChild('siteSelector', { static: true }) - siteSelector: DotSiteSelectorComponent; - - current: DotTheme; - visible = true; - dialogActions: DotDialogActions; - - private destroy$: Subject = new Subject(); - private SEARCH_PARAM = 'searchParam'; - private initialLoad = true; - - constructor( - private dotMessageService: DotMessageService, - public paginatorService: PaginatorService, - private siteService: SiteService, - public cd: ChangeDetectorRef - ) {} - - ngOnInit() { - this.dialogActions = { - accept: { - label: this.dotMessageService.get('dot.common.apply'), - disabled: true, - action: () => { - this.apply(); - } - }, - cancel: { - label: this.dotMessageService.get('dot.common.cancel') - } - }; - this.current = this.value; - this.paginatorService.url = 'v1/themes'; - this.paginatorService.setExtraParams( - 'hostId', - this.current?.hostId || this.siteService.currentSite.identifier - ); - this.paginatorService.deleteExtraParams(this.SEARCH_PARAM); - this.paginatorService.paginationPerPage = 8; - observableFromEvent(this.searchInput.nativeElement, 'keyup') - .pipe(debounceTime(500), takeUntil(this.destroy$)) - .subscribe((keyboardEvent: Event) => { - this.filterThemes(keyboardEvent.target['value']); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - } - - /** - * Load new page of themes. - * @param LazyLoadEvent event - * - * @memberof DotThemeSelectorComponent - */ - paginate($event: LazyLoadEvent): void { - this.paginatorService - .getWithOffset($event.first) - .pipe(take(1)) - .subscribe((themes: DotTheme[]) => { - if (this.noThemesInInitialLoad(themes, $event)) { - this.siteService.getSiteById('SYSTEM_HOST').subscribe((site: Site) => { - this.siteSelector.searchableDropdown.handleClick(site); - this.cd.detectChanges(); - }); - } else { - this.themes = themes; - this.cd.detectChanges(); - } - - this.initialLoad = false; - }); - this.dataView.first = $event.first; - } - - /** - * Handle change in the host to load the corresponding themes. - * @param Site site - * - * @memberof DotThemeSelectorComponent - */ - siteChange(site: Site): void { - this.searchInput.nativeElement.value = null; - this.paginatorService.setExtraParams('hostId', site.identifier); - this.filterThemes(''); - } - - /** - * Set the selected Theme by the user while the modal is open. - * @param DotTheme theme - * - * @memberof DotThemeSelectorComponent - */ - selectTheme(theme: DotTheme): void { - this.current = theme; - this.dialogActions = { - ...this.dialogActions, - accept: { - ...this.dialogActions.accept, - disabled: this.value.inode === this.current.inode - } - }; - } - - /** - * Propagate the selected theme once the user apply the changes. - * - * @memberof DotThemeSelectorComponent - */ - apply(): void { - this.selected.emit(this.current); - } - - /** - * Propagate the shutdown event when the modal closes. - * - * @memberof DotThemeSelectorComponent - */ - hideDialog(): void { - this.shutdown.emit(false); - } - - private filterThemes(searchCriteria?: string): void { - this.paginatorService.setExtraParams(this.SEARCH_PARAM, searchCriteria); - this.paginate({ first: 0 }); - } - - private noThemesInInitialLoad(themes: DotTheme[], $event: LazyLoadEvent): boolean { - return this.initialLoad && !themes.length && !$event.first; - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.module.ts deleted file mode 100644 index 90ea32692f9c..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/components/dot-theme-selector/dot-theme-selector.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { DataViewModule } from 'primeng/dataview'; -import { DialogModule } from 'primeng/dialog'; -import { DropdownModule } from 'primeng/dropdown'; -import { InputTextModule } from 'primeng/inputtext'; - -import { DotSiteSelectorModule } from '@components/_common/dot-site-selector/dot-site-selector.module'; -import { DotThemesService } from '@dotcms/data-access'; -import { DotDialogModule, DotIconModule, DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; - -import { DotThemeSelectorComponent } from './dot-theme-selector.component'; - -@NgModule({ - declarations: [DotThemeSelectorComponent], - imports: [ - CommonModule, - DropdownModule, - ButtonModule, - FormsModule, - DialogModule, - DotSiteSelectorModule, - InputTextModule, - DataViewModule, - DotDialogModule, - DotIconModule, - DotSafeHtmlPipe, - DotMessagePipe - ], - exports: [DotThemeSelectorComponent], - providers: [DotThemesService] -}) -export class DotThemeSelectorModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.html deleted file mode 100644 index 46796cd4509f..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.html +++ /dev/null @@ -1,63 +0,0 @@ - - -
- -
- -
- - - - - - - - - - -
-
-
- -
diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.scss b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.scss deleted file mode 100644 index 0bd3c772e902..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.scss +++ /dev/null @@ -1,51 +0,0 @@ -@use "variables" as *; -@import "mixins"; - -:host { - min-width: 750px; - - ::ng-deep { - .p-button-vertical { - .p-button-label { - text-transform: capitalize !important; - } - } - } -} - -form { - display: flex; - flex-direction: column; - height: 100%; - margin-right: 80px; -} - -.dot-edit-layout__actions { - display: flex; - height: $dot-secondary-toolbar-height; - justify-content: flex-end; - padding: $spacing-3; -} - -.dot-edit-layout__toolbar { - background-color: $white; - display: block; - position: relative; - z-index: 1; - - ::ng-deep { - .p-toolbar { - border-bottom: solid 1px $color-palette-gray-500; - padding: $spacing-3; - } - } -} - -.dot-edit-layout__toolbar-action-cancel { - margin-right: $spacing-1; -} - -.dot-edit__layout-actions-right { - display: flex; - align-items: center; -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.spec.ts deleted file mode 100644 index ff0f6af3b5d8..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.spec.ts +++ /dev/null @@ -1,400 +0,0 @@ -import * as _ from 'lodash'; -import { of as observableOf, of } from 'rxjs'; - -import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - ControlContainer, - FormsModule, - ReactiveFormsModule, - UntypedFormGroup -} from '@angular/forms'; -import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ButtonModule } from 'primeng/button'; -import { TooltipModule } from 'primeng/tooltip'; - -import { DotActionButtonModule } from '@components/_common/dot-action-button/dot-action-button.module'; -import { DotGlobalMessageModule } from '@components/_common/dot-global-message/dot-global-message.module'; -import { DotSecondaryToolbarModule } from '@components/dot-secondary-toolbar'; -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { DotTemplateContainersCacheService } from '@dotcms/app/api/services/dot-template-containers-cache/dot-template-containers-cache.service'; -import { - DotEventsService, - DotHttpErrorManagerService, - DotMessageService, - DotRouterService, - DotThemesService, - DotGlobalMessageService -} from '@dotcms/data-access'; -import { DotTemplateDesigner, DotTheme } from '@dotcms/dotcms-models'; -import { DotFieldValidationMessageComponent, DotMessagePipe } from '@dotcms/ui'; -import { - cleanUpDialog, - DotThemesServiceMock, - mockDotLayout, - MockDotMessageService, - mockDotRenderedPage, - mockDotThemes -} from '@dotcms/utils-testing'; -import { DotEditPageInfoModule } from '@portlets/dot-edit-page/components/dot-edit-page-info/dot-edit-page-info.module'; - -import { DotEditLayoutDesignerComponent } from './dot-edit-layout-designer.component'; - -@Component({ - selector: 'dot-template-addtional-actions-menu', - template: '' -}) -class AdditionalOptionsMockComponent { - @Input() inode: ''; -} - -@Component({ - selector: 'dot-layout-properties', - template: '' -}) -class DotLayoutPropertiesMockComponent { - @Input() group: UntypedFormGroup; -} - -@Component({ - selector: 'dot-layout-designer', - template: '' -}) -class DotLayoutDesignerMockComponent { - constructor(public group: ControlContainer) {} -} - -@Component({ - selector: 'dot-theme-selector', - template: '' -}) -class DotThemeSelectorMockComponent { - @Input() value: DotTheme; - @Output() selected = new EventEmitter(); -} - -const messageServiceMock = new MockDotMessageService({ - 'dot.common.message.saving': 'saving...', - 'dot.common.cancel': 'Cancel', - 'editpage.layout.dialog.edit.page': 'Edit Page', - 'editpage.layout.dialog.edit.template': 'Edit Template', - 'editpage.layout.dialog.header': 'Edit some', - 'editpage.layout.dialog.info': 'This is the message', - 'editpage.layout.toolbar.action.save': 'Save', - 'editpage.layout.toolbar.save.template': 'Save as template', - 'editpage.layout.toolbar.template.name': 'Name of the template', - 'org.dotcms.frontend.content.submission.not.proper.permissions': 'No Read Permission' -}); - -let component: DotEditLayoutDesignerComponent; -let fixture: ComponentFixture; -let dotThemesService: DotThemesService; - -describe('DotEditLayoutDesignerComponent', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - DotEditLayoutDesignerComponent, - AdditionalOptionsMockComponent, - DotLayoutPropertiesMockComponent, - DotThemeSelectorMockComponent, - DotLayoutDesignerMockComponent - ], - imports: [ - DotMessagePipe, - DotActionButtonModule, - DotEditPageInfoModule, - DotSecondaryToolbarModule, - DotGlobalMessageModule, - DotFieldValidationMessageComponent, - FormsModule, - ReactiveFormsModule, - RouterTestingModule, - TooltipModule, - ButtonModule - ], - providers: [ - DotEditLayoutService, - DotThemesService, - { provide: DotMessageService, useValue: messageServiceMock }, - { provide: DotThemesService, useClass: DotThemesServiceMock }, - { - provide: DotRouterService, - useValue: { - goToSiteBrowser: jasmine.createSpy() - } - }, - { - provide: DotEventsService, - useValue: { - notify: jasmine.createSpy(), - listen: jasmine.createSpy().and.returnValue(of({})) - } - }, - { - provide: DotHttpErrorManagerService, - useValue: { - handle: jasmine.createSpy().and.returnValue(of({})) - } - }, - { - provide: DotTemplateContainersCacheService, - useValue: { - set: jasmine.createSpy - } - }, - { - provide: DotGlobalMessageService, - useValue: { - display: jasmine.createSpy(), - loading: jasmine.createSpy(), - customDisplay: jasmine.createSpy() - } - } - ] - }); - - fixture = TestBed.createComponent(DotEditLayoutDesignerComponent); - component = fixture.componentInstance; - dotThemesService = TestBed.inject(DotThemesService); - }); - - describe('edit layout', () => { - beforeEach(() => { - component.layout = mockDotLayout(); - component.theme = '123'; - component.disablePublish = false; - fixture.detectChanges(); - }); - // these need to be fixed when this is fixed correctly https://github.com/dotCMS/core/issues/18830 - it('should have dot-secondary-toolbar with right content', () => { - const dotSecondaryToolbar = fixture.debugElement.query(By.css('dot-secondary-toolbar')); - const dotEditPageInfo = fixture.debugElement.query( - By.css('dot-secondary-toolbar .main-toolbar-left dot-edit-page-info') - ); - const dotLayoutActions = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-action-themes') - ); - - expect(dotSecondaryToolbar).not.toBeNull(); - expect(dotEditPageInfo).not.toBeNull(); - expect(dotLayoutActions).not.toBeNull(); - }); - // these need to be fixed when this is fixed correctly https://github.com/dotCMS/core/issues/18830 - it('should show dot-edit-page-info', () => { - const dotEditPageInfo: DebugElement = fixture.debugElement.query( - By.css('dot-edit-page-info') - ); - expect(dotEditPageInfo).toBeTruthy(); - // TODO: NEED EXTRA EXPECTS - }); - - it('should enable publish button when editing the form.', () => { - component.form.get('title').setValue('Hello'); - fixture.detectChanges(); - expect(component.disablePublish).toBe(false); - }); - - it('should not show template name input', () => { - const templateNameInput: DebugElement = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-template-name') - ); - expect(templateNameInput).toBe(null); - }); - - it('should not show checkbox to save as template', () => { - const checkboxSave: DebugElement = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-save-template') - ); - expect(checkboxSave).toBe(null); - }); - - it('should emit pushAndPublish event when button clicked', () => { - spyOn(component.saveAndPublish, 'emit').and.callThrough(); - fixture.detectChanges(); - const publishButton = fixture.debugElement.query(By.css('[data-testId="publishBtn"]')); - publishButton.triggerEventHandler('click', null); - fixture.detectChanges(); - expect(component.saveAndPublish.emit).toHaveBeenCalledWith( - component.form.value as DotTemplateDesigner - ); - }); - - it('should save changes when editing the form.', () => { - spyOn(component.updateTemplate, 'emit'); - component.form.get('title').setValue('Hello'); - fixture.detectChanges(); - expect(component.updateTemplate.emit).toHaveBeenCalledTimes(1); - }); - - it('should show dot-layout-properties and bind attr correctly', () => { - fixture.detectChanges(); - const layoutProperties: DebugElement = fixture.debugElement.query( - By.css('dot-layout-properties') - ); - - expect(layoutProperties).toBeTruthy(); - expect(layoutProperties.componentInstance.group).toEqual(component.form.get('layout')); - }); - - it('should have dot-layout-designer', () => { - fixture.detectChanges(); - const layoutDesigner: DebugElement = fixture.debugElement.query( - By.css('dot-layout-designer') - ); - - expect(layoutDesigner.componentInstance.group.name).toEqual('layout'); - }); - - it('should not show dot-template-addtional-actions-menu', () => { - const aditionalOptions: DebugElement = fixture.debugElement.query( - By.css('dot-template-addtional-actions-menu') - ); - - expect(aditionalOptions).toBe(null); - }); - - it('should not show save as template checkbox', () => { - const saveAsTemplate: DebugElement = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-save-template') - ); - - expect(saveAsTemplate).toBe(null); - }); - - it('should set form model correctly', () => { - expect(component.form.value).toEqual({ - title: '', - themeId: '123', - layout: { - body: mockDotRenderedPage().layout.body, - header: mockDotRenderedPage().layout.header, - footer: mockDotRenderedPage().layout.footer, - sidebar: { - location: mockDotRenderedPage().layout.sidebar.location, - containers: mockDotRenderedPage().layout.sidebar.containers, - width: mockDotRenderedPage().layout.sidebar.width - }, - title: mockDotRenderedPage().layout.title, - width: mockDotRenderedPage().layout.width - } - }); - }); - }); - - describe('themes', () => { - let themeSelector: DotThemeSelectorMockComponent; - let themeButton; - - beforeEach(() => { - component.layout = mockDotLayout(); - component.themeDialogVisibility = true; - }); - - it('should expose theme selector component & Theme button be enabled', () => { - fixture.detectChanges(); - themeButton = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-action-themes') - ).nativeElement; - themeButton.click(); - themeSelector = fixture.debugElement.query( - By.css('dot-theme-selector') - ).componentInstance; - - expect(themeSelector).not.toBe(null); - }); - - it('should Theme button be disabled', () => { - spyOn(dotThemesService, 'get').and.returnValue(observableOf(null)); - fixture.detectChanges(); - const themeSelectorBtn = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-action-themes') - ); - expect(themeSelectorBtn.nativeElement.disabled).toBe(true); - expect(themeSelectorBtn.attributes['ng-reflect-text']).toBe('No Read Permission'); - }); - - it('should get the emitted value from themes and trigger a save', () => { - spyOn(component, 'changeThemeHandler').and.callThrough(); - fixture.detectChanges(); - themeButton = fixture.debugElement.query( - By.css('.dot-edit-layout__toolbar-action-themes') - ).nativeElement; - themeButton.click(); - themeSelector = fixture.debugElement.query( - By.css('dot-theme-selector') - ).componentInstance; - const mockTheme = _.cloneDeep(mockDotThemes[0]); - themeSelector.selected.emit(mockTheme); - expect(component.changeThemeHandler).toHaveBeenCalledWith(mockTheme); - }); - }); - - describe('containers model', () => { - beforeEach(() => { - component.layout = mockDotLayout(); - }); - - it('should have a sidebar containers', () => { - fixture.detectChanges(); - expect(component.form.value.layout.sidebar.containers).toEqual([ - { - identifier: 'fc193c82-8c32-4abe-ba8a-49522328c93e', - uuid: 'LEGACY_RELATION_TYPE' - } - ]); - }); - - it('should have a null sidebar containers', () => { - const layout = mockDotLayout(); - - component.layout = { - ...layout, - sidebar: { - ...layout.sidebar, - containers: [] - } - }; - fixture.detectChanges(); - expect(component.form.value.layout.sidebar.containers).toEqual([]); - }); - }); - - describe('edit layout No sidebars', () => { - beforeEach(() => { - const layout = mockDotLayout(); - - component.layout = { - ...layout, - sidebar: null - }; - component.theme = '123'; - fixture.detectChanges(); - }); - - it('should not break when sidebar property in layout is null', () => { - expect(component.form.value).toEqual({ - title: '', - themeId: '123', - layout: { - body: mockDotRenderedPage().layout.body, - header: mockDotRenderedPage().layout.header, - footer: mockDotRenderedPage().layout.footer, - sidebar: { - location: '', - containers: [], - width: 'small' - }, - title: '', - width: '' - } - }); - }); - }); - - afterEach(() => { - cleanUpDialog(fixture); - }); -}); diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.ts deleted file mode 100644 index ced345cfcac0..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.component.ts +++ /dev/null @@ -1,300 +0,0 @@ -import * as _ from 'lodash'; -import { Observable, Subject } from 'rxjs'; - -import { HttpErrorResponse } from '@angular/common/http'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges, - ViewChild -} from '@angular/core'; -import { FormControl, FormGroup, UntypedFormBuilder } from '@angular/forms'; - -import { take, takeUntil, tap } from 'rxjs/operators'; - -import { DotEditLayoutService } from '@dotcms/app/api/services/dot-edit-layout/dot-edit-layout.service'; -import { - DotEventsService, - DotHttpErrorHandled, - DotHttpErrorManagerService, - DotRouterService, - DotThemesService -} from '@dotcms/data-access'; -import { - DotLayout, - DotLayoutBody, - DotLayoutColumn, - DotLayoutRow, - DotLayoutSideBar, - DotPageContainer, - DotTemplateDesigner, - DotTheme -} from '@dotcms/dotcms-models'; - -type TemplateDesignerForm = { - title: FormControl; - themeId: FormControl; - layout: FormControl; -}; - -@Component({ - selector: 'dot-edit-layout-designer', - templateUrl: './dot-edit-layout-designer.component.html', - styleUrls: ['./dot-edit-layout-designer.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotEditLayoutDesignerComponent implements OnInit, OnDestroy, OnChanges { - @ViewChild('templateName') - templateName: ElementRef; - - @Input() - layout: DotLayout; - - @Input() - title = ''; - - @Input() - theme: string; - - @Input() - apiLink: string; - - @Input() - url: string; - - @Input() - disablePublish = true; - - @Output() - save: EventEmitter = new EventEmitter(); - - @Output() - saveAndPublish: EventEmitter = new EventEmitter(); - - @Output() - updateTemplate: EventEmitter = new EventEmitter(); - - form: FormGroup; - initialFormValue: DotTemplateDesigner; - themeDialogVisibility = false; - - currentTheme: DotTheme; - - saveAsTemplate: boolean; - showTemplateLayoutSelectionDialog = false; - - private destroy$: Subject = new Subject(); - - constructor( - private dotEditLayoutService: DotEditLayoutService, - private dotEventsService: DotEventsService, - private dotHttpErrorManagerService: DotHttpErrorManagerService, - private dotRouterService: DotRouterService, - private dotThemesService: DotThemesService, - private fb: UntypedFormBuilder, - private cd: ChangeDetectorRef - ) {} - - ngOnInit(): void { - this.setupLayout(); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.theme && !changes.theme.firstChange) { - this.form.get('themeId').setValue(this.theme); - this.updateModel(); - } - - if (changes.layout && !changes.layout.firstChange) { - this.setFormValue(changes.layout.currentValue); - } - } - - ngOnDestroy(): void { - this.destroy$.next(true); - this.destroy$.complete(); - } - - /** - * Calling the service to add a new box - * - * @memberof DotEditLayoutDesignerComponent - */ - addGridBox() { - this.dotEditLayoutService.addBox(); - } - - /** - * Emit save event - * - * @memberof DotEditLayoutDesignerComponent - */ - onSave(): void { - this.save.emit(this.form.value as DotTemplateDesigner); - } - - /** - * Emit publish event - * - * @memberof DotEditLayoutDesignerComponent - */ - - onSaveAndPublish(): void { - this.disablePublish = true; - this.saveAndPublish.emit(this.form.value as DotTemplateDesigner); - } - - /** - * Handle the changes in the Theme Selector component. - * @param DotTheme theme - * - * @memberof DotEditLayoutDesignerComponent - */ - changeThemeHandler(theme: DotTheme): void { - this.currentTheme = theme; - this.form.get('themeId').setValue(theme.inode); - this.themeDialogVisibility = false; - this.cd.detectChanges(); - } - - /** - * Close the Theme Dialog. - * - * @memberof DotEditLayoutDesignerComponent - */ - closeThemeDialog(): void { - this.themeDialogVisibility = false; - } - - private setupLayout(): void { - this.initForm(); - // Emit event to redraw the grid when the sidebar change - this.form - .get('layout.sidebar') - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe(() => { - this.dotEventsService.notify('layout-sidebar-change'); - }); - } - - // The POST request returns a 400 if we send the same properties we get - // ISSUE: https://github.com/dotCMS/core/issues/16344 - private cleanUpBody(body: DotLayoutBody): DotLayoutBody { - return body - ? { - rows: body.rows.map((row: DotLayoutRow) => { - return { - ...row, - columns: row.columns.map((column: DotLayoutColumn) => { - return { - containers: column.containers, - leftOffset: column.leftOffset, - width: column.width, - styleClass: column.styleClass - }; - }) - }; - }) - } - : null; - } - - private setFormValue(layout: DotLayout): void { - const currentLayout = this.form.get('layout').value; - if (_.isEqual(currentLayout, layout)) { - return; - } - - this.form.setValue( - { - title: this.title, - themeId: this.theme, - layout: { - body: this.cleanUpBody(layout.body), - header: layout.header, - footer: layout.footer, - sidebar: this.createSidebarForm(layout), - title: layout.title, - width: layout.width - } - }, - { emitEvent: false } - ); - this.updateModel(); - } - - private initForm(): void { - this.form = this.fb.group({ - title: this.title, - themeId: this.theme, - layout: this.fb.group({ - body: this.cleanUpBody(this.layout.body), - header: this.layout.header, - footer: this.layout.footer, - sidebar: this.createSidebarForm(this.layout), - title: this.layout.title, - width: this.layout.width - }) - }); - this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => { - this.disablePublish = false; - if (!_.isEqual(this.form.value, this.initialFormValue)) { - this.updateTemplate.emit(this.form.value as DotTemplateDesigner); - } - }); - this.updateModel(); - } - - private updateModel(): void { - if (this.theme) { - this.dotThemesService - .get(this.theme) - .pipe(take(1)) - .subscribe((theme: DotTheme) => { - this.currentTheme = theme; - this.cd.detectChanges(); - }); - this.initialFormValue = structuredClone(this.form.value) as DotTemplateDesigner; - } - } - - private createSidebarForm(layout: DotLayout): DotLayoutSideBar { - return { - location: this.getSidebarLocation(layout), - containers: this.getSidebarContainers(layout), - width: this.getSidebarWidth(layout) - }; - } - - private getSidebarLocation(layout: DotLayout): string { - return layout?.sidebar?.location || ''; - } - - private getSidebarContainers(layout: DotLayout): DotPageContainer[] { - return layout?.sidebar?.containers || []; - } - - private getSidebarWidth(layout: DotLayout): string { - return layout?.sidebar?.width || 'small'; - } - - private errorHandler(err: HttpErrorResponse): Observable { - return this.dotHttpErrorManagerService.handle(err).pipe( - tap((res: DotHttpErrorHandled) => { - if (!res.redirected) { - this.dotRouterService.goToSiteBrowser(); - } - - this.currentTheme = err.status === 403 ? null : this.currentTheme; - }) - ); - } -} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.module.ts b/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.module.ts deleted file mode 100644 index 1d20ae1ea1b9..000000000000 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-edit-layout-designer/dot-edit-layout-designer.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { CheckboxModule } from 'primeng/checkbox'; -import { DialogModule } from 'primeng/dialog'; -import { InputTextModule } from 'primeng/inputtext'; -import { ToolbarModule } from 'primeng/toolbar'; -import { TooltipModule } from 'primeng/tooltip'; - -import { DotActionButtonModule } from '@components/_common/dot-action-button/dot-action-button.module'; -import { DotGlobalMessageModule } from '@components/_common/dot-global-message/dot-global-message.module'; -import { DotContainerSelectorModule } from '@components/dot-container-selector/dot-container-selector.module'; -import { DotSecondaryToolbarModule } from '@components/dot-secondary-toolbar'; -import { DotPageLayoutService } from '@dotcms/data-access'; -import { TemplateBuilderModule } from '@dotcms/template-builder'; -import { DotDialogModule, DotMessagePipe, DotSafeHtmlPipe } from '@dotcms/ui'; -import { DotEditPageInfoModule } from '@portlets/dot-edit-page/components/dot-edit-page-info/dot-edit-page-info.module'; - -import { DotLayoutDesignerModule } from './components/dot-layout-designer/dot-layout-designer.module'; -import { DotSidebarPropertiesModule } from './components/dot-sidebar-properties/dot-sidebar-properties.module'; -import { DotThemeSelectorModule } from './components/dot-theme-selector/dot-theme-selector.module'; -import { DotEditLayoutDesignerComponent } from './dot-edit-layout-designer.component'; - -@NgModule({ - declarations: [DotEditLayoutDesignerComponent], - imports: [ - ButtonModule, - CheckboxModule, - CommonModule, - DialogModule, - DotActionButtonModule, - DotContainerSelectorModule, - DotEditPageInfoModule, - DotGlobalMessageModule, - DotSidebarPropertiesModule, - DotThemeSelectorModule, - FormsModule, - InputTextModule, - ReactiveFormsModule, - ToolbarModule, - TooltipModule, - DotSecondaryToolbarModule, - DotSafeHtmlPipe, - DotLayoutDesignerModule, - DotDialogModule, - TemplateBuilderModule, - DotMessagePipe - ], - exports: [DotEditLayoutDesignerComponent], - providers: [DotPageLayoutService] -}) -export class DotEditLayoutDesignerModule {} diff --git a/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/plugins/bubble-link-form.plugin.ts b/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/plugins/bubble-link-form.plugin.ts index 05bbdf8b3748..febca3c1fc77 100644 --- a/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/plugins/bubble-link-form.plugin.ts +++ b/core-web/libs/block-editor/src/lib/extensions/bubble-link-form/plugins/bubble-link-form.plugin.ts @@ -1,4 +1,3 @@ -import isEqual from 'lodash.isequal'; import { EditorState, Plugin, PluginKey, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Subject } from 'rxjs'; @@ -10,6 +9,8 @@ import { takeUntil } from 'rxjs/operators'; import { Editor, posToDOMRect } from '@tiptap/core'; +import { isEqual } from '@dotcms/utils'; + import { ImageNode } from '../../../nodes'; import { BASIC_TIPPY_OPTIONS, getPosAtDocCoords } from '../../../shared'; import { isValidURL } from '../../bubble-menu/utils'; diff --git a/core-web/libs/dot-rules/src/lib/components/dropdown/dropdown.ts b/core-web/libs/dot-rules/src/lib/components/dropdown/dropdown.ts index 39225de2a5ae..bc8523af7537 100644 --- a/core-web/libs/dot-rules/src/lib/components/dropdown/dropdown.ts +++ b/core-web/libs/dot-rules/src/lib/components/dropdown/dropdown.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { of, Observable, from } from 'rxjs'; import { @@ -7,9 +6,11 @@ import { Optional, OnChanges, SimpleChanges, - ViewChild + ViewChild, + Output, + Input, + ChangeDetectionStrategy } from '@angular/core'; -import { Output, Input, ChangeDetectionStrategy } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; import { SelectItem } from 'primeng/api'; @@ -17,6 +18,8 @@ import { Dropdown as PDropdown } from 'primeng/dropdown'; import { map, mergeMap, toArray } from 'rxjs/operators'; +import { isEmpty } from '@dotcms/utils'; + /** * Angular wrapper around OLD Semantic UI Dropdown Module. * @@ -120,7 +123,7 @@ export class Dropdown implements ControlValueAccessor, OnChanges { onTouched: Function = () => {}; writeValue(value: any): void { - this.modelValue = _.isEmpty(value) ? '' : value; + this.modelValue = isEmpty(value) ? '' : value; } registerOnChange(fn): void { diff --git a/core-web/libs/dot-rules/src/lib/components/input-date/input-date.ts b/core-web/libs/dot-rules/src/lib/components/input-date/input-date.ts index 0258df9e9982..1a5bcae8d7d9 100644 --- a/core-web/libs/dot-rules/src/lib/components/input-date/input-date.ts +++ b/core-web/libs/dot-rules/src/lib/components/input-date/input-date.ts @@ -1,5 +1,3 @@ -import * as _ from 'lodash'; - import { ChangeDetectionStrategy, Component, @@ -11,6 +9,8 @@ import { } from '@angular/core'; import { NgControl, ControlValueAccessor } from '@angular/forms'; +import { isEmpty } from '@dotcms/utils'; + // @dynamic @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -98,7 +98,7 @@ export class InputDate implements ControlValueAccessor { } writeValue(value: any): void { - this.modelValue = _.isEmpty(value) ? InputDate.DEFAULT_VALUE : new Date(value); + this.modelValue = isEmpty(value) ? InputDate.DEFAULT_VALUE : new Date(value); } registerOnChange(fn): void { diff --git a/core-web/libs/dot-rules/src/lib/components/restdropdown/RestDropdown.ts b/core-web/libs/dot-rules/src/lib/components/restdropdown/RestDropdown.ts index 329a79e74fb6..543f65799ead 100644 --- a/core-web/libs/dot-rules/src/lib/components/restdropdown/RestDropdown.ts +++ b/core-web/libs/dot-rules/src/lib/components/restdropdown/RestDropdown.ts @@ -1,13 +1,21 @@ -import * as _ from 'lodash'; import { Observable } from 'rxjs'; -import { Component, EventEmitter, OnChanges, Optional } from '@angular/core'; -import { AfterViewInit, Output, Input, ChangeDetectionStrategy } from '@angular/core'; +import { + Component, + EventEmitter, + OnChanges, + Optional, + AfterViewInit, + Output, + Input, + ChangeDetectionStrategy +} from '@angular/core'; import { NgControl, ControlValueAccessor } from '@angular/forms'; import { map } from 'rxjs/operators'; import { CoreWebService } from '@dotcms/dotcms-js'; +import { isEmpty } from '@dotcms/utils'; import { Verify } from '../../services/validation/Verify'; @@ -67,7 +75,7 @@ export class RestDropdown implements AfterViewInit, OnChanges, ControlValueAcces if (value && value.indexOf(',') > -1) { this._modelValue = value.split(','); } else { - this._modelValue = _.isEmpty(value) ? null : value; + this._modelValue = isEmpty(value) ? null : value; } } diff --git a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.ts b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.ts index 5f9a56d7ca1e..c11a7bcbb80a 100644 --- a/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.ts +++ b/core-web/libs/template-builder/src/lib/components/template-builder/template-builder.component.ts @@ -251,7 +251,7 @@ export class TemplateBuilderComponent implements OnInit, AfterViewInit, OnDestro opacity: '1' }; this.cd.detectChanges(); - }, 250); + }, 350); this.grid = GridStack.init(gridOptions).on('change', (_: Event, nodes: GridStackNode[]) => { this.store.moveRow(nodes as DotGridStackWidget[]); diff --git a/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts b/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts index 55ae01194c69..c18d93d48342 100644 --- a/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import { of as observableOf, Observable } from 'rxjs'; import { DotCMSWorkflow } from '@dotcms/dotcms-models'; @@ -117,12 +116,12 @@ export const WORKFLOW_STATUS_MOCK = { export class DotWorkflowServiceMock { get(): Observable { - return observableOf(_.cloneDeep(mockWorkflows)); + return observableOf(structuredClone(mockWorkflows)); } getSystem(): Observable { const systemWorkflow = mockWorkflows.filter((workflow: DotCMSWorkflow) => workflow.system); - return observableOf(_.cloneDeep(systemWorkflow[0])); + return observableOf(structuredClone(systemWorkflow[0])); } } diff --git a/core-web/libs/utils-testing/src/lib/field-variable-service.mock.ts b/core-web/libs/utils-testing/src/lib/field-variable-service.mock.ts index 3777c6247c9f..f8ff62fb79d0 100644 --- a/core-web/libs/utils-testing/src/lib/field-variable-service.mock.ts +++ b/core-web/libs/utils-testing/src/lib/field-variable-service.mock.ts @@ -1,4 +1,3 @@ -import { cloneDeep } from 'lodash'; import { Observable, of } from 'rxjs'; import { DotFieldVariable } from '@dotcms/dotcms-models'; @@ -29,11 +28,11 @@ export const mockFieldVariables: DotFieldVariable[] = [ export class DotFieldVariablesServiceMock { load(): Observable { - return of(cloneDeep(mockFieldVariables)); + return of(structuredClone(mockFieldVariables)); } save(): Observable { - return of(cloneDeep(mockFieldVariables[0])); + return of(structuredClone(mockFieldVariables[0])); } delete(): Observable { diff --git a/core-web/libs/utils/src/index.ts b/core-web/libs/utils/src/index.ts index adbf84c28a76..832c578bcdd5 100644 --- a/core-web/libs/utils/src/index.ts +++ b/core-web/libs/utils/src/index.ts @@ -3,3 +3,4 @@ export * from './lib/dot-utils'; export * from './lib/services/dot-loading-indicator.service'; export * from './lib/shared/const'; +export * from './lib/shared/lodash/functions'; diff --git a/core-web/libs/utils/src/lib/shared/lodash/functions.spec.ts b/core-web/libs/utils/src/lib/shared/lodash/functions.spec.ts new file mode 100644 index 000000000000..49f7e30c229c --- /dev/null +++ b/core-web/libs/utils/src/lib/shared/lodash/functions.spec.ts @@ -0,0 +1,83 @@ +import { camelCase, isEmpty, isEqual } from './functions'; + +describe('Utility Functions', () => { + describe('isEmpty', () => { + it('should return true for null or undefined', () => { + expect(isEmpty(null)).toBe(true); + expect(isEmpty(undefined)).toBe(true); + }); + + it('should return true for an empty object', () => { + expect(isEmpty({})).toBe(true); + }); + + it('should return false for a non-empty object', () => { + expect(isEmpty({ key: 'value' })).toBe(false); + }); + + it('should return true for an empty array', () => { + expect(isEmpty([])).toBe(true); + }); + + it('should return false for a non-empty array', () => { + expect(isEmpty([1, 2, 3])).toBe(false); + }); + + it('should return true for an empty string', () => { + expect(isEmpty('')).toBe(true); + expect(isEmpty(' ')).toBe(true); + }); + + it('should return false for a non-empty string', () => { + expect(isEmpty('text')).toBe(false); + }); + }); + + describe('isEqual', () => { + it('should return true for identical values', () => { + expect(isEqual(1, 1)).toBe(true); + expect(isEqual('string', 'string')).toBe(true); + expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(isEqual({ a: 1 }, { a: 1 })).toBe(true); + }); + + it('should return false for different types', () => { + expect(isEqual(1, '1')).toBe(false); + expect(isEqual({}, [])).toBe(false); + }); + + it('should return false for different values', () => { + expect(isEqual(1, 2)).toBe(false); + expect(isEqual('a', 'b')).toBe(false); + expect(isEqual([1, 2, 3], [3, 2, 1])).toBe(false); + expect(isEqual({ a: 1 }, { a: 2 })).toBe(false); + }); + + it('should return true for deeply equal objects', () => { + expect(isEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true); + }); + + it('should return false for deeply unequal objects', () => { + expect(isEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false); + }); + }); + + describe('camelCase', () => { + it('should convert a string to camelCase', () => { + expect(camelCase('hello world')).toBe('helloWorld'); + expect(camelCase('Hello World')).toBe('helloWorld'); + }); + + it('should return an empty string if input is empty', () => { + expect(camelCase('')).toBe(''); + }); + + it('should handle single word strings', () => { + expect(camelCase('word')).toBe('word'); + }); + + it('should handle strings with multiple spaces', () => { + expect(camelCase(' hello world ')).toBe('helloWorld'); + }); + }); +}); diff --git a/core-web/libs/utils/src/lib/shared/lodash/functions.ts b/core-web/libs/utils/src/lib/shared/lodash/functions.ts new file mode 100644 index 000000000000..4fd73987e21a --- /dev/null +++ b/core-web/libs/utils/src/lib/shared/lodash/functions.ts @@ -0,0 +1,125 @@ +/** + * Check if a value is empty + * + * Replacement for lodash.isEmpty + * https://gist.github.com/inPhoenix/45a9f9e2568126d206f1125caebcd122 + * @export + * @param {unknown} value + * @return {*} {boolean} + */ +export function isEmpty(value: unknown): boolean { + return ( + value == null || // From standard.js: Always use === - but obj == null is allowed to check null || undefined + (typeof value === 'object' && Object.keys(value).length === 0) || // This catches arrays and objects + (typeof value === 'string' && value.trim().length === 0) + ); +} + +/** + * Check if two objects are equal + * + * Replacement for lodash.isEqual + * https://gist.github.com/jsjain/a2ba5d40f20e19f734a53c0aad937fbb + * @export + * @param {*} first + * @param {*} second + * @return {*} {boolean} + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isEqual(first: any, second: any): boolean { + if (first === second) { + return true; + } + + if ( + (first === undefined || second === undefined || first === null || second === null) && + (first || second) + ) { + return false; + } + + const firstType = first?.constructor.name; + const secondType = second?.constructor.name; + if (firstType !== secondType) { + return false; + } + + if (firstType === 'Array') { + if (first.length !== second.length) { + return false; + } + + let equal = true; + for (let i = 0; i < first.length; i++) { + if (!isEqual(first[i], second[i])) { + equal = false; + break; + } + } + + return equal; + } + + if (firstType === 'Object') { + let equal = true; + const fKeys = Object.keys(first); + const sKeys = Object.keys(second); + if (fKeys.length !== sKeys.length) { + return false; + } + + for (let i = 0; i < fKeys.length; i++) { + if (first[fKeys[i]] && second[fKeys[i]]) { + if (first[fKeys[i]] === second[fKeys[i]]) { + continue; // eslint-disable-line + } + + if ( + first[fKeys[i]] && + (first[fKeys[i]].constructor.name === 'Array' || + first[fKeys[i]].constructor.name === 'Object') + ) { + equal = isEqual(first[fKeys[i]], second[fKeys[i]]); + if (!equal) { + break; + } + } else if (first[fKeys[i]] !== second[fKeys[i]]) { + equal = false; + break; + } + } else if ( + (first[fKeys[i]] && !second[fKeys[i]]) || + (!first[fKeys[i]] && second[fKeys[i]]) + ) { + equal = false; + break; + } + } + + return equal; + } + + return first === second; +} + +/** + * Convert a string to camel case + * This function does not handle special characters + * + * Replacement for lodash.camelCase + * https://stackoverflow.com/questions/2970525/converting-a-string-with-spaces-into-camel-case + * + * @export + * @param {string} [str=''] + * @return {*} + */ +export function camelCase(str = ''): string { + return ( + str + ?.trim() + ?.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { + return index === 0 ? word.toLowerCase() : word.toUpperCase(); + }) + .replace(/\s+/g, '') ?? '' + ); +} diff --git a/core-web/package.json b/core-web/package.json index 2e0934c3ae1b..21e1af628fc8 100644 --- a/core-web/package.json +++ b/core-web/package.json @@ -105,8 +105,6 @@ "gridstack": "^8.1.1", "htmldiff-js": "^1.0.5", "jstat": "^1.9.6", - "lodash": "^4.17.20", - "lodash.isequal": "^4.5.0", "md5": "^2.3.0", "next": "^14.0.4", "ng-packagr": "17.3.0", @@ -186,7 +184,6 @@ "@types/jasmine": "4.0.3", "@types/jasminewd2": "~2.0.8", "@types/jest": "29.5.10", - "@types/lodash": "^4.14.202", "@types/md5": "^2.3.5", "@types/node": "^18.16.9", "@types/puppeteer": "^5.4.2", diff --git a/core-web/yarn.lock b/core-web/yarn.lock index e90d19c034d2..11e90672221e 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -5765,7 +5765,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.14.167", "@types/lodash@^4.14.202": +"@types/lodash@^4.14.167": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== @@ -20623,7 +20623,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20641,15 +20641,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -20746,7 +20737,7 @@ stringify-package@^1.0.0, stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20774,13 +20765,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -22717,7 +22701,7 @@ worker-farm@^1.6.0, worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22752,15 +22736,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java index 388afb7545e3..63ffac55695e 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java @@ -161,8 +161,8 @@ public Set getOrPullSupportedModels() { final AppConfig appConfig = appConfigSupplier.get(); if (!appConfig.isEnabled()) { - AppConfig.debugLogger(getClass(), () -> "dotAI is not enabled, returning empty list of supported models"); - throw new DotRuntimeException("App dotAI config without API urls or API key"); + AppConfig.debugLogger(getClass(), () -> "dotAI is not enabled, returning empty set of supported models"); + return Set.of(); } final CircuitBreakerUrl.Response response = fetchOpenAIModels(appConfig); diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java index 1053537f79f9..0d3d5cb6bc3f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java @@ -2,7 +2,6 @@ import com.dotcms.security.apps.Secret; import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.liferay.util.StringPool; @@ -29,10 +28,8 @@ public class AppConfig implements Serializable { private static final String AI_API_URL_KEY = "AI_API_URL"; private static final String AI_IMAGE_API_URL_KEY = "AI_IMAGE_API_URL"; private static final String AI_EMBEDDINGS_API_URL_KEY = "AI_EMBEDDINGS_API_URL"; - private static final String AI_DEBUG_LOGGER_KEY = "AI_DEBUG_LOGGER"; private static final String SYSTEM_HOST = "System Host"; private static final AtomicReference SYSTEM_HOST_CONFIG = new AtomicReference<>(); - private static final boolean DEBUG_LOGGING = Config.getBooleanProperty(AI_DEBUG_LOGGER_KEY, false); public static final Pattern SPLITTER = Pattern.compile("\\s?,\\s?"); @@ -107,7 +104,7 @@ public static AppConfig getSystemHostConfig() { * @param message The {@link Supplier} with the message to log. */ public static void debugLogger(final Class clazz, final Supplier message) { - if (getSystemHostConfig().getConfigBoolean(AppKeys.DEBUG_LOGGING) || DEBUG_LOGGING) { + if (getSystemHostConfig().getConfigBoolean(AppKeys.DEBUG_LOGGING)) { Logger.info(clazz, message.get()); } } diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIClient.java new file mode 100644 index 000000000000..9b0bb3f25bb9 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIClient.java @@ -0,0 +1,106 @@ +package com.dotcms.ai.client; + +import com.dotcms.ai.domain.AIProvider; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; + +import javax.ws.rs.HttpMethod; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Interface representing an AI client capable of sending requests to an AI service. + * + *

+ * This interface defines methods for obtaining the AI provider and sending requests + * to the AI service. Implementations of this interface should handle the specifics + * of interacting with the AI service, including request formatting and response handling. + *

+ * + *

+ * The interface also provides a NOOP implementation that throws an + * {@link UnsupportedOperationException} for all operations. + *

+ * + * @author vico + */ +public interface AIClient { + + AIClient NOOP = new AIClient() { + @Override + public AIProvider getProvider() { + return AIProvider.NONE; + } + + @Override + public void sendRequest(final AIRequest request, final OutputStream output) { + throwUnsupported(); + } + + private void throwUnsupported() { + throw new UnsupportedOperationException("Noop client does not support sending requests"); + } + }; + + /** + * Resolves the appropriate HTTP method for the given method name and URL. + * + * @param method the HTTP method name (e.g., "GET", "POST", "PUT", "DELETE", "patch") + * @param url the URL to which the request will be sent + * @return the corresponding {@link HttpUriRequest} for the given method and URL + */ + static HttpUriRequest resolveMethod(final String method, final String url) { + switch(method) { + case HttpMethod.POST: + return new HttpPost(url); + case HttpMethod.PUT: + return new HttpPut(url); + case HttpMethod.DELETE: + return new HttpDelete(url); + case "patch": + return new HttpPatch(url); + case HttpMethod.GET: + default: + return new HttpGet(url); + } + } + + /** + * Validates and casts the given AI request to a {@link JSONObjectAIRequest}. + * + * @param the type of the request payload + * @param request the AI request to be validated and cast + * @return the validated and cast {@link JSONObjectAIRequest} + * @throws UnsupportedOperationException if the request is not an instance of {@link JSONObjectAIRequest} + */ + static JSONObjectAIRequest useRequestOrThrow(final AIRequest request) { + // When we get rid of JSONObject usage, we can remove this check + if (request instanceof JSONObjectAIRequest) { + return (JSONObjectAIRequest) request; + } + + throw new UnsupportedOperationException("Only JSONObjectAIRequest (JSONObject) is supported"); + } + + /** + * Returns the AI provider associated with this client. + * + * @return the AI provider + */ + AIProvider getProvider(); + + /** + * Sends the given AI request to the AI service and writes the response to the provided output stream. + * + * @param the type of the request payload + * @param request the AI request to be sent + * @param output the output stream to which the response will be written + * @throws Exception if any error occurs during the request execution + */ + void sendRequest(AIRequest request, OutputStream output); + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIClientStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIClientStrategy.java new file mode 100644 index 000000000000..36c520c6b775 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIClientStrategy.java @@ -0,0 +1,39 @@ +package com.dotcms.ai.client; + +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Interface representing a strategy for handling AI client requests and responses. + * + *

+ * This interface defines a method for applying a strategy to an AI client request, + * allowing for different handling mechanisms to be implemented. The NOOP strategy + * is provided as a default implementation that performs no operations. + *

+ * + *

+ * Implementations of this interface should define how to process the AI request + * and handle the response, potentially writing the response to an output stream. + *

+ * + * @author vico + */ +public interface AIClientStrategy { + + AIClientStrategy NOOP = (client, handler, request, output) -> AIResponse.builder().build(); + + /** + * Applies the strategy to the given AI client request and handles the response. + * + * @param client the AI client to which the request is sent + * @param handler the response evaluator to handle the response + * @param request the AI request to be processed + * @param output the output stream to which the response will be written + */ + void applyStrategy(AIClient client, + AIResponseEvaluator handler, + AIRequest request, + OutputStream output); + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIDefaultStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIDefaultStrategy.java new file mode 100644 index 000000000000..02149d98a7b1 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIDefaultStrategy.java @@ -0,0 +1,32 @@ +package com.dotcms.ai.client; + +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Default implementation of the {@link AIClientStrategy} interface. + * + *

+ * This class provides a default strategy for handling AI client requests by + * directly sending the request using the provided AI client and writing the + * response to the given output stream. + *

+ * + *

+ * The default strategy does not perform any additional processing or handling + * of the request or response, delegating the entire operation to the AI client. + *

+ * + * @author vico + */ +public class AIDefaultStrategy implements AIClientStrategy { + + @Override + public void applyStrategy(final AIClient client, + final AIResponseEvaluator handler, + final AIRequest request, + final OutputStream output) { + client.sendRequest(request, output); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxiedClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxiedClient.java new file mode 100644 index 000000000000..6cedcdc47712 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxiedClient.java @@ -0,0 +1,83 @@ +package com.dotcms.ai.client; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Optional; + +/** + * A proxy client for interacting with an AI service using a specified strategy. + * + *

+ * This class provides a mechanism to send requests to an AI service through a proxied client, + * applying a given strategy for handling the requests and responses. It supports a NOOP implementation + * that performs no operations. + *

+ * + *

+ * The class allows for the creation of proxied clients with different strategies and response evaluators, + * enabling flexible handling of AI service interactions. + *

+ * + * @author vico + */ +public class AIProxiedClient { + + public static final AIProxiedClient NOOP = new AIProxiedClient(null, AIClientStrategy.NOOP, null); + + private final AIClient client; + private final AIClientStrategy strategy; + private final AIResponseEvaluator responseEvaluator; + + private AIProxiedClient(final AIClient client, + final AIClientStrategy strategy, + final AIResponseEvaluator responseEvaluator) { + this.client = client; + this.strategy = strategy; + this.responseEvaluator = responseEvaluator; + } + + /** + * Creates an AIProxiedClient with the specified client, strategy, and response evaluator. + * + * @param client the AI client to be proxied + * @param strategy the strategy to be applied for handling requests and responses + * @param responseParser the response evaluator to process responses + * @return a new instance of AIProxiedClient + */ + public static AIProxiedClient of(final AIClient client, + final AIProxyStrategy strategy, + final AIResponseEvaluator responseParser) { + return new AIProxiedClient(client, strategy.getStrategy(), responseParser); + } + + /** + * Creates an AIProxiedClient with the specified client and strategy. + * + * @param client the AI client to be proxied + * @param strategy the strategy to be applied for handling requests and responses + * @return a new instance of AIProxiedClient + */ + public static AIProxiedClient of(final AIClient client, final AIProxyStrategy strategy) { + return of(client, strategy, null); + } + + /** + * Sends the given AI request to the AI service and writes the response to the provided output stream. + * + * @param the type of the request payload + * @param request the AI request to be sent + * @param output the output stream to which the response will be written + * @return the AI response + */ + public AIResponse sendToAI(final AIRequest request, final OutputStream output) { + final OutputStream finalOutput = Optional.ofNullable(output).orElseGet(ByteArrayOutputStream::new); + + strategy.applyStrategy(client, responseEvaluator, request, finalOutput); + + return Optional.ofNullable(output) + .map(out -> AIResponse.EMPTY) + .orElseGet(() -> AIResponse.builder().withResponse(finalOutput.toString()).build()); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java new file mode 100644 index 000000000000..0f91c0cee1e7 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java @@ -0,0 +1,111 @@ +package com.dotcms.ai.client; + +import com.dotcms.ai.client.openai.OpenAIClient; +import com.dotcms.ai.client.openai.OpenAIResponseEvaluator; +import com.dotcms.ai.domain.AIProvider; +import io.vavr.Lazy; + +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A proxy client for managing and interacting with multiple AI service providers. + * + *

+ * This class provides a mechanism to send requests to various AI service providers through proxied clients, + * applying different strategies for handling the requests and responses. It supports adding new clients and + * switching between different AI providers. + *

+ * + *

+ * The class allows for flexible handling of AI service interactions by maintaining a map of proxied clients + * and providing methods to send requests to the current or specified provider. + *

+ * + * @author vico + */ +public class AIProxyClient { + + private static final Lazy INSTANCE = Lazy.of(AIProxyClient::new); + + private final ConcurrentMap proxiedClients; + private final AtomicReference currentProvider; + + private AIProxyClient() { + proxiedClients = new ConcurrentHashMap<>(); + addClient( + AIProvider.OPEN_AI.name(), + AIProxiedClient.of(OpenAIClient.get(), AIProxyStrategy.MODEL_FALLBACK, OpenAIResponseEvaluator.get())); + currentProvider = new AtomicReference<>(AIProvider.OPEN_AI); + } + + public static AIProxyClient get() { + return INSTANCE.get(); + } + + /** + * Adds a proxied client for the specified AI provider. + * + * @param provider the AI provider for which the client is added + * @param client the proxied client to be added + */ + public void addClient(final String provider, final AIProxiedClient client) { + proxiedClients.put(provider, client); + } + + /** + * Sends the given AI request to the specified AI provider and writes the response to the provided output stream. + * + * @param provider the AI provider to which the request is sent + * @param request the AI request to be sent + * @param output the output stream to which the response will be written + * @return the AI response + */ + public AIResponse callToAI(final String provider, + final AIRequest request, + final OutputStream output) { + return Optional.ofNullable(proxiedClients.getOrDefault(provider, AIProxiedClient.NOOP)) + .map(client -> client.sendToAI(request, output)) + .orElse(AIResponse.EMPTY); + } + + /** + * Sends the given AI request to the specified AI provider. + * + * @param the type of the request payload + * @param provider the AI provider to which the request is sent + * @param request the AI request to be sent + * @return the AI response + */ + public AIResponse callToAI(final String provider, final AIRequest request) { + return callToAI(provider, request, null); + } + + /** + * Sends the given AI request to the current AI provider and writes the response to the provided output stream. + * + * @param the type of the request payload + * @param request the AI request to be sent + * @param output the output stream to which the response will be written + * @return the AI response + */ + public AIResponse callToAI(final AIRequest request, final OutputStream output) { + return callToAI(currentProvider.get().name(), request, output); + } + + /** + * Sends the given AI request to the current AI provider. + * + * @param the type of the request payload + * @param request the AI request to be sent + * @return the AI response + */ + public AIResponse callToAI(final AIRequest request) { + return callToAI(request, null); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java new file mode 100644 index 000000000000..1040f3516cf6 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java @@ -0,0 +1,36 @@ +package com.dotcms.ai.client; + +/** + * Enumeration representing different strategies for proxying AI client requests. + * + *

+ * This enum provides different strategies for handling AI client requests, including + * a default strategy and a model fallback strategy. Each strategy is associated with + * an implementation of the {@link AIClientStrategy} interface. + *

+ * + *

+ * The strategies can be used to customize the behavior of AI client interactions, + * allowing for flexible handling of requests and responses. + *

+ * + * @author vico + */ +public enum AIProxyStrategy { + + DEFAULT(new AIDefaultStrategy()), + // TODO: pr-split -> uncomment this line + //MODEL_FALLBACK(new AIModelFallbackStrategy()); + MODEL_FALLBACK(null); + + private final AIClientStrategy strategy; + + AIProxyStrategy(final AIClientStrategy strategy) { + this.strategy = strategy; + } + + public AIClientStrategy getStrategy() { + return strategy; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIRequest.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIRequest.java new file mode 100644 index 000000000000..2d0ce2b42259 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIRequest.java @@ -0,0 +1,225 @@ +package com.dotcms.ai.client; + +import com.dotcms.ai.app.AIModelType; +import com.dotcms.ai.app.AppConfig; + +import javax.ws.rs.HttpMethod; +import java.io.Serializable; + +/** + * Represents a request to an AI service. + * + *

+ * This class encapsulates the details of an AI request, including the URL, HTTP method, + * configuration, model type, payload, and user ID. It provides methods to create and + * configure AI requests for different model types such as text, image, and embeddings. + *

+ * + * @param the type of the request payload + * @author vico + */ +public class AIRequest { + + private final String url; + private final String method; + private final AppConfig config; + private final AIModelType type; + private final T payload; + private final String userId; + + > AIRequest(final Builder builder) { + this.url = builder.url; + this.method = builder.method; + this.config = builder.config; + this.type = builder.type; + this.payload = builder.payload; + this.userId = builder.userId; + } + + /** + * Creates a quick text AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @param the type of the request payload + * @param the type of the AIRequest + * @return a new AIRequest instance + */ + public static > R quickText(final AppConfig appConfig, + final T payload, + final String userId) { + return quick(AIModelType.TEXT, appConfig, payload, userId); + } + + /** + * Creates a quick image AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @param the type of the request payload + * @param the type of the AIRequest + * @return a new AIRequest instance + */ + public static > R quickImage(final AppConfig appConfig, + final T payload, + final String userId) { + return quick(AIModelType.IMAGE, appConfig, payload, userId); + } + + /** + * Creates a quick embeddings AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @param the type of the request payload + * @param the type of the AIRequest + * @return a new AIRequest instance + */ + public static > R quickEmbeddings(final AppConfig appConfig, + final T payload, + final String userId) { + return quick(AIModelType.EMBEDDINGS, appConfig, payload, userId); + } + + public static > Builder builder() { + return new Builder<>(); + } + + /** + * Resolves the URL for the specified model type and application configuration. + * + * @param type the AI model type + * @param appConfig the application configuration + * @return the resolved URL + */ + static String resolveUrl(final AIModelType type, final AppConfig appConfig) { + final String resolved; + switch (type) { + case TEXT: + resolved = appConfig.getApiUrl(); + break; + case IMAGE: + resolved = appConfig.getApiImageUrl(); + break; + case EMBEDDINGS: + resolved = appConfig.getApiEmbeddingsUrl(); + break; + default: + throw new IllegalArgumentException("Invalid AIModelType: " + type); + } + + return resolved; + } + + @SuppressWarnings("unchecked") + private static , R extends AIRequest> R quick( + final String url, + final AppConfig appConfig, + final AIModelType type, + final T payload, + final String usderId) { + return (R) AIRequest.builder() + .withUrl(url) + .withConfig(appConfig) + .withType(type) + .withPayload(payload) + .withUserId(usderId) + .build(); + } + + private static > R quick( + final AIModelType type, + final AppConfig appConfig, + final T payload, + final String userId) { + return quick(resolveUrl(type, appConfig), appConfig, type, payload, userId); + } + + public String getUrl() { + return url; + } + + public String getMethod() { + return method; + } + + public AppConfig getConfig() { + return config; + } + + public AIModelType getType() { + return type; + } + + public T getPayload() { + return payload; + } + + public String getUserId() { + return userId; + } + + @Override + public String toString() { + return "AIRequest{" + + "url='" + url + '\'' + + ", method='" + method + '\'' + + ", config=" + config + + ", type=" + type + + ", payload=" + payloadToString() + + ", userId='" + userId + '\'' + + '}'; + } + + public String payloadToString() { + return payload.toString(); + } + + public static class Builder> { + + String url; + String method = HttpMethod.POST; + AppConfig config; + AIModelType type; + T payload; + String userId; + + @SuppressWarnings("unchecked") + B self() { + return (B) this; + } + + public B withUrl(final String url) { + this.url = url; + return self(); + } + + public B withConfig(final AppConfig config) { + this.config = config; + return self(); + } + + public B withType(final AIModelType type) { + this.type = type; + return self(); + } + + public B withPayload(final T payload) { + this.payload = payload; + return self(); + } + + public B withUserId(final String userId) { + this.userId = userId; + return self(); + } + + public AIRequest build() { + return new AIRequest<>(this); + } + + } +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIResponse.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIResponse.java new file mode 100644 index 000000000000..07c122abf126 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIResponse.java @@ -0,0 +1,50 @@ +package com.dotcms.ai.client; + +/** + * Represents a response from an AI service. + * + *

+ * This class encapsulates the details of an AI response, including the response content. + * It provides methods to build and retrieve the response. + *

+ * + *

+ * The class also provides a static instance representing an empty response. + *

+ * + * @author vico + */ +public class AIResponse { + + public static final AIResponse EMPTY = builder().build(); + + private final String response; + + private AIResponse(final Builder builder) { + this.response = builder.response; + } + + public static Builder builder() { + return new Builder(); + } + + public String getResponse() { + return response; + } + + public static class Builder { + + private String response; + + public Builder withResponse(final String response) { + this.response = response; + return this; + } + + + public AIResponse build() { + return new AIResponse(this); + } + + } +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIResponseEvaluator.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIResponseEvaluator.java new file mode 100644 index 000000000000..d7f8d2ba5ce4 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIResponseEvaluator.java @@ -0,0 +1,36 @@ +package com.dotcms.ai.client; + +import com.dotcms.ai.domain.AIResponseData; + +/** + * Interface for evaluating AI responses. + * It provides methods to process responses and exceptions, updating the provided metadata. + * + *

Methods:

+ *
    + *
  • \fromResponse\ - Processes a response string and updates the metadata.
  • + *
  • \fromThrowable\ - Processes an exception and updates the metadata.
  • + *
+ * + * @author vico + */ +public interface AIResponseEvaluator { + + /** + * Processes a response string and updates the metadata. + * + * @param response the response string to process + * @param metadata the metadata to update based on the response + * @param jsonExpected flag for expecting the response to be a JSON + */ + void fromResponse(String response, AIResponseData metadata, boolean jsonExpected); + + /** + * Processes an exception and updates the metadata. + * + * @param exception the exception to process + * @param metadata the metadata to update based on the exception + */ + void fromException(Throwable exception, AIResponseData metadata); + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/JSONObjectAIRequest.java b/dotCMS/src/main/java/com/dotcms/ai/client/JSONObjectAIRequest.java new file mode 100644 index 000000000000..c988fabd2ee6 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/JSONObjectAIRequest.java @@ -0,0 +1,106 @@ +package com.dotcms.ai.client; + +import com.dotcms.ai.app.AIModelType; +import com.dotcms.ai.app.AppConfig; +import com.dotmarketing.util.json.JSONObject; + +/** + * Represents a request to an AI service with a JSON payload. + * + *

+ * This class encapsulates the details of an AI request with a JSON payload, including the URL, HTTP method, + * configuration, model type, payload, and user ID. It provides methods to create and configure AI requests + * for different model types such as text, image, and embeddings. + *

+ * + * @author vico + */ +public class JSONObjectAIRequest extends AIRequest { + + JSONObjectAIRequest(final Builder builder) { + super(builder); + } + + /** + * Creates a quick text AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @return a new JSONObjectAIRequest instance + */ + public static JSONObjectAIRequest quickText(final AppConfig appConfig, + final JSONObject payload, + final String userId) { + + return quick(AIModelType.TEXT, appConfig, payload, userId); + } + + /** + * Creates a quick image AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @return a new JSONObjectAIRequest instance + */ + public static JSONObjectAIRequest quickImage(final AppConfig appConfig, + final JSONObject payload, + final String userId) { + return quick(AIModelType.IMAGE, appConfig, payload, userId); + } + + /** + * Creates a quick embeddings AI request with the specified configuration, payload, and user ID. + * + * @param appConfig the application configuration + * @param payload the request payload + * @param userId the user ID + * @return a new JSONObjectAIRequest instance + */ + public static JSONObjectAIRequest quickEmbeddings(final AppConfig appConfig, + final JSONObject payload, + final String userId) { + return quick(AIModelType.EMBEDDINGS, appConfig, payload, userId); + } + + private static JSONObjectAIRequest quick(final String url, + final AppConfig appConfig, + final AIModelType type, + final JSONObject payload, + final String userId) { + return JSONObjectAIRequest.builder() + .withUrl(url) + .withConfig(appConfig) + .withType(type) + .withPayload(payload) + .withUserId(userId) + .build(); + } + + private static JSONObjectAIRequest quick(final AIModelType type, + final AppConfig appConfig, + final JSONObject payload, + final String userId) { + return quick(resolveUrl(type, appConfig), appConfig, type, payload, userId); + } + + @Override + public String payloadToString() { + return getPayload().toString(2); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AIRequest.Builder { + + @Override + public JSONObjectAIRequest build() { + return new JSONObjectAIRequest(this); + } + + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java new file mode 100644 index 000000000000..89d3638d15a7 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java @@ -0,0 +1,166 @@ +package com.dotcms.ai.client.openai; + +import com.dotcms.ai.AiKeys; +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AppConfig; +import com.dotcms.ai.app.AppKeys; +import com.dotcms.ai.client.AIClient; +import com.dotcms.ai.domain.AIProvider; +import com.dotcms.ai.client.AIRequest; +import com.dotcms.ai.client.JSONObjectAIRequest; +import com.dotcms.ai.domain.Model; +import com.dotcms.ai.exception.DotAIAppConfigDisabledException; +import com.dotcms.ai.exception.DotAIClientConnectException; +import com.dotcms.ai.exception.DotAIModelNotFoundException; +import com.dotcms.ai.exception.DotAIModelNotOperationalException; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.json.JSONObject; +import io.vavr.Lazy; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.control.Try; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import javax.ws.rs.core.MediaType; +import java.io.BufferedInputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Implementation of the {@link AIClient} interface for interacting with the OpenAI service. + * + *

+ * This class provides methods to send requests to the OpenAI service and handle responses. + * It includes functionality to manage rate limiting and ensure that models are operational + * before sending requests. + *

+ * + *

+ * The class uses a singleton pattern to ensure a single instance of the client is used + * throughout the application. It also maintains a record of the last REST call for each + * model to enforce rate limiting. + *

+ * + * @auhor vico + */ +public class OpenAIClient implements AIClient { + + private static final Lazy INSTANCE = Lazy.of(OpenAIClient::new); + + private final ConcurrentHashMap lastRestCall; + + public static OpenAIClient get() { + return INSTANCE.get(); + } + + private OpenAIClient() { + lastRestCall = new ConcurrentHashMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public AIProvider getProvider() { + return AIProvider.OPEN_AI; + } + + /** + * {@inheritDoc} + */ + @Override + public void sendRequest(final AIRequest request, final OutputStream output) { + final JSONObjectAIRequest jsonRequest = AIClient.useRequestOrThrow(request); + final AppConfig appConfig = jsonRequest.getConfig(); + + AppConfig.debugLogger( + OpenAIClient.class, + () -> String.format( + "Posting to [%s] with method [%s]%s with app config:%s%s the payload: %s", + jsonRequest.getUrl(), + jsonRequest.getMethod(), + System.lineSeparator(), + appConfig.toString(), + System.lineSeparator(), + jsonRequest.payloadToString())); + + if (!appConfig.isEnabled()) { + AppConfig.debugLogger(OpenAIClient.class, () -> "App dotAI is not enabled and will not send request."); + throw new DotAIAppConfigDisabledException("App dotAI config without API urls or API key"); + } + + final JSONObject payload = jsonRequest.getPayload(); + final String modelName = Optional + .ofNullable(payload.optString(AiKeys.MODEL)) + .orElseThrow(() -> new DotAIModelNotFoundException("Model is not present in the request")); + // TODO: pr-split -> uncomment this line + //final Tuple2 modelTuple = appConfig.resolveModelOrThrow(modelName, jsonRequest.getType()); + final Tuple2 modelTuple = Tuple.of(null, null); + final AIModel aiModel = modelTuple._1; + + if (!modelTuple._2.isOperational()) { + AppConfig.debugLogger( + getClass(), + () -> String.format("Resolved model [%s] is not operational, avoiding its usage", modelName)); + throw new DotAIModelNotOperationalException(String.format("Model [%s] is not operational", modelName)); + } + + final long sleep = lastRestCall.computeIfAbsent(aiModel, m -> 0L) + + aiModel.minIntervalBetweenCalls() + - System.currentTimeMillis(); + if (sleep > 0) { + Logger.info( + this, + "Rate limit:" + + aiModel.getApiPerMinute() + + "/minute, or 1 every " + + aiModel.minIntervalBetweenCalls() + + "ms. Sleeping:" + + sleep); + Try.run(() -> Thread.sleep(sleep)); + } + + lastRestCall.put(aiModel, System.currentTimeMillis()); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + final StringEntity jsonEntity = new StringEntity(payload.toString(), ContentType.APPLICATION_JSON); + final HttpUriRequest httpRequest = AIClient.resolveMethod(jsonRequest.getMethod(), jsonRequest.getUrl()); + httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + appConfig.getApiKey()); + + if (!payload.getAsMap().isEmpty()) { + Try.run(() -> HttpEntityEnclosingRequestBase.class.cast(httpRequest).setEntity(jsonEntity)); + } + + try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { + final BufferedInputStream in = new BufferedInputStream(response.getEntity().getContent()); + final byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) != -1) { + output.write(buffer, 0, len); + output.flush(); + } + } + } catch (Exception e) { + if (appConfig.getConfigBoolean(AppKeys.DEBUG_LOGGING)){ + Logger.warn(this, "INVALID REQUEST: " + e.getMessage(), e); + } else { + Logger.warn(this, "INVALID REQUEST: " + e.getMessage()); + } + + Logger.warn(this, " - " + jsonRequest.getMethod() + " : " + payload); + + throw new DotAIClientConnectException("Error while sending request to OpenAI", e); + } + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java new file mode 100644 index 000000000000..63f92f7618dc --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java @@ -0,0 +1,91 @@ +package com.dotcms.ai.client.openai; + +import com.dotcms.ai.AiKeys; +import com.dotcms.ai.client.AIResponseEvaluator; +import com.dotcms.ai.domain.AIResponseData; +import com.dotcms.ai.domain.ModelStatus; +import com.dotcms.ai.exception.DotAIModelNotFoundException; +import com.dotcms.ai.exception.DotAIModelNotOperationalException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.json.JSONObject; +import io.vavr.Lazy; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Evaluates AI responses from OpenAI and updates the provided metadata. + * This class implements the singleton pattern and provides methods to process responses and exceptions. + * + *

Methods:

+ *
    + *
  • \fromResponse\ - Processes a response string and updates the metadata.
  • + *
  • \fromThrowable\ - Processes an exception and updates the metadata.
  • + *
+ * + * @author vico + */ +public class OpenAIResponseEvaluator implements AIResponseEvaluator { + + private static final String JSON_ERROR_FIELD = "\"error\":"; + private static final Lazy INSTANCE = Lazy.of(OpenAIResponseEvaluator::new); + + public static OpenAIResponseEvaluator get() { + return INSTANCE.get(); + } + + private OpenAIResponseEvaluator() { + } + + /** + * {@inheritDoc} + */ + @Override + public void fromResponse(final String response, final AIResponseData metadata, final boolean jsonExpected) { + Optional.ofNullable(response) + .ifPresent(resp -> { + if (jsonExpected || resp.contains(JSON_ERROR_FIELD)) { + final JSONObject jsonResponse = new JSONObject(resp); + if (jsonResponse.has(AiKeys.ERROR)) { + final JSONObject error = jsonResponse.getJSONObject(AiKeys.ERROR); + final String message = error.getString(AiKeys.MESSAGE); + metadata.setError(message); + metadata.setStatus(resolveStatus(message)); + } + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void fromException(final Throwable exception, final AIResponseData metadata) { + metadata.setError(exception.getMessage()); + metadata.setStatus(resolveStatus(exception)); + metadata.setException(exception instanceof DotRuntimeException + ? (DotRuntimeException) exception + : new DotRuntimeException(exception)); + } + + private ModelStatus resolveStatus(final String error) { + if (error.contains("has been deprecated")) { + return ModelStatus.DECOMMISSIONED; + } else if (error.contains("does not exist or you do not have access to it")) { + return ModelStatus.INVALID; + } else { + return ModelStatus.UNKNOWN; + } + } + + private ModelStatus resolveStatus(final Throwable throwable) { + if (Stream + .of(DotAIModelNotFoundException.class, DotAIModelNotOperationalException.class) + .anyMatch(exception -> exception.isInstance(throwable))) { + return ModelStatus.INVALID; + } + + return ModelStatus.UNKNOWN; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java b/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java new file mode 100644 index 000000000000..9e844d47619f --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java @@ -0,0 +1,35 @@ +package com.dotcms.ai.domain; + +/** + * Enumeration representing different AI service providers. + * + *

+ * This enum defines various AI service providers that can be used within the application. + * Each provider is associated with a specific name that identifies the AI service. + *

+ * + *

+ * The providers can be used to configure and manage interactions with different AI services, + * allowing for flexible integration and switching between multiple AI providers. + *

+ * + * @author vico + */ +public enum AIProvider { + + NONE("None"), + OPEN_AI("OpenAI"), + BEDROCK("Amazon Bedrock"), + GEMINI("Google Gemini"); + + private final String provider; + + AIProvider(final String provider) { + this.provider = provider; + } + + public String getProvider() { + return provider; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/domain/AIResponseData.java b/dotCMS/src/main/java/com/dotcms/ai/domain/AIResponseData.java new file mode 100644 index 000000000000..85ac2d9d0483 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/domain/AIResponseData.java @@ -0,0 +1,68 @@ +package com.dotcms.ai.domain; + +import com.dotmarketing.exception.DotRuntimeException; +import org.apache.commons.lang3.StringUtils; + +/** + * Represents the data of a response from an AI service. + * + *

+ * This class encapsulates the details of an AI response, including the response content, error message, + * status, and any exceptions that may have occurred. It provides methods to retrieve and set these details, + * as well as a method to check if the response was successful. + *

+ * + * @author vico + */ +public class AIResponseData { + + private String response; + private String error; + private ModelStatus status; + private DotRuntimeException exception; + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public String getError() { + return error; + } + + public void setError(final String error) { + this.error = error; + } + + public ModelStatus getStatus() { + return status; + } + + public void setStatus(ModelStatus status) { + this.status = status; + } + + public DotRuntimeException getException() { + return exception; + } + + public void setException(DotRuntimeException exception) { + this.exception = exception; + } + + public boolean isSuccess() { + return StringUtils.isBlank(error); + } + + @Override + public String toString() { + return "AIResponseData{" + + "response='" + response + '\'' + + ", error='" + error + '\'' + + ", status=" + status + + '}'; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/domain/Model.java b/dotCMS/src/main/java/com/dotcms/ai/domain/Model.java new file mode 100644 index 000000000000..7b2b9ca150ba --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/domain/Model.java @@ -0,0 +1,104 @@ +package com.dotcms.ai.domain; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Represents an AI model with a name, status, and index. + * + *

+ * This class encapsulates the details of an AI model, including its name, status, and index. + * It provides methods to retrieve and set these details, as well as methods to check if the model is operational. + *

+ * + *

+ * The class also provides a builder for constructing instances of the model. + *

+ * + * @author vico + */ +public class Model { + + private final String name; + private final AtomicReference status; + private final AtomicInteger index; + + private Model(final Builder builder) { + name = builder.name; + status = new AtomicReference<>(null); + index = new AtomicInteger(builder.index); + } + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public ModelStatus getStatus() { + return status.get(); + } + + public void setStatus(final ModelStatus status) { + this.status.set(status); + } + + public int getIndex() { + return index.get(); + } + + public void setIndex(final int index) { + this.index.set(index); + } + + public boolean isOperational() { + return ModelStatus.ACTIVE == status.get(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Model model = (Model) o; + return Objects.equals(name, model.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return "Model{" + + "name='" + name + '\'' + + ", status=" + status + + ", index=" + index.get() + + '}'; + } + + public static class Builder { + + private String name; + private int index; + + public Builder withName(final String name) { + this.name = name.toLowerCase().trim(); + return this; + } + + public Builder withIndex(final int index) { + this.index = index; + return this; + } + + public Model build() { + return new Model(this); + } + + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/domain/ModelStatus.java b/dotCMS/src/main/java/com/dotcms/ai/domain/ModelStatus.java new file mode 100644 index 000000000000..15aa6bd9b69c --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/domain/ModelStatus.java @@ -0,0 +1,30 @@ +package com.dotcms.ai.domain; + +/** + * Represents the status of an AI model. + * + *

+ * This enum defines various statuses that an AI model can have, such as active, invalid, decommissioned, or unknown. + * Each status may have different implications for the operation of the model. + *

+ * + * @author vico + */ +public enum ModelStatus { + + ACTIVE(false), + INVALID(false), + DECOMMISSIONED(false), + UNKNOWN(true); + + private final boolean needsToThrow; + + ModelStatus(final boolean needsToThrow) { + this.needsToThrow = needsToThrow; + } + + public boolean doesNeedToThrow() { + return needsToThrow; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAllModelsExhaustedException.java b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAllModelsExhaustedException.java new file mode 100644 index 000000000000..6833fdabb252 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAllModelsExhaustedException.java @@ -0,0 +1,22 @@ +package com.dotcms.ai.exception; + +import com.dotmarketing.exception.DotRuntimeException; + +/** + * Exception thrown when all AI models have been exhausted. + * + *

+ * This exception is used to indicate that all available AI models have been exhausted and no further models + * are available for processing. It extends the {@link DotRuntimeException} to provide additional context + * specific to AI model exhaustion scenarios. + *

+ * + * @author vico + */ +public class DotAIAllModelsExhaustedException extends DotRuntimeException { + + public DotAIAllModelsExhaustedException(final String message) { + super(message); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAppConfigDisabledException.java b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAppConfigDisabledException.java new file mode 100644 index 000000000000..5b549c74fed5 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIAppConfigDisabledException.java @@ -0,0 +1,22 @@ +package com.dotcms.ai.exception; + +import com.dotmarketing.exception.DotRuntimeException; + +/** + * Exception thrown when the AI application configuration is disabled. + * + *

+ * This exception is used to indicate that the AI application configuration is disabled and cannot be used. + * It extends the {@link DotRuntimeException} to provide additional context specific to AI application configuration + * disabled scenarios. + *

+ * + * @author vico + */ +public class DotAIAppConfigDisabledException extends DotRuntimeException { + + public DotAIAppConfigDisabledException(final String message) { + super(message); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIClientConnectException.java b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIClientConnectException.java new file mode 100644 index 000000000000..e17bf9b8d09f --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIClientConnectException.java @@ -0,0 +1,21 @@ +package com.dotcms.ai.exception; + +import com.dotmarketing.exception.DotRuntimeException; + +/** + * Exception thrown when there is a connection error with the AI client. + * + *

+ * This exception is used to indicate that there is a connection error with the AI client. It extends the {@link DotRuntimeException} + * to provide additional context specific to AI client connection error scenarios. + *

+ * + * @author vico + */ +public class DotAIClientConnectException extends DotRuntimeException { + + public DotAIClientConnectException(final String message, final Throwable cause) { + super(message, cause); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotFoundException.java b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotFoundException.java new file mode 100644 index 000000000000..3bd70811b123 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotFoundException.java @@ -0,0 +1,21 @@ +package com.dotcms.ai.exception; + +import com.dotmarketing.exception.DotRuntimeException; + +/** + * Exception thrown when an AI model is not found. + * + *

+ * This exception is used to indicate that a specific AI model could not be found. It extends the {@link DotRuntimeException} + * to provide additional context specific to AI model not found scenarios. + *

+ * + * @author vico + */ +public class DotAIModelNotFoundException extends DotRuntimeException { + + public DotAIModelNotFoundException(final String message) { + super(message); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotOperationalException.java b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotOperationalException.java new file mode 100644 index 000000000000..ddea5f6866f8 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/exception/DotAIModelNotOperationalException.java @@ -0,0 +1,21 @@ +package com.dotcms.ai.exception; + +import com.dotmarketing.exception.DotRuntimeException; + +/** + * Exception thrown when there is a connection error with the AI client. + * + *

+ * This exception is used to indicate that there is a connection error with the AI client. It extends the {@link DotRuntimeException} + * to provide additional context specific to AI client connection error scenarios. + *

+ * + * @author vico + */ +public class DotAIModelNotOperationalException extends DotRuntimeException { + + public DotAIModelNotOperationalException(final String message) { + super(message); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java b/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java new file mode 100644 index 000000000000..f2036cba8955 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java @@ -0,0 +1,126 @@ +package com.dotcms.ai.validator; + +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AppConfig; +import com.dotcms.api.system.event.message.MessageSeverity; +import com.dotcms.api.system.event.message.SystemMessageEventUtil; +import com.dotcms.api.system.event.message.builder.SystemMessage; +import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; +import com.dotmarketing.util.DateUtil; +import com.google.common.annotations.VisibleForTesting; +import com.liferay.portal.language.LanguageUtil; +import io.vavr.Lazy; +import io.vavr.control.Try; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/** + * The AIAppValidator class is responsible for validating AI configurations and model usage. + * It ensures that the AI models specified in the application configuration are supported + * and not exhausted. + * + * @author vico + */ +public class AIAppValidator { + + private static final Lazy INSTANCE = Lazy.of(AIAppValidator::new); + + private SystemMessageEventUtil systemMessageEventUtil; + + private AIAppValidator() { + setSystemMessageEventUtil(SystemMessageEventUtil.getInstance()); + } + + public static AIAppValidator get() { + return INSTANCE.get(); + } + + /** + * Validates the AI configuration for the specified user. + * If the user ID is null, the validation is skipped. + * Checks if the models specified in the application configuration are supported. + * If any unsupported models are found, a warning message is pushed to the user. + * + * @param appConfig the application configuration + * @param userId the user ID + */ + public void validateAIConfig(final AppConfig appConfig, final String userId) { + if (Objects.isNull(userId)) { + AppConfig.debugLogger(getClass(), () -> "User Id is null, skipping AI configuration validation"); + return; + } + + // TODO: pr-split -> uncomment this lines + /*final Set supportedModels = AIModels.get().getOrPullSupportedModels(appConfig.getApiKey()); + final Set unsupportedModels = Stream.of( + appConfig.getModel(), + appConfig.getImageModel(), + appConfig.getEmbeddingsModel()) + .flatMap(aiModel -> aiModel.getModels().stream()) + .map(Model::getName) + .filter(model -> !supportedModels.contains(model)) + .collect(Collectors.toSet());*/ + final Set supportedModels = Set.of(); + final Set unsupportedModels = Set.of(); + if (unsupportedModels.isEmpty()) { + return; + } + + final String unsupported = String.join(", ", unsupportedModels); + final String message = Try + .of(() -> LanguageUtil.get("ai.unsupported.models", unsupported)) + .getOrElse(String.format("The following models are not supported: [%s]", unsupported)); + final SystemMessage systemMessage = new SystemMessageBuilder() + .setMessage(message) + .setSeverity(MessageSeverity.WARNING) + .setLife(DateUtil.SEVEN_SECOND_MILLIS) + .create(); + + systemMessageEventUtil.pushMessage(systemMessage, Collections.singletonList(userId)); + } + + /** + * Validates the usage of AI models for the specified user. + * If the user ID is null, the validation is skipped. + * Checks if the models specified in the AI model are exhausted or invalid. + * If any exhausted or invalid models are found, a warning message is pushed to the user. + * + * @param aiModel the AI model + * @param userId the user ID + */ + public void validateModelsUsage(final AIModel aiModel, final String userId) { + if (Objects.isNull(userId)) { + AppConfig.debugLogger(getClass(), () -> "User Id is null, skipping AI models usage validation"); + return; + } + + // TODO: pr-split -> uncomment this line + /*final String unavailableModels = aiModel.getModels() + .stream() + .map(Model::getName) + .collect(Collectors.joining(", "));*/ + final String unavailableModels = ""; + final String message = Try + .of(() -> LanguageUtil.get("ai.models.exhausted", aiModel.getType(), unavailableModels)). + getOrElse( + String.format( + "All the %s models: [%s] have been exhausted since they are invalid or has been decommissioned", + aiModel.getType(), + unavailableModels)); + final SystemMessage systemMessage = new SystemMessageBuilder() + .setMessage(message) + .setSeverity(MessageSeverity.WARNING) + .setLife(DateUtil.SEVEN_SECOND_MILLIS) + .create(); + + systemMessageEventUtil.pushMessage(systemMessage, Collections.singletonList(userId)); + } + + @VisibleForTesting + void setSystemMessageEventUtil(SystemMessageEventUtil systemMessageEventUtil) { + this.systemMessageEventUtil = systemMessageEventUtil; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java index 482d54e45f15..3848e75359ba 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/workflow/WorkflowResource.java @@ -2537,12 +2537,16 @@ private void mergeContentletsByDefaultAction(final List cont new DotConcurrentFactory.SubmitterConfigBuilder().poolSize(2).maxPoolSize(5).queueCapacity(CONTENTLETS_LIMIT).build()); final CompletionService> completionService = new ExecutorCompletionService<>(dotSubmitter); final List>> futures = new ArrayList<>(); + // todo: add the mock request + final HttpServletRequest statelessRequest = RequestUtil.INSTANCE.createStatelessRequest(request); + for (final SingleContentQuery singleContentQuery : contentletsToMergeList) { // this triggers the merges final Future> future = completionService.submit(() -> { + HttpServletRequestThreadLocal.INSTANCE.setRequest(statelessRequest); final Map resultMap = new HashMap<>(); final String inode = singleContentQuery.getInode(); final String identifier = singleContentQuery.getIdentifier(); @@ -2552,7 +2556,7 @@ private void mergeContentletsByDefaultAction(final List cont try { - fireTransactionalAction(systemAction, fireActionForm, request, mode, + fireTransactionalAction(systemAction, fireActionForm, statelessRequest, mode, initDataObject, resultMap, inode, identifier, languageId, user, indexPolicy); } catch (Exception e) { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/user/ajax/UserAjax.java b/dotCMS/src/main/java/com/dotmarketing/portlets/user/ajax/UserAjax.java index 25df6088cff3..b054152f89ce 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/user/ajax/UserAjax.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/user/ajax/UserAjax.java @@ -202,7 +202,7 @@ public Map addUser(String userId, String firstName, String lastN if (localTransaction) { HibernateUtil.rollbackTransaction(); } - throw new DotDataException(LanguageUtil.get(uWebAPI.getLoggedInUser(request),"User-Info-Save-Failed : " + e.getMessage()),"User-Info-Save-Failed",e); + throw new DotDataException(LanguageUtil.get(uWebAPI.getLoggedInUser(request), e.getMessage()),"User-Info-Save-Failed",e); } @@ -312,7 +312,7 @@ public Map updateUser(String userId, String newUserID, String fi } catch(DotDataException | DotStateException e) { ActivityLogger.logInfo(getClass(), "Error Updating User", "Date: " + date + "; "+ "User:" + modUser.getUserId()); AdminLogger.log(getClass(), "Error Updating User", "Date: " + date + "; "+ "User:" + modUser.getUserId()); - throw new DotDataException(LanguageUtil.get(uWebAPI.getLoggedInUser(request),"User-Info-Save-Failed " + e.getMessage()),"User-Info-Save-Failed",e); + throw new DotDataException(LanguageUtil.get(uWebAPI.getLoggedInUser(request), e.getMessage()),"User-Info-Save-Failed",e); } diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 782f5f57793a..98cae197cf4b 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -4739,7 +4739,7 @@ user-id=User ID User-Info-Save-Failed=User Information Save Failed User-Info-Save-First-Name-Failed=First Name not valid User-Info-Save-Last-Name-Failed=Last Name not valid -User-Info-Save-Password-Failed=The password does not meet the system security requirements. It must contain at least 8 characters and no white spaces. +User-Info-Save-Password-Failed=Your password must be at least 8 characters long and include at least one special character from the list # % + : = ? @ It must not contain spaces. User-Info-Save-Password-Recycle-Failed=The password has been already used lately and cannot be reused yet. user-info-saved=User info saved User-Info-Saved=User Information Saved diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java index e08965e20843..b24cebcae020 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java @@ -7,7 +7,6 @@ import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotDataException; -import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.DateUtil; import com.github.tomakehurst.wiremock.WireMockServer; @@ -157,13 +156,13 @@ public void test_getOrPullSupportedModules() throws DotDataException, DotSecurit * When the getOrPullSupportedModules method is called * Then an empty list of supported models should be returned. */ - @Test(expected = DotRuntimeException.class) + @Test public void test_getOrPullSupportedModules_withNetworkError() { AIModels.get().cleanSupportedModelsCache(); IPUtils.disabledIpPrivateSubnet(false); final Set supported = aiModels.getOrPullSupportedModels(); - assertSupported(supported); + assertTrue(supported.isEmpty()); IPUtils.disabledIpPrivateSubnet(true); AIModels.get().setAppConfigSupplier(ConfigService.INSTANCE::config); @@ -174,12 +173,14 @@ public void test_getOrPullSupportedModules_withNetworkError() { * When the getOrPullSupportedModules method is called * Then an empty list of supported models should be returned. */ - @Test(expected = DotRuntimeException.class) + @Test public void test_getOrPullSupportedModules_noApiKey() throws DotDataException, DotSecurityException { AiTest.aiAppSecrets(wireMockServer, APILocator.systemHost(), null); AIModels.get().cleanSupportedModelsCache(); - aiModels.getOrPullSupportedModels(); + final Set supported = aiModels.getOrPullSupportedModels(); + + assertTrue(supported.isEmpty()); } /** @@ -187,12 +188,14 @@ public void test_getOrPullSupportedModules_noApiKey() throws DotDataException, D * When the getOrPullSupportedModules method is called * Then an empty list of supported models should be returned. */ - @Test(expected = DotRuntimeException.class) + @Test public void test_getOrPullSupportedModules_noSystemHost() throws DotDataException, DotSecurityException { AiTest.removeSecrets(APILocator.systemHost()); AIModels.get().cleanSupportedModelsCache(); - aiModels.getOrPullSupportedModels(); + final Set supported = aiModels.getOrPullSupportedModels(); + + assertTrue(supported.isEmpty()); } private void saveSecrets(final Host host, diff --git a/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json b/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json index 63536de96d0d..c576f63c1873 100644 --- a/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json +++ b/dotcms-postman/src/main/resources/postman/Workflow_Resource_Tests.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "3e3539fd-4915-4514-a0d0-dc1d142a6f7f", + "_postman_id": "2a422520-eec4-42b3-94c3-5126f2691138", "name": "Workflow Resource Tests [/api/v1/workflows]", "description": "Test the necesary validations to every end point of the worlflow resource ", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "31066048" + "_exporter_id": "4500400" }, "item": [ { @@ -9672,6 +9672,71 @@ "description": "Fire any action using the actionId\n\nOptional: If you pass ?inode={inode}, you don't need body here.\n\n@Path(\"/actions/{actionId}/fire\")" }, "response": [] + }, + { + "name": "UpdateContent", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200 \", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "", + "pm.test(\"Valid response\", function () {", + " pm.expect(pm.response.text()).to.include(\"SucessRequest\");", + "});", + "", + "var jsonData = pm.response.json();", + "", + "pm.test(\"Key Value updated\", function () {", + " let contentIdentifier = pm.collectionVariables.get(\"contentletIdentifier\");", + " let contentlet = jsonData.entity.results[0][contentIdentifier];", + " pm.expect(contentlet.key).include(\"SucessRequest-Updated\"); ", + " pm.expect(contentlet.value).include(\"SucessRequest-Updated\");", + "});", + "", + "", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{ \"contentlet\" : {\n \"identifier\": \"{{contentletIdentifier}}\", \n \"key\": \"SucessRequest-Updated{{$timestamp}}\",\n \"value\": \"SucessRequest-Updated{{$timestamp}}\"\n}\n}" + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ] + }, + "description": "Updates content fields \"key\" and \"value\" by using the PATCH verb.\n\nAsserts the fields \"key\" and \"value\" were effectively updated." + }, + "response": [] } ] }, @@ -17413,6 +17478,46 @@ "script": { "type": "text/javascript", "exec": [ + "if (!pm.environment.get('jwt')) {", + " console.log(\"generating....\")", + " const serverURL = pm.environment.get('serverURL'); // Get the server URL from the environment variable", + " const apiUrl = `${serverURL}/api/v1/apitoken`; // Construct the full API URL", + "", + " if (!pm.environment.get('jwt')) {", + " const username = pm.environment.get(\"user\");", + " const password = pm.environment.get(\"password\");", + " const basicAuth = Buffer.from(`${username}:${password}`).toString('base64');", + "", + " const requestOptions = {", + " url: apiUrl,", + " method: \"POST\",", + " header: {", + " \"accept\": \"*/*\",", + " \"content-type\": \"application/json\",", + " \"Authorization\": `Basic ${basicAuth}`", + " },", + " body: {", + " mode: \"raw\",", + " raw: JSON.stringify({", + " \"expirationSeconds\": 7200,", + " \"userId\": \"dotcms.org.1\",", + " \"network\": \"0.0.0.0/0\",", + " \"claims\": {\"label\": \"postman-tests\"}", + " })", + " }", + " };", + "", + " pm.sendRequest(requestOptions, function (err, response) {", + " if (err) {", + " console.log(err);", + " } else {", + " const jwt = response.json().entity.jwt;", + " pm.environment.set('jwt', jwt);", + " console.log(jwt);", + " }", + " });", + " }", + "}", "" ] } @@ -17451,6 +17556,18 @@ { "key": "temporalFileOneId", "value": "" + }, + { + "key": "contentletIdentifier", + "value": "" + }, + { + "key": "contentletInode", + "value": "" + }, + { + "key": "fireActionLanguageKey", + "value": "" } ] } \ No newline at end of file