From 0f53ef24d9d77c3b0a6df687379f05e8666cfa24 Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Fri, 26 Jul 2024 21:30:58 -0300 Subject: [PATCH] chore(uve): Update state management to NgRx Signal Store #28947 (#29239) # Proposed Changes - Rewrite and apply new architecture to the Old Store to make it work with the new Signal Store and Custom Features. Use Computed Signals as a Reflection of the UI and State and Methods as source of information. - Centralize reusable code - Minimize the logic responsibility of the components to make them more simple and more readable - Replace all `ngFor` `ngIf` and `ngSwitch` for the new angular syntax - Add missing `themeId` on layout payload - Add missing functionality to fetch personas on page navigation - Add validation to replace urls from `/` to `index` to maintain consistency - Remove not natural reloads, extra calls to functions and anti-patterns - Fix inconsistencies across shared logic - Remove logic from the templates - Overall cleaning of the code to minimize tech debt - Fix out of place message for No Contentlets in Palette - Enhance overall perfomance of the tool - Fix internal navigation for Traditional Pages (VTL) - Enhance and minimize logic for Inline Editing (It was colliding with the navigation for Traditional Pages) - Add `SCROLLING` state to the Editor - Remove not necessary `MODE` from the editor, since now our store is a reflection of the UI - Separate `status` of the UVE from `state` of the editor to enhance reloads and the natural cycle of the tool - Remove all calls to `queryParams` from the router, since params now live in the store and are in sync with the url - Remove all references of old `EmaStore` - Remove not needed `Enums`, `Types`, `Interfaces` and `Mocks` --- .../dot-ema-dialog.component.html | 52 +- .../dot-ema-dialog.component.spec.ts | 2 +- .../dot-ema-dialog.component.ts | 5 - .../store/dot-ema-dialog.store.spec.ts | 3 +- .../edit-ema-navigation-bar.component.html | 8 +- .../edit-ema-navigation-bar.component.spec.ts | 16 +- .../edit-ema-navigation-bar.component.ts | 26 +- .../dot-ema-shell.component.html | 37 +- .../dot-ema-shell.component.spec.ts | 165 +- .../dot-ema-shell/dot-ema-shell.component.ts | 189 +- .../dot-ema-shell/store/dot-ema.store.spec.ts | 1555 ----------------- .../lib/dot-ema-shell/store/dot-ema.store.ts | 1055 ----------- ...dot-edit-ema-workflow-actions.component.ts | 3 +- .../dot-ema-info-display.component.html | 24 +- .../dot-ema-info-display.component.spec.ts | 358 ++-- .../dot-ema-info-display.component.ts | 175 +- .../dot-ema-running-experiment.component.html | 1 - .../dot-ema-running-experiment.component.ts | 13 +- .../edit-ema-language-selector.component.ts | 4 +- ...it-ema-palette-content-type.component.html | 11 +- ...edit-ema-palette-content-type.component.ts | 3 +- ...dit-ema-palette-contentlets.component.html | 89 +- ...dit-ema-palette-contentlets.component.scss | 7 + .../edit-ema-palette-contentlets.component.ts | 3 +- .../edit-ema-palette.component.html | 37 +- .../edit-ema-palette.component.ts | 9 +- .../edit-ema-persona-selector.component.html | 41 +- ...dit-ema-persona-selector.component.spec.ts | 9 + .../edit-ema-persona-selector.component.ts | 16 +- .../edit-ema-toolbar.component.html | 157 +- .../edit-ema-toolbar.component.spec.ts | 760 ++++---- .../edit-ema-toolbar.component.ts | 114 +- .../ema-contentlet-tools.component.html | 99 +- .../ema-contentlet-tools.component.ts | 4 +- .../ema-page-dropzone.component.html | 53 +- .../ema-page-dropzone.component.scss | 1 + .../ema-page-dropzone.component.spec.ts | 73 +- .../ema-page-dropzone.component.ts | 4 +- .../pipes/error/dot-error.pipe.spec.ts | 2 +- .../pipes/position/dot-position.pipe.spec.ts | 2 +- .../edit-ema-editor.component.html | 143 +- .../edit-ema-editor.component.scss | 13 +- .../edit-ema-editor.component.spec.ts | 778 +++------ .../edit-ema-editor.component.ts | 589 +++---- .../edit-ema-layout.component.html | 7 +- .../edit-ema-layout.component.spec.ts | 18 +- .../edit-ema-layout.component.ts | 21 +- .../services/guards/edit-ema.guard.spec.ts | 21 + .../src/lib/services/guards/edit-ema.guard.ts | 15 +- .../inline-edit/inline-edit.service.ts | 4 + .../edit-ema/portlet/src/lib/shared/consts.ts | 456 +---- .../edit-ema/portlet/src/lib/shared/enums.ts | 30 +- .../edit-ema/portlet/src/lib/shared/mocks.ts | 698 ++++++++ .../edit-ema/portlet/src/lib/shared/models.ts | 62 +- .../src/lib/store/dot-uve.store.spec.ts | 1439 +++++++++++++++ .../portlet/src/lib/store/dot-uve.store.ts | 121 ++ .../src/lib/store/features/editor/models.ts | 108 ++ .../store/features/editor/save/withSave.ts | 79 + .../editor/toolbar/withEditorToolbar.ts | 201 +++ .../lib/store/features/editor/withEditor.ts | 303 ++++ .../src/lib/store/features/layout/models.ts | 11 + .../lib/store/features/layout/withLayout.ts | 54 + .../src/lib/store/features/load/withLoad.ts | 206 +++ .../edit-ema/portlet/src/lib/store/models.ts | 38 + .../edit-ema/portlet/src/lib/utils/index.ts | 210 ++- .../portlet/src/lib/utils/utils.spec.ts | 233 ++- .../src/lib/dot-page-render.mock.ts | 196 ++- .../WEB-INF/messages/Language.properties | 7 +- examples/nextjs/package-lock.json | 28 +- 69 files changed, 5582 insertions(+), 5692 deletions(-) delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts delete mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.spec.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/save/withSave.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withEditorToolbar.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/models.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/withLayout.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/load/withLoad.ts create mode 100644 core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.html index 5d36d2242fad..8efd156b8f32 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.html @@ -11,32 +11,32 @@ data-testId="dialog" styleClass="edit-ema-dialog" (onHide)="onHide()"> - - - - - - - - + @switch (ds.type) { + @case ('form') { + + } + @case ('content') { + @if (ds.url) { + + } + @if (ds.status === dialogStatus.LOADING) { + + } + } + } { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.ts index 29df2c879014..930c73360455 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/dot-ema-dialog.component.ts @@ -1,6 +1,5 @@ import { fromEvent } from 'rxjs'; -import { NgIf, NgStyle, NgSwitch, NgSwitchCase } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -48,10 +47,6 @@ import { EmaFormSelectorComponent } from '../ema-form-selector/ema-form-selector templateUrl: './dot-ema-dialog.component.html', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - NgIf, - NgSwitch, - NgSwitchCase, - NgStyle, SafeUrlPipe, EmaFormSelectorComponent, DialogModule, diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/store/dot-ema-dialog.store.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/store/dot-ema-dialog.store.spec.ts index 3ab0596852aa..576b77f1d6a5 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/store/dot-ema-dialog.store.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/components/dot-ema-dialog/store/dot-ema-dialog.store.spec.ts @@ -8,7 +8,8 @@ import { MockDotMessageService } from '@dotcms/utils-testing'; import { DialogStatus, DotEmaDialogStore } from './dot-ema-dialog.store'; import { DotActionUrlService } from '../../../services/dot-action-url/dot-action-url.service'; -import { LAYOUT_URL, PAYLOAD_MOCK } from '../../../shared/consts'; +import { LAYOUT_URL } from '../../../shared/consts'; +import { PAYLOAD_MOCK } from '../../../shared/mocks'; import { DotPage } from '../../../shared/models'; describe('DotEmaDialogStoreService', () => { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/components/edit-ema-navigation-bar/edit-ema-navigation-bar.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/components/edit-ema-navigation-bar/edit-ema-navigation-bar.component.html index 8f5fac9f91b3..543bb8ad223e 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/components/edit-ema-navigation-bar/edit-ema-navigation-bar.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/components/edit-ema-navigation-bar/edit-ema-navigation-bar.component.html @@ -1,9 +1,9 @@ @@ -28,7 +28,7 @@ {{ persona.name }} - - - {{ 'modes.persona.personalized' | dm }} - + @if (persona.personalized) { + + + {{ 'modes.persona.personalized' | dm }} + + } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.spec.ts index 9213cca528e7..ba8ac24a67af 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.spec.ts @@ -231,5 +231,14 @@ describe('EditEmaPersonaSelectorComponent', () => { // but the API starts at 1, so we need to add 1 expect(fetchPersonasSpy).toHaveBeenCalledWith(2); }); + + it('should call fetchPersonas when pageId changes', () => { + const fetchPersonasSpy = jest.spyOn(component, 'fetchPersonas'); + + spectator.setInput('pageId', '456'); + spectator.detectChanges(); + + expect(fetchPersonasSpy).toHaveBeenCalled(); + }); }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.ts index 3f16d5e9a816..fbd23688f3da 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-persona-selector/edit-ema-persona-selector.component.ts @@ -1,14 +1,14 @@ import { of } from 'rxjs'; -import { CommonModule } from '@angular/common'; +import { NgClass } from '@angular/common'; import { AfterViewInit, Component, EventEmitter, Input, OnChanges, - OnInit, Output, + SimpleChanges, ViewChild, inject, signal @@ -40,7 +40,7 @@ interface PersonaSelector { selector: 'dot-edit-ema-persona-selector', standalone: true, imports: [ - CommonModule, + NgClass, ButtonModule, AvatarModule, OverlayPanelModule, @@ -55,7 +55,7 @@ interface PersonaSelector { templateUrl: './edit-ema-persona-selector.component.html', styleUrls: ['./edit-ema-persona-selector.component.scss'] }) -export class EditEmaPersonaSelectorComponent implements OnInit, AfterViewInit, OnChanges { +export class EditEmaPersonaSelectorComponent implements AfterViewInit, OnChanges { @ViewChild('listbox') listbox: Listbox; private readonly pageApiService = inject(DotPageApiService); @@ -75,11 +75,11 @@ export class EditEmaPersonaSelectorComponent implements OnInit, AfterViewInit, O @Output() despersonalize: EventEmitter = new EventEmitter(); - ngOnInit(): void { - this.fetchPersonas(); - } + ngOnChanges(changes: SimpleChanges): void { + if (changes.pageId) { + this.fetchPersonas(); + } - ngOnChanges(): void { // To select the correct persona when the page is reloaded with no queryParams if (this.listbox) { this.resetValue(); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.html index fe22358d3b74..008aeebd3145 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.html @@ -1,89 +1,82 @@ - - -
- + +
+ - + - @if ( - es.editorData.mode === editorMode.EDIT || - es.editorData.mode === editorMode.EDIT_VARIANT - ) { - - } - - + @if ($toolbarProps().urlContentMap; as urlContentMap) { - -
-
+ data-testId="edit-url-content-map" /> + } + + + + +
+
+ @if ($toolbarProps().runningExperiment; as runningExperiment) { - - - + } + + + + + + @if ($toolbarProps().workflowActionsInode; as inode) { + + } + @if ($toolbarProps().unlockButton; as unlockButton) { + + } +
+
- @if (es.editorData.page.isLocked && es.editorData.page.canLock) { - - } -
-
- -
+@if ($toolbarProps().showInfoDisplay) { + +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.spec.ts index cd17df9cd176..08625ab24f71 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.spec.ts @@ -1,27 +1,54 @@ import { expect, describe, it } from '@jest/globals'; -import { SpectatorRouting, byTestId, createRoutingFactory } from '@ngneat/spectator/jest'; -import { MockComponent, MockProvider, MockProviders } from 'ng-mocks'; +import { + Spectator, + SpyObject, + byTestId, + createComponentFactory, + mockProvider +} from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; -import { ClipboardModule } from '@angular/cdk/clipboard'; -import { DebugElement } from '@angular/core'; +import { DebugElement, signal } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { ConfirmationService, MessageService } from 'primeng/api'; -import { ButtonModule } from 'primeng/button'; -import { MenuModule } from 'primeng/menu'; -import { ToolbarModule } from 'primeng/toolbar'; -import { DotMessageService, DotPersonalizeService } from '@dotcms/data-access'; -import { DotExperimentStatus } from '@dotcms/dotcms-models'; +import { + DotContentletLockerService, + DotExperimentsService, + DotLanguagesService, + DotLicenseService, + DotMessageService, + DotPersonalizeService +} from '@dotcms/data-access'; +import { LoginService } from '@dotcms/dotcms-js'; import { DotDeviceSelectorSeoComponent } from '@dotcms/portlets/dot-ema/ui'; -import { DotMessagePipe, mockDotDevices } from '@dotcms/utils-testing'; +import { + CurrentUserDataMock, + DotLanguagesServiceMock, + getRunningExperimentMock, + mockDotDevices +} from '@dotcms/utils-testing'; import { EditEmaToolbarComponent } from './edit-ema-toolbar.component'; -import { EditEmaStore } from '../../../dot-ema-shell/store/dot-ema.store'; -import { EDITOR_MODE, EDITOR_STATE } from '../../../shared/enums'; +import { DotPageApiService } from '../../../services/dot-page-api.service'; +import { DEFAULT_PERSONA } from '../../../shared/consts'; +import { + HEADLESS_BASE_QUERY_PARAMS, + MOCK_RESPONSE_HEADLESS, + PAGE_RESPONSE_BY_LANGUAGE_ID, + URL_CONTENT_MAP_MOCK +} from '../../../shared/mocks'; +import { UVEStore } from '../../../store/dot-uve.store'; +import { + sanitizeURL, + createPageApiUrlWithQueryParams, + createFavoritePagesURL, + createPureURL +} from '../../../utils'; import { DotEditEmaWorkflowActionsComponent } from '../dot-edit-ema-workflow-actions/dot-edit-ema-workflow-actions.component'; import { DotEmaBookmarksComponent } from '../dot-ema-bookmarks/dot-ema-bookmarks.component'; import { DotEmaInfoDisplayComponent } from '../dot-ema-info-display/dot-ema-info-display.component'; @@ -30,16 +57,15 @@ import { EditEmaLanguageSelectorComponent } from '../edit-ema-language-selector/ import { EditEmaPersonaSelectorComponent } from '../edit-ema-persona-selector/edit-ema-persona-selector.component'; describe('EditEmaToolbarComponent', () => { - let spectator: SpectatorRouting; - let store: EditEmaStore; + let spectator: Spectator; + let store: SpyObject>; let messageService: MessageService; let router: Router; let confirmationService: ConfirmationService; - const createComponent = createRoutingFactory({ + const createComponent = createComponentFactory({ component: EditEmaToolbarComponent, - declarations: [ - DotMessagePipe, + imports: [ MockComponent(DotDeviceSelectorSeoComponent), MockComponent(DotEditEmaWorkflowActionsComponent), MockComponent(DotEmaBookmarksComponent), @@ -48,16 +74,19 @@ describe('EditEmaToolbarComponent', () => { MockComponent(EditEmaLanguageSelectorComponent), MockComponent(EditEmaPersonaSelectorComponent) ], - imports: [MenuModule, ButtonModule, ToolbarModule, ClipboardModule], providers: [ - MockProviders(Router), - MockProvider(ConfirmationService, { + UVEStore, + mockProvider(ActivatedRoute), + mockProvider(DotExperimentsService), + mockProvider(Router), + mockProvider(DotContentletLockerService), + mockProvider(ConfirmationService, { confirm: jest.fn() }), - MockProvider(MessageService, { + mockProvider(MessageService, { add: jest.fn() }), - MockProvider(DotMessageService, { + mockProvider(DotMessageService, { get: (key) => { const data = { Copied: 'Copied', @@ -72,7 +101,31 @@ describe('EditEmaToolbarComponent', () => { return data[key]; } - }) + }), + { + provide: DotLanguagesService, + useValue: new DotLanguagesServiceMock() + }, + { + provide: DotLicenseService, + useValue: { + isEnterprise: () => of(true) + } + }, + { + provide: LoginService, + useValue: { + getCurrentUser: () => of(CurrentUserDataMock) + } + }, + { + provide: DotPageApiService, + useValue: { + get({ language_id }) { + return PAGE_RESPONSE_BY_LANGUAGE_ID[language_id]; + } + } + } ], componentProviders: [ { @@ -84,55 +137,54 @@ describe('EditEmaToolbarComponent', () => { ] }); - describe('edit', () => { + const params = HEADLESS_BASE_QUERY_PARAMS; + const url = sanitizeURL(params?.url); + + const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params); + const pageAPIResponse = MOCK_RESPONSE_HEADLESS; + + const pageAPI = `/api/v1/page/${'json'}/${pageAPIQueryParams}`; + + const shouldShowInfoDisplay = false || pageAPIResponse?.page.locked || false || false; + + const bookmarksUrl = createFavoritePagesURL({ + languageId: Number(params?.language_id), + pageURI: url, + siteId: pageAPIResponse?.site.identifier + }); + + describe('base state', () => { beforeEach(() => { spectator = createComponent({ providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - previewURL: 'http://localhost:8080/index', - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - pureURL: 'http://localhost:8080/index', - editorData: { - mode: EDITOR_MODE.EDIT, - canEditPage: true, - page: { - isLocked: false, - canLock: true, - lockedByUser: '' - } - }, - editor: { - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } - }, - showWorkflowActions: true, - showInfoDisplay: false - }), - load: jest.fn(), - setDevice: jest.fn(), - setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } + mockProvider(UVEStore, { + $toolbarProps: signal({ + bookmarksUrl, + copyUrl: createPureURL(params), + apiUrl: `${'http://localhost'}${pageAPI}`, + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: null, + runningExperiment: null, + workflowActionsInode: pageAPIResponse?.page.inode, + unlockButton: null, + showInfoDisplay: shouldShowInfoDisplay, + deviceSelector: { + apiLink: `${params?.clientHost ?? 'http://localhost'}${pageAPI}`, + hideSocialMedia: true + }, + personaSelector: { + pageId: pageAPIResponse?.page.identifier, + value: pageAPIResponse?.viewAs.persona ?? DEFAULT_PERSONA + } + }), + setDevice: jest.fn(), + setSocialMedia: jest.fn(), + params: signal(params) + }) ] }); - store = spectator.inject(EditEmaStore); + + store = spectator.inject(UVEStore); messageService = spectator.inject(MessageService); router = spectator.inject(Router); confirmationService = spectator.inject(ConfirmationService); @@ -140,18 +192,37 @@ describe('EditEmaToolbarComponent', () => { describe('dot-device-selector-seo', () => { let deviceSelector: DebugElement; + let emaPreviewButton: DebugElement; beforeEach(() => { deviceSelector = spectator.debugElement.query( By.css('[data-testId="dot-device-selector"]') ); + emaPreviewButton = spectator.debugElement.query( + By.css('[data-testId="ema-preview"]') + ); }); - it('should have attr', () => { - expect(deviceSelector.attributes).toEqual({ - appendTo: 'body', - 'data-testId': 'dot-device-selector', - 'ng-reflect-api-link': 'http://localhost:8080/index', - 'ng-reflect-hide-social-media': 'true' + it('should have correct values', () => { + const deviceSelectorComponent = deviceSelector.componentInstance; + + expect(deviceSelectorComponent.apiLink).toBe( + `${'http://localhost:3000'}${pageAPI}` + ); + expect(deviceSelectorComponent.hideSocialMedia).toBe(true); + }); + + it('should call deviceSelector.openMenu', () => { + const deviceSelector = spectator.debugElement.query( + By.css('[data-testId="dot-device-selector"]') + ); + + jest.spyOn(deviceSelector.componentInstance, 'openMenu'); + + spectator.triggerEventHandler(emaPreviewButton, 'onClick', { hello: 'world' }); + spectator.detectChanges(); + + expect(deviceSelector.componentInstance.openMenu).toHaveBeenCalledWith({ + hello: 'world' }); }); @@ -161,7 +232,6 @@ describe('EditEmaToolbarComponent', () => { const iphone = { ...mockDotDevices[0], icon: 'someIcon' }; spectator.triggerEventHandler(deviceSelector, 'selected', iphone); - spectator.detectChanges(); expect(store.setDevice).toHaveBeenCalledWith(iphone); }); @@ -189,47 +259,11 @@ describe('EditEmaToolbarComponent', () => { }); }); - describe('ema-preview', () => { - let emaPreviewButton: DebugElement; - - beforeEach(() => { - emaPreviewButton = spectator.debugElement.query( - By.css('[data-testId="ema-preview"]') - ); - }); - - it('should have attr', () => { - expect(emaPreviewButton.attributes).toEqual({ - class: 'p-element', - 'data-testId': 'ema-preview', - icon: 'pi pi-desktop', - 'ng-reflect-icon': 'pi pi-desktop', - 'ng-reflect-style-class': 'p-button-text p-button-sm', - styleClass: 'p-button-text p-button-sm' - }); - }); - - it('should call deviceSelector.openMenu', () => { - const deviceSelector = spectator.debugElement.query( - By.css('[data-testId="dot-device-selector"]') - ); - - jest.spyOn(deviceSelector.componentInstance, 'openMenu'); - - spectator.triggerEventHandler(emaPreviewButton, 'onClick', { hello: 'world' }); - spectator.detectChanges(); - - expect(deviceSelector.componentInstance.openMenu).toHaveBeenCalledWith({ - hello: 'world' - }); - }); - }); - describe('dot-ema-bookmarks', () => { it('should have attr', () => { const bookmarks = spectator.query(DotEmaBookmarksComponent); - expect(bookmarks.url).toBe('http://localhost:8080/fav'); + expect(bookmarks.url).toBe('/test-url?host_id=123-xyz-567-xxl&language_id=1'); }); }); @@ -242,12 +276,11 @@ describe('EditEmaToolbarComponent', () => { it('should have attr', () => { expect(button.attributes).toEqual({ - class: 'p-element', 'data-testId': 'ema-copy-url', icon: 'pi pi-copy', 'ng-reflect-icon': 'pi pi-copy', 'ng-reflect-style-class': 'p-button-text p-button-sm', - 'ng-reflect-text': 'http://localhost:8080/index', + 'ng-reflect-text': 'http://localhost:3000/test-url', styleClass: 'p-button-text p-button-sm' }); }); @@ -267,15 +300,19 @@ describe('EditEmaToolbarComponent', () => { it('should have attr', () => { const languageSelector = spectator.query(EditEmaLanguageSelectorComponent); - expect(languageSelector.language).toEqual({ id: 1 }); + expect(languageSelector.language).toEqual({ + country: 'United States', + countryCode: 'US', + id: 1, + language: 'English', + languageCode: '1' + }); }); it('should set language', () => { spectator.triggerEventHandler(EditEmaLanguageSelectorComponent, 'selected', 2); spectator.detectChanges(); - expect(store.updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.LOADING); - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: { language_id: 2 }, queryParamsHandling: 'merge' @@ -289,7 +326,33 @@ describe('EditEmaToolbarComponent', () => { expect(personaSelector.pageId).toBe('123'); expect(personaSelector.value).toEqual({ - id: '123' + archived: false, + baseType: 'PERSONA', + contentType: 'persona', + folder: 'SYSTEM_FOLDER', + hasLiveVersion: false, + hasTitleImage: false, + host: 'SYSTEM_HOST', + hostFolder: 'SYSTEM_HOST', + hostName: 'System Host', + identifier: 'modes.persona.no.persona', + inode: '', + keyTag: 'dot:persona', + languageId: 1, + live: false, + locked: false, + modDate: '0', + modUser: 'system', + modUserName: 'system user system user', + name: 'Default Visitor', + owner: 'SYSTEM_USER', + personalized: false, + sortOrder: 0, + stInode: 'c938b15f-bcb6-49ef-8651-14d455a97045', + title: 'Default Visitor', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + url: 'demo.dotcms.com', + working: false }); }); @@ -302,8 +365,6 @@ describe('EditEmaToolbarComponent', () => { } as any); spectator.detectChanges(); - expect(store.updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.LOADING); - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: { 'com.dotmarketing.persona.id': '123' }, queryParamsHandling: 'merge' @@ -361,8 +422,9 @@ describe('EditEmaToolbarComponent', () => { it('should have attr', () => { const workflowActions = spectator.query(DotEditEmaWorkflowActionsComponent); - expect(workflowActions.inode).toBe('456'); + expect(workflowActions.inode).toBe('123-i'); }); + it('should update page', () => { spectator.triggerEventHandler(DotEditEmaWorkflowActionsComponent, 'newPage', { pageURI: '/path-and-stuff', @@ -373,10 +435,14 @@ describe('EditEmaToolbarComponent', () => { spectator.detectChanges(); - expect(store.updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.LOADING); - expect(router.navigate).toHaveBeenCalledWith([], { - queryParams: { language_id: '1', url: '/path-and-stuff' }, + queryParams: { + clientHost: 'http://localhost:3000', + 'com.dotmarketing.persona.id': 'dot:persona', + language_id: '1', + url: '/path-and-stuff', + variantName: 'DEFAULT' + }, queryParamsHandling: 'merge' }); }); @@ -397,356 +463,186 @@ describe('EditEmaToolbarComponent', () => { }); }); - describe('preview', () => { - beforeEach(() => { - spectator = createComponent({ - providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - editorData: { - mode: EDITOR_MODE.DEVICE, - canEditPage: true, - page: { - isLocked: false, - canLock: true, - lockedByUser: '' - } - }, - editor: { - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } + describe('constrains', () => { + describe('dot-ema-info-display', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(UVEStore, { + $toolbarProps: signal({ + bookmarksUrl, + copyUrl: '', + apiUrl: '', + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: null, + runningExperiment: null, + workflowActionsInode: '', + unlockButton: null, + showInfoDisplay: true, + deviceSelector: { + apiLink: '', + hideSocialMedia: true }, - showWorkflowActions: true, - showInfoDisplay: true + personaSelector: { + pageId: '', + value: DEFAULT_PERSONA + } }), - load: jest.fn(), setDevice: jest.fn(), setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } - ] - }); - store = spectator.inject(EditEmaStore); - messageService = spectator.inject(MessageService); - router = spectator.inject(Router); - confirmationService = spectator.inject(ConfirmationService); - }); - - describe('dot-ema-running-experiment', () => { - it('should be hidden', () => { - const experiments = spectator.query(byTestId('ema-running-experiment')); - expect(experiments).toBeNull(); + params: signal(params) + }) + ] + }); + store = spectator.inject(UVEStore); + messageService = spectator.inject(MessageService); + router = spectator.inject(Router); + confirmationService = spectator.inject(ConfirmationService); }); - }); - - describe('dot-ema-info-display', () => { - it('should have attr', () => { + it('should show when showInfoDisplay is true in the store', () => { const infoDisplay = spectator.query(DotEmaInfoDisplayComponent); - expect(infoDisplay.editorData).toEqual({ - canEditPage: true, - mode: EDITOR_MODE.DEVICE, - page: { - isLocked: false, - canLock: true, - lockedByUser: '' - } - }); - - expect(infoDisplay.currentExperiment).not.toBeDefined(); + expect(infoDisplay).toBeDefined(); }); }); - }); + describe('experiments', () => { + const experiment = getRunningExperimentMock(); - describe('experiments', () => { - beforeEach(() => { - spectator = createComponent({ - providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - editorData: { - mode: EDITOR_MODE.DEVICE, - canEditPage: true, - page: { - isLocked: false, - canLock: true, - lockedByUser: '' - } - }, - currentExperiment: { - status: DotExperimentStatus.RUNNING - }, - editor: { - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(UVEStore, { + $toolbarProps: signal({ + bookmarksUrl, + copyUrl: '', + apiUrl: '', + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: null, + runningExperiment: experiment, + workflowActionsInode: '', + unlockButton: null, + showInfoDisplay: true, + deviceSelector: { + apiLink: '', + hideSocialMedia: true }, - showWorkflowActions: true, - showInfoDisplay: true + personaSelector: { + pageId: '', + value: DEFAULT_PERSONA + } }), - load: jest.fn(), setDevice: jest.fn(), setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } - ] + params: signal(params) + }) + ] + }); }); - store = spectator.inject(EditEmaStore); - messageService = spectator.inject(MessageService); - router = spectator.inject(Router); - confirmationService = spectator.inject(ConfirmationService); - }); - describe('dot-ema-running-experiment', () => { - it('should have attr', () => { - const experiments = spectator.query(DotEmaRunningExperimentComponent); - expect(experiments.runningExperiment).toEqual({ - status: DotExperimentStatus.RUNNING + describe('dot-ema-running-experiment', () => { + it('should have attr', () => { + const experiments = spectator.query(DotEmaRunningExperimentComponent); + expect(experiments.runningExperiment).toEqual(experiment); }); }); }); - }); - - describe('urlContentMap', () => { - beforeEach(() => { - spectator = createComponent({ - providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - editorData: { - mode: EDITOR_MODE.EDIT, - canEditPage: true, - page: { - isLocked: false, - canLock: true, - lockedByUser: '' - } - }, - editor: { - urlContentMap: { - identifier: '123', - inode: '456', - title: 'This is the content title' - }, - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } + describe('urlContentMap', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(UVEStore, { + $toolbarProps: signal({ + bookmarksUrl, + copyUrl: '', + apiUrl: '', + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: URL_CONTENT_MAP_MOCK, + runningExperiment: getRunningExperimentMock(), + workflowActionsInode: '', + unlockButton: null, + showInfoDisplay: true, + deviceSelector: { + apiLink: '', + hideSocialMedia: true }, - showWorkflowActions: true, - showInfoDisplay: true + personaSelector: { + pageId: '', + value: DEFAULT_PERSONA + } }), - load: jest.fn(), setDevice: jest.fn(), setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } - ] + params: signal(params) + }) + ] + }); }); - store = spectator.inject(EditEmaStore); - messageService = spectator.inject(MessageService); - router = spectator.inject(Router); - confirmationService = spectator.inject(ConfirmationService); - }); - it('should have attr', () => { - const editURLContentButton = spectator.debugElement.query( - By.css('[data-testId="edit-url-content-map"]') - ); + it('should have attr', () => { + const editURLContentButton = spectator.debugElement.query( + By.css('[data-testId="edit-url-content-map"]') + ); - expect(editURLContentButton.attributes).toEqual({ - class: 'p-element', - 'data-testId': 'edit-url-content-map', - icon: 'pi pi-pencil', - 'ng-reflect-icon': 'pi pi-pencil', - 'ng-reflect-style-class': 'p-button-text p-button-sm', - styleClass: 'p-button-text p-button-sm' + expect(editURLContentButton.attributes).toEqual({ + 'data-testId': 'edit-url-content-map', + icon: 'pi pi-pencil', + 'ng-reflect-icon': 'pi pi-pencil', + 'ng-reflect-style-class': 'p-button-text p-button-sm', + styleClass: 'p-button-text p-button-sm' + }); }); - }); - it('should emit', () => { - let output; - spectator.output('editUrlContentMap').subscribe((result) => (output = result)); + it('should emit', () => { + let output; + spectator.output('editUrlContentMap').subscribe((result) => (output = result)); - const editURLContentButton = spectator.debugElement.query( - By.css('[data-testId="edit-url-content-map"]') - ); + const editURLContentButton = spectator.debugElement.query( + By.css('[data-testId="edit-url-content-map"]') + ); - spectator.triggerEventHandler(editURLContentButton, 'onClick', { - identifier: '123', - inode: '456', - title: 'This is the content title' - }); + spectator.triggerEventHandler(editURLContentButton, 'onClick', {}); - expect(output).toEqual({ - identifier: '123', - inode: '456', - title: 'This is the content title' + expect(output).toEqual(URL_CONTENT_MAP_MOCK); }); }); - }); - describe('locked', () => { - describe('locked with unlock permission', () => { + describe('locked', () => { beforeEach(() => { spectator = createComponent({ providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - editorData: { - mode: EDITOR_MODE.EDIT, - canEditPage: true, - page: { - isLocked: true, - canLock: true, - lockedByUser: 'user' - } - }, - editor: { - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } - }, - showWorkflowActions: false, - showInfoDisplay: true - }), - load: jest.fn(), - setDevice: jest.fn(), - setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } + mockProvider(UVEStore, { + $toolbarProps: signal({ + bookmarksUrl, + copyUrl: '', + apiUrl: '', + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: URL_CONTENT_MAP_MOCK, + runningExperiment: getRunningExperimentMock(), + workflowActionsInode: '', + unlockButton: { + inode: '1234', + isLoading: false + }, + showInfoDisplay: true, + deviceSelector: { + apiLink: '', + hideSocialMedia: true + }, + personaSelector: { + pageId: '', + value: DEFAULT_PERSONA + } + }), + setDevice: jest.fn(), + setSocialMedia: jest.fn(), + params: signal(params) + }) ] }); - store = spectator.inject(EditEmaStore); - messageService = spectator.inject(MessageService); - router = spectator.inject(Router); - confirmationService = spectator.inject(ConfirmationService); }); - it('should render a unlock button', () => { + it('should render a unlock button when unlockButton is not null', () => { spectator.detectChanges(); expect(spectator.query(byTestId('unlock-button'))).toBeDefined(); }); }); - - describe('locked without unlock permission', () => { - beforeEach(() => { - spectator = createComponent({ - providers: [ - { - provide: EditEmaStore, - useValue: { - editorToolbarData$: of({ - favoritePageURL: 'http://localhost:8080/fav', - iframeURL: 'http://localhost:8080/index', - clientHost: 'http://localhost:3000', - apiURL: 'http://localhost/api/v1/page/json/page-one', - editorData: { - mode: EDITOR_MODE.EDIT, - canEditPage: true, - page: { - isLocked: true, - canLock: false, - lockedByUser: 'user' - } - }, - editor: { - page: { - identifier: '123', - inode: '456' - }, - viewAs: { - persona: { - id: '123' - }, - language: { - id: 1 - } - } - }, - showWorkflowActions: false, - showInfoDisplay: true - }), - load: jest.fn(), - setDevice: jest.fn(), - setSocialMedia: jest.fn(), - updateEditorState: jest.fn() - } - } - ] - }); - store = spectator.inject(EditEmaStore); - messageService = spectator.inject(MessageService); - router = spectator.inject(Router); - confirmationService = spectator.inject(ConfirmationService); - }); - - it('should not render a unlock button', () => { - spectator.detectChanges(); - expect(spectator.query(byTestId('unlock-button'))).toBeNull(); - }); - }); }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.ts index 5b6fe9d9a908..4cadcc391d87 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/edit-ema-toolbar/edit-ema-toolbar.component.ts @@ -1,5 +1,6 @@ +import { tapResponse } from '@ngrx/operators'; + import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -8,27 +9,24 @@ import { ViewChild, inject } from '@angular/core'; -import { ActivatedRoute, Params, Router } from '@angular/router'; +import { Params, Router } from '@angular/router'; import { ConfirmationService, MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { MenuModule } from 'primeng/menu'; import { ToolbarModule } from 'primeng/toolbar'; -import { DotMessageService, DotPersonalizeService } from '@dotcms/data-access'; import { - DotCMSContentlet, - DotDevice, - DotExperimentStatus, - DotPersona -} from '@dotcms/dotcms-models'; + DotContentletLockerService, + DotMessageService, + DotPersonalizeService +} from '@dotcms/data-access'; +import { DotCMSContentlet, DotDevice, DotPersona } from '@dotcms/dotcms-models'; import { DotDeviceSelectorSeoComponent } from '@dotcms/portlets/dot-ema/ui'; import { DotMessagePipe } from '@dotcms/ui'; -import { EditEmaStore } from '../../../dot-ema-shell/store/dot-ema.store'; -import { DotPageApiParams } from '../../../services/dot-page-api.service'; import { DEFAULT_PERSONA } from '../../../shared/consts'; -import { EDITOR_MODE, EDITOR_STATE } from '../../../shared/enums'; +import { UVEStore } from '../../../store/dot-uve.store'; import { DotEditEmaWorkflowActionsComponent } from '../dot-edit-ema-workflow-actions/dot-edit-ema-workflow-actions.component'; import { DotEmaBookmarksComponent } from '../dot-ema-bookmarks/dot-ema-bookmarks.component'; import { DotEmaInfoDisplayComponent } from '../dot-ema-info-display/dot-ema-info-display.component'; @@ -40,7 +38,6 @@ import { EditEmaPersonaSelectorComponent } from '../edit-ema-persona-selector/ed selector: 'dot-edit-ema-toolbar', standalone: true, imports: [ - CommonModule, MenuModule, ButtonModule, ToolbarModule, @@ -65,22 +62,16 @@ export class EditEmaToolbarComponent { @ViewChild('personaSelector') personaSelector!: EditEmaPersonaSelectorComponent; - private readonly store = inject(EditEmaStore); - private readonly messageService = inject(MessageService); - private readonly dotMessageService = inject(DotMessageService); - private readonly router = inject(Router); - private readonly confirmationService = inject(ConfirmationService); - private readonly personalizeService = inject(DotPersonalizeService); - private readonly activatedRouter = inject(ActivatedRoute); - - readonly editorToolbarData$ = this.store.editorToolbarData$; - readonly editorState = EDITOR_STATE; - readonly editorMode = EDITOR_MODE; - readonly experimentStatus = DotExperimentStatus; - - get queryParams(): DotPageApiParams { - return this.activatedRouter.snapshot.queryParams as DotPageApiParams; - } + readonly #messageService = inject(MessageService); + readonly #dotMessageService = inject(DotMessageService); + readonly #router = inject(Router); + readonly #dotContentletLockerService = inject(DotContentletLockerService); + readonly #confirmationService = inject(ConfirmationService); + readonly #personalizeService = inject(DotPersonalizeService); + + readonly uveStore = inject(UVEStore); + + protected readonly $toolbarProps = this.uveStore.$toolbarProps; /** * Update the current device @@ -89,7 +80,7 @@ export class EditEmaToolbarComponent { * @memberof EditEmaToolbarComponent */ updateCurrentDevice(device: DotDevice & { icon?: string }) { - this.store.setDevice(device); + this.uveStore.setDevice(device); } /** @@ -99,7 +90,7 @@ export class EditEmaToolbarComponent { * @memberof EditEmaToolbarComponent */ onSeoMediaChange(seoMedia: string) { - this.store.setSocialMedia(seoMedia); + this.uveStore.setSocialMedia(seoMedia); } /** @@ -108,9 +99,9 @@ export class EditEmaToolbarComponent { * @memberof EditEmaToolbarComponent */ triggerCopyToast() { - this.messageService.add({ + this.#messageService.add({ severity: 'success', - summary: this.dotMessageService.get('Copied'), + summary: this.#dotMessageService.get('Copied'), life: 3000 }); } @@ -139,16 +130,16 @@ export class EditEmaToolbarComponent { 'com.dotmarketing.persona.id': persona.identifier }); } else { - this.confirmationService.confirm({ - header: this.dotMessageService.get('editpage.personalization.confirm.header'), - message: this.dotMessageService.get( + this.#confirmationService.confirm({ + header: this.#dotMessageService.get('editpage.personalization.confirm.header'), + message: this.#dotMessageService.get( 'editpage.personalization.confirm.message', persona.name ), - acceptLabel: this.dotMessageService.get('dot.common.dialog.accept'), - rejectLabel: this.dotMessageService.get('dot.common.dialog.reject'), + acceptLabel: this.#dotMessageService.get('dot.common.dialog.accept'), + rejectLabel: this.#dotMessageService.get('dot.common.dialog.reject'), accept: () => { - this.personalizeService + this.#personalizeService .personalized(persona.pageId, persona.keyTag) .subscribe(() => { this.updateQueryParams({ @@ -172,16 +163,16 @@ export class EditEmaToolbarComponent { * @memberof EditEmaToolbarComponent */ onDespersonalize(persona: DotPersona & { pageId: string; selected: boolean }) { - this.confirmationService.confirm({ - header: this.dotMessageService.get('editpage.personalization.delete.confirm.header'), - message: this.dotMessageService.get( + this.#confirmationService.confirm({ + header: this.#dotMessageService.get('editpage.personalization.delete.confirm.header'), + message: this.#dotMessageService.get( 'editpage.personalization.delete.confirm.message', persona.name ), - acceptLabel: this.dotMessageService.get('dot.common.dialog.accept'), - rejectLabel: this.dotMessageService.get('dot.common.dialog.reject'), + acceptLabel: this.#dotMessageService.get('dot.common.dialog.accept'), + rejectLabel: this.#dotMessageService.get('dot.common.dialog.reject'), accept: () => { - this.personalizeService + this.#personalizeService .despersonalized(persona.pageId, persona.keyTag) .subscribe(() => { this.personaSelector.fetchPersonas(); @@ -206,7 +197,7 @@ export class EditEmaToolbarComponent { handleNewPage(page: DotCMSContentlet): void { const { pageURI, url, languageId } = page; const params = { - ...this.updateQueryParams, + ...this.uveStore.params(), url: pageURI ?? url, language_id: languageId?.toString() }; @@ -223,12 +214,37 @@ export class EditEmaToolbarComponent { * @memberof EditEmaToolbarComponent */ unlockPage(inode: string) { - this.store.unlockPage(inode); + this.#messageService.add({ + severity: 'info', + summary: this.#dotMessageService.get('edit.ema.page.unlock'), + detail: this.#dotMessageService.get('edit.ema.page.is.being.unlocked') + }); + + this.#dotContentletLockerService + .unlock(inode) + .pipe( + tapResponse({ + next: () => { + this.#messageService.add({ + severity: 'success', + summary: this.#dotMessageService.get('edit.ema.page.unlock'), + detail: this.#dotMessageService.get('edit.ema.page.unlock.success') + }); + }, + error: () => { + this.#messageService.add({ + severity: 'error', + summary: this.#dotMessageService.get('edit.ema.page.unlock'), + detail: this.#dotMessageService.get('edit.ema.page.unlock.error') + }); + } + }) + ) + .subscribe(() => this.uveStore.reload()); } private updateQueryParams(params: Params) { - this.store.updateEditorState(EDITOR_STATE.LOADING); - this.router.navigate([], { + this.#router.navigate([], { queryParams: params, queryParamsHandling: 'merge' }); @@ -236,7 +252,7 @@ export class EditEmaToolbarComponent { private shouldReload(params: Params): boolean { const { url: newUrl, language_id: newLanguageId } = params; - const { url, language_id } = this.queryParams; + const { url, language_id } = this.uveStore.params(); return newUrl != url || newLanguageId != language_id; } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html index 6d6558bf9f2c..2351de681c60 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html @@ -4,54 +4,65 @@ [ngStyle]="styles.topButton" data-testId="add-top-button" icon="pi pi-plus"> - + @if (!isContainerEmpty) { + + } } -
- @if (contentletArea.payload.vtlFiles?.length) { - - - } - @if (contentletArea.payload.container) { +@if (!isContainerEmpty) { +
+ @if (contentletArea.payload.vtlFiles?.length) { + + + } + @if (contentletArea.payload.container) { + + + } + - - } - - -
+ icon="pi pi-pencil" /> +
+}
-
- {{ contentletArea.payload.contentlet.contentType }} -
+ +@if (contentletArea) { +
+ {{ contentletArea.payload.contentlet.contentType }} +
+} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.ts index 5d2bd150f0a9..49e8aa54d990 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@angular/common'; +import { JsonPipe, NgStyle } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -33,7 +33,7 @@ const INITIAL_ACTIONS_CONTAINER_WIDTH = 128; @Component({ selector: 'dot-ema-contentlet-tools', standalone: true, - imports: [CommonModule, ButtonModule, MenuModule], + imports: [NgStyle, ButtonModule, MenuModule, JsonPipe], templateUrl: './ema-contentlet-tools.component.html', styleUrls: ['./ema-contentlet-tools.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html index 963d6f0810c3..3c473cedc604 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html @@ -1,37 +1,38 @@ -
+@for (container of containers; track $index) {
+ [attr.data-empty]="true" + data-type="container"> + @for (contentlet of container.contentlets; track $index) { +
+ } - -
+ + +} @if (containers.length > 0) {
} -
- -
+ @if (error.message.length) { +
+ +
+ }
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.scss b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.scss index 8d88d6a459c4..d06e57acc421 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.scss +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.scss @@ -9,6 +9,7 @@ pointer-events: none; user-select: none; position: absolute; + z-index: 2; } [data-type="container"] { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.spec.ts index a52f76bb0c0a..438abc275f77 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.spec.ts @@ -7,71 +7,14 @@ import { DotMessageService } from '@dotcms/data-access'; import { MockDotMessageService } from '@dotcms/utils-testing'; import { EmaPageDropzoneComponent } from './ema-page-dropzone.component'; -import { Container } from './types'; - -import { ClientData } from '../../../shared/models'; - -const ACTION_MOCK: ClientData = { - container: { - acceptTypes: 'file', - identifier: '789', - maxContentlets: 100, - uuid: '2', - variantId: '1' - } -}; - -const ITEM_MOCK = { - contentType: 'file', - baseType: 'FILEASSET', - draggedPayload: null -}; - -const getBoundsMockWithEmptyContainer = (payload: ClientData): Container[] => { - return [ - { - x: 10, - y: 10, - width: 980, - height: 180, - contentlets: [], - payload - } - ]; -}; - -const getBoundsMock = (payload: ClientData): Container[] => { - return [ - { - x: 10, - y: 10, - width: 980, - height: 180, - contentlets: [ - { - x: 20, - y: 20, - width: 940, - height: 140, - payload: null - }, - { - x: 40, - y: 20, - width: 940, - height: 140, - payload: null - } - ], - payload - } - ]; -}; - -export const BOUNDS_MOCK: Container[] = getBoundsMock(ACTION_MOCK); - -export const BOUNDS_EMPTY_CONTAINER_MOCK: Container[] = - getBoundsMockWithEmptyContainer(ACTION_MOCK); + +import { + ITEM_MOCK, + BOUNDS_MOCK, + getBoundsMock, + ACTION_MOCK, + BOUNDS_EMPTY_CONTAINER_MOCK +} from '../../../shared/mocks'; const messageServiceMock = new MockDotMessageService({ 'edit.ema.page.dropzone.invalid.contentlet.type': diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.ts index 32bf68b161dc..cec243f8deab 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@angular/common'; +import { NgStyle, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -24,7 +24,7 @@ const POINTER_INITIAL_POSITION = { @Component({ selector: 'dot-ema-page-dropzone', standalone: true, - imports: [CommonModule, DotPositionPipe, DotErrorPipe, DotMessagePipe], + imports: [DotPositionPipe, DotErrorPipe, DotMessagePipe, NgStyle, NgTemplateOutlet], templateUrl: './ema-page-dropzone.component.html', styleUrls: ['./ema-page-dropzone.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/error/dot-error.pipe.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/error/dot-error.pipe.spec.ts index efa60f973d2c..704f8b71cd24 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/error/dot-error.pipe.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/error/dot-error.pipe.spec.ts @@ -1,6 +1,6 @@ import { DotErrorPipe } from './dot-error.pipe'; -import { PAYLOAD_MOCK } from '../../../../../shared/consts'; +import { PAYLOAD_MOCK } from '../../../../../shared/mocks'; import { ClientData } from '../../../../../shared/models'; import { Container, EmaDragItem } from '../../types'; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/position/dot-position.pipe.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/position/dot-position.pipe.spec.ts index f596c9b6c6e5..1e7a80fe4ebb 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/position/dot-position.pipe.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/pipes/position/dot-position.pipe.spec.ts @@ -1,6 +1,6 @@ import { DotPositionPipe } from './dot-position.pipe'; -import { PAYLOAD_MOCK } from '../../../../../shared/consts'; +import { PAYLOAD_MOCK } from '../../../../../shared/mocks'; import { ContentletArea } from '../../types'; describe('DotPositionPipe', () => { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html index bf03fa5d59b3..af8bcb118f42 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.html @@ -1,102 +1,83 @@ - - + + +@if ($editorProps().seoResults && ogTagsResults$) { -
+} +@if ($editorProps().showEditorContent) { +
- - - - + @if ($editorProps().progressBar) { + + } + @if ($editorProps().contentletTools; as contentletTools) { + + } + @if ($editorProps().dropzone; as dropzone) { + + }
- +} +@if ($editorProps().palette; as palette) { - - @if ( - es.editorData.canEditVariant && - (es.editorData.mode === editorMode.EDIT || es.editorData.mode === editorMode.EDIT_VARIANT) - ) { - - - } - +} +@if ($editorProps().showDialogs) { + + +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.scss b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.scss index 738be743f24d..999928284c17 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.scss +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.scss @@ -41,13 +41,6 @@ dot-results-seo-tool { gap: $spacing-1; overflow: auto; } -.editor-content--device { - grid-column: 1 / -1; -} - -.editor-content--hidden { - display: none; -} .iframe-wrapper { position: relative; @@ -55,6 +48,8 @@ dot-results-seo-tool { flex-grow: 1; margin: 0 auto; border: solid 1px $color-palette-gray-300; + height: 100%; + width: 100%; iframe { border: none; @@ -72,7 +67,3 @@ a:focus, a:active { text-decoration: none; } - -.editor-content--expanded { - grid-column: span 2; -} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts index 3888698c12ba..debc5a6b84f7 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts @@ -73,32 +73,28 @@ import { import { DotEditEmaWorkflowActionsComponent } from './components/dot-edit-ema-workflow-actions/dot-edit-ema-workflow-actions.component'; import { DotEmaRunningExperimentComponent } from './components/dot-ema-running-experiment/dot-ema-running-experiment.component'; import { CONTENT_TYPE_MOCK } from './components/edit-ema-palette/components/edit-ema-palette-content-type/edit-ema-palette-content-type.component.spec'; -import { EditEmaPaletteComponent } from './components/edit-ema-palette/edit-ema-palette.component'; import { CONTENTLETS_MOCK } from './components/edit-ema-palette/edit-ema-palette.component.spec'; import { EditEmaToolbarComponent } from './components/edit-ema-toolbar/edit-ema-toolbar.component'; import { EmaContentletToolsComponent } from './components/ema-contentlet-tools/ema-contentlet-tools.component'; import { EditEmaEditorComponent } from './edit-ema-editor.component'; import { DotEmaDialogComponent } from '../components/dot-ema-dialog/dot-ema-dialog.component'; -import { EditEmaStore } from '../dot-ema-shell/store/dot-ema.store'; import { DotActionUrlService } from '../services/dot-action-url/dot-action-url.service'; import { DotPageApiService } from '../services/dot-page-api.service'; +import { DEFAULT_PERSONA, WINDOW, HOST } from '../shared/consts'; +import { EDITOR_STATE, NG_CUSTOM_EVENTS, UVE_STATUS } from '../shared/enums'; import { - DEFAULT_PERSONA, - WINDOW, - HOST, - PAYLOAD_MOCK, - EDIT_ACTION_PAYLOAD_MOCK, - PAGE_INODE_MOCK, QUERY_PARAMS_MOCK, - TREE_NODE_MOCK, + PAGE_INODE_MOCK, + dotPageContainerStructureMock, URL_CONTENT_MAP_MOCK, + EDIT_ACTION_PAYLOAD_MOCK, + TREE_NODE_MOCK, newContentlet, - dotPageContainerStructureMock, - SHOW_CONTENTLET_TOOLS_PATCH_MOCK -} from '../shared/consts'; -import { EDITOR_MODE, EDITOR_STATE, NG_CUSTOM_EVENTS } from '../shared/enums'; + PAYLOAD_MOCK +} from '../shared/mocks'; import { ActionPayload, ContentTypeDragPayload } from '../shared/models'; +import { UVEStore } from '../store/dot-uve.store'; import { SDK_EDITOR_SCRIPT_SOURCE } from '../utils'; global.URL.createObjectURL = jest.fn( @@ -146,7 +142,7 @@ const createRouting = (permissions: { canEdit: boolean; canRead: boolean }) => componentProviders: [ ConfirmationService, MessageService, - EditEmaStore, + UVEStore, DotFavoritePageService, DotESContentService, { @@ -218,6 +214,7 @@ const createRouting = (permissions: { canEdit: boolean; canRead: boolean }) => } ], providers: [ + Router, DotSeoMetaTagsUtilService, DialogService, DotCopyContentService, @@ -529,7 +526,7 @@ const createRouting = (permissions: { canEdit: boolean; canRead: boolean }) => describe('EditEmaEditorComponent', () => { describe('with queryParams and permission', () => { let spectator: SpectatorRouting; - let store: EditEmaStore; + let store: InstanceType; let confirmationService: ConfirmationService; let messageService: MessageService; let addMessageSpy: jest.SpyInstance; @@ -539,6 +536,8 @@ describe('EditEmaEditorComponent', () => { let dotHttpErrorManagerService: DotHttpErrorManagerService; let dotTempFileUploadService: DotTempFileUploadService; let dotWorkflowActionsFireService: DotWorkflowActionsFireService; + let router: Router; + let dotPageApiService: DotPageApiService; const createComponent = createRouting({ canEdit: true, canRead: true }); @@ -560,7 +559,7 @@ describe('EditEmaEditorComponent', () => { } }); - store = spectator.inject(EditEmaStore, true); + store = spectator.inject(UVEStore, true); confirmationService = spectator.inject(ConfirmationService, true); messageService = spectator.inject(MessageService, true); dotCopyContentModalService = spectator.inject(DotCopyContentModalService, true); @@ -569,6 +568,8 @@ describe('EditEmaEditorComponent', () => { dotContentletService = spectator.inject(DotContentletService, true); dotTempFileUploadService = spectator.inject(DotTempFileUploadService, true); dotWorkflowActionsFireService = spectator.inject(DotWorkflowActionsFireService, true); + router = spectator.inject(Router, true); + dotPageApiService = spectator.inject(DotPageApiService, true); addMessageSpy = jest.spyOn(messageService, 'add'); @@ -580,11 +581,9 @@ describe('EditEmaEditorComponent', () => { }); spectator.detectChanges(); - - store.updateEditorState(EDITOR_STATE.IDLE); }); - describe('Preview mode', () => { + describe('DOM', () => { beforeEach(() => { jest.useFakeTimers(); // Mock the timers }); @@ -593,7 +592,7 @@ describe('EditEmaEditorComponent', () => { jest.useRealTimers(); // Restore the real timers after each test }); - it('should hide the components that are not needed for preview mode', () => { + it('should hide components when the store changes', () => { const componentsToHide = [ 'palette', 'dropzone', @@ -613,7 +612,7 @@ describe('EditEmaEditorComponent', () => { }); }); - it('should hide the editor components when there is a running experiement and initialize the editor in a variant', () => { + it('should hide components when the store changes for a variant', () => { const componentsToHide = [ 'palette', 'dropzone', @@ -696,7 +695,7 @@ describe('EditEmaEditorComponent', () => { position: 'after' }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -720,22 +719,9 @@ describe('EditEmaEditorComponent', () => { .querySelector('.p-confirm-dialog-accept') .dispatchEvent(new Event('click')); // This is the internal button, coudln't find a better way to test it - expect(saveMock).toHaveBeenCalledWith({ - pageContainers: [ - { - identifier: '123', - uuid: '123', - contentletsId: [], - personaTag: undefined - } - ], - pageId: '123', - whenSaved: expect.any(Function), - params: { - language_id: 1, - url: 'page-one' - } - }); + expect(saveMock).toHaveBeenCalledWith([ + { contentletsId: [], identifier: '123', personaTag: undefined, uuid: '123' } + ]); }); }); @@ -774,7 +760,7 @@ describe('EditEmaEditorComponent', () => { By.css('[data-testId="ema-dialog"]') ); - store.setContentletArea(baseContentletPayload); + store.setEditorContentletArea(baseContentletPayload); spectator.detectComponentChanges(); @@ -850,12 +836,7 @@ describe('EditEmaEditorComponent', () => { }) }); - expect(reloadSpy).toHaveBeenCalledWith({ - params: { - language_id: 1, - url: 'page-one' - } - }); + expect(reloadSpy).toHaveBeenCalled(); expect(messageSpy).toHaveBeenCalledWith({ severity: 'success', @@ -945,7 +926,7 @@ describe('EditEmaEditorComponent', () => { By.css('[data-testId="ema-dialog"]') ); - store.setContentletArea(baseContentletPayload); + store.setEditorContentletArea(baseContentletPayload); editURLContentButton.triggerEventHandler('onClick', {}); @@ -999,7 +980,7 @@ describe('EditEmaEditorComponent', () => { }); it('should update the query params after editing a urlContentMap if the url changed', () => { - const SpyEditorState = jest.spyOn(store, 'updateEditorState'); + const SpyEditorState = jest.spyOn(store, 'setEditorState'); const queryParams = { queryParams: { url: URL_MAP_CONTENTLET.URL_MAP_FOR_CONTENT @@ -1019,7 +1000,7 @@ describe('EditEmaEditorComponent', () => { }); it('should handler error ', () => { - const SpyEditorState = jest.spyOn(store, 'updateEditorState'); + const SpyEditorState = jest.spyOn(store, 'setEditorState'); const SpyHandlerError = jest .spyOn(dotHttpErrorManagerService, 'handle') .mockReturnValue(of(null)); @@ -1078,9 +1059,7 @@ describe('EditEmaEditorComponent', () => { spectator.component.iframe.nativeElement.contentWindow, 'postMessage' ); - jest.spyOn(spectator.component, 'currentTreeNode').mockReturnValue( - TREE_NODE_MOCK - ); + jest.spyOn(store, 'getCurrentTreeNode').mockReturnValue(TREE_NODE_MOCK); }); it('should copy and open edit dialog', () => { @@ -1089,7 +1068,7 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); - store.setContentletArea(CONTENTLET_MOCK); + store.setEditorContentletArea(CONTENTLET_MOCK); spectator.detectComponentChanges(); @@ -1117,7 +1096,7 @@ describe('EditEmaEditorComponent', () => { modalSpy.mockReturnValue(of({ shouldCopy: true })); spectator.detectChanges(); - store.setContentletArea(CONTENTLET_MOCK); + store.setEditorContentletArea(CONTENTLET_MOCK); spectator.detectComponentChanges(); @@ -1144,7 +1123,7 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); - store.setContentletArea(CONTENTLET_MOCK); + store.setEditorContentletArea(CONTENTLET_MOCK); spectator.detectComponentChanges(); @@ -1164,6 +1143,25 @@ describe('EditEmaEditorComponent', () => { expect(modalSpy).toHaveBeenCalled(); expect(reloadIframeSpy).not.toHaveBeenCalledWith(); }); + + it('should trigger copy contentlet dialog', () => { + store.setEditorContentletArea(CONTENTLET_MOCK); + window.dispatchEvent( + new MessageEvent('message', { + origin: HOST, + data: { + action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING, + payload: { + inode: '123' + } + } + }) + ); + + spectator.detectComponentChanges(); + + expect(modalSpy).toHaveBeenCalled(); + }); }); beforeEach(() => { @@ -1179,7 +1177,7 @@ describe('EditEmaEditorComponent', () => { const payload: ActionPayload = { ...PAYLOAD_MOCK }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1229,15 +1227,7 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); - expect(savePageMock).toHaveBeenCalledWith({ - pageContainers: PAYLOAD_MOCK.pageContainers, - pageId: PAYLOAD_MOCK.pageId, - whenSaved: expect.any(Function), - params: { - language_id: 1, - url: 'page-one' - } - }); + expect(savePageMock).toHaveBeenCalledWith(PAYLOAD_MOCK.pageContainers); spectator.detectChanges(); }); @@ -1247,7 +1237,7 @@ describe('EditEmaEditorComponent', () => { const payload: ActionPayload = { ...PAYLOAD_MOCK }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1338,7 +1328,7 @@ describe('EditEmaEditorComponent', () => { position: 'after' }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1375,25 +1365,17 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); - expect(saveMock).toHaveBeenCalledWith({ - pageContainers: [ - { - identifier: 'container-identifier-123', - uuid: 'uuid-123', - contentletsId: [ - 'contentlet-identifier-123', - 'new-contentlet-identifier-123' - ], - personaTag: undefined - } - ], - pageId: 'test', - whenSaved: expect.any(Function), - params: { - language_id: 1, - url: 'page-one' + expect(saveMock).toHaveBeenCalledWith([ + { + identifier: 'container-identifier-123', + uuid: 'uuid-123', + contentletsId: [ + 'contentlet-identifier-123', + 'new-contentlet-identifier-123' + ], + personaTag: undefined } - }); + ]); }); it('should not add contentlet after backend emit CONTENT_SEARCH_SELECT and contentlet is dupe', () => { @@ -1427,7 +1409,7 @@ describe('EditEmaEditorComponent', () => { position: 'before' }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1505,7 +1487,7 @@ describe('EditEmaEditorComponent', () => { position: 'after' }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1542,25 +1524,17 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); - expect(saveMock).toHaveBeenCalledWith({ - pageContainers: [ - { - identifier: 'container-identifier-123', - uuid: 'uuid-123', - contentletsId: [ - 'contentlet-identifier-123', - 'new-contentlet-identifier-123' - ], - personaTag: undefined - } - ], - pageId: 'test', - whenSaved: expect.any(Function), - params: { - language_id: 1, - url: 'page-one' + expect(saveMock).toHaveBeenCalledWith([ + { + identifier: 'container-identifier-123', + uuid: 'uuid-123', + contentletsId: [ + 'contentlet-identifier-123', + 'new-contentlet-identifier-123' + ], + personaTag: undefined } - }); + ]); }); it('should not add widget after backend emit CONTENT_SEARCH_SELECT and widget is dupe', () => { @@ -1594,7 +1568,7 @@ describe('EditEmaEditorComponent', () => { position: 'before' }; - store.setContentletArea({ + store.setEditorContentletArea({ x: 100, y: 100, width: 500, @@ -1642,8 +1616,8 @@ describe('EditEmaEditorComponent', () => { describe('drag and drop', () => { describe('drag start', () => { - it('should call the setDragItem from the store for content-types', () => { - const setDragItemSpy = jest.spyOn(store, 'setDragItem'); + it('should call the setEditorDragItem from the store for content-types', () => { + const setEditorDragItemSpy = jest.spyOn(store, 'setEditorDragItem'); const target = { target: { @@ -1670,7 +1644,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragStart); - expect(setDragItemSpy).toHaveBeenCalledWith({ + expect(setEditorDragItemSpy).toHaveBeenCalledWith({ baseType: 'test', contentType: 'test', draggedPayload: { @@ -1684,10 +1658,10 @@ describe('EditEmaEditorComponent', () => { }); }); - it('should call the setDragItem from the store for contentlets', () => { + it('should call the setEditorDragItem from the store for contentlets', () => { const contentlet = CONTENTLETS_MOCK[0]; - const setDragItemSpy = jest.spyOn(store, 'setDragItem'); + const setEditorDragItemSpy = jest.spyOn(store, 'setEditorDragItem'); const target = { target: { @@ -1710,7 +1684,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragStart); - expect(setDragItemSpy).toHaveBeenCalledWith({ + expect(setEditorDragItemSpy).toHaveBeenCalledWith({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -1723,7 +1697,7 @@ describe('EditEmaEditorComponent', () => { }); }); - it('should call the setDragItem from the store for contentlets and move', () => { + it('should call the setEditorDragItem from the store for contentlets and move', () => { const contentlet = CONTENTLETS_MOCK[0]; const container = { @@ -1738,7 +1712,7 @@ describe('EditEmaEditorComponent', () => { ] }; - const setDragItemSpy = jest.spyOn(store, 'setDragItem'); + const setEditorDragItemSpy = jest.spyOn(store, 'setEditorDragItem'); const target = { target: { @@ -1762,7 +1736,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragStart); - expect(setDragItemSpy).toHaveBeenCalledWith({ + expect(setEditorDragItemSpy).toHaveBeenCalledWith({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -1789,8 +1763,8 @@ describe('EditEmaEditorComponent', () => { }); describe('drag end', () => { - it('should reset the editor state to IDLE when dropEffect is none', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + it('should reset the editor properties when dropEffect is none', () => { + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); const dragEnd = new Event('dragend'); @@ -1803,10 +1777,10 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragEnd); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); - it('should not reset the editor state to IDLE when dropEffect is not none', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + it('should not reset the editor properties when dropEffect is not none', () => { + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); const dragEnd = new Event('dragend'); @@ -1819,13 +1793,13 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragEnd); - expect(updateEditorStateSpy).not.toHaveBeenCalled(); + expect(resetEditorPropertiesSpy).not.toHaveBeenCalled(); }); }); describe('drag leave', () => { it('should set the editor state to OUT_OF_BOUNDS', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const setEditorStateSpy = jest.spyOn(store, 'setEditorState'); const dragLeave = new Event('dragleave'); @@ -1843,12 +1817,10 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragLeave); - expect(updateEditorStateSpy).toHaveBeenCalledWith( - EDITOR_STATE.OUT_OF_BOUNDS - ); + expect(setEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.OUT_OF_BOUNDS); }); it('should not set the editor state to OUT_OF_BOUNDS when the leave is from an element in the window', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const setEditorStateSpy = jest.spyOn(store, 'setEditorState'); const dragLeave = new Event('dragleave'); @@ -1866,7 +1838,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragLeave); - expect(updateEditorStateSpy).not.toHaveBeenCalled(); + expect(setEditorStateSpy).not.toHaveBeenCalled(); }); }); @@ -1887,7 +1859,7 @@ describe('EditEmaEditorComponent', () => { }); it('should set the dragItem if there is no dragItem', () => { - const setDragItemSpy = jest.spyOn(store, 'setDragItem'); + const setEditorDragItemSpy = jest.spyOn(store, 'setEditorDragItem'); const dragEnter = new Event('dragenter'); @@ -1898,7 +1870,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragEnter); - expect(setDragItemSpy).toHaveBeenCalledWith({ + expect(setEditorDragItemSpy).toHaveBeenCalledWith({ baseType: 'dotAsset', contentType: 'dotAsset', draggedPayload: { @@ -1908,7 +1880,7 @@ describe('EditEmaEditorComponent', () => { }); it('should set the editor to DRAGGING if there is dragItem and the state is OUT_OF_BOUNDS', () => { - store.setDragItem({ + store.setEditorDragItem({ baseType: 'dotAsset', contentType: 'dotAsset', draggedPayload: { @@ -1916,9 +1888,9 @@ describe('EditEmaEditorComponent', () => { } }); // Simulate drag start - store.updateEditorState(EDITOR_STATE.OUT_OF_BOUNDS); // Simulate drag leave + store.setEditorState(EDITOR_STATE.OUT_OF_BOUNDS); // Simulate drag leave - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const setEditorStateSpy = jest.spyOn(store, 'setEditorState'); const dragEnter = new Event('dragenter'); @@ -1929,7 +1901,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragEnter); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); + expect(setEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); }); }); @@ -1955,7 +1927,7 @@ describe('EditEmaEditorComponent', () => { it('should update the editor state when the drop is not in a dropzone', () => { const drop = new Event('drop'); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); Object.defineProperty(drop, 'target', { writable: false, @@ -1968,15 +1940,15 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(drop); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); it('should do the place item flow when dropping a contentlet and is not moving', () => { const contentlet = CONTENTLETS_MOCK[0]; - const savePapeSpy = jest.spyOn(store, 'savePage'); + const savePageSpy = jest.spyOn(store, 'savePage'); - store.setDragItem({ + store.setEditorDragItem({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -2017,27 +1989,20 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(drop); - expect(savePapeSpy).toHaveBeenCalledWith({ - pageContainers: [ - { - identifier: '123', - uuid: '123', - personaTag: 'dot:persona', - contentletsId: ['123', contentlet.identifier, '456'] // Before 456 - }, - { - identifier: '123', - uuid: '456', - personaTag: 'dot:persona', - contentletsId: ['123'] - } - ], - pageId: '123', - params: { - language_id: 1, - url: 'page-one' + expect(savePageSpy).toHaveBeenCalledWith([ + { + identifier: '123', + uuid: '123', + personaTag: 'dot:persona', + contentletsId: ['123', contentlet.identifier, '456'] // Before 456 + }, + { + identifier: '123', + uuid: '456', + personaTag: 'dot:persona', + contentletsId: ['123'] } - }); + ]); }); it('should handle duplicated content', () => { @@ -2045,9 +2010,9 @@ describe('EditEmaEditorComponent', () => { const savePapeSpy = jest.spyOn(store, 'savePage'); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); - store.setDragItem({ + store.setEditorDragItem({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -2100,7 +2065,7 @@ describe('EditEmaEditorComponent', () => { summary: 'Content already added' }); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); it('should do the place item flow when dropping a contentlet and is moving', () => { @@ -2108,7 +2073,7 @@ describe('EditEmaEditorComponent', () => { const savePapeSpy = jest.spyOn(store, 'savePage'); - store.setDragItem({ + store.setEditorDragItem({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -2163,36 +2128,29 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(drop); - expect(savePapeSpy).toHaveBeenCalledWith({ - pageContainers: [ - { - identifier: '123', - uuid: '123', - personaTag: 'dot:persona', - contentletsId: ['123'] - }, - { - identifier: '123', - uuid: '456', - personaTag: 'dot:persona', - contentletsId: ['456', '123'] // before pivot contentlet - } - ], - pageId: '123', - params: { - language_id: 1, - url: 'page-one' + expect(savePapeSpy).toHaveBeenCalledWith([ + { + identifier: '123', + uuid: '123', + personaTag: 'dot:persona', + contentletsId: ['123'] + }, + { + identifier: '123', + uuid: '456', + personaTag: 'dot:persona', + contentletsId: ['456', '123'] // before pivot contentlet } - }); + ]); }); it('should handle duplicated content when moving', () => { const contentlet = CONTENTLETS_MOCK[0]; - const savePapeSpy = jest.spyOn(store, 'savePage'); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const savePageSpy = jest.spyOn(store, 'savePage'); + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); - store.setDragItem({ + store.setEditorDragItem({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -2246,7 +2204,7 @@ describe('EditEmaEditorComponent', () => { }); window.dispatchEvent(drop); - expect(savePapeSpy).not.toHaveBeenCalled(); + expect(savePageSpy).not.toHaveBeenCalled(); expect(addMessageSpy).toHaveBeenCalledWith({ detail: 'This content is already added to this container', @@ -2255,15 +2213,17 @@ describe('EditEmaEditorComponent', () => { summary: 'Content already added' }); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); it('should open dialog when dropping a content-type', () => { const contentType = CONTENT_TYPE_MOCK[0]; - jest.spyOn(store, 'updateEditorState'); + jest.spyOn(store, 'setEditorState'); + + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); - store.setDragItem({ + store.setEditorDragItem({ baseType: contentType.baseType, contentType: contentType.variable, draggedPayload: { @@ -2314,14 +2274,15 @@ describe('EditEmaEditorComponent', () => { ); expect(dialog.attributes['ng-reflect-visible']).toBe('true'); - expect(store.updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); it('should advice and reset the state to IDLE when the dropped file is not an image', () => { const drop = new Event('drop'); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); - store.setDragItem({ + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); + + store.setEditorDragItem({ baseType: 'dotAsset', contentType: 'dotAsset', draggedPayload: { @@ -2364,21 +2325,14 @@ describe('EditEmaEditorComponent', () => { life: 3000 }); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); - it('should advice and reset state to IDLE when the dropped image failed uploading ', () => { + it('should add an image successfully', () => { const drop = new Event('drop'); - jest.spyOn(dotTempFileUploadService, 'upload').mockReturnValue( - of([ - { - image: null, - id: 'temp_file_test' - } - ] as DotCMSTempFile[]) - ); + const savePageSpy = jest.spyOn(store, 'savePage'); - store.setDragItem({ + store.setEditorDragItem({ baseType: 'dotAsset', contentType: 'dotAsset', draggedPayload: { @@ -2386,81 +2340,23 @@ describe('EditEmaEditorComponent', () => { } }); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); - - Object.defineProperties(drop, { - dataTransfer: { - writable: false, - value: { - files: [new File([''], 'test.png', { type: 'image/png' })] - } - }, - target: { - value: { - dataset: { - dropzone: 'true', - position: 'before', - payload: JSON.stringify({ - container: { - acceptTypes: 'Banner,Activity,DotAsset', - identifier: '123', - maxContentlets: 25, - variantId: 'DEFAULT', - uuid: '456' - } - }) + jest.spyOn(dotTempFileUploadService, 'upload') + .mockReset() + .mockReturnValueOnce( + of([ + { + image: true, + id: 'temp_file_test' } - } - } - }); - - window.dispatchEvent(drop); - expect(addMessageSpy).toHaveBeenNthCalledWith(1, { - severity: 'info', - summary: 'upload-image', - detail: 'editpage.file.uploading', - life: 3000 - }); - - expect(addMessageSpy).toHaveBeenNthCalledWith(2, { - severity: 'error', - summary: 'upload-image', - detail: 'editpage.file.upload.error', - life: 3000 - }); - - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); - }); - - // This case is not probable but I added it anyways - it('should not add an image when it is duplicated', () => { - const drop = new Event('drop'); - const savePapeSpy = jest.spyOn(store, 'savePage'); - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); - - jest.spyOn(dotTempFileUploadService, 'upload').mockReturnValue( - of([ - { - image: true, - id: 'temp_file_test' - } - ] as DotCMSTempFile[]) - ); - - store.setDragItem({ - baseType: 'dotAsset', - contentType: 'dotAsset', - draggedPayload: { - type: 'temp' - } - }); + ] as DotCMSTempFile[]) + ); jest.spyOn( dotWorkflowActionsFireService, 'publishContentletAndWaitForIndex' ).mockReturnValue( of({ - identifier: '123', + identifier: '789', inode: '123', title: 'test', contentType: 'dotAsset', @@ -2487,6 +2383,12 @@ describe('EditEmaEditorComponent', () => { maxContentlets: 25, variantId: 'DEFAULT', uuid: '456' + }, + contentlet: { + identifier: '123', + title: 'Explore the World', + inode: 'bef551b3-77ae-4dc8-a030-fe27a2ac056f', + contentType: 'Banner' } }) } @@ -2509,32 +2411,34 @@ describe('EditEmaEditorComponent', () => { life: 3000 }); - expect(addMessageSpy).toHaveBeenNthCalledWith(3, { - detail: 'This content is already added to this container', - life: 2000, - severity: 'info', - summary: 'Content already added' - }); - - expect(savePapeSpy).not.toHaveBeenCalled(); - - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(savePageSpy).toHaveBeenCalledWith([ + { + contentletsId: ['123', '456'], + identifier: '123', + personaTag: 'dot:persona', + uuid: '123' + }, + { + contentletsId: ['789', '123'], // image inserted before + identifier: '123', + personaTag: 'dot:persona', + uuid: '456' + } + ]); }); - it('should add an image successfully', () => { + it('should advice and reset editor properties when the dropped image failed uploading ', () => { const drop = new Event('drop'); - const savePapeSpy = jest.spyOn(store, 'savePage'); - - jest.spyOn(dotTempFileUploadService, 'upload').mockReturnValue( + jest.spyOn(dotTempFileUploadService, 'upload').mockReturnValueOnce( of([ { - image: true, + image: null, id: 'temp_file_test' } ] as DotCMSTempFile[]) ); - store.setDragItem({ + store.setEditorDragItem({ baseType: 'dotAsset', contentType: 'dotAsset', draggedPayload: { @@ -2542,18 +2446,7 @@ describe('EditEmaEditorComponent', () => { } }); - jest.spyOn( - dotWorkflowActionsFireService, - 'publishContentletAndWaitForIndex' - ).mockReturnValue( - of({ - identifier: '789', - inode: '123', - title: 'test', - contentType: 'dotAsset', - baseType: 'IMAGE' - }) - ); + const resetEditorPropertiesSpy = jest.spyOn(store, 'resetEditorProperties'); Object.defineProperties(drop, { dataTransfer: { @@ -2574,12 +2467,6 @@ describe('EditEmaEditorComponent', () => { maxContentlets: 25, variantId: 'DEFAULT', uuid: '456' - }, - contentlet: { - identifier: '123', - title: 'Explore the World', - inode: 'bef551b3-77ae-4dc8-a030-fe27a2ac056f', - contentType: 'Banner' } }) } @@ -2596,33 +2483,13 @@ describe('EditEmaEditorComponent', () => { }); expect(addMessageSpy).toHaveBeenNthCalledWith(2, { - severity: 'info', - summary: 'Workflow-Action', - detail: 'editpage.file.publishing', + severity: 'error', + summary: 'upload-image', + detail: 'editpage.file.upload.error', life: 3000 }); - expect(savePapeSpy).toHaveBeenCalledWith({ - pageContainers: [ - { - contentletsId: ['123', '456'], - identifier: '123', - personaTag: 'dot:persona', - uuid: '123' - }, - { - contentletsId: ['789', '123'], // image inserted before - identifier: '123', - personaTag: 'dot:persona', - uuid: '456' - } - ], - pageId: '123', - params: { - language_id: 1, - url: 'page-one' - } - }); + expect(resetEditorPropertiesSpy).toHaveBeenCalled(); }); }); }); @@ -2639,7 +2506,10 @@ describe('EditEmaEditorComponent', () => { 'postMessage' ); - const scrollingStateSpy = jest.spyOn(store, 'setScrollingState'); + const updateEditorScrollDragStateSpy = jest.spyOn( + store, + 'updateEditorScrollDragState' + ); jest.spyOn( spectator.component.iframe.nativeElement, @@ -2654,7 +2524,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragOver); spectator.detectChanges(); expect(postMessageSpy).toHaveBeenCalled(); - expect(scrollingStateSpy).toHaveBeenCalled(); + expect(updateEditorScrollDragStateSpy).toHaveBeenCalled(); }); it('should reset state to dragging when drag outside iframe', () => { @@ -2663,7 +2533,7 @@ describe('EditEmaEditorComponent', () => { Object.defineProperty(dragOver, 'clientY', { value: 200, enumerable: true }); Object.defineProperty(dragOver, 'clientX', { value: 90, enumerable: true }); - const updateEditorState = jest.spyOn(store, 'updateEditorState'); + const setEditorState = jest.spyOn(store, 'setEditorState'); jest.spyOn( spectator.component.iframe.nativeElement, @@ -2677,7 +2547,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragOver); spectator.detectChanges(); - expect(updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); + expect(setEditorState).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); }); it('should change state to dragging when drag outsite scroll trigger area', () => { @@ -2686,7 +2556,7 @@ describe('EditEmaEditorComponent', () => { Object.defineProperty(dragOver, 'clientY', { value: 300, enumerable: true }); Object.defineProperty(dragOver, 'clientX', { value: 120, enumerable: true }); - const updateEditorState = jest.spyOn(store, 'updateEditorState'); + const setEditorState = jest.spyOn(store, 'setEditorState'); jest.spyOn( spectator.component.iframe.nativeElement, @@ -2700,7 +2570,7 @@ describe('EditEmaEditorComponent', () => { window.dispatchEvent(dragOver); spectator.detectChanges(); - expect(updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); + expect(setEditorState).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING); }); }); @@ -2713,8 +2583,8 @@ describe('EditEmaEditorComponent', () => { expect(progressbar).toBeNull(); }); - it('should show a loader when the editor state is loading', () => { - store.updateEditorState(EDITOR_STATE.LOADING); + it('should show a loader when the UVE is loading', () => { + store.setUveStatus(UVE_STATUS.LOADING); spectator.detectChanges(); @@ -2728,7 +2598,7 @@ describe('EditEmaEditorComponent', () => { const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]')); expect(iframe.nativeElement.src).toBe( - 'http://localhost:3000/page-one?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona&variantName=DEFAULT' + 'http://localhost:3000/index?clientHost=http%3A%2F%2Flocalhost%3A3000&language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona&variantName=DEFAULT' ); }); @@ -2749,6 +2619,7 @@ describe('EditEmaEditorComponent', () => { }); it('iframe should have the correct content when is VTL', () => { + spectator.detectChanges(); jest.runOnlyPendingTimers(); const iframe = spectator.debugElement.query( @@ -2764,12 +2635,6 @@ describe('EditEmaEditorComponent', () => { }); it('iframe should have reload the page and add the new content, maintaining scroll', () => { - const params = { - language_id: '4', - url: 'index', - 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier - }; - const iframe = spectator.debugElement.query( By.css('[data-testId="iframe"]') ); @@ -2782,11 +2647,10 @@ describe('EditEmaEditorComponent', () => { iframe.nativeElement.contentWindow.scrollTo(0, 100); //Scroll down - store.reload({ - params, - whenReloaded: () => { - /* */ - } + store.load({ + url: 'index', + language_id: '4', + 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier }); spectator.detectChanges(); @@ -2843,7 +2707,7 @@ describe('EditEmaEditorComponent', () => { data: { action: 'set-url', payload: { - url: 'page-one' + url: 'index' } } }) @@ -2853,7 +2717,7 @@ describe('EditEmaEditorComponent', () => { }); it('set url to a different route should set the editor state to loading', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const navigateSpy = jest.spyOn(router, 'navigate'); spectator.detectChanges(); @@ -2869,20 +2733,27 @@ describe('EditEmaEditorComponent', () => { }) ); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.LOADING); + expect(navigateSpy).toHaveBeenCalledWith([], { + queryParams: { + 'com.dotmarketing.persona.id': 'modes.persona.no.persona', + url: '/some' + }, + queryParamsHandling: 'merge' + }); }); it('set url to the same route should set the editor state to IDLE', () => { - const updateEditorStateSpy = jest.spyOn(store, 'updateEditorState'); + const setEditorStateSpy = jest.spyOn(store, 'setEditorState'); const url = "/ultra-cool-url-that-doesn't-exist"; - spectator.detectChanges(); - spectator.triggerNavigation({ - url: [], - queryParams: { url } + store.load({ + url, + language_id: '5', + 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier }); + spectator.detectChanges(); window.dispatchEvent( new MessageEvent('message', { origin: HOST, @@ -2895,7 +2766,7 @@ describe('EditEmaEditorComponent', () => { }) ); - expect(updateEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); + expect(setEditorStateSpy).toHaveBeenCalledWith(EDITOR_STATE.IDLE); }); it('should have a confirm dialog with acceptIcon and rejectIcon attribute', () => { @@ -2984,51 +2855,10 @@ describe('EditEmaEditorComponent', () => { }); }); - describe('without edit permission', () => { - let spectator: SpectatorRouting; - let store: EditEmaStore; - - const createComponent = createRouting({ canEdit: false, canRead: true }); - beforeEach(() => { - spectator = createComponent({ - queryParams: { language_id: 1, url: 'page-one' } - }); - - store = spectator.inject(EditEmaStore, true); - - store.load({ - url: 'index', - language_id: '1', - clientHost: '', - 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier - }); - }); - - it('should not render components', () => { - spectator.detectChanges(); - expect(spectator.query(EmaContentletToolsComponent)).toBeNull(); - expect(spectator.query(EditEmaPaletteComponent)).toBeNull(); - }); - - it('should render a "Dont have permission" message', () => { - spectator.detectChanges(); - expect(spectator.query(byTestId('editor-banner'))).toBeDefined(); - }); - - it('should iframe wrapper to be expanded', () => { - spectator.detectChanges(); - expect(spectator.query(byTestId('editor-content')).classList).toContain( - 'editor-content--expanded' - ); - }); - }); - describe('inline editing', () => { it('should save from inline edited contentlet', () => { - const saveFromInlineEditedContentletSpy = jest.spyOn( - store, - 'saveFromInlineEditedContentlet' - ); + const saveContentletSpy = jest.spyOn(dotPageApiService, 'saveContentlet'); + window.dispatchEvent( new MessageEvent('message', { origin: HOST, @@ -3050,70 +2880,20 @@ describe('EditEmaEditorComponent', () => { }) ); - expect(saveFromInlineEditedContentletSpy).toHaveBeenCalledWith({ + expect(saveContentletSpy).toHaveBeenCalledWith({ contentlet: { inode: '123', title: 'Hello World' - }, - params: { - language_id: 1, - url: 'page-one' } }); }); - it('should dont trigger save from inline edited contentlet when dont have changes', () => { - const saveFromInlineEditedContentletSpy = jest.spyOn( - store, - 'saveFromInlineEditedContentlet' - ); - const setEditorModeSpy = jest.spyOn(store, 'setEditorMode'); - - spectator.setRouteQueryParam('variantName', DEFAULT_VARIANT_ID); - window.dispatchEvent( - new MessageEvent('message', { - origin: HOST, - data: { - action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING, - payload: null - } - }) - ); - - expect(saveFromInlineEditedContentletSpy).not.toHaveBeenCalled(); - expect(setEditorModeSpy).toHaveBeenCalledWith(EDITOR_MODE.EDIT); - }); - - it('should dont trigger save from inline edited contentlet when dont have changes and does not have variantName', () => { - const saveFromInlineEditedContentletSpy = jest.spyOn( - store, - 'saveFromInlineEditedContentlet' - ); - const setEditorModeSpy = jest.spyOn(store, 'setEditorMode'); + it('should not trigger save from inline edited contentlet when dont have changes', () => { + const saveContentletSpy = jest + .spyOn(dotPageApiService, 'saveContentlet') + .mockClear(); - spectator.setRouteQueryParam('variantName', undefined); - window.dispatchEvent( - new MessageEvent('message', { - origin: HOST, - data: { - action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING, - payload: null - } - }) - ); - - expect(saveFromInlineEditedContentletSpy).not.toHaveBeenCalled(); - expect(setEditorModeSpy).toHaveBeenCalledWith(EDITOR_MODE.EDIT); - }); - - it('should dont trigger save from inline edited contentlet when dont have changes and its a variant', () => { - const saveFromInlineEditedContentletSpy = jest.spyOn( - store, - 'saveFromInlineEditedContentlet' - ); - const setEditorModeSpy = jest.spyOn(store, 'setEditorMode'); - - spectator.setRouteQueryParam('variantName', 'hello-there'); + const setEditorState = jest.spyOn(store, 'setEditorState'); window.dispatchEvent( new MessageEvent('message', { @@ -3125,94 +2905,10 @@ describe('EditEmaEditorComponent', () => { }) ); - expect(saveFromInlineEditedContentletSpy).not.toHaveBeenCalled(); - expect(setEditorModeSpy).toHaveBeenCalledWith(EDITOR_MODE.EDIT_VARIANT); - }); - - it('should trigger copy contentlet dialog when inline editing', () => { - const copyContentletSpy = jest.spyOn(dotCopyContentModalService, 'open'); - window.dispatchEvent( - new MessageEvent('message', { - origin: HOST, - data: { - action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING, - payload: { - inode: '123', - language: '1' - } - } - }) - ); - - expect(copyContentletSpy).toHaveBeenCalledWith(); - }); - }); - - describe('locked', () => { - describe('locked with unlock permission', () => { - let spectator: SpectatorRouting; - let store: EditEmaStore; - - const createComponent = createRouting({ canEdit: true, canRead: true }); - beforeEach(() => { - spectator = createComponent({ - queryParams: { language_id: 7, url: 'page-one' } - }); - - store = spectator.inject(EditEmaStore, true); - - store.load({ - url: 'index', - language_id: '7', - clientHost: '', - 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier - }); - }); - - it('should not render components', () => { - spectator.detectChanges(); - expect(spectator.query(EmaContentletToolsComponent)).toBeNull(); - expect(spectator.query(EditEmaPaletteComponent)).toBeNull(); - }); - - it('should render a banner', () => { - spectator.detectChanges(); - expect(spectator.query(byTestId('editor-banner'))).toBeDefined(); - }); - - it('should iframe wrapper to be expanded', () => { - spectator.detectChanges(); - expect(spectator.query(byTestId('editor-content')).classList).toContain( - 'editor-content--expanded' - ); - }); + expect(saveContentletSpy).not.toHaveBeenCalled(); + expect(setEditorState).toHaveBeenCalledWith(EDITOR_STATE.IDLE); }); }); }); - - describe('Components Inputs', () => { - it('should set right inputs for the dot-ema-contentlet-tools tag', () => { - store.load({ - url: 'index', - language_id: '5', - 'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier, - variantName: 'hello-there', - experimentId: 'i-have-a-running-experiment' - }); - - spectator.detectChanges(); - - store.patchState(SHOW_CONTENTLET_TOOLS_PATCH_MOCK); - - spectator.detectChanges(); - const contentletTool = spectator.query(EmaContentletToolsComponent); - - expect(contentletTool).not.toBeNull(); - expect(contentletTool.contentletArea).toEqual( - SHOW_CONTENTLET_TOOLS_PATCH_MOCK.contentletArea - ); - expect(contentletTool.isEnterprise).toBeTruthy(); - }); - }); }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts index 5c9dfb791e21..ad2c72bcfc3a 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts @@ -1,27 +1,24 @@ -import { Observable, Subject, fromEvent, of } from 'rxjs'; +import { tapResponse } from '@ngrx/operators'; +import { EMPTY, Observable, Subject, fromEvent, of } from 'rxjs'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - DestroyRef, ElementRef, HostListener, OnDestroy, OnInit, - Signal, ViewChild, WritableSignal, - computed, + effect, inject, - signal, - untracked + signal } from '@angular/core'; -import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, Params, Router } from '@angular/router'; +import { Params, Router } from '@angular/router'; import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmDialogModule } from 'primeng/confirmdialog'; @@ -41,10 +38,8 @@ import { DotWorkflowActionsFireService } from '@dotcms/data-access'; import { - DEFAULT_VARIANT_ID, DotCMSContentlet, DotCMSTempFile, - DotExperimentStatus, DotTreeNode, SeoMetaTags, SeoMetaTagsResult @@ -66,16 +61,15 @@ import { EmaDragItem, ClientContentletArea, Container, - UpdatedContentlet, - InlineEditingContentletDataset + InlineEditingContentletDataset, + UpdatedContentlet } from './components/ema-page-dropzone/types'; import { DotEmaDialogComponent } from '../components/dot-ema-dialog/dot-ema-dialog.component'; -import { EditEmaStore } from '../dot-ema-shell/store/dot-ema.store'; -import { DotPageApiParams } from '../services/dot-page-api.service'; +import { DotPageApiService } from '../services/dot-page-api.service'; import { InlineEditService } from '../services/inline-edit/inline-edit.service'; import { DEFAULT_PERSONA, IFRAME_SCROLL_ZONE, WINDOW } from '../shared/consts'; -import { EDITOR_MODE, EDITOR_STATE, NG_CUSTOM_EVENTS, NOTIFY_CUSTOMER } from '../shared/enums'; +import { EDITOR_STATE, NG_CUSTOM_EVENTS, NOTIFY_CUSTOMER, UVE_STATUS } from '../shared/enums'; import { ActionPayload, PositionPayload, @@ -91,9 +85,9 @@ import { PostMessagePayload, ReorderPayload } from '../shared/models'; +import { UVEStore } from '../store/dot-uve.store'; import { SDK_EDITOR_SCRIPT_SOURCE, - areContainersEquals, deleteContentletFromContainer, insertContentletInContainer } from '../utils'; @@ -105,7 +99,8 @@ import { styleUrls: ['./edit-ema-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, + NgClass, + NgStyle, FormsModule, SafeUrlPipe, DotSpinnerModule, @@ -132,9 +127,9 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { @ViewChild('dialog') dialog: DotEmaDialogComponent; @ViewChild('iframe') iframe!: ElementRef; + protected readonly uveStore = inject(UVEStore); + private readonly router = inject(Router); - private readonly activatedRouter = inject(ActivatedRoute); - private readonly store = inject(EditEmaStore); private readonly dotMessageService = inject(DotMessageService); private readonly confirmationService = inject(ConfirmationService); private readonly messageService = inject(MessageService); @@ -149,127 +144,73 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { private readonly tempFileUploadService = inject(DotTempFileUploadService); private readonly dotWorkflowActionsFireService = inject(DotWorkflowActionsFireService); private readonly inlineEditingService = inject(InlineEditService); - private readonly destroyRef = inject(DestroyRef); + private readonly dotPageApiService = inject(DotPageApiService); - readonly editorState$ = this.store.editorState$; - readonly dragState$ = this.store.dragState$; readonly destroy$ = new Subject(); protected ogTagsResults$: Observable; - readonly pageData = toSignal(this.store.pageData$); - - readonly ogTags: WritableSignal = signal(undefined); - - readonly clientData: WritableSignal = signal(undefined); - - readonly actionPayload: Signal = computed(() => { - const clientData = this.clientData(); - const { containers, languageId, id, personaTag } = this.pageData(); - const { contentletsId } = containers.find((container) => - areContainersEquals(container, clientData.container) - ) ?? { contentletsId: [] }; - - const container = clientData.container - ? { - ...clientData.container, - contentletsId - } - : null; - - return { - ...clientData, - language_id: languageId.toString(), - pageId: id, - pageContainers: containers, - personaTag, - container - } as ActionPayload; - }); - - readonly currentTreeNode: Signal = computed(() => { - const { contentlet, container } = this.actionPayload(); - const { identifier: contentId } = contentlet; - const { variantId, uuid: relationType, contentletsId, identifier: containerId } = container; - const { personalization, id: pageId } = untracked(() => this.pageData()); - const treeOrder = contentletsId.findIndex((id) => id === contentId).toString(); - - return { - contentId, - containerId, - relationType, - variantId, - personalization, - treeOrder, - pageId - }; - }); + readonly $ogTags: WritableSignal = signal(undefined); readonly host = '*'; - readonly editorState = EDITOR_STATE; - readonly editorMode = EDITOR_MODE; - readonly experimentStatus = DotExperimentStatus; - get queryParams(): DotPageApiParams { - return this.activatedRouter.snapshot.queryParams as DotPageApiParams; - } + readonly $editorProps = this.uveStore.$editorProps; get contentWindow(): Window { return this.iframe.nativeElement.contentWindow; } - isVTLPage = toSignal(this.store.clientHost$.pipe(map((clientHost) => !clientHost))); - $isInlineEditing = toSignal( - this.store.editorMode$.pipe(map((mode) => mode === EDITOR_MODE.INLINE_EDITING)) - ); + readonly $handleReloadContentEffect = effect( + () => { + const { code, isTraditionalPage, isEditState, isEnterprise } = + this.uveStore.$reloadEditorContent(); - ngOnInit(): void { - this.handleReloadContent(); - this.handleDragEvents(); - - fromEvent(this.window, 'message') - .pipe(takeUntil(this.destroy$)) - .subscribe((event: MessageEvent) => { - this.handlePostMessage(event)?.(); - }); + this.uveStore.resetEditorProperties(); - // In VTL Page if user click in a link in the page, we need to update the URL in the editor - this.store.pageRendered$ - .pipe( - takeUntil(this.destroy$), - filter(() => this.isVTLPage()) - ) - .subscribe(() => { - requestAnimationFrame(() => { - const win = this.contentWindow; + this.dialog?.resetDialog(); - fromEvent(win, 'click').subscribe((e: MouseEvent) => { - this.handleInternalNav(e); - }); - }); - }); + if (isTraditionalPage) { + this.setIframeContent(code); - this.store.vtlIframePage$ - .pipe( - takeUntil(this.destroy$), - filter(({ isEnterprise }) => this.isVTLPage() && isEnterprise) - ) - .subscribe(({ mode }) => { requestAnimationFrame(() => { const win = this.contentWindow; - if ( - mode === EDITOR_MODE.EDIT || - mode === EDITOR_MODE.EDIT_VARIANT || - mode === EDITOR_MODE.INLINE_EDITING - ) { + const canHaveInlineEditing = isEnterprise && isEditState; + + if (canHaveInlineEditing) { this.inlineEditingService.injectInlineEdit(this.iframe); - fromEvent(win, 'click').subscribe((e: MouseEvent) => { - this.handleInlineEditing(e); - }); } else { this.inlineEditingService.removeInlineEdit(this.iframe); } + + fromEvent(win, 'click').subscribe((e: MouseEvent) => { + this.handleInternalNav(e); + this.handleInlineEditing(e); // If inline editing is not active this will do nothing + }); }); + } else { + this.reloadIframeContent(); + } + }, + { + allowSignalWrites: true + } + ); + + readonly $handleIsDraggingEffect = effect(() => { + const isDragging = this.uveStore.$editorIsInDraggingState(); + + if (isDragging) { + this.contentWindow?.postMessage(NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS, this.host); + } + }); + + ngOnInit(): void { + this.handleDragEvents(); + + fromEvent(this.window, 'message') + .pipe(takeUntil(this.destroy$)) + .subscribe((event: MessageEvent) => { + this.handlePostMessage(event)?.(); }); } @@ -284,21 +225,27 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { (e.target as HTMLAnchorElement)?.href || (e.target as HTMLElement)?.closest('a')?.getAttribute('href'); + e.preventDefault(); + if (href) { - e.preventDefault(); - const url = new URL(href); + const dataset = (e.target as HTMLElement).dataset; - // Check if the URL is not external - if (url.hostname === window.location.hostname) { - this.updateQueryParams({ - url: url.pathname - }); + if (dataset['mode'] && dataset['fieldName'] && dataset['inode']) { + // We clicked on the inline editing element, we need to prevent navigation + return; + } + + const url = new URL(href, window.location.origin); + + if (url.hostname !== window.location.hostname) { + this.window.open(href, '_blank'); return; } - // Open external links in a new tab - this.window.open(href, '_blank'); + this.updateQueryParams({ + url: url.pathname + }); } } @@ -320,10 +267,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } handleDragEvents() { - this.store.isUserDragging$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { - this.contentWindow?.postMessage(NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS, this.host); - }); - fromEvent(this.window, 'dragstart') .pipe(takeUntil(this.destroy$)) .subscribe((event: DragEvent) => { @@ -334,7 +277,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { const { contentType, contentlet, container, move } = parsedItem; if (dataset.type === 'content-type') { - this.store.setDragItem({ + this.uveStore.setEditorDragItem({ baseType: contentType.baseType, contentType: contentType.variable, draggedPayload: { @@ -347,7 +290,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } as ContentTypeDragPayload }); } else { - this.store.setDragItem({ + this.uveStore.setEditorDragItem({ baseType: contentlet.baseType, contentType: contentlet.contentType, draggedPayload: { @@ -366,51 +309,35 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { .pipe( takeUntil(this.destroy$), // For some reason the fromElement is not in the DragEvent type - filter((event: DragEvent & { fromElement: HTMLElement }) => !event.fromElement), // I just want to trigger this when we are dragging from the outside - switchMap((event) => - this.dragState$.pipe( - take(1), - map(({ dragItem, editorState }) => ({ - event, - dragItem, - editorState - })) - ) - ) + filter((event: DragEvent & { fromElement: HTMLElement }) => !event.fromElement) // I just want to trigger this when we are dragging from the outside ) - .subscribe( - ({ - dragItem, - event, - editorState - }: { - dragItem: EmaDragItem; - event: DragEvent; - editorState: EDITOR_STATE; - }) => { - event.preventDefault(); - // Set the temp item to be dragged, which is the outsider file if there is not a drag item - if (!dragItem) { - this.store.setDragItem({ - baseType: 'dotAsset', - contentType: 'dotAsset', - draggedPayload: { - type: 'temp' - } - }); - } else if (editorState === EDITOR_STATE.OUT_OF_BOUNDS) { - this.store.updateEditorState(EDITOR_STATE.DRAGGING); - } + .subscribe((event: DragEvent) => { + event.preventDefault(); + + const dragItem = this.uveStore.dragItem(); + const editorState = this.uveStore.state(); - this.contentWindow?.postMessage(NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS, this.host); + // Set the temp item to be dragged, which is the outsider file if there is not a drag item + if (!dragItem) { + this.uveStore.setEditorDragItem({ + baseType: 'dotAsset', + contentType: 'dotAsset', + draggedPayload: { + type: 'temp' + } + }); + } else if (editorState === EDITOR_STATE.OUT_OF_BOUNDS) { + this.uveStore.setEditorState(EDITOR_STATE.DRAGGING); } - ); + + this.contentWindow?.postMessage(NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS, this.host); + }); fromEvent(this.window, 'dragend') .pipe(takeUntil(this.destroy$)) .subscribe((event: DragEvent) => { if (event.dataTransfer.dropEffect === 'none') { - this.store.updateEditorState(EDITOR_STATE.IDLE); + this.uveStore.resetEditorProperties(); } }); @@ -424,12 +351,12 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { event.clientX > iframeRect.left && event.clientX < iframeRect.right; if (!isInsideIframe) { - this.store.updateEditorState(EDITOR_STATE.DRAGGING); + this.uveStore.setEditorState(EDITOR_STATE.DRAGGING); return; } - let direction; + let direction: 'up' | 'down'; if ( event.clientY > iframeRect.top && @@ -446,12 +373,12 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } if (!direction) { - this.store.updateEditorState(EDITOR_STATE.DRAGGING); + this.uveStore.setEditorState(EDITOR_STATE.DRAGGING); return; } - this.store.setScrollingState(); + this.uveStore.updateEditorScrollDragState(); this.contentWindow?.postMessage( { name: NOTIFY_CUSTOMER.EMA_SCROLL_INSIDE_IFRAME, direction }, @@ -460,19 +387,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { }); fromEvent(this.window, 'drop') - .pipe( - takeUntil(this.destroy$), - switchMap((event) => - this.dragState$.pipe( - take(1), - map(({ dragItem }) => ({ - event, - dragItem - })) - ) - ) - ) - .subscribe(({ event, dragItem }: { event: DragEvent; dragItem: EmaDragItem }) => { + .pipe(takeUntil(this.destroy$)) + .subscribe((event: DragEvent) => { event.preventDefault(); const target = event.target as HTMLDivElement; @@ -480,7 +396,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { // If we drop in a container that is not a dropzone, we just reset the editor state if (dropzone !== 'true') { - this.store.updateEditorState(EDITOR_STATE.IDLE); + this.uveStore.resetEditorProperties(); return; } @@ -489,6 +405,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { const file = event.dataTransfer?.files[0]; // We are sure that is comes but in the tests we don't have DragEvent class + const dragItem = this.uveStore.dragItem(); + if (file) { // I need to publish the temp file to use it. this.handleFileUpload({ @@ -514,7 +432,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { ) .subscribe(() => { // I need to do this to hide the dropzone but maintain the current dragItem - this.store.updateEditorState(EDITOR_STATE.OUT_OF_BOUNDS); // The user is dragging outside the window, we set this to know that user can potentially drop a file outside the window + this.uveStore.setEditorState(EDITOR_STATE.OUT_OF_BOUNDS); // The user is dragging outside the window, we set this to know that user can potentially drop a file outside the window }); } @@ -528,48 +446,12 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { resetEditorWhenOutOfBounds(event: MouseEvent) { event.preventDefault(); - this.dragState$ - .pipe( - take(1), - filter( - ({ dragItem, editorState }) => - !!dragItem && editorState === EDITOR_STATE.OUT_OF_BOUNDS // If the user dropped outside of the window and we still have a dragItem we need to clean the editor - ) - ) - .subscribe(() => { - this.store.updateEditorState(EDITOR_STATE.IDLE); - }); - } - - /** - * Handles the reload of content in the editor. - * If the editor state is LOADED and the content is not VTL, it reloads the iframe. - * If the content is VTL, it loads the VTL iframe content. - * @memberof EditEmaEditorComponent - */ - handleReloadContent() { - this.store.contentState$ - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ shouldReload, code, isVTL }) => { - // If we are idle then we are not dragging - - this.resetDragProperties(); - - if (!shouldReload) { - /** We have some EDITOR_STATE values that we don't want to reload the content - * Only when we should realod the content we do it - */ - return; - } + const dragItem = this.uveStore.dragItem(); + const editorState = this.uveStore.state(); - if (isVTL) { - this.setIframeContent(code); - } else { - this.reloadIframeContent(); - } - - this.store.setShouldReload(false); - }); + if (!!dragItem && editorState === EDITOR_STATE.OUT_OF_BOUNDS) { + this.uveStore.resetEditorProperties(); + } } /** @@ -578,11 +460,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @param {string} clientHost * @memberof EditEmaEditorComponent */ - onIframePageLoad(editorMode: EDITOR_MODE) { - this.store.updateEditorState(EDITOR_STATE.IDLE); - - //The iframe is loaded after copy contentlet to inline editing. - if (editorMode === EDITOR_MODE.INLINE_EDITING) { + onIframePageLoad() { + if (this.uveStore.state() === EDITOR_STATE.INLINE_EDITING) { this.inlineEditingService.initEditor(); } } @@ -703,7 +582,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @memberof EditEmaEditorComponent */ placeItem(positionPayload: PositionPayload, dragItem: EmaDragItem): void { - let payload = this.getPageSavePayload(positionPayload); + let payload = this.uveStore.getPageSavePayload(positionPayload); const destinationContainer = payload.container; const pivotContentlet = payload.contentlet; @@ -746,15 +625,11 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { return; } - this.store.savePage({ - pageContainers, - pageId: payload.pageId, - params: this.queryParams - }); + this.uveStore.savePage(pageContainers); return; } else if (dragItem.draggedPayload.type === 'content-type') { - this.store.updateEditorState(EDITOR_STATE.IDLE); // In case the user cancels the creation of the contentlet, we already have the editor in idle state + this.uveStore.resetEditorProperties(); // In case the user cancels the creation of the contentlet, we already have the editor in idle state this.dialog.createContentletFromPalette({ ...dragItem.draggedPayload.item, payload }); } else if (dragItem.draggedPayload.type === 'temp') { @@ -769,11 +644,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { return; } - this.store.savePage({ - pageContainers, - pageId: payload.pageId, - params: this.queryParams - }); + this.uveStore.savePage(pageContainers); } } /** @@ -795,14 +666,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { acceptLabel: this.dotMessageService.get('dot.common.dialog.accept'), rejectLabel: this.dotMessageService.get('dot.common.dialog.reject'), accept: () => { - this.store.savePage({ - pageContainers, - pageId: payload.pageId, - params: this.queryParams, - whenSaved: () => { - this.dialog.resetDialog(); - } - }); // Save when selected + this.uveStore.savePage(pageContainers); } }); } @@ -824,7 +688,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { doc.write(newFile); doc.close(); - this.ogTags.set(this.dotSeoMetaTagsUtilService.getMetaTags(doc)); + this.uveStore.setOgTags(this.dotSeoMetaTagsUtilService.getMetaTags(doc)); this.ogTagsResults$ = this.dotSeoMetaTagsService .getMetaTagsResults(doc) .pipe(take(1)); @@ -851,15 +715,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { return; } - // Save when selected - this.store.savePage({ - pageContainers, - pageId: payload.pageId, - params: this.queryParams, - whenSaved: () => { - this.dialog.resetDialog(); - } - }); + this.uveStore.savePage(pageContainers); }, [NG_CUSTOM_EVENTS.SAVE_PAGE]: () => { const { shouldReloadPage, contentletIdentifier } = detail.payload; @@ -871,9 +727,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } if (!payload) { - this.store.reload({ - params: this.queryParams - }); + this.uveStore.reload(); return; } @@ -889,14 +743,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { return; } - this.store.savePage({ - pageContainers, - pageId: payload.pageId, - params: this.queryParams, - whenSaved: () => { - this.dialog.resetDialog(); - } - }); + this.uveStore.savePage(pageContainers); }, [NG_CUSTOM_EVENTS.CREATE_CONTENTLET]: () => { this.dialog.createContentlet({ @@ -907,16 +754,33 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { this.cd.detectChanges(); }, [NG_CUSTOM_EVENTS.FORM_SELECTED]: () => { - const identifier = detail.data.identifier; + const formId = detail.data.identifier; - this.store.saveFormToPage({ - payload, - formId: identifier, - params: this.queryParams, - whenSaved: () => { - this.dialog.resetDialog(); - } - }); + this.dotPageApiService + .getFormIndetifier(payload.container.identifier, formId) + .pipe( + tap(() => { + this.uveStore.setUveStatus(UVE_STATUS.LOADING); + }), + map((newFormId: string) => { + return { + ...payload, + newContentletId: newFormId + }; + }), + catchError(() => EMPTY), + take(1) + ) + .subscribe((response) => { + const { pageContainers, didInsert } = insertContentletInContainer(response); + + if (!didInsert) { + this.handleDuplicatedContentlet(); + this.uveStore.setUveStatus(UVE_STATUS.LOADED); + } else { + this.uveStore.savePage(pageContainers); + } + }); }, [NG_CUSTOM_EVENTS.SAVE_MENU_ORDER]: () => { this.messageService.add({ @@ -928,9 +792,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { life: 2000 }); - this.store.reload({ - params: this.queryParams - }); + this.uveStore.reload(); this.dialog.resetDialog(); }, [NG_CUSTOM_EVENTS.ERROR_SAVING_MENU_ORDER]: () => { @@ -977,12 +839,10 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { // When we set the url, we trigger in the shell component a load to get the new state of the page // This triggers a rerender that makes nextjs to send the set_url again // But this time the params are the same so the shell component wont trigger a load and there we know that the page is loaded - const isSameUrl = this.queryParams.url === payload.url; + const isSameUrl = this.uveStore.params()?.url === payload.url; if (isSameUrl) { - // TODO: HOW DO WE DO THIS NOW? - // this.personaSelector.fetchPersonas(); // We need to fetch the personas again because the page is loaded - this.store.updateEditorState(EDITOR_STATE.IDLE); + this.uveStore.setEditorState(EDITOR_STATE.IDLE); } else { this.updateQueryParams({ url: payload.url, @@ -991,40 +851,44 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } }, [CUSTOMER_ACTIONS.SET_BOUNDS]: () => { - this.store.setBounds(data.payload); + this.uveStore.setEditorBounds(data.payload); }, [CUSTOMER_ACTIONS.SET_CONTENTLET]: () => { const contentletArea = data.payload; - const payload = this.getPageSavePayload(contentletArea.payload); + const payload = this.uveStore.getPageSavePayload(contentletArea.payload); - this.store.setContentletArea({ + this.uveStore.setEditorContentletArea({ ...contentletArea, payload }); }, [CUSTOMER_ACTIONS.IFRAME_SCROLL]: () => { - this.store.updateEditorScrollState(); + this.uveStore.updateEditorScrollState(); }, [CUSTOMER_ACTIONS.IFRAME_SCROLL_END]: () => { - this.store.updateEditorDragState(); + this.uveStore.updateEditorOnScrollEnd(); }, [CUSTOMER_ACTIONS.INIT_INLINE_EDITING]: () => { // The iframe says that the editor is ready to start inline editing // The dataset of the inline-editing contentlet is ready inside the service. this.inlineEditingService.initEditor(); + this.uveStore.setEditorState(EDITOR_STATE.INLINE_EDITING); }, [CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING]: () => { - // The iframe say the contentlet that try to be inline edited is in multiple pages - // So the editor open the dialog to question if the edit is in ALL contentlets or only in this page. + // The iframe say the contentlet that the content is queue to be inline edited is in multiple pages + // So the editor should open the dialog to ask if the edit is in ALL contentlets or only in this page. - if (this.$isInlineEditing()) { - // If is already in inline editing, dont open the dialog. + if (this.uveStore.state() === EDITOR_STATE.INLINE_EDITING) { return; } const payload = <{ dataset: InlineEditingContentletDataset }>data.payload; + const { contentlet, container } = this.uveStore.contentletArea().payload; + + const currentTreeNode = this.uveStore.getCurrentTreeNode(container, contentlet); + this.dotCopyContentModalService .open() .pipe( @@ -1033,11 +897,13 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { return of(null); } - return this.handleCopyContent(); + return this.handleCopyContent(currentTreeNode); }), tap((res) => { + this.uveStore.setEditorState(EDITOR_STATE.INLINE_EDITING); + if (res) { - this.store.reload({ params: this.queryParams }); + this.uveStore.reload(); } }) ) @@ -1050,7 +916,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { language: payload.dataset.language }; - if (!this.isVTLPage()) { + if (!this.uveStore.isTraditionalPage()) { const message = { name: NOTIFY_CUSTOMER.COPY_CONTENTLET_INLINE_EDITING_SUCCESS, payload: data @@ -1062,7 +928,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } this.inlineEditingService.setTargetInlineMCEDataset(data); - this.store.setEditorMode(EDITOR_MODE.INLINE_EDITING); if (!res) { this.inlineEditingService.initEditor(); @@ -1072,25 +937,48 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { [CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING]: () => { const payload = data.payload; - if (!payload) { - const mode = - this.queryParams.variantName && - this.queryParams.variantName !== DEFAULT_VARIANT_ID - ? EDITOR_MODE.EDIT_VARIANT - : EDITOR_MODE.EDIT; - - this.store.setEditorMode(mode); + this.uveStore.setEditorState(EDITOR_STATE.IDLE); + // If there is no payload, we don't need to do anything + if (!payload) { return; } - this.store.saveFromInlineEditedContentlet({ - contentlet: { - inode: payload.dataset['inode'], - [payload.dataset.fieldName]: payload.content - }, - params: this.queryParams - }); + const dataset = payload.dataset; + + const contentlet = { + inode: dataset['inode'], + [dataset.fieldName]: payload.content + }; + + this.dotPageApiService + .saveContentlet({ contentlet }) + .pipe( + take(1), + tap(() => { + this.uveStore.setUveStatus(UVE_STATUS.LOADING); + }), + tapResponse( + () => { + this.messageService.add({ + severity: 'success', + summary: this.dotMessageService.get('message.content.saved'), + life: 2000 + }); + }, + (e) => { + console.error(e); + this.messageService.add({ + severity: 'error', + summary: this.dotMessageService.get( + 'editpage.content.update.contentlet.error' + ), + life: 2000 + }); + } + ) + ) + .subscribe(() => this.uveStore.reload()); }, [CUSTOMER_ACTIONS.REORDER_MENU]: () => { const { reorderUrl } = data.payload; @@ -1117,7 +1005,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { */ reloadIframeContent() { this.iframe?.nativeElement?.contentWindow?.postMessage( - { name: NOTIFY_CUSTOMER.SET_PAGE_DATA, payload: this.store.state().editor }, + { name: NOTIFY_CUSTOMER.SET_PAGE_DATA, payload: this.uveStore.pageAPIResponse() }, this.host ); } @@ -1130,7 +1018,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @memberof EditEmaEditorComponent */ private updateQueryParams(params: Params) { - this.store.updateEditorState(EDITOR_STATE.LOADING); this.router.navigate([], { queryParams: params, queryParamsHandling: 'merge' @@ -1145,22 +1032,9 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { life: 2000 }); - this.store.updateEditorState(EDITOR_STATE.IDLE); - this.dialog.resetDialog(); - } + this.uveStore.resetEditorProperties(); - /** - * Get the page save payload - * - * @private - * @param {PositionPayload} positionPayload - * @return {*} {ActionPayload} - * @memberof EditEmaEditorComponent - */ - private getPageSavePayload(positionPayload: PositionPayload): ActionPayload { - this.clientData.set(positionPayload); - - return this.actionPayload(); + this.dialog.resetDialog(); } /** @@ -1172,15 +1046,17 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @memberof EditEmaEditorComponent */ protected handleEditContentlet(payload: ActionPayload) { - const { contentlet } = payload; + const { contentlet, container } = payload; const { onNumberOfPages = '1', title } = contentlet; if (Number(onNumberOfPages) <= 1) { - this.dialog.editContentlet(contentlet); + this.dialog?.editContentlet(contentlet); return; } + const currentTreeNode = this.uveStore.getCurrentTreeNode(container, contentlet); + this.dotCopyContentModalService .open() .pipe( @@ -1191,7 +1067,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { this.dialog.showLoadingIframe(title); - return this.handleCopyContent(); + return this.handleCopyContent(currentTreeNode); }) ) .subscribe((contentlet) => { @@ -1227,8 +1103,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @return {*} * @memberof DotEmaDialogComponent */ - private handleCopyContent(): Observable { - return this.dotCopyContentService.copyInPage(this.currentTreeNode()).pipe( + private handleCopyContent(currentTreeNode: DotTreeNode): Observable { + return this.dotCopyContentService.copyInPage(currentTreeNode).pipe( catchError((error) => this.dotHttpErrorManagerService.handle(error).pipe( tap(() => this.dialog.resetDialog()), // If there is an error, we set the status to idle @@ -1239,16 +1115,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { ); } - /** - * Reset the drag properties - * - * @private - * @memberof EditEmaEditorComponent - */ - protected resetDragProperties() { - this.store.resetDragProperties(); - } - /** * Create the payload to delete a contentlet * @@ -1286,7 +1152,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { */ private reloadURLContentMapPage(inodeOrIdentifier: string): void { // Set loading state to prevent the user to interact with the iframe - this.store.updateEditorState(EDITOR_STATE.LOADING); + this.uveStore.setUveStatus(UVE_STATUS.LOADING); this.dotContentletService .getContentletByInode(inodeOrIdentifier) @@ -1295,8 +1161,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { filter((contentlet) => !!contentlet) ) .subscribe(({ URL_MAP_FOR_CONTENT }) => { - if (URL_MAP_FOR_CONTENT != this.queryParams.url) { - this.store.updateEditorState(EDITOR_STATE.IDLE); + if (URL_MAP_FOR_CONTENT != this.uveStore.params().url) { // If the URL is different, we need to navigate to the new URL this.updateQueryParams({ url: URL_MAP_FOR_CONTENT }); @@ -1304,9 +1169,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { } // If the URL is the same, we need to fetch the new page data - this.store.reload({ - params: this.queryParams - }); + this.uveStore.reload(); }); } @@ -1355,7 +1218,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * @memberof EditEmaEditorComponent */ private handlerError(error: HttpErrorResponse) { - this.store.updateEditorState(EDITOR_STATE.ERROR); + // CHECK IF HAVE TO SET THE UVE TO ERROR + this.uveStore.setEditorState(EDITOR_STATE.ERROR); return this.dotHttpErrorManagerService.handle(error).pipe(map(() => null)); } @@ -1364,7 +1228,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { * Reloads the component from the dialog. */ reloadFromDialog() { - this.store.reload({ params: this.queryParams }); + this.uveStore.reload(); } /** @@ -1396,6 +1260,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { file: File; dragItem: EmaDragItem; }): void { + this.uveStore.resetEditorProperties(); + if (!/image.*/.exec(file.type)) { this.messageService.add({ severity: 'error', @@ -1404,8 +1270,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { life: 3000 }); - this.store.updateEditorState(EDITOR_STATE.IDLE); - return; } @@ -1452,9 +1316,8 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { }) ) .subscribe((contentlet) => { - // If there is no contentlet then the file was not uploaded if (!contentlet) { - this.store.updateEditorState(EDITOR_STATE.IDLE); + this.uveStore.resetEditorProperties(); return; } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.html index b8f29651d765..4b745298eb6e 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.html @@ -1,7 +1,6 @@ diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.spec.ts index b14892dcca16..f6e765bf286d 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.spec.ts @@ -1,12 +1,13 @@ import { expect, describe } from '@jest/globals'; import { SpyObject } from '@ngneat/spectator'; import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; +import { MockModule } from 'ng-mocks'; import { of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute, Router } from '@angular/router'; import { MessageService } from 'primeng/api'; @@ -21,7 +22,7 @@ import { DotRouterService } from '@dotcms/data-access'; import { CoreWebService, LoginService } from '@dotcms/dotcms-js'; -import { TemplateBuilderComponent } from '@dotcms/template-builder'; +import { TemplateBuilderComponent, TemplateBuilderModule } from '@dotcms/template-builder'; import { DotExperimentsServiceMock, DotLanguagesServiceMock, @@ -30,15 +31,15 @@ import { import { EditEmaLayoutComponent } from './edit-ema-layout.component'; -import { EditEmaStore } from '../dot-ema-shell/store/dot-ema.store'; import { DotActionUrlService } from '../services/dot-action-url/dot-action-url.service'; import { DotPageApiService } from '../services/dot-page-api.service'; +import { UVEStore } from '../store/dot-uve.store'; describe('EditEmaLayoutComponent', () => { let spectator: Spectator; let component: EditEmaLayoutComponent; let dotRouter: SpyObject; - let store: EditEmaStore; + let store: SpyObject>; let templateBuilder: TemplateBuilderComponent; let layoutService: DotPageLayoutService; let messageService: MessageService; @@ -48,12 +49,14 @@ describe('EditEmaLayoutComponent', () => { const createComponent = createComponentFactory({ component: EditEmaLayoutComponent, - imports: [HttpClientTestingModule, RouterTestingModule], + imports: [HttpClientTestingModule, MockModule(TemplateBuilderModule)], providers: [ - EditEmaStore, + UVEStore, MessageService, DotMessageService, DotActionUrlService, + mockProvider(Router), + mockProvider(ActivatedRoute), { provide: DotExperimentsService, useValue: DotExperimentsServiceMock @@ -133,7 +136,7 @@ describe('EditEmaLayoutComponent', () => { spectator = createComponent(); component = spectator.component; dotRouter = spectator.inject(DotRouterService); - store = spectator.inject(EditEmaStore); + store = spectator.inject(UVEStore); layoutService = spectator.inject(DotPageLayoutService); messageService = spectator.inject(MessageService); @@ -147,7 +150,6 @@ describe('EditEmaLayoutComponent', () => { }); spectator.detectChanges(); - await spectator.fixture.whenStable(); templateBuilder = spectator.debugElement.query( By.css('[data-testId="edit-ema-layout"]') diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.ts index 86ce96ea71d9..fc536106e4fd 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-layout/edit-ema-layout.component.ts @@ -12,15 +12,14 @@ import { finalize, switchMap, take, - takeUntil, - tap + takeUntil } from 'rxjs/operators'; import { DotMessageService, DotPageLayoutService, DotRouterService } from '@dotcms/data-access'; import { DotPageRender, DotTemplateDesigner } from '@dotcms/dotcms-models'; import { TemplateBuilderModule } from '@dotcms/template-builder'; -import { EditEmaStore } from '../dot-ema-shell/store/dot-ema.store'; +import { UVEStore } from '../store/dot-uve.store'; export const DEBOUNCE_TIME = 5000; @@ -33,19 +32,15 @@ export const DEBOUNCE_TIME = 5000; changeDetection: ChangeDetectionStrategy.OnPush }) export class EditEmaLayoutComponent implements OnInit, OnDestroy { - private readonly store = inject(EditEmaStore); private readonly dotRouterService = inject(DotRouterService); private readonly dotPageLayoutService = inject(DotPageLayoutService); private readonly messageService = inject(MessageService); private readonly dotMessageService = inject(DotMessageService); - readonly layoutProperties$ = this.store.layoutProperties$.pipe( - tap((properties) => { - this.pageId = properties.pageId; - }) - ); + protected readonly uveStore = inject(UVEStore); + + protected readonly $layoutProperties = this.uveStore.$layoutProps; - private pageId: string; private lastTemplate: DotTemplateDesigner; updateTemplate$ = new Subject(); @@ -89,7 +84,7 @@ export class EditEmaLayoutComponent implements OnInit, OnDestroy { this.dotPageLayoutService // To save a layout and no a template the title should be null - .save(this.pageId, { ...template, title: null }) + .save(this.uveStore.$layoutProps().pageId, { ...template, title: null }) .pipe(take(1)) .subscribe( (updatedPage: DotPageRender) => this.handleSuccessSaveTemplate(updatedPage), @@ -126,7 +121,7 @@ export class EditEmaLayoutComponent implements OnInit, OnDestroy { }); return this.dotPageLayoutService - .save(this.pageId, { + .save(this.uveStore.$layoutProps().pageId, { ...layout, title: null }) @@ -153,7 +148,7 @@ export class EditEmaLayoutComponent implements OnInit, OnDestroy { detail: this.dotMessageService.get('dot.common.message.saved') }); - this.store.updatePageLayout(page.layout); + this.uveStore.updateLayout(page.layout); } /** diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.spec.ts index 1fff697f01f6..28496c42d70f 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.spec.ts @@ -113,4 +113,25 @@ describe('EditEmaGuard', () => { replaceUrl: true }); }); + + it('should navigate to "edit-page" and sanitize url when the url is "/"', () => { + const route: ActivatedRouteSnapshot = { + firstChild: { + url: [{ path: 'content' }] + }, + queryParams: { url: '/' } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + TestBed.runInInjectionContext(() => editEmaGuard(route, state) as Observable); + + expect(router.navigate).toHaveBeenCalledWith(['/edit-page/content'], { + queryParams: { + 'com.dotmarketing.persona.id': 'modes.persona.no.persona', + language_id: 1, + url: 'index' + }, + replaceUrl: true + }); + }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.ts index f3b35791980c..8337332d5fb9 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/guards/edit-ema.guard.ts @@ -40,13 +40,14 @@ function confirmQueryParams(queryParams: Params): { if (!queryParams[curr.key]) { acc[curr.key] = curr.value; acc.missing = true; - } else if ( - curr.key === 'url' && - queryParams[curr.key] !== 'index' && - queryParams[curr.key].endsWith('/index') - ) { - acc[curr.key] = sanitizeURL(queryParams[curr.key]); - acc.missing = true; + } else if (curr.key === 'url') { + if (queryParams[curr.key] !== 'index' && queryParams[curr.key].endsWith('/index')) { + acc[curr.key] = sanitizeURL(queryParams[curr.key]); + acc.missing = true; + } else if (queryParams[curr.key] === '/') { + acc[curr.key] = 'index'; + acc.missing = true; + } } return acc; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts index d55fcc339eb0..2f5ac8ef587c 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts @@ -107,6 +107,10 @@ export class InlineEditService { * @param dataset - The contentlet dataset to be edited inline. */ handleInlineEdit(dataset: InlineEditingContentletDataset): void { + if (!this.$isInlineEditingEnable()) { + return; + } + this.$inlineEditingTargetDataset.set(dataset); if (this.isInMultiplePages(this.$inlineEditingTargetDataset())) { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts index 234496b41d51..764a21436b0c 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts @@ -1,25 +1,9 @@ -import { of } from 'rxjs'; - import { InjectionToken } from '@angular/core'; -import { mockSites } from '@dotcms/dotcms-js'; -import { - CONTAINER_SOURCE, - DEFAULT_VARIANT_ID, - DotPageContainerStructure, - DotPersona -} from '@dotcms/dotcms-models'; -import { - mockDotLayout, - mockDotTemplate, - mockDotContainers, - dotcmsContentletMock -} from '@dotcms/utils-testing'; - -import { EDITOR_MODE, EDITOR_STATE } from './enums'; -import { ActionPayload } from './models'; +import { DotPersona } from '@dotcms/dotcms-models'; -import { DotPageApiResponse } from '../services/dot-page-api.service'; +import { CommonErrors } from './enums'; +import { CommonErrorsInfo } from './models'; export const LAYOUT_URL = '/c/portal/layout'; @@ -35,6 +19,25 @@ export const VIEW_CONTENT_CALLBACK_FUNCTION = 'angularWorkflowEventCallback'; export const IFRAME_SCROLL_ZONE = 100; +export const BASE_IFRAME_MEASURE_UNIT = 'px'; + +export const COMMON_ERRORS: CommonErrorsInfo = { + [CommonErrors.NOT_FOUND]: { + icon: 'compass', + title: 'editema.infopage.notfound.title', + description: 'editema.infopage.notfound.description', + buttonPath: '/pages', + buttonText: 'editema.infopage.button.gotopages' + }, + [CommonErrors.ACCESS_DENIED]: { + icon: 'ban', + title: 'editema.infopage.accessdenied.title', + description: 'editema.infopage.accessdenied.description', + buttonPath: '/pages', + buttonText: 'editema.infopage.button.gotopages' + } +}; + export const DEFAULT_PERSONA: DotPersona = { hostFolder: 'SYSTEM_HOST', inode: '', @@ -64,418 +67,3 @@ export const DEFAULT_PERSONA: DotPersona = { hasLiveVersion: false, modUser: 'system' }; - -export const PAYLOAD_MOCK: ActionPayload = { - container: { - acceptTypes: 'Banner', - contentletsId: ['19c5ecc0c59b17b5780acd624ad52444', '2e5d54e6-7ea3-4d72-8577-b8731b206ca0'], - identifier: '//demo.dotcms.com/application/containers/banner/', - maxContentlets: 25, - uuid: '1', - variantId: '1' - }, - contentlet: { - identifier: '19c5ecc0c59b17b5780acd624ad52444', - title: 'Zelda Cafe', - inode: 'ff10d5db-b06e-4298-870b-fbe2f5001ac2', - onNumberOfPages: 1, - contentType: 'Banner' - }, - language_id: '1', - pageContainers: [ - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '10', - contentletsId: ['c151d3f0-9572-4bcc-8c8f-00c9da9758e0'] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '1', - contentletsId: ['df591adf-10fe-461a-a12e-f847df0fd2fb'] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '2', - contentletsId: [ - '4694d40b-d9be-4e09-b031-64ee3e7c9642', - '6ac5921e-e062-49a6-9808-f41aff9343c5' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '3', - contentletsId: [ - '574f0aec-185a-4160-9c17-6d037b298318', - '50351143-3ba6-4c54-9e9b-0d8a90d2f9b0' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '4', - contentletsId: [ - '8ccfa397-4369-44bb-b450-33151387eb02', - '6a8102b5-fdb0-4ad5-9a5d-e982bcdb54c8' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '5', - contentletsId: [ - '50757fb4-75df-4e2c-8335-35d36bdb944b', - '0e9340f8-08d2-46e3-ae25-be0137c575d0' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '6', - contentletsId: [ - 'f40c6030-3532-4e75-9ca8-0d92261264e3', - 'd1f449ec-ad6a-4b59-ab38-98ba2e9c3231', - 'b0df5dbd-0e3c-4df8-b478-55b4d9cee344' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '7', - contentletsId: [ - '39aa1441-2933-4c81-b3f4-cc154195595b', - 'acfcb298-af27-40bc-835b-faf634b0f888', - '46e52dc2-e72a-4641-8925-026abf2adccd' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '8', - contentletsId: [ - 'e9fdab13-72f2-486c-b645-0e2315d5c33b', - '5985eec0-6bc7-4d87-be13-d4dc83516da2', - 'df26d95c-4f1f-4503-ac81-8176bb1c417d' - ] - }, - { - identifier: '//demo.dotcms.com/application/containers/default/', - uuid: '9', - contentletsId: ['cbe5573b-a201-477b-aea1-5ff3e75a1072'] - }, - { - identifier: '//demo.dotcms.com/application/containers/banner/', - uuid: '1', - contentletsId: [ - '19c5ecc0c59b17b5780acd624ad52444', - '2e5d54e6-7ea3-4d72-8577-b8731b206ca0' - ] - } - ], - pageId: 'a9f30020-54ef-494e-92ed-645e757171c2', - position: 'before' -}; - -export const MOCK_RESPONSE_HEADLESS: DotPageApiResponse = { - page: { - pageURI: 'test-url', - title: 'Test Page', - identifier: '123', - inode: '123-i', - canEdit: true, - canRead: true, - contentType: 'htmlpageasset', - canLock: true, - locked: false, - lockedBy: '', - lockedByName: '', - live: true - }, - viewAs: { - language: { - id: 1, - language: 'English', - countryCode: 'US', - languageCode: '1', - country: 'United States' - }, - variantId: DEFAULT_VARIANT_ID, - persona: { - ...DEFAULT_PERSONA - } - }, - site: mockSites[0], - layout: mockDotLayout(), - template: mockDotTemplate(), - containers: mockDotContainers() -}; - -export const dotPageContainerStructureMock: DotPageContainerStructure = { - '123': { - container: { - archived: false, - categoryId: '123', - deleted: false, - friendlyName: '123', - identifier: '123', - live: false, - locked: false, - maxContentlets: 123, - name: '123', - path: '123', - pathName: '123', - postLoop: '123', - preLoop: '123', - source: CONTAINER_SOURCE.DB, - title: '123', - type: '123', - working: false - }, - containerStructures: [ - { - contentTypeVar: '123' - } - ], - contentlets: { - '123': [ - { - baseType: '123', - content: 'something', - contentType: '123', - dateCreated: '123', - dateModifed: '123', - folder: '123', - host: '123', - identifier: '123', - inode: '123', - languageId: 123, - live: false, - locked: false, - modDate: '123', - modUser: '123', - owner: '123', - working: false, - url: '123', - stInode: '123', - deleted: false, - hostName: '123', - archived: false, - hasTitleImage: false, - image: '123', - title: '123', - sortOrder: 123, - __icon__: '123', - modUserName: '123', - titleImage: '123' - }, - { - baseType: '456', - content: 'something', - contentType: '456', - dateCreated: '456', - folder: '456', - identifier: '456', - inode: '456', - languageId: 456, - live: false, - dateModifed: '456', - modDate: '456', - host: '456', - working: false, - title: '456', - locked: false, - archived: false, - owner: '456', - url: '456', - modUser: '456', - __icon__: '456', - deleted: false, - hasTitleImage: false, - titleImage: '456', - hostName: '456', - sortOrder: 456, - image: '456', - stInode: '456', - modUserName: '456' - } - ], - '456': [ - { - contentType: '123', - content: 'something', - dateCreated: '123', - baseType: '123', - folder: '123', - dateModifed: '123', - identifier: '123', - host: '123', - live: false, - inode: '123', - locked: false, - languageId: 123, - owner: '123', - working: false, - modDate: '123', - modUser: '123', - title: '123', - image: '123', - archived: false, - titleImage: '123', - url: '123', - __icon__: '123', - deleted: false, - hasTitleImage: false, - hostName: '123', - modUserName: '123', - stInode: '123', - sortOrder: 123 - } - ] - } - } -}; - -export const PAGE_INODE_MOCK = '1234'; - -export const QUERY_PARAMS_MOCK = { language_id: 1, url: 'page-one' }; - -export const TREE_NODE_MOCK = { - containerId: '123', - contentId: '123', - pageId: '123', - relationType: 'test', - treeOrder: '1', - variantId: 'test', - personalization: 'dot:default' -}; - -export const newContentlet = { - ...dotcmsContentletMock, - inode: '123', - title: 'test' -}; - -export const EDIT_ACTION_PAYLOAD_MOCK: ActionPayload = { - language_id: '1', - pageContainers: [ - { - identifier: 'test', - uuid: 'test', - contentletsId: [] - } - ], - contentlet: { - identifier: 'contentlet-identifier-123', - inode: 'contentlet-inode-123', - title: 'Hello World', - contentType: 'test', - onNumberOfPages: 1 - }, - container: { - identifier: 'test', - acceptTypes: 'test', - uuid: 'test', - maxContentlets: 1, - contentletsId: ['123'], - variantId: '123' - }, - pageId: 'test', - position: 'before' -}; - -export const URL_CONTENT_MAP_MOCK = { - contentType: 'Blog', - identifier: '123', - inode: '1234', - title: 'hello world' -}; - -export const SHOW_CONTENTLET_TOOLS_PATCH_MOCK = { - editorState: EDITOR_STATE.IDLE, - editorData: { - mode: EDITOR_MODE.EDIT, - canEditVariant: true, - device: null, - page: { - lockedByUser: '', - canLock: true, - isLocked: false - } - }, - contentletArea: { - x: 0, - y: 0, - width: 100, - height: 100, - payload: { - language_id: '', - pageContainers: [], - pageId: '', - container: { - acceptTypes: '', - identifier: '', - maxContentlets: 0, - variantId: '', - uuid: '' - }, - contentlet: { - identifier: '123', - inode: '', - title: '', - contentType: '' - } - } - } -}; - -export const PAGE_RESPONSE_BY_LANGUAGE_ID = { - 1: of({ - page: { - title: 'hello world', - identifier: '123', - inode: '123', - canEdit: true, - canRead: true, - pageURI: 'index', - liveInode: '1234', - stInode: '12345', - live: true - }, - viewAs: { - language: { - id: 1, - language: 'English', - countryCode: 'US', - languageCode: 'EN', - country: 'United States' - }, - persona: DEFAULT_PERSONA - }, - site: mockSites[0], - template: { - drawed: true - } - }), - - 2: of({ - page: { - title: 'hello world', - identifier: '123', - inode: '123', - canEdit: true, - canRead: true, - pageURI: 'index', - liveInode: '1234', - stInode: '12345', - live: true - }, - viewAs: { - language: { - id: 2, - languageCode: 'IT', - countryCode: '', - language: 'Italian', - country: 'Italy' - }, - persona: DEFAULT_PERSONA - }, - site: mockSites[0], - template: { - drawed: true - } - }) -}; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts index 411d9cc883e2..108964cf3cbf 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts @@ -23,24 +23,21 @@ export enum NG_CUSTOM_EVENTS { EDIT_CONTENTLET_UPDATED = 'edit-contentlet-data-updated' } -// The current state of the editor -export enum EDITOR_STATE { +// Status of the whole UVE +export enum UVE_STATUS { LOADING = 'loading', + LOADED = 'loaded', + ERROR = 'error' +} + +export enum EDITOR_STATE { + ERROR = 'error', IDLE = 'idle', DRAGGING = 'dragging', - ERROR = 'error', OUT_OF_BOUNDS = 'out-of-bounds', - SCROLL_DRAG = 'scroll-drag' -} - -export enum EDITOR_MODE { - EDIT = 'edit', - EDIT_VARIANT = 'edit-variant', - PREVIEW_VARIANT = 'preview-variant', - DEVICE = 'device', - SOCIAL_MEDIA = 'social-media', - INLINE_EDITING = 'inline-editing', - LOCKED = 'locked' + SCROLL_DRAG = 'scroll-drag', + SCROLLING = 'scrolling', + INLINE_EDITING = 'inline-editing' } export enum PAGE_MODE { @@ -48,3 +45,8 @@ export enum PAGE_MODE { PREVIEW = 'PREVIEW_MODE', LIVE = 'LIVE' } + +export enum CommonErrors { + 'NOT_FOUND' = '404', + 'ACCESS_DENIED' = '403' +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts new file mode 100644 index 000000000000..c5a6873bc754 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/mocks.ts @@ -0,0 +1,698 @@ +import { of } from 'rxjs'; + +import { + DEFAULT_VARIANT_ID, + DotPageContainerStructure, + CONTAINER_SOURCE +} from '@dotcms/dotcms-models'; +import { + mockSites, + mockDotLayout, + mockDotTemplate, + mockDotContainers, + dotcmsContentletMock, + mockLanguageArray +} from '@dotcms/utils-testing'; + +import { DEFAULT_PERSONA } from './consts'; +import { ActionPayload, ClientData } from './models'; + +import { + EmaDragItem, + ContentletArea, + Container +} from '../edit-ema-editor/components/ema-page-dropzone/types'; +import { DotPageApiResponse } from '../services/dot-page-api.service'; + +export const HEADLESS_BASE_QUERY_PARAMS = { + url: 'test-url', + language_id: '1', + 'com.dotmarketing.persona.id': DEFAULT_PERSONA.keyTag, + variantName: DEFAULT_VARIANT_ID, + clientHost: 'http://localhost:3000' +}; + +export const VTL_BASE_QUERY_PARAMS = { + url: 'test-url', + language_id: '1', + 'com.dotmarketing.persona.id': DEFAULT_PERSONA.keyTag, + variantName: DEFAULT_VARIANT_ID +}; + +export const PAYLOAD_MOCK: ActionPayload = { + container: { + acceptTypes: 'Banner', + contentletsId: ['19c5ecc0c59b17b5780acd624ad52444', '2e5d54e6-7ea3-4d72-8577-b8731b206ca0'], + identifier: '//demo.dotcms.com/application/containers/banner/', + maxContentlets: 25, + uuid: '1', + variantId: '1' + }, + contentlet: { + identifier: '19c5ecc0c59b17b5780acd624ad52444', + title: 'Zelda Cafe', + inode: 'ff10d5db-b06e-4298-870b-fbe2f5001ac2', + onNumberOfPages: 1, + contentType: 'Banner' + }, + language_id: '1', + pageContainers: [ + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '10', + contentletsId: ['c151d3f0-9572-4bcc-8c8f-00c9da9758e0'] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '1', + contentletsId: ['df591adf-10fe-461a-a12e-f847df0fd2fb'] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '2', + contentletsId: [ + '4694d40b-d9be-4e09-b031-64ee3e7c9642', + '6ac5921e-e062-49a6-9808-f41aff9343c5' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '3', + contentletsId: [ + '574f0aec-185a-4160-9c17-6d037b298318', + '50351143-3ba6-4c54-9e9b-0d8a90d2f9b0' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '4', + contentletsId: [ + '8ccfa397-4369-44bb-b450-33151387eb02', + '6a8102b5-fdb0-4ad5-9a5d-e982bcdb54c8' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '5', + contentletsId: [ + '50757fb4-75df-4e2c-8335-35d36bdb944b', + '0e9340f8-08d2-46e3-ae25-be0137c575d0' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '6', + contentletsId: [ + 'f40c6030-3532-4e75-9ca8-0d92261264e3', + 'd1f449ec-ad6a-4b59-ab38-98ba2e9c3231', + 'b0df5dbd-0e3c-4df8-b478-55b4d9cee344' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '7', + contentletsId: [ + '39aa1441-2933-4c81-b3f4-cc154195595b', + 'acfcb298-af27-40bc-835b-faf634b0f888', + '46e52dc2-e72a-4641-8925-026abf2adccd' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '8', + contentletsId: [ + 'e9fdab13-72f2-486c-b645-0e2315d5c33b', + '5985eec0-6bc7-4d87-be13-d4dc83516da2', + 'df26d95c-4f1f-4503-ac81-8176bb1c417d' + ] + }, + { + identifier: '//demo.dotcms.com/application/containers/default/', + uuid: '9', + contentletsId: ['cbe5573b-a201-477b-aea1-5ff3e75a1072'] + }, + { + identifier: '//demo.dotcms.com/application/containers/banner/', + uuid: '1', + contentletsId: [ + '19c5ecc0c59b17b5780acd624ad52444', + '2e5d54e6-7ea3-4d72-8577-b8731b206ca0' + ] + } + ], + pageId: 'a9f30020-54ef-494e-92ed-645e757171c2', + position: 'before' +}; + +export const MOCK_RESPONSE_HEADLESS: DotPageApiResponse = { + page: { + pageURI: 'test-url', + title: 'Test Page', + identifier: '123', + inode: '123-i', + canEdit: true, + canRead: true, + contentType: 'htmlpageasset', + canLock: true, + locked: false, + lockedBy: '', + lockedByName: '', + live: true + }, + viewAs: { + language: { + id: 1, + language: 'English', + countryCode: 'US', + languageCode: '1', + country: 'United States' + }, + variantId: DEFAULT_VARIANT_ID, + persona: { + ...DEFAULT_PERSONA + } + }, + site: mockSites[0], + layout: mockDotLayout(), + template: mockDotTemplate(), + containers: mockDotContainers() +}; + +export const MOCK_RESPONSE_VTL: DotPageApiResponse = { + page: { + pageURI: 'test-url', + title: 'Test Page', + identifier: '123', + inode: '123-i', + canEdit: true, + canRead: true, + rendered: '

Hello, World!

', + contentType: 'htmlpageasset', + canLock: true, + locked: false, + lockedBy: '', + lockedByName: '', + live: true, + liveInode: '1234', + stInode: '12345' + }, + viewAs: { + language: { + id: 1, + language: 'English', + countryCode: 'US', + languageCode: '1', + country: 'United States' + }, + + persona: { + ...DEFAULT_PERSONA + } + }, + site: mockSites[0], + layout: mockDotLayout(), + template: mockDotTemplate(), + containers: mockDotContainers() +}; + +export const dotPageContainerStructureMock: DotPageContainerStructure = { + '123': { + container: { + archived: false, + categoryId: '123', + deleted: false, + friendlyName: '123', + identifier: '123', + live: false, + locked: false, + maxContentlets: 123, + name: '123', + path: '123', + pathName: '123', + postLoop: '123', + preLoop: '123', + source: CONTAINER_SOURCE.DB, + title: '123', + type: '123', + working: false + }, + containerStructures: [ + { + contentTypeVar: '123' + } + ], + contentlets: { + '123': [ + { + baseType: '123', + content: 'something', + contentType: '123', + dateCreated: '123', + dateModifed: '123', + folder: '123', + host: '123', + identifier: '123', + inode: '123', + languageId: 123, + live: false, + locked: false, + modDate: '123', + modUser: '123', + owner: '123', + working: false, + url: '123', + stInode: '123', + deleted: false, + hostName: '123', + archived: false, + hasTitleImage: false, + image: '123', + title: '123', + sortOrder: 123, + __icon__: '123', + modUserName: '123', + titleImage: '123' + }, + { + baseType: '456', + content: 'something', + contentType: '456', + dateCreated: '456', + folder: '456', + identifier: '456', + inode: '456', + languageId: 456, + live: false, + dateModifed: '456', + modDate: '456', + host: '456', + working: false, + title: '456', + locked: false, + archived: false, + owner: '456', + url: '456', + modUser: '456', + __icon__: '456', + deleted: false, + hasTitleImage: false, + titleImage: '456', + hostName: '456', + sortOrder: 456, + image: '456', + stInode: '456', + modUserName: '456' + } + ], + '456': [ + { + contentType: '123', + content: 'something', + dateCreated: '123', + baseType: '123', + folder: '123', + dateModifed: '123', + identifier: '123', + host: '123', + live: false, + inode: '123', + locked: false, + languageId: 123, + owner: '123', + working: false, + modDate: '123', + modUser: '123', + title: '123', + image: '123', + archived: false, + titleImage: '123', + url: '123', + __icon__: '123', + deleted: false, + hasTitleImage: false, + hostName: '123', + modUserName: '123', + stInode: '123', + sortOrder: 123 + } + ] + } + } +}; + +export const PAGE_INODE_MOCK = '1234'; + +export const QUERY_PARAMS_MOCK = { language_id: 1, url: 'page-one' }; + +export const TREE_NODE_MOCK = { + containerId: '123', + contentId: '123', + pageId: '123', + relationType: 'test', + treeOrder: '1', + variantId: 'test', + personalization: 'dot:default' +}; + +export const newContentlet = { + ...dotcmsContentletMock, + inode: '123', + title: 'test' +}; + +export const EDIT_ACTION_PAYLOAD_MOCK: ActionPayload = { + language_id: '1', + pageContainers: [ + { + identifier: 'test', + uuid: 'test', + contentletsId: [] + } + ], + contentlet: { + identifier: 'contentlet-identifier-123', + inode: 'contentlet-inode-123', + title: 'Hello World', + contentType: 'test', + onNumberOfPages: 1 + }, + container: { + identifier: 'test', + acceptTypes: 'test', + uuid: 'test', + maxContentlets: 1, + contentletsId: ['123'], + variantId: '123' + }, + pageId: 'test', + position: 'before' +}; + +export const URL_CONTENT_MAP_MOCK = { + contentType: 'Blog', + identifier: '123', + inode: '1234', + title: 'hello world' +}; + +export const PAGE_RESPONSE_BY_LANGUAGE_ID = { + 1: of({ + page: { + title: 'hello world', + identifier: '123', + inode: '123', + canEdit: true, + canRead: true, + pageURI: 'index', + liveInode: '1234', + stInode: '12345', + live: true + }, + viewAs: { + language: { + id: 1, + language: 'English', + countryCode: 'US', + languageCode: 'EN', + country: 'United States' + }, + persona: DEFAULT_PERSONA + }, + site: mockSites[0], + template: { + drawed: true + } + }), + + 2: of({ + page: { + title: 'hello world', + identifier: '123', + inode: '123', + canEdit: true, + canRead: true, + pageURI: 'index', + liveInode: '1234', + stInode: '12345', + live: true + }, + viewAs: { + language: { + id: 2, + languageCode: 'IT', + countryCode: '', + language: 'Italian', + country: 'Italy' + }, + persona: DEFAULT_PERSONA + }, + site: mockSites[0], + template: { + drawed: true + } + }) +}; + +export const getVanityUrl = (url, mock) => ({ + vanityUrl: { + ...mock, + url + } +}); + +export const FORWARD_VANITY_URL = { + pattern: '', + vanityUrlId: '', + url: 'test-url', + siteId: '', + languageId: 1, + forwardTo: 'vanity-url', + response: 200, + order: 1, + temporaryRedirect: false, + permanentRedirect: false, + forward: true +}; + +export const PERMANENT_REDIRECT_VANITY_URL = { + pattern: '', + vanityUrlId: '', + url: 'test-url', + siteId: '', + languageId: 1, + forwardTo: 'vanity-url', + response: 200, + order: 1, + temporaryRedirect: false, + permanentRedirect: true, + forward: false +}; + +export const TEMPORARY_REDIRECT_VANITY_URL = { + pattern: '', + vanityUrlId: '', + url: 'test-url', + siteId: '', + languageId: 1, + forwardTo: 'vanity-url', + response: 200, + order: 1, + temporaryRedirect: true, + permanentRedirect: false, + forward: false +}; + +export const EMA_DRAG_ITEM_CONTENTLET_MOCK: EmaDragItem = { + baseType: 'CONTENT', + contentType: 'kenobi', + draggedPayload: { + type: 'contentlet', + item: { + container: { + identifier: '321', + acceptTypes: 'kenobi,theChosenOne,yoda', + maxContentlets: 3, + uuid: '123', + variantId: '123' + }, + contentlet: { + identifier: '321', + inode: '123', + title: 'title', + contentType: 'kenobi' + } + }, + move: true + } +}; + +export const MOCK_CONTENTLET_AREA: ContentletArea = { + x: 200, + y: 180, + width: 100, + height: 100, + payload: { + language_id: '', + pageContainers: [], + pageId: '', + container: { + acceptTypes: '', + identifier: '', + maxContentlets: 0, + variantId: '', + uuid: '' + }, + contentlet: { + identifier: '123', + inode: '', + title: '', + contentType: '' + } + } +}; + +export const ACTION_MOCK: ClientData = { + container: { + acceptTypes: 'file', + identifier: '789', + maxContentlets: 100, + uuid: '2', + variantId: '1' + } +}; + +export const ITEM_MOCK = { + contentType: 'file', + baseType: 'FILEASSET', + draggedPayload: null +}; + +export const getBoundsMockWithEmptyContainer = (payload: ClientData): Container[] => { + return [ + { + x: 10, + y: 10, + width: 980, + height: 180, + contentlets: [], + payload + } + ]; +}; + +export const getBoundsMock = (payload: ClientData): Container[] => { + return [ + { + x: 10, + y: 10, + width: 980, + height: 180, + contentlets: [ + { + x: 20, + y: 20, + width: 940, + height: 140, + payload: null as unknown as ActionPayload + }, + { + x: 40, + y: 20, + width: 940, + height: 140, + payload: null as unknown as ActionPayload + } + ], + payload + } + ]; +}; + +export const BOUNDS_MOCK: Container[] = getBoundsMock(ACTION_MOCK); + +export const BOUNDS_EMPTY_CONTAINER_MOCK: Container[] = + getBoundsMockWithEmptyContainer(ACTION_MOCK); + +export const ACTION_PAYLOAD_MOCK: ActionPayload = { + language_id: '1', + pageContainers: [ + { + identifier: 'container-identifier-123', + uuid: 'uuid-123', + contentletsId: ['contentlet-identifier-123'] + } + ], + contentlet: { + identifier: 'contentlet-identifier-123', + inode: 'contentlet-inode-123', + title: 'Hello World', + contentType: 'test', + onNumberOfPages: 1 + }, + container: { + identifier: 'container-identifier-123', + acceptTypes: 'test', + uuid: 'uuid-123', + maxContentlets: 1, + contentletsId: ['123'], + variantId: '123' + }, + pageId: 'test', + position: 'after' +}; + +export const BASE_SHELL_PROPS_RESPONSE = { + canRead: true, + error: null, + translateProps: { + page: MOCK_RESPONSE_HEADLESS.page, + languageId: 1, + languages: mockLanguageArray + }, + seoParams: { + siteId: MOCK_RESPONSE_HEADLESS.site.identifier, + languageId: 1, + currentUrl: '/test-url', + requestHostName: 'http://localhost:3000' + }, + items: [ + { + icon: 'pi-file', + label: 'editema.editor.navbar.content', + href: 'content', + id: 'content' + }, + { + icon: 'pi-table', + label: 'editema.editor.navbar.layout', + href: 'layout', + id: 'layout', + isDisabled: false, + tooltip: null + }, + { + icon: 'pi-sliders-h', + label: 'editema.editor.navbar.rules', + id: 'rules', + href: `rules/${MOCK_RESPONSE_HEADLESS.page.identifier}`, + isDisabled: false + }, + { + iconURL: 'experiments', + label: 'editema.editor.navbar.experiments', + href: `experiments/${MOCK_RESPONSE_HEADLESS.page.identifier}`, + id: 'experiments', + isDisabled: false + }, + { + icon: 'pi-th-large', + label: 'editema.editor.navbar.page-tools', + id: 'page-tools' + }, + { + icon: 'pi-ellipsis-v', + label: 'editema.editor.navbar.properties', + id: 'properties' + } + ] +}; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/models.ts index 0868d7965425..dcad2076d1fe 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/models.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/models.ts @@ -1,15 +1,24 @@ -import { DotDevice, DotExperiment, DotLanguage } from '@dotcms/dotcms-models'; +import { DotDevice } from '@dotcms/dotcms-models'; +import { InfoPage } from '@dotcms/ui'; -import { EDITOR_MODE, EDITOR_STATE } from './enums'; +import { CommonErrors } from './enums'; import { ClientContentletArea, Container, - ContentletArea, - EmaDragItem, UpdatedContentlet } from '../edit-ema-editor/components/ema-page-dropzone/types'; -import { DotPageApiParams, DotPageApiResponse } from '../services/dot-page-api.service'; +import { DotPageApiParams } from '../services/dot-page-api.service'; + +export interface InfoOptions { + icon: string; + info: { + message: string; + args: string[]; + }; + id: string; + actionIcon?: string; +} export interface VTLFile { inode: string; @@ -75,47 +84,14 @@ export interface SavePagePayload { whenSaved?: () => void; } -export interface ReloadPagePayload { - params: DotPageApiParams; - whenReloaded?: () => void; -} - export interface NavigationBarItem { icon?: string; iconURL?: string; label: string; href?: string; - action?: () => void; + id: string; isDisabled?: boolean; -} - -export interface EditorData { - mode: EDITOR_MODE; - device?: DotDevice & { icon?: string }; - socialMedia?: string; - canEditVariant?: boolean; - canEditPage?: boolean; - variantId?: string; - page?: { - isLocked: boolean; - canLock: boolean; - lockedByUser: string; - }; -} - -export interface EditEmaState { - clientHost: string; - error?: number; - editor: DotPageApiResponse; - isEnterpriseLicense: boolean; - editorState: EDITOR_STATE; - bounds: Container[]; - contentletArea: ContentletArea; - editorData: EditorData; - currentExperiment?: DotExperiment; - dragItem?: EmaDragItem; - shouldReload: boolean; - languages: DotLanguage[]; + tooltip?: string; } export interface MessageInfo { @@ -217,3 +193,9 @@ export interface DotPage { liveInode?: string; stInode?: string; } + +export interface DotDeviceWithIcon extends DotDevice { + icon?: string; +} + +export type CommonErrorsInfo = Record; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.spec.ts new file mode 100644 index 000000000000..f56f2dbf88b7 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.spec.ts @@ -0,0 +1,1439 @@ +import { describe, expect } from '@jest/globals'; +import { + createServiceFactory, + mockProvider, + SpectatorService, + SpyObject +} from '@ngneat/spectator/jest'; +import { patchState } from '@ngrx/signals'; +import { of } from 'rxjs'; + +import { ActivatedRoute, ActivatedRouteSnapshot, ParamMap, Router } from '@angular/router'; + +import { MessageService } from 'primeng/api'; + +import { + DotExperimentsService, + DotLanguagesService, + DotLicenseService, + DotMessageService +} from '@dotcms/data-access'; +import { LoginService } from '@dotcms/dotcms-js'; +import { DEFAULT_VARIANT_ID, DEFAULT_VARIANT_NAME, DotCMSContentlet } from '@dotcms/dotcms-models'; +import { + MockDotMessageService, + getRunningExperimentMock, + getScheduleExperimentMock, + getDraftExperimentMock, + DotLanguagesServiceMock, + CurrentUserDataMock, + mockLanguageArray, + mockDotDevices, + seoOGTagsMock +} from '@dotcms/utils-testing'; + +import { UVEStore } from './dot-uve.store'; + +import { DotPageApiResponse, DotPageApiService } from '../services/dot-page-api.service'; +import { BASE_IFRAME_MEASURE_UNIT, COMMON_ERRORS, DEFAULT_PERSONA } from '../shared/consts'; +import { EDITOR_STATE, UVE_STATUS } from '../shared/enums'; +import { + ACTION_MOCK, + ACTION_PAYLOAD_MOCK, + BASE_SHELL_PROPS_RESPONSE, + EMA_DRAG_ITEM_CONTENTLET_MOCK, + getBoundsMock, + getVanityUrl, + HEADLESS_BASE_QUERY_PARAMS, + MOCK_CONTENTLET_AREA, + MOCK_RESPONSE_HEADLESS, + MOCK_RESPONSE_VTL, + PERMANENT_REDIRECT_VANITY_URL, + TEMPORARY_REDIRECT_VANITY_URL, + VTL_BASE_QUERY_PARAMS +} from '../shared/mocks'; +import { DotDeviceWithIcon } from '../shared/models'; +import { + getPersonalization, + mapContainerStructureToArrayOfContainers, + mapContainerStructureToDotContainerMap +} from '../utils'; + +const buildPageAPIResponseFromMock = + (mock) => + ({ url }) => + of({ + ...mock, + page: { + ...mock.page, + pageURI: url + } + }); + +describe('UVEStore', () => { + let spectator: SpectatorService>; + let store: InstanceType; + let dotPageApiService: SpyObject; + let activatedRoute: SpyObject; + let router: SpyObject; + + const createService = createServiceFactory({ + service: UVEStore, + providers: [ + MessageService, + mockProvider(Router), + mockProvider(ActivatedRoute), + { + provide: DotPageApiService, + useValue: { + get() { + return of({}); + }, + save: jest.fn() + } + }, + { + provide: DotLicenseService, + useValue: { + isEnterprise: () => of(true) + } + }, + { + provide: DotMessageService, + useValue: new MockDotMessageService({}) + }, + { + provide: DotExperimentsService, + useValue: { + getById(experimentId: string) { + if (experimentId == 'i-have-a-running-experiment') { + return of(getRunningExperimentMock()); + } else if (experimentId == 'i-have-a-scheduled-experiment') { + return of(getScheduleExperimentMock()); + } else if (experimentId) return of(getDraftExperimentMock()); + + return of(undefined); + } + } + }, + { + provide: LoginService, + useValue: { + getCurrentUser: () => of(CurrentUserDataMock) + } + }, + { + provide: DotLanguagesService, + useValue: new DotLanguagesServiceMock() + } + ] + }); + + beforeEach(() => { + spectator = createService(); + store = spectator.service; + + dotPageApiService = spectator.inject(DotPageApiService); + router = spectator.inject(Router); + activatedRoute = spectator.inject(ActivatedRoute); + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock(MOCK_RESPONSE_HEADLESS) + ); + + store.load(HEADLESS_BASE_QUERY_PARAMS); + }); + + describe('withComputed', () => { + describe('$shellProps', () => { + it('should return the shell props for Headless Pages', () => { + expect(store.$shellProps()).toEqual(BASE_SHELL_PROPS_RESPONSE); + }); + it('should return the error for 404', () => { + patchState(store, { errorCode: 404 }); + + expect(store.$shellProps()).toEqual({ + ...BASE_SHELL_PROPS_RESPONSE, + error: { + code: 404, + pageInfo: COMMON_ERRORS['404'] + } + }); + }); + it('should return the error for 403', () => { + patchState(store, { errorCode: 403 }); + + expect(store.$shellProps()).toEqual({ + ...BASE_SHELL_PROPS_RESPONSE, + error: { + code: 403, + pageInfo: COMMON_ERRORS['403'] + } + }); + }); + it('should return the error for 401', () => { + patchState(store, { errorCode: 401 }); + + expect(store.$shellProps()).toEqual({ + ...BASE_SHELL_PROPS_RESPONSE, + error: { + code: 401, + pageInfo: null + } + }); + }); + + it('should return the shell props for Legacy Pages', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock(MOCK_RESPONSE_VTL) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(store.$shellProps()).toEqual({ + canRead: true, + error: null, + translateProps: { + page: MOCK_RESPONSE_VTL.page, + languageId: 1, + languages: mockLanguageArray + }, + seoParams: { + siteId: MOCK_RESPONSE_VTL.site.identifier, + languageId: 1, + currentUrl: '/test-url', + requestHostName: 'http://localhost' + }, + items: [ + { + icon: 'pi-file', + label: 'editema.editor.navbar.content', + href: 'content', + id: 'content' + }, + { + icon: 'pi-table', + label: 'editema.editor.navbar.layout', + href: 'layout', + id: 'layout', + isDisabled: false, + tooltip: null + }, + { + icon: 'pi-sliders-h', + label: 'editema.editor.navbar.rules', + id: 'rules', + href: `rules/${MOCK_RESPONSE_VTL.page.identifier}`, + isDisabled: false + }, + { + iconURL: 'experiments', + label: 'editema.editor.navbar.experiments', + href: `experiments/${MOCK_RESPONSE_VTL.page.identifier}`, + id: 'experiments', + isDisabled: false + }, + { + icon: 'pi-th-large', + label: 'editema.editor.navbar.page-tools', + id: 'page-tools' + }, + { + icon: 'pi-ellipsis-v', + label: 'editema.editor.navbar.properties', + id: 'properties' + } + ] + }); + }); + + it('should return item for layout as disable', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock({ + ...MOCK_RESPONSE_VTL, + page: { + ...MOCK_RESPONSE_VTL.page, + canEdit: false + } + }) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + const layoutItem = store.$shellProps().items.find((item) => item.id === 'layout'); + + expect(layoutItem.isDisabled).toBe(true); + }); + + it('should return item for layout as disable and with a tooltip', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock({ + ...MOCK_RESPONSE_VTL, + template: { + ...MOCK_RESPONSE_VTL.template, + drawed: false + } + }) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + const layoutItem = store.$shellProps().items.find((item) => item.id === 'layout'); + + expect(layoutItem.isDisabled).toBe(true); + expect(layoutItem.tooltip).toBe( + 'editema.editor.navbar.layout.tooltip.cannot.edit.advanced.template' + ); + }); + + it('should return rules and experiments as disable when page cannot be edited', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock({ + ...MOCK_RESPONSE_VTL, + page: { + ...MOCK_RESPONSE_VTL.page, + canEdit: false + } + }) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + const rules = store.$shellProps().items.find((item) => item.id === 'rules'); + const experiments = store + .$shellProps() + .items.find((item) => item.id === 'experiments'); + + expect(rules.isDisabled).toBe(true); + expect(experiments.isDisabled).toBe(true); + }); + }); + }); + + describe('withMethods', () => { + describe('setUveStatus', () => { + it('should set the status of the UVEStore', () => { + expect(store.status()).toBe(UVE_STATUS.LOADED); + + store.setUveStatus(UVE_STATUS.LOADING); + + expect(store.status()).toBe(UVE_STATUS.LOADING); + }); + }); + }); + + describe('withLoad', () => { + describe('withMethods', () => { + it('should load the store with the base data', () => { + expect(store.pageAPIResponse()).toEqual(MOCK_RESPONSE_HEADLESS); + expect(store.isEnterprise()).toBe(true); + expect(store.currentUser()).toEqual(CurrentUserDataMock); + expect(store.experiment()).toBe(undefined); + expect(store.languages()).toBe(mockLanguageArray); + expect(store.params()).toEqual(HEADLESS_BASE_QUERY_PARAMS); + expect(store.canEditPage()).toBe(true); + expect(store.pageIsLocked()).toBe(false); + expect(store.status()).toBe(UVE_STATUS.LOADED); + expect(store.isTraditionalPage()).toBe(false); + }); + + it('should load the store with the base data for traditional page', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock(MOCK_RESPONSE_VTL) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(store.pageAPIResponse()).toEqual(MOCK_RESPONSE_VTL); + expect(store.isEnterprise()).toBe(true); + expect(store.currentUser()).toEqual(CurrentUserDataMock); + expect(store.experiment()).toBe(undefined); + expect(store.languages()).toBe(mockLanguageArray); + expect(store.params()).toEqual(VTL_BASE_QUERY_PARAMS); + expect(store.canEditPage()).toBe(true); + expect(store.pageIsLocked()).toBe(false); + expect(store.status()).toBe(UVE_STATUS.LOADED); + expect(store.isTraditionalPage()).toBe(true); + }); + + it('should navigate when the page is a vanityUrl permanent redirect', () => { + const permanentRedirect = getVanityUrl( + VTL_BASE_QUERY_PARAMS.url, + PERMANENT_REDIRECT_VANITY_URL + ) as unknown as DotPageApiResponse; + + const forwardTo = PERMANENT_REDIRECT_VANITY_URL.forwardTo; + + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of(permanentRedirect) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).toHaveBeenCalledWith([], { + queryParams: { + ...VTL_BASE_QUERY_PARAMS, + url: forwardTo + }, + queryParamsHandling: 'merge' + }); + }); + + it('should navigate when the page is a vanityUrl temporary redirect', () => { + const temporaryRedirect = getVanityUrl( + VTL_BASE_QUERY_PARAMS.url, + TEMPORARY_REDIRECT_VANITY_URL + ) as unknown as DotPageApiResponse; + + const forwardTo = TEMPORARY_REDIRECT_VANITY_URL.forwardTo; + + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of(temporaryRedirect) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).toHaveBeenCalledWith([], { + queryParams: { + ...VTL_BASE_QUERY_PARAMS, + url: forwardTo + }, + queryParamsHandling: 'merge' + }); + }); + + it('should navigate to content when the layout is disable by page.canEdit and current route is layout', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of({ + ...MOCK_RESPONSE_VTL, + page: { + ...MOCK_RESPONSE_VTL.page, + canEdit: false + } + }) + ); + + jest.spyOn(activatedRoute, 'firstChild', 'get').mockReturnValue({ + snapshot: { + url: [ + { + path: 'layout', + parameters: {}, + parameterMap: {} as unknown as ParamMap + } + ] + } as unknown as ActivatedRouteSnapshot + } as unknown as ActivatedRoute); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).toHaveBeenCalledWith(['edit-page/content'], { + queryParamsHandling: 'merge' + }); + }); + + it('should navigate to content when the layout is disable by template.drawed and current route is layout', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of({ + ...MOCK_RESPONSE_VTL, + template: { + ...MOCK_RESPONSE_VTL.template, + drawed: false + } + }) + ); + + jest.spyOn(activatedRoute, 'firstChild', 'get').mockReturnValue({ + snapshot: { + url: [ + { + path: 'layout', + parameters: {}, + parameterMap: {} as unknown as ParamMap + } + ] + } as unknown as ActivatedRouteSnapshot + } as unknown as ActivatedRoute); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).toHaveBeenCalledWith(['edit-page/content'], { + queryParamsHandling: 'merge' + }); + }); + + it('should not navigate to content when the layout is disable by template.drawed and current route is not layout', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of({ + ...MOCK_RESPONSE_VTL, + template: { + ...MOCK_RESPONSE_VTL.template, + drawed: false + } + }) + ); + + jest.spyOn(activatedRoute, 'firstChild', 'get').mockReturnValue({ + snapshot: { + url: [ + { + path: 'rules', + parameters: {}, + parameterMap: {} as unknown as ParamMap + } + ] + } as unknown as ActivatedRouteSnapshot + } as unknown as ActivatedRoute); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should not navigate to content when the layout is disable by page.canEdit and current route is not layout', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation(() => + of({ + ...MOCK_RESPONSE_VTL, + page: { + ...MOCK_RESPONSE_VTL.page, + canEdit: false + } + }) + ); + + jest.spyOn(activatedRoute, 'firstChild', 'get').mockReturnValue({ + snapshot: { + url: [ + { + path: 'rules', + parameters: {}, + parameterMap: {} as unknown as ParamMap + } + ] + } as unknown as ActivatedRouteSnapshot + } as unknown as ActivatedRoute); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should reload the store with the same queryParams', () => { + const getPageSpy = jest.spyOn(dotPageApiService, 'get'); + + store.reload(); + + expect(getPageSpy).toHaveBeenCalledWith(store.params()); + }); + }); + }); + + describe('withLayout', () => { + describe('withComputed', () => { + describe('$layoutProps', () => { + it('should return the layout props', () => { + expect(store.$layoutProps()).toEqual({ + containersMap: mapContainerStructureToDotContainerMap( + MOCK_RESPONSE_HEADLESS.containers + ), + layout: MOCK_RESPONSE_HEADLESS.layout, + template: { + identifier: MOCK_RESPONSE_HEADLESS.template.identifier, + themeId: MOCK_RESPONSE_HEADLESS.template.theme + }, + pageId: MOCK_RESPONSE_HEADLESS.page.identifier + }); + }); + }); + }); + + describe('withMethods', () => { + it('should update the layout', () => { + const layout = { + ...MOCK_RESPONSE_HEADLESS.layout, + title: 'New layout' + }; + + store.updateLayout(layout); + + expect(store.pageAPIResponse().layout).toEqual(layout); + }); + }); + }); + + describe('withEditor', () => { + describe('withEditorToolbar', () => { + describe('withComputed', () => { + describe('$toolbarProps', () => { + it('should return the base info', () => { + expect(store.$toolbarProps()).toEqual({ + apiUrl: 'http://localhost/api/v1/page/json/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT&clientHost=http%3A%2F%2Flocalhost%3A3000', + bookmarksUrl: '/test-url?host_id=123-xyz-567-xxl&language_id=1', + copyUrl: + 'http://localhost:3000/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT', + currentLanguage: MOCK_RESPONSE_HEADLESS.viewAs.language, + deviceSelector: { + apiLink: + 'http://localhost:3000/api/v1/page/json/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT&clientHost=http%3A%2F%2Flocalhost%3A3000', + hideSocialMedia: true + }, + personaSelector: { + pageId: MOCK_RESPONSE_HEADLESS.page.identifier, + value: MOCK_RESPONSE_HEADLESS.viewAs.persona ?? DEFAULT_PERSONA + }, + runningExperiment: null, + showInfoDisplay: false, + unlockButton: null, + urlContentMap: null, + workflowActionsInode: MOCK_RESPONSE_HEADLESS.page.inode + }); + }); + + describe('urlContentMap', () => { + it('should return the urlContentMap if the state is edit', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + urlContentMap: { + title: 'Title', + inode: '123', + contentType: 'test' + } as unknown as DotCMSContentlet + } + }); + + expect(store.$toolbarProps().urlContentMap).toEqual({ + title: 'Title', + inode: '123', + contentType: 'test' + }); + }); + + it('should not return the urlContentMap if the state is not edit', () => { + patchState(store, { isEditState: false }); + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + urlContentMap: { + title: 'Title', + inode: '123', + contentType: 'test' + } as unknown as DotCMSContentlet + } + }); + + expect(store.$toolbarProps().urlContentMap).toEqual(null); + }); + }); + + describe('runningExperiment', () => { + it('should have a runningExperiment if the experiment is running', () => { + patchState(store, { experiment: getRunningExperimentMock() }); + + expect(store.$toolbarProps().runningExperiment).toEqual( + getRunningExperimentMock() + ); + }); + }); + + describe('workflowActionsInode', () => { + it("should not have an workflowActionsInode if the user can't edit the page", () => { + patchState(store, { canEditPage: false }); + + expect(store.$toolbarProps().workflowActionsInode).toBe(null); + }); + }); + + describe('unlockButton', () => { + it('should have unlockButton if the page is locked and the user can lock the page', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page, + locked: true, + canLock: true + } + } + }); + + expect(store.$toolbarProps().unlockButton).toEqual({ + inode: '123-i', + loading: false + }); + }); + }); + + describe('shouldShowInfoDisplay', () => { + it("should have shouldShowInfoDisplay as true if the user can't edit the page", () => { + patchState(store, { canEditPage: false }); + + expect(store.$toolbarProps().showInfoDisplay).toBe(true); + }); + + it('should have shouldShowInfoDisplay as true if the device is set', () => { + patchState(store, { device: mockDotDevices[0] }); + + expect(store.$toolbarProps().showInfoDisplay).toBe(true); + }); + + it('should have shouldShowInfoDisplay as true if the socialMedia is set', () => { + patchState(store, { socialMedia: 'facebook' }); + + expect(store.$toolbarProps().showInfoDisplay).toBe(true); + }); + + it('should have shouldShowInfoDisplay as true if the page is a variant different from default', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + viewAs: { + ...MOCK_RESPONSE_HEADLESS.viewAs, + variantId: 'test' + } + } + }); + + expect(store.$toolbarProps().showInfoDisplay).toBe(true); + }); + }); + }); + + describe('$infoDisplayOptions', () => { + it('should be null in regular conditions', () => { + expect(store.$infoDisplayOptions()).toBe(null); + }); + + it('should return info for device', () => { + const device = mockDotDevices[0] as DotDeviceWithIcon; + + patchState(store, { device }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: device.icon, + info: { + message: 'iphone 200 x 100', + args: [] + }, + id: 'device', + actionIcon: 'pi pi-times' + }); + }); + + it('should return info for socialMedia', () => { + patchState(store, { socialMedia: 'Facebook' }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-facebook', + info: { + message: 'Viewing Facebook social media preview', + args: [] + }, + id: 'socialMedia', + actionIcon: 'pi pi-times' + }); + }); + + it('should return info when visiting a variant and can edit', () => { + const currentExperiment = getRunningExperimentMock(); + + const variantID = currentExperiment.trafficProportion.variants.find( + (variant) => variant.name !== DEFAULT_VARIANT_NAME + ).id; + + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + viewAs: { + ...MOCK_RESPONSE_HEADLESS.viewAs, + variantId: variantID + } + }, + experiment: currentExperiment + }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-file-edit', + info: { + message: 'editpage.editing.variant', + args: ['Variant A'] + }, + id: 'variant', + actionIcon: 'pi pi-arrow-left' + }); + }); + + it('should return info when visiting a variant and can not edit', () => { + const currentExperiment = getRunningExperimentMock(); + + const variantID = currentExperiment.trafficProportion.variants.find( + (variant) => variant.name !== DEFAULT_VARIANT_NAME + ).id; + + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page + }, + viewAs: { + ...MOCK_RESPONSE_HEADLESS.viewAs, + variantId: variantID + } + }, + experiment: currentExperiment, + canEditPage: false + }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-file-edit', + info: { + message: 'editpage.viewing.variant', + args: ['Variant A'] + }, + id: 'variant', + actionIcon: 'pi pi-arrow-left' + }); + }); + + it('should return info when the page is locked and can lock', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page, + locked: true, + canLock: true, + lockedByName: 'John Doe' + } + } + }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-lock', + info: { + message: 'editpage.locked-by', + args: ['John Doe'] + }, + id: 'locked' + }); + }); + + it('should return info when the page is locked and cannot lock', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page, + locked: true, + canLock: false, + lockedByName: 'John Doe' + } + } + }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-lock', + info: { + message: 'editpage.locked-contact-with', + args: ['John Doe'] + }, + id: 'locked' + }); + }); + + it('should return info when you cannot edit the page', () => { + patchState(store, { canEditPage: false }); + + expect(store.$infoDisplayOptions()).toEqual({ + icon: 'pi pi-exclamation-circle warning', + info: { message: 'editema.dont.have.edit.permission', args: [] }, + id: 'no-permission' + }); + }); + }); + }); + + describe('withMethods', () => { + it('should set the device with setDevice', () => { + const device = { + identifier: '123', + cssHeight: '120', + cssWidth: '120', + name: 'square', + inode: '1234', + icon: 'icon' + }; + + store.setDevice(device); + + expect(store.device()).toEqual(device); + expect(store.isEditState()).toBe(false); + }); + + it('should set the socialMedia with setSocialMedia', () => { + const socialMedia = 'facebook'; + + store.setSocialMedia(socialMedia); + + expect(store.socialMedia()).toEqual(socialMedia); + expect(store.isEditState()).toBe(false); + }); + + it('should reset the state with clearDeviceAndSocialMedia', () => { + store.clearDeviceAndSocialMedia(); + + expect(store.device()).toBe(null); + expect(store.socialMedia()).toBe(null); + expect(store.isEditState()).toBe(true); + }); + }); + }); + + describe('withSave', () => { + describe('withMethods', () => { + describe('savePage', () => { + it('should perform a save and patch the state', () => { + const saveSpy = jest + .spyOn(dotPageApiService, 'save') + .mockImplementation(() => of({})); + + // It's impossible to get a VTL when we are in Headless + // but I just want to check the state is being patched + const getSpy = jest + .spyOn(dotPageApiService, 'get') + .mockImplementation(() => of(MOCK_RESPONSE_VTL)); + + const payload = { + pageContainers: ACTION_PAYLOAD_MOCK.pageContainers, + pageId: MOCK_RESPONSE_HEADLESS.page.identifier, + params: store.params() + }; + + store.savePage(ACTION_PAYLOAD_MOCK.pageContainers); + + expect(saveSpy).toHaveBeenCalledWith(payload); + + expect(getSpy).toHaveBeenCalledWith(store.params()); + + expect(store.status()).toBe(UVE_STATUS.LOADED); + expect(store.pageAPIResponse()).toEqual(MOCK_RESPONSE_VTL); + }); + }); + }); + }); + + describe('withComputed', () => { + describe('$pageData', () => { + it('should return the expected data', () => { + expect(store.$pageData()).toEqual({ + containers: mapContainerStructureToArrayOfContainers( + MOCK_RESPONSE_HEADLESS.containers + ), + id: MOCK_RESPONSE_HEADLESS.page.identifier, + personalization: getPersonalization(MOCK_RESPONSE_HEADLESS.viewAs.persona), + languageId: MOCK_RESPONSE_HEADLESS.viewAs.language.id, + personaTag: MOCK_RESPONSE_HEADLESS.viewAs.persona.keyTag + }); + }); + }); + + describe('$reloadEditorContent', () => { + it('should return the expected data for Headless', () => { + expect(store.$reloadEditorContent()).toEqual({ + code: MOCK_RESPONSE_HEADLESS.page.rendered, + isTraditionalPage: false, + isEditState: true, + isEnterprise: true + }); + }); + it('should return the expected data for VTL', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock(MOCK_RESPONSE_VTL) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(store.$reloadEditorContent()).toEqual({ + code: MOCK_RESPONSE_VTL.page.rendered, + isTraditionalPage: true, + isEditState: true, + isEnterprise: true + }); + }); + }); + + describe('$editorIsInDraggingState', () => { + it("should return the editor's dragging state", () => { + expect(store.$editorIsInDraggingState()).toBe(false); + }); + + it("should return the editor's dragging state after a change", () => { + // This will trigger a change in the dragging state + store.setEditorDragItem(EMA_DRAG_ITEM_CONTENTLET_MOCK); + + expect(store.$editorIsInDraggingState()).toBe(true); + }); + }); + + describe('$editorProps', () => { + it('should return the expected data on init', () => { + expect(store.$editorProps()).toEqual({ + showDialogs: true, + showEditorContent: true, + iframe: { + opacity: '1', + pointerEvents: 'auto', + src: 'http://localhost:3000/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT&clientHost=http%3A%2F%2Flocalhost%3A3000', + wrapper: null + }, + progressBar: false, + contentletTools: null, + dropzone: null, + palette: { + variantId: DEFAULT_VARIANT_ID, + languageId: MOCK_RESPONSE_HEADLESS.viewAs.language.id, + containers: MOCK_RESPONSE_HEADLESS.containers + }, + seoResults: null + }); + }); + + describe('showDialogs', () => { + it('should have the value of false when we cannot edit the page', () => { + patchState(store, { canEditPage: false }); + + expect(store.$editorProps().showDialogs).toBe(false); + }); + + it('should have the value of false when we are not on edit state', () => { + patchState(store, { isEditState: false }); + + expect(store.$editorProps().showDialogs).toBe(false); + }); + }); + + describe('showEditorContent', () => { + it('should have showEditorContent as true when there is no socialMedia', () => { + expect(store.$editorProps().showEditorContent).toBe(true); + }); + }); + + describe('iframe', () => { + it('should have an opacity of 0.5 when loading', () => { + patchState(store, { status: UVE_STATUS.LOADING }); + + expect(store.$editorProps().iframe.opacity).toBe('0.5'); + }); + + it('should have pointerEvents as none when dragging', () => { + patchState(store, { state: EDITOR_STATE.DRAGGING }); + + expect(store.$editorProps().iframe.pointerEvents).toBe('none'); + }); + + it('should have pointerEvents as none when scroll-drag', () => { + patchState(store, { state: EDITOR_STATE.SCROLL_DRAG }); + + expect(store.$editorProps().iframe.pointerEvents).toBe('none'); + }); + + it('should have src as empty when the page is traditional', () => { + jest.spyOn(dotPageApiService, 'get').mockImplementation( + buildPageAPIResponseFromMock(MOCK_RESPONSE_VTL) + ); + + store.load(VTL_BASE_QUERY_PARAMS); + + expect(store.$editorProps().iframe.src).toBe(''); + }); + + it('should have a wrapper when a device is present', () => { + const device = mockDotDevices[0] as DotDeviceWithIcon; + + patchState(store, { device }); + + expect(store.$editorProps().iframe.wrapper).toEqual({ + width: device.cssWidth + BASE_IFRAME_MEASURE_UNIT, + height: device.cssHeight + BASE_IFRAME_MEASURE_UNIT + }); + }); + }); + + describe('progressBar', () => { + it('should have progressBar as true when the status is loading', () => { + patchState(store, { status: UVE_STATUS.LOADING }); + + expect(store.$editorProps().progressBar).toBe(true); + }); + }); + + describe('contentletTools', () => { + it('should have contentletTools when contentletArea are present, can edit the page, is in edit state and not scrolling', () => { + patchState(store, { + isEditState: true, + canEditPage: true, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.IDLE + }); + + expect(store.$editorProps().contentletTools).toEqual({ + isEnterprise: true, + contentletArea: MOCK_CONTENTLET_AREA, + hide: false + }); + }); + + it('should have hide as true when dragging', () => { + patchState(store, { + isEditState: true, + canEditPage: true, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.DRAGGING + }); + + expect(store.$editorProps().contentletTools).toEqual({ + isEnterprise: true, + contentletArea: MOCK_CONTENTLET_AREA, + hide: true + }); + }); + + it('should be null when scroll drag', () => { + patchState(store, { + isEditState: true, + canEditPage: true, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.SCROLL_DRAG + }); + + expect(store.$editorProps().contentletTools).toBe(null); + }); + + it("should not have contentletTools when the page can't be edited", () => { + patchState(store, { + isEditState: true, + canEditPage: false, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.IDLE + }); + + expect(store.$editorProps().contentletTools).toBe(null); + }); + + it('should not have contentletTools when the contentletArea is not present', () => { + patchState(store, { + isEditState: true, + canEditPage: true, + state: EDITOR_STATE.IDLE + }); + + expect(store.$editorProps().contentletTools).toBe(null); + }); + + it('should not have contentletTools when the we are not in edit state', () => { + patchState(store, { + isEditState: false, + canEditPage: true, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.IDLE + }); + + expect(store.$editorProps().contentletTools).toBe(null); + }); + + it('should not have contentletTools when the we are scrolling', () => { + patchState(store, { + isEditState: true, + canEditPage: true, + contentletArea: MOCK_CONTENTLET_AREA, + state: EDITOR_STATE.SCROLLING + }); + + expect(store.$editorProps().contentletTools).toBe(null); + }); + }); + describe('dropzone', () => { + const bounds = getBoundsMock(ACTION_MOCK); + + it('should have dropzone when the state is dragging and the page can be edited', () => { + patchState(store, { + state: EDITOR_STATE.DRAGGING, + canEditPage: true, + dragItem: EMA_DRAG_ITEM_CONTENTLET_MOCK, + bounds + }); + + expect(store.$editorProps().dropzone).toEqual({ + dragItem: EMA_DRAG_ITEM_CONTENTLET_MOCK, + bounds + }); + }); + + it("should not have dropzone when the page can't be edited", () => { + patchState(store, { + state: EDITOR_STATE.DRAGGING, + canEditPage: false, + dragItem: EMA_DRAG_ITEM_CONTENTLET_MOCK, + bounds + }); + + expect(store.$editorProps().dropzone).toBe(null); + }); + }); + + describe('palette', () => { + it('should be null if is not enterprise', () => { + patchState(store, { isEnterprise: false }); + + expect(store.$editorProps().palette).toBe(null); + }); + + it('should be null if canEditPage is false', () => { + patchState(store, { canEditPage: false }); + + expect(store.$editorProps().palette).toBe(null); + }); + + it('should be null if isEditState is false', () => { + patchState(store, { isEditState: false }); + + expect(store.$editorProps().palette).toBe(null); + }); + }); + + describe('seoResults', () => { + it('should have the expected data when ogTags and socialMedia is present', () => { + patchState(store, { + ogTags: seoOGTagsMock, + socialMedia: 'facebook' + }); + + expect(store.$editorProps().seoResults).toEqual({ + ogTags: seoOGTagsMock, + socialMedia: 'facebook' + }); + }); + + it('should be null when ogTags is not present', () => { + patchState(store, { + socialMedia: 'facebook' + }); + + expect(store.$editorProps().seoResults).toBe(null); + }); + + it('should be null when socialMedia is not present', () => { + patchState(store, { + ogTags: seoOGTagsMock + }); + + expect(store.$editorProps().seoResults).toBe(null); + }); + }); + }); + }); + + describe('withMethods', () => { + describe('updateEditorScrollState', () => { + it("should update the editor's scroll state when there is no drag item", () => { + store.updateEditorScrollState(); + + expect(store.state()).toEqual(EDITOR_STATE.SCROLLING); + }); + + it("should update the editor's scroll state when there is drag item", () => { + store.setEditorDragItem(EMA_DRAG_ITEM_CONTENTLET_MOCK); + + store.updateEditorScrollState(); + + expect(store.state()).toEqual(EDITOR_STATE.SCROLL_DRAG); + }); + + it("should not update the editor's scroll state when the state is OUT_OF_BOUNDS", () => { + store.setEditorState(EDITOR_STATE.OUT_OF_BOUNDS); + + store.updateEditorScrollState(); + + expect(store.state()).toEqual(EDITOR_STATE.OUT_OF_BOUNDS); + }); + }); + + describe('updateEditorOnScrollEnd', () => { + it("should update the editor's drag state when there is no drag item", () => { + store.updateEditorOnScrollEnd(); + + expect(store.state()).toEqual(EDITOR_STATE.IDLE); + }); + + it("should update the editor's drag state when there is drag item", () => { + store.setEditorDragItem(EMA_DRAG_ITEM_CONTENTLET_MOCK); + + store.updateEditorOnScrollEnd(); + + expect(store.state()).toEqual(EDITOR_STATE.DRAGGING); + }); + + it("should not update the editor's drag state when the state is OUT_OF_BOUNDS", () => { + store.setEditorState(EDITOR_STATE.OUT_OF_BOUNDS); + + store.updateEditorOnScrollEnd(); + + expect(store.state()).toEqual(EDITOR_STATE.OUT_OF_BOUNDS); + }); + }); + + describe('updateEditorScrollDragState', () => { + it('should update the store correctly', () => { + store.updateEditorScrollDragState(); + + expect(store.state()).toEqual(EDITOR_STATE.SCROLL_DRAG); + expect(store.bounds()).toEqual([]); + }); + }); + + describe('setEditorState', () => { + it('should update the state correctly', () => { + store.setEditorState(EDITOR_STATE.SCROLLING); + + expect(store.state()).toEqual(EDITOR_STATE.SCROLLING); + }); + }); + + describe('setEditorDragItem', () => { + it('should update the store correctly', () => { + store.setEditorDragItem(EMA_DRAG_ITEM_CONTENTLET_MOCK); + + expect(store.dragItem()).toEqual(EMA_DRAG_ITEM_CONTENTLET_MOCK); + expect(store.state()).toEqual(EDITOR_STATE.DRAGGING); + }); + }); + + describe('setEditorContentletArea', () => { + it("should update the store's contentlet area", () => { + store.setEditorContentletArea(MOCK_CONTENTLET_AREA); + + expect(store.contentletArea()).toEqual(MOCK_CONTENTLET_AREA); + expect(store.state()).toEqual(EDITOR_STATE.IDLE); + }); + + it('should not update contentletArea if it is the same', () => { + store.setEditorContentletArea(MOCK_CONTENTLET_AREA); + + // We can have contentletArea and state at the same time we are inline editing + store.setEditorState(EDITOR_STATE.INLINE_EDITING); + + store.setEditorContentletArea(MOCK_CONTENTLET_AREA); + + expect(store.contentletArea()).toEqual(MOCK_CONTENTLET_AREA); + // State should not change + expect(store.state()).toEqual(EDITOR_STATE.INLINE_EDITING); + }); + }); + + describe('setEditorBounds', () => { + const bounds = getBoundsMock(ACTION_MOCK); + + it('should update the store correcly', () => { + store.setEditorBounds(bounds); + + expect(store.bounds()).toEqual(bounds); + }); + }); + + describe('resetEditorProperties', () => { + it('should reset the editor props corretcly', () => { + store.setEditorDragItem(EMA_DRAG_ITEM_CONTENTLET_MOCK); + store.setEditorState(EDITOR_STATE.SCROLLING); + store.setEditorContentletArea(MOCK_CONTENTLET_AREA); + store.setEditorBounds(getBoundsMock(ACTION_MOCK)); + + store.resetEditorProperties(); + + expect(store.dragItem()).toBe(null); + expect(store.state()).toEqual(EDITOR_STATE.IDLE); + expect(store.contentletArea()).toBe(null); + expect(store.bounds()).toEqual([]); + }); + }); + describe('getPageSavePayload', () => { + it("should return the page's save payload", () => { + expect(store.getPageSavePayload(ACTION_PAYLOAD_MOCK)).toEqual({ + container: { + acceptTypes: 'test', + contentletsId: [], + identifier: 'container-identifier-123', + maxContentlets: 1, + uuid: 'uuid-123', + variantId: '123' + }, + contentlet: { + contentType: 'test', + identifier: 'contentlet-identifier-123', + inode: 'contentlet-inode-123', + onNumberOfPages: 1, + title: 'Hello World' + }, + language_id: '1', + pageContainers: [ + { + contentletsId: ['123', '456'], + identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', + uuid: '123' + }, + { + contentletsId: ['123'], + identifier: '5363c6c6-5ba0-4946-b7af-cf875188ac2e', + uuid: '456' + }, + { + contentletsId: ['123', '456'], + identifier: '/container/path', + uuid: '123' + }, + { + contentletsId: ['123'], + identifier: '/container/path', + uuid: '456' + } + ], + pageId: '123', + personaTag: 'dot:persona', + position: 'after' + }); + }); + }); + + describe('getCurrentTreeNode', () => { + it('should return the current TreeNode', () => { + const { container, contentlet } = ACTION_PAYLOAD_MOCK; + + expect(store.getCurrentTreeNode(container, contentlet)).toEqual({ + containerId: 'container-identifier-123', + contentId: 'contentlet-identifier-123', + pageId: '123', + personalization: 'dot:persona:dot:persona', + relationType: 'uuid-123', + treeOrder: '-1', + variantId: '123' + }); + }); + }); + + describe('setOgTags', () => { + it('should set the ogTags correctly', () => { + const ogTags = { + title: 'Title', + description: 'Description', + image: 'Image', + type: 'Type', + url: 'URL' + }; + + store.setOgTags(ogTags); + + expect(store.ogTags()).toEqual(ogTags); + }); + }); + }); + }); +}); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.ts new file mode 100644 index 000000000000..d237342fb652 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/dot-uve.store.ts @@ -0,0 +1,121 @@ +import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals'; + +import { computed } from '@angular/core'; + +import { withEditor } from './features/editor/withEditor'; +import { withLayout } from './features/layout/withLayout'; +import { withLoad } from './features/load/withLoad'; +import { ShellProps, UVEState } from './models'; + +import { UVE_STATUS } from '../shared/enums'; +import { getErrorPayload, getRequestHostName, sanitizeURL } from '../utils'; + +const initialState: UVEState = { + isEnterprise: false, + languages: [], + pageAPIResponse: null, + currentUser: null, + experiment: null, + errorCode: null, + params: null, + status: UVE_STATUS.LOADING, + isTraditionalPage: true, + canEditPage: false, + pageIsLocked: true +}; + +export const UVEStore = signalStore( + withState(initialState), + withComputed(({ pageAPIResponse, isTraditionalPage, params, languages, errorCode: error }) => { + return { + $shellProps: computed(() => { + const response = pageAPIResponse(); + + const currentUrl = '/' + sanitizeURL(response?.page.pageURI); + + const requestHostName = getRequestHostName(isTraditionalPage(), params()); + + const page = response?.page; + const templateDrawed = response?.template.drawed; + + const isLayoutDisabled = !page?.canEdit || !templateDrawed; + + const languageId = response?.viewAs.language.id; + const translatedLanguages = languages(); + const errorCode = error(); + + const errorPayload = getErrorPayload(errorCode); + + return { + canRead: page?.canRead, + error: errorPayload, + translateProps: { + page, + languageId, + languages: translatedLanguages + }, + seoParams: { + siteId: response?.site.identifier, + languageId: response?.viewAs.language.id, + currentUrl, + requestHostName + }, + items: [ + { + icon: 'pi-file', + label: 'editema.editor.navbar.content', + href: 'content', + id: 'content' + }, + { + icon: 'pi-table', + label: 'editema.editor.navbar.layout', + href: 'layout', + id: 'layout', + isDisabled: isLayoutDisabled, + tooltip: templateDrawed + ? null + : 'editema.editor.navbar.layout.tooltip.cannot.edit.advanced.template' + }, + { + icon: 'pi-sliders-h', + label: 'editema.editor.navbar.rules', + id: 'rules', + href: `rules/${page?.identifier}`, + isDisabled: !page?.canEdit + }, + { + iconURL: 'experiments', + label: 'editema.editor.navbar.experiments', + href: `experiments/${page?.identifier}`, + id: 'experiments', + isDisabled: !page?.canEdit + }, + { + icon: 'pi-th-large', + label: 'editema.editor.navbar.page-tools', + id: 'page-tools' + }, + { + icon: 'pi-ellipsis-v', + label: 'editema.editor.navbar.properties', + id: 'properties' + } + ] + }; + }) + }; + }), + withMethods((store) => { + return { + setUveStatus(status: UVE_STATUS) { + patchState(store, { + status + }); + } + }; + }), + withLoad(), + withLayout(), + withEditor() +); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts new file mode 100644 index 000000000000..b4c4813f6f7c --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/models.ts @@ -0,0 +1,108 @@ +import { + DotCMSContentlet, + DotExperiment, + DotLanguage, + DotPageContainerStructure, + DotPersona, + SeoMetaTags +} from '@dotcms/dotcms-models'; + +import { + Container, + ContentletArea, + EmaDragItem +} from '../../../edit-ema-editor/components/ema-page-dropzone/types'; +import { EDITOR_STATE } from '../../../shared/enums'; +import { DotDeviceWithIcon } from '../../../shared/models'; + +export interface EditorState { + bounds: Container[]; + state: EDITOR_STATE; + contentletArea?: ContentletArea; + dragItem?: EmaDragItem; + ogTags?: SeoMetaTags; +} + +export interface EditorToolbarState { + device?: DotDeviceWithIcon; + socialMedia?: string; + isEditState: boolean; +} + +export interface PageDataContainer { + identifier: string; + uuid: string; + contentletsId: string[]; +} + +export interface PageData { + containers: PageDataContainer[]; + personalization: string; + id: string; + languageId: number; + personaTag: string; +} + +export interface ReloadEditorContent { + code: string; + isTraditionalPage: boolean; + isEditState: boolean; + isEnterprise: boolean; +} + +export interface EditorProps { + seoResults?: { + ogTags: SeoMetaTags; + socialMedia: string; + }; + iframe: { + wrapper?: { + width: string; + height: string; + }; + src: string; + pointerEvents: string; + opacity: string; + }; + + contentletTools?: { + contentletArea: ContentletArea; + hide: boolean; + isEnterprise: boolean; + }; + dropzone?: { + bounds: Container[]; + dragItem: EmaDragItem; + }; + palette?: { + languageId: number; + containers: DotPageContainerStructure; + variantId: string; + }; + showDialogs: boolean; + progressBar: boolean; + showEditorContent: boolean; +} + +export interface ToolbarProps { + urlContentMap?: DotCMSContentlet; + bookmarksUrl: string; + copyUrl: string; + apiUrl: string; + showInfoDisplay: boolean; + currentLanguage: DotLanguage; + runningExperiment?: DotExperiment; + workflowActionsInode?: string; + personaSelector: { + pageId: string; + value: DotPersona; + }; + unlockButton?: { + inode: string; + loading: boolean; + }; + deviceSelector: { + apiLink: string; + hideSocialMedia: boolean; + }; +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/save/withSave.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/save/withSave.ts new file mode 100644 index 000000000000..511537a08889 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/save/withSave.ts @@ -0,0 +1,79 @@ +import { tapResponse } from '@ngrx/component-store'; +import { patchState, signalStoreFeature, type, withMethods } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { EMPTY, pipe } from 'rxjs'; + +import { inject } from '@angular/core'; + +import { catchError, switchMap, tap } from 'rxjs/operators'; + +import { DotPageApiResponse, DotPageApiService } from '../../../../services/dot-page-api.service'; +import { UVE_STATUS } from '../../../../shared/enums'; +import { PageContainer } from '../../../../shared/models'; +import { UVEState } from '../../../models'; + +/** + * Add methods to save the page + * + * @export + * @return {*} + */ +export function withSave() { + return signalStoreFeature( + { + state: type() + }, + withMethods((store) => { + const dotPageApiService = inject(DotPageApiService); + + return { + savePage: rxMethod( + pipe( + tap(() => { + patchState(store, { + status: UVE_STATUS.LOADING + }); + }), + switchMap((pageContainers) => { + const payload = { + pageContainers, + pageId: store.pageAPIResponse().page.identifier, + params: store.params() + }; + + return dotPageApiService.save(payload).pipe( + switchMap(() => + dotPageApiService.get(payload.params).pipe( + tapResponse( + (pageAPIResponse: DotPageApiResponse) => { + patchState(store, { + status: UVE_STATUS.LOADED, + pageAPIResponse: pageAPIResponse + }); + }, + (e) => { + console.error(e); + + patchState(store, { + status: UVE_STATUS.ERROR + }); + } + ) + ) + ), + catchError((e) => { + console.error(e); + patchState(store, { + status: UVE_STATUS.ERROR + }); + + return EMPTY; + }) + ); + }) + ) + ) + }; + }) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withEditorToolbar.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withEditorToolbar.ts new file mode 100644 index 000000000000..d48eb322da18 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withEditorToolbar.ts @@ -0,0 +1,201 @@ +import { + signalStoreFeature, + type, + withState, + withMethods, + patchState, + withComputed +} from '@ngrx/signals'; + +import { computed } from '@angular/core'; + +import { DotDevice, DotExperimentStatus } from '@dotcms/dotcms-models'; + +import { DEFAULT_PERSONA } from '../../../../shared/consts'; +import { UVE_STATUS } from '../../../../shared/enums'; +import { InfoOptions } from '../../../../shared/models'; +import { + createFavoritePagesURL, + createPageApiUrlWithQueryParams, + createPureURL, + getIsDefaultVariant, + sanitizeURL +} from '../../../../utils'; +import { UVEState } from '../../../models'; +import { EditorToolbarState, ToolbarProps } from '../models'; + +const initialState: EditorToolbarState = { + device: null, + socialMedia: null, + isEditState: true +}; + +/** + * Add computed properties and methods to the store to handle the Editor Toolbar UI + * + * @export + * @return {*} + */ +export function withEditorToolbar() { + return signalStoreFeature( + { + state: type() + }, + withState(initialState), + withComputed((store) => ({ + $toolbarProps: computed(() => { + const params = store.params(); + const url = sanitizeURL(params?.url); + + const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params); + const pageAPIResponse = store.pageAPIResponse(); + const experiment = store.experiment?.(); + + const pageAPI = `/api/v1/page/${ + store.isTraditionalPage() ? 'render' : 'json' + }/${pageAPIQueryParams}`; + + const isExperimentRunning = experiment?.status === DotExperimentStatus.RUNNING; + const shouldShowUnlock = + pageAPIResponse?.page.locked && pageAPIResponse?.page.canLock; + + const unlockButton = { + inode: pageAPIResponse?.page.inode, + loading: store.status() === UVE_STATUS.LOADING + }; + + const shouldShowInfoDisplay = + !getIsDefaultVariant(pageAPIResponse?.viewAs.variantId) || + !store.canEditPage() || + pageAPIResponse?.page.locked || + !!store.device() || + !!store.socialMedia(); + + const bookmarksUrl = createFavoritePagesURL({ + languageId: Number(params?.language_id), + pageURI: url, + siteId: pageAPIResponse?.site.identifier + }); + + return { + bookmarksUrl, + copyUrl: createPureURL(params), + apiUrl: `${window.location.origin}${pageAPI}`, + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: store.isEditState() + ? (pageAPIResponse?.urlContentMap ?? null) + : null, + runningExperiment: isExperimentRunning ? experiment : null, + workflowActionsInode: store.canEditPage() ? pageAPIResponse?.page.inode : null, + unlockButton: shouldShowUnlock ? unlockButton : null, + showInfoDisplay: shouldShowInfoDisplay, + deviceSelector: { + apiLink: `${params?.clientHost ?? window.location.origin}${pageAPI}`, + hideSocialMedia: !store.isTraditionalPage() + }, + personaSelector: { + pageId: pageAPIResponse?.page.identifier, + value: pageAPIResponse?.viewAs.persona ?? DEFAULT_PERSONA + } + }; + }), + $infoDisplayOptions: computed(() => { + const pageAPIResponse = store.pageAPIResponse(); + const canEditPage = store.canEditPage(); + const device = store.device(); + const socialMedia = store.socialMedia(); + + if (device) { + return { + icon: device.icon, + info: { + message: `${device.name} ${device.cssWidth} x ${device.cssHeight}`, + args: [] + }, + id: 'device', + actionIcon: 'pi pi-times' + }; + } else if (socialMedia) { + return { + icon: `pi pi-${socialMedia.toLowerCase()}`, + id: 'socialMedia', + info: { + message: `Viewing ${socialMedia} social media preview`, + args: [] + }, + actionIcon: 'pi pi-times' + }; + } else if (!getIsDefaultVariant(pageAPIResponse?.viewAs.variantId)) { + const variantId = pageAPIResponse.viewAs.variantId; + + const currentExperiment = store.experiment?.(); + + const name = + currentExperiment?.trafficProportion.variants.find( + (variant) => variant.id === variantId + )?.name ?? 'Unknown Variant'; + + return { + info: { + message: canEditPage + ? 'editpage.editing.variant' + : 'editpage.viewing.variant', + args: [name] + }, + icon: 'pi pi-file-edit', + id: 'variant', + actionIcon: 'pi pi-arrow-left' + }; + } + + if (pageAPIResponse?.page.locked) { + let message = 'editpage.locked-by'; + + if (!pageAPIResponse.page.canLock) { + message = 'editpage.locked-contact-with'; + } + + return { + icon: 'pi pi-lock', + id: 'locked', + info: { + message, + args: [pageAPIResponse.page.lockedByName] + } + }; + } + + if (!canEditPage) { + return { + icon: 'pi pi-exclamation-circle warning', + id: 'no-permission', + info: { message: 'editema.dont.have.edit.permission', args: [] } + }; + } + + return null; + }) + })), + withMethods((store) => { + return { + setDevice: (device: DotDevice) => { + patchState(store, { + device, + socialMedia: null, + isEditState: false + }); + }, + setSocialMedia: (socialMedia: string) => { + patchState(store, { + socialMedia, + device: null, + isEditState: false + }); + }, + clearDeviceAndSocialMedia: () => { + patchState(store, initialState); + } + }; + }) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts new file mode 100644 index 000000000000..430bd1d08d71 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.ts @@ -0,0 +1,303 @@ +import { + patchState, + signalStoreFeature, + type, + withComputed, + withMethods, + withState +} from '@ngrx/signals'; + +import { computed } from '@angular/core'; + +import { DotTreeNode, SeoMetaTags } from '@dotcms/dotcms-models'; + +import { + EditorProps, + EditorState, + PageData, + PageDataContainer, + ReloadEditorContent +} from './models'; +import { withSave } from './save/withSave'; +import { withEditorToolbar } from './toolbar/withEditorToolbar'; + +import { + Container, + ContentletArea, + EmaDragItem +} from '../../../edit-ema-editor/components/ema-page-dropzone/types'; +import { BASE_IFRAME_MEASURE_UNIT } from '../../../shared/consts'; +import { EDITOR_STATE, UVE_STATUS } from '../../../shared/enums'; +import { + ActionPayload, + ContainerPayload, + ContentletPayload, + PositionPayload +} from '../../../shared/models'; +import { + sanitizeURL, + createPageApiUrlWithQueryParams, + mapContainerStructureToArrayOfContainers, + getPersonalization, + areContainersEquals, + getEditorStates +} from '../../../utils'; +import { UVEState } from '../../models'; +const initialState: EditorState = { + bounds: [], + state: EDITOR_STATE.IDLE, + contentletArea: null, + dragItem: null, + ogTags: null +}; + +/** + * Add computed and methods to handle the Editor UI + * + * @export + * @return {*} + */ +export function withEditor() { + return signalStoreFeature( + { + state: type() + }, + withState(initialState), + withEditorToolbar(), + withSave(), + withComputed((store) => { + return { + $pageData: computed(() => { + const pageAPIResponse = store.pageAPIResponse(); + + const containers: PageDataContainer[] = + mapContainerStructureToArrayOfContainers(pageAPIResponse.containers); + const personalization = getPersonalization(pageAPIResponse.viewAs?.persona); + + return { + containers, + personalization, + id: pageAPIResponse.page.identifier, + languageId: pageAPIResponse.viewAs.language.id, + personaTag: pageAPIResponse.viewAs.persona?.keyTag + }; + }), + $reloadEditorContent: computed(() => { + return { + code: store.pageAPIResponse()?.page?.rendered, + isTraditionalPage: store.isTraditionalPage(), + isEditState: store.isEditState(), + isEnterprise: store.isEnterprise() + }; + }), + $editorIsInDraggingState: computed( + () => store.state() === EDITOR_STATE.DRAGGING + ), + $editorProps: computed(() => { + const pageAPIResponse = store.pageAPIResponse(); + const socialMedia = store.socialMedia(); + const ogTags = store.ogTags(); + const device = store.device(); + const canEditPage = store.canEditPage(); + const isEnterprise = store.isEnterprise(); + const state = store.state(); + const params = store.params(); + const isTraditionalPage = store.isTraditionalPage(); + const contentletArea = store.contentletArea(); + const bounds = store.bounds(); + const dragItem = store.dragItem(); + const isEditState = store.isEditState(); + const isLoading = store.status() === UVE_STATUS.LOADING; + + const { dragIsActive, isScrolling, isDragging } = getEditorStates(state); + + const url = sanitizeURL(params?.url); + + const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params); + + const showDialogs = canEditPage && isEditState; + + const showContentletTools = + !!contentletArea && canEditPage && isEditState && !isScrolling; + + const showDropzone = canEditPage && isDragging; + + const showPalette = isEnterprise && canEditPage && isEditState; + + const shouldShowSeoResults = socialMedia && ogTags; + + return { + showDialogs: showDialogs, + showEditorContent: !socialMedia, + iframe: { + opacity: isLoading ? '0.5' : '1', + pointerEvents: dragIsActive ? 'none' : 'auto', + src: !isTraditionalPage + ? `${params.clientHost}/${pageAPIQueryParams}` + : '', + wrapper: device + ? { + width: `${device.cssWidth}${BASE_IFRAME_MEASURE_UNIT}`, + height: `${device.cssHeight}${BASE_IFRAME_MEASURE_UNIT}` + } + : null + }, + progressBar: isLoading, + contentletTools: showContentletTools + ? { + isEnterprise, + contentletArea, + hide: dragIsActive + } + : null, + dropzone: showDropzone + ? { + bounds, + dragItem + } + : null, + palette: showPalette + ? { + variantId: params?.variantName, + containers: pageAPIResponse?.containers, + languageId: pageAPIResponse?.viewAs.language.id + } + : null, + + seoResults: shouldShowSeoResults + ? { + ogTags, + socialMedia + } + : null + }; + }) + }; + }), + withMethods((store) => { + return { + updateEditorScrollState() { + // We dont want to change the state if the editor is out of bounds + // The scroll event is triggered after the user leaves the window + // And that is changing the state in an unnatural way + + // The only way to get out of OUT_OF_BOUNDS is through the mouse over in the editor + if (store.state() === EDITOR_STATE.OUT_OF_BOUNDS) { + return; + } + + patchState(store, { + state: store.dragItem() ? EDITOR_STATE.SCROLL_DRAG : EDITOR_STATE.SCROLLING + }); + }, + updateEditorOnScrollEnd() { + // We dont want to change the state if the editor is out of bounds + // The scroll end event is triggered after the user leaves the window + // And that is changing the state in an unnatural way + + // The only way to get out of OUT_OF_BOUNDS is through the mouse over in the editor + if (store.state() === EDITOR_STATE.OUT_OF_BOUNDS) { + return; + } + + patchState(store, { + state: store.dragItem() ? EDITOR_STATE.DRAGGING : EDITOR_STATE.IDLE + }); + }, + updateEditorScrollDragState() { + patchState(store, { state: EDITOR_STATE.SCROLL_DRAG, bounds: [] }); + }, + setEditorState(state: EDITOR_STATE) { + patchState(store, { state: state }); + }, + setEditorDragItem(dragItem: EmaDragItem) { + patchState(store, { dragItem, state: EDITOR_STATE.DRAGGING }); + }, + setEditorContentletArea(contentletArea: ContentletArea) { + const currentContentletArea = store.contentletArea(); + + if ( + currentContentletArea?.x === contentletArea.x && + currentContentletArea?.y === contentletArea.y + ) { + // Prevent updating the state if the contentlet area is the same + // This is because in inline editing, when we select to not copy the content and edit global + // The contentlet area is updated on focus with the same values and IDLE + // Losing the INLINE_EDITING state and making the user to open the dialog for checking whether to copy the content or not + // Which is an awful UX + + return; + } + + patchState(store, { + contentletArea: contentletArea, + state: EDITOR_STATE.IDLE + }); + }, + setEditorBounds(bounds: Container[]) { + patchState(store, { bounds }); + }, + resetEditorProperties() { + patchState(store, { + dragItem: null, + contentletArea: null, + bounds: [], + state: EDITOR_STATE.IDLE + }); + }, + getPageSavePayload(positionPayload: PositionPayload): ActionPayload { + const { containers, languageId, id, personaTag } = store.$pageData(); + + const { contentletsId } = containers.find((container) => + areContainersEquals(container, positionPayload.container) + ) ?? { contentletsId: [] }; + + const container = positionPayload.container + ? { + ...positionPayload.container, + contentletsId + } + : null; + + return { + ...positionPayload, + language_id: languageId.toString(), + pageId: id, + pageContainers: containers, + personaTag, + container + }; + }, + getCurrentTreeNode( + container: ContainerPayload, + contentlet: ContentletPayload + ): DotTreeNode { + const { identifier: contentId } = contentlet; + const { + variantId, + uuid: relationType, + contentletsId, + identifier: containerId + } = container; + + const { personalization, id: pageId } = store.$pageData(); + + const treeOrder = contentletsId.findIndex((id) => id === contentId).toString(); + + return { + contentId, + containerId, + relationType, + variantId, + personalization, + treeOrder, + pageId + }; + }, + setOgTags(ogTags: SeoMetaTags) { + patchState(store, { ogTags }); + } + }; + }) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/models.ts new file mode 100644 index 000000000000..07138790e8a0 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/models.ts @@ -0,0 +1,11 @@ +import { DotContainerMap, DotLayout } from '@dotcms/dotcms-models'; + +export interface LayoutProps { + containersMap: DotContainerMap; + layout: DotLayout; + template: { + identifier: string; + themeId: string; + }; + pageId: string; +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/withLayout.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/withLayout.ts new file mode 100644 index 000000000000..730f90972478 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/layout/withLayout.ts @@ -0,0 +1,54 @@ +import { patchState, signalStoreFeature, type, withComputed, withMethods } from '@ngrx/signals'; + +import { computed } from '@angular/core'; + +import { DotLayout } from '@dotcms/dotcms-models'; + +import { LayoutProps } from './models'; + +import { mapContainerStructureToDotContainerMap } from '../../../utils'; +import { UVEState } from '../../models'; + +/** + * Add computed properties to the store to handle the Layout UI + * + * @export + * @return {*} + */ +export function withLayout() { + return signalStoreFeature( + { + state: type() + }, + withComputed(({ pageAPIResponse }) => ({ + $layoutProps: computed(() => { + const response = pageAPIResponse(); + + return { + containersMap: mapContainerStructureToDotContainerMap( + response?.containers ?? {} + ), + layout: response?.layout, + template: { + identifier: response?.template.identifier, + // The themeId should be here, in the old store we had a bad reference and we were saving all the templates with themeId undefined + themeId: response?.template.theme + }, + pageId: response?.page.identifier + }; + }) + })), + withMethods((store) => { + return { + updateLayout: (layout: DotLayout) => { + patchState(store, { + pageAPIResponse: { + ...store.pageAPIResponse(), + layout + } + }); + } + }; + }) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/load/withLoad.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/load/withLoad.ts new file mode 100644 index 000000000000..0d2503dbbdeb --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/load/withLoad.ts @@ -0,0 +1,206 @@ +import { tapResponse } from '@ngrx/component-store'; +import { patchState, signalStoreFeature, type, withMethods } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { pipe, forkJoin, of, EMPTY } from 'rxjs'; + +import { HttpErrorResponse } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; + +import { switchMap, shareReplay, catchError, tap, take, map } from 'rxjs/operators'; + +import { DotLanguagesService, DotLicenseService, DotExperimentsService } from '@dotcms/data-access'; +import { LoginService } from '@dotcms/dotcms-js'; + +import { DotPageApiService, DotPageApiParams } from '../../../services/dot-page-api.service'; +import { UVE_STATUS } from '../../../shared/enums'; +import { computeCanEditPage, computePageIsLocked, isForwardOrPage } from '../../../utils'; +import { UVEState } from '../../models'; + +/** + * Add load and reload method to the store + * + * @export + * @return {*} + */ +export function withLoad() { + return signalStoreFeature( + { + state: type() + }, + withMethods((store) => { + const dotPageApiService = inject(DotPageApiService); + const dotLanguagesService = inject(DotLanguagesService); + const dotLicenseService = inject(DotLicenseService); + const loginService = inject(LoginService); + const dotExperimentsService = inject(DotExperimentsService); + const router = inject(Router); + const activatedRoute = inject(ActivatedRoute); + + return { + load: rxMethod( + pipe( + tap(() => { + patchState(store, { status: UVE_STATUS.LOADING }); + }), + switchMap((params) => { + return forkJoin({ + pageAPIResponse: dotPageApiService.get(params).pipe( + switchMap((pageAPIResponse) => { + const { vanityUrl } = pageAPIResponse; + + // If there is no vanity and is not a redirect we just return the pageAPI response + if (isForwardOrPage(vanityUrl)) { + return of(pageAPIResponse); + } + + const queryParams = { + ...params, + url: vanityUrl.forwardTo.replace('/', '') + }; + + // We navigate to the new url and return undefined + router.navigate([], { + queryParams, + queryParamsHandling: 'merge' + }); + + return of(undefined); + }), + tap({ + next: (pageAPIResponse) => { + if (!pageAPIResponse) { + return; + } + + const { page, template } = pageAPIResponse; + + const isLayoutDisabled = + !page?.canEdit || !template?.drawed; + const pathIsLayout = + activatedRoute?.firstChild?.snapshot?.url?.[0] + .path === 'layout'; + + if (isLayoutDisabled && pathIsLayout) { + // If the user can't edit the page or the template is not drawed we navigate to the content page + router.navigate(['edit-page/content'], { + queryParamsHandling: 'merge' + }); + } + } + }) + ), + isEnterprise: dotLicenseService + .isEnterprise() + .pipe(take(1), shareReplay()), + currentUser: loginService.getCurrentUser() + }).pipe( + tap({ + error: ({ status: errorStatus }: HttpErrorResponse) => { + patchState(store, { + errorCode: errorStatus, + status: UVE_STATUS.ERROR + }); + } + }), + switchMap(({ pageAPIResponse, isEnterprise, currentUser }) => + forkJoin({ + experiment: dotExperimentsService + .getById(params.experimentId) + .pipe( + // If there is an error, we return undefined + // This is to avoid blocking the page if there is an error with the experiment + catchError(() => of(undefined)) + ), + languages: dotLanguagesService.getLanguagesUsedPage( + pageAPIResponse.page.identifier + ) + }).pipe( + tap({ + next: ({ experiment, languages }) => { + const canEditPage = computeCanEditPage( + pageAPIResponse?.page, + currentUser, + experiment + ); + + const pageIsLocked = computePageIsLocked( + pageAPIResponse?.page, + currentUser + ); + + patchState(store, { + pageAPIResponse, + isEnterprise, + currentUser, + experiment, + languages, + params, + canEditPage, + pageIsLocked, + status: UVE_STATUS.LOADED, + isTraditionalPage: !params.clientHost // If we don't send the clientHost we are using as VTL page + }); + } + }) + ) + ) + ); + }) + ) + ), + reload: rxMethod( + pipe( + tap(() => { + patchState(store, { status: UVE_STATUS.LOADING }); + }), + switchMap(() => { + return dotPageApiService.get(store.params()).pipe( + switchMap((pageAPIResponse) => + dotLanguagesService + .getLanguagesUsedPage(pageAPIResponse.page.identifier) + .pipe( + map((languages) => ({ + pageAPIResponse, + languages + })) + ) + ), + tapResponse({ + next: ({ pageAPIResponse, languages }) => { + const canEditPage = computeCanEditPage( + pageAPIResponse?.page, + store.currentUser(), + store.experiment() + ); + + const pageIsLocked = computePageIsLocked( + pageAPIResponse?.page, + store.currentUser() + ); + + patchState(store, { + pageAPIResponse, + languages, + canEditPage, + pageIsLocked, + status: UVE_STATUS.LOADED, + isTraditionalPage: !store.params().clientHost // If we don't send the clientHost we are using as VTL page + }); + }, + error: ({ status: errorStatus }: HttpErrorResponse) => { + patchState(store, { + errorCode: errorStatus, + status: UVE_STATUS.ERROR + }); + } + }), + catchError(() => EMPTY) + ); + }) + ) + ) + }; + }) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts new file mode 100644 index 000000000000..5e8a40779fe8 --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/models.ts @@ -0,0 +1,38 @@ +import { CurrentUser } from '@dotcms/dotcms-js'; +import { DotExperiment, DotLanguage, DotPageToolUrlParams } from '@dotcms/dotcms-models'; +import { InfoPage } from '@dotcms/ui'; + +import { DotPageApiParams, DotPageApiResponse } from '../services/dot-page-api.service'; +import { UVE_STATUS } from '../shared/enums'; +import { DotPage, NavigationBarItem } from '../shared/models'; + +export interface UVEState { + isEnterprise: boolean; + pageAPIResponse?: DotPageApiResponse; + languages: DotLanguage[]; + currentUser?: CurrentUser; + experiment?: DotExperiment; + errorCode?: number; + params?: DotPageApiParams; + status: UVE_STATUS; + isTraditionalPage: boolean; + canEditPage: boolean; + pageIsLocked: boolean; +} + +export interface ShellProps { + canRead: boolean; + error: { + code: number; + pageInfo: InfoPage; + }; + items: NavigationBarItem[]; + translateProps: TranslateProps; + seoParams: DotPageToolUrlParams; +} + +export interface TranslateProps { + page: DotPage; + languageId: number; + languages: DotLanguage[]; +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts index 28efd17fefac..e5c1ad76e329 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts @@ -1,8 +1,17 @@ -import { DEFAULT_VARIANT_ID } from '@dotcms/dotcms-models'; +import { CurrentUser } from '@dotcms/dotcms-js'; +import { + DEFAULT_VARIANT_ID, + DotContainerMap, + DotExperiment, + DotExperimentStatus, + DotPageContainerStructure, + VanityUrl +} from '@dotcms/dotcms-models'; import { DotPageApiParams } from '../services/dot-page-api.service'; -import { DEFAULT_PERSONA } from '../shared/consts'; -import { ActionPayload, ContainerPayload, PageContainer } from '../shared/models'; +import { COMMON_ERRORS, DEFAULT_PERSONA } from '../shared/consts'; +import { EDITOR_STATE } from '../shared/enums'; +import { ActionPayload, ContainerPayload, DotPage, PageContainer } from '../shared/models'; export const SDK_EDITOR_SCRIPT_SOURCE = '/html/js/editor-js/sdk-editor.js'; @@ -193,10 +202,10 @@ export function createPageApiUrlWithQueryParams( // Set default values const completedParams = { ...params, - language_id: params.language_id ?? '1', + language_id: params?.language_id ?? '1', 'com.dotmarketing.persona.id': - params['com.dotmarketing.persona.id'] ?? DEFAULT_PERSONA.identifier, - variantName: params.variantName ?? DEFAULT_VARIANT_ID + params?.['com.dotmarketing.persona.id'] ?? DEFAULT_PERSONA.identifier, + variantName: params?.variantName ?? DEFAULT_VARIANT_ID }; // Filter out undefined values and url @@ -222,3 +231,192 @@ export function createPageApiUrlWithQueryParams( export function getIsDefaultVariant(variant?: string): boolean { return !variant || variant === DEFAULT_VARIANT_ID; } + +/** + * Check if the param is a forward or page + * + * @export + * @param {VanityUrl} vanityUrl + * @return {*} + */ +export function isForwardOrPage(vanityUrl?: VanityUrl): boolean { + return !vanityUrl || (!vanityUrl.permanentRedirect && !vanityUrl.temporaryRedirect); +} + +/** + * Create the url to add a page to favorites + * + * @private + * @param {{ + * languageId: number; + * pageURI: string; + * deviceInode?: string; + * siteId?: string; + * }} params + * @return {*} {string} + * @memberof EditEmaStore + */ +export function createFavoritePagesURL(params: { + languageId: number; + pageURI: string; + siteId: string; +}): string { + const { languageId, pageURI, siteId } = params; + + return ( + `/${pageURI}?` + + (siteId ? `host_id=${siteId}` : '') + + `&language_id=${languageId}` + ).replace(/\/\//g, '/'); +} + +/** + * Create a pure URL from the params + * + * @export + * @param {DotPageApiParams} params + * @return {*} {string} + */ +export function createPureURL(params: DotPageApiParams): string { + // If we are going to delete properties from the params, we need to make a copy of it + const paramsCopy = { ...params }; + + const clientHost = paramsCopy?.clientHost ?? window.location.origin; + const url = paramsCopy?.url; + + // Clean the params that are not needed for the page + delete paramsCopy?.clientHost; + delete paramsCopy?.url; + delete paramsCopy?.mode; + + const searchParams = new URLSearchParams(paramsCopy as unknown as Record); + + return `${clientHost}/${url}?${searchParams.toString()}`; +} + +/** + * Check if the page can be edited + * + * @export + * @param {DotPage} page + * @param {CurrentUser} currentUser + * @param {DotExperiment} [experiment] + * @return {*} {boolean} + */ +export function computeCanEditPage( + page: DotPage, + currentUser: CurrentUser, + experiment?: DotExperiment +): boolean { + const pageCanBeEdited = page.canEdit; + + const isLocked = computePageIsLocked(page, currentUser); + + const editingBlockedByExperiment = [ + DotExperimentStatus.RUNNING, + DotExperimentStatus.SCHEDULED + ].includes(experiment?.status); + + return !!pageCanBeEdited && !isLocked && !editingBlockedByExperiment; +} + +/** + * Check if the page is locked + * + * @export + * @param {DotPage} page + * @param {CurrentUser} currentUser + * @return {*} + */ +export function computePageIsLocked(page: DotPage, currentUser: CurrentUser): boolean { + return !!page?.locked && page?.lockedBy !== currentUser?.userId; +} + +/** + * Map the containerStructure to a DotContainerMap + * + * @private + * @param {DotPageContainerStructure} containers + * @return {*} {DotContainerMap} + */ +export function mapContainerStructureToDotContainerMap( + containers: DotPageContainerStructure +): DotContainerMap { + return Object.keys(containers).reduce((acc, id) => { + acc[id] = containers[id].container; + + return acc; + }, {}); +} + +/** + * Map the containerStructure to an array + * + * @private + * @param {DotPageContainerStructure} containers + */ +export const mapContainerStructureToArrayOfContainers = (containers: DotPageContainerStructure) => { + return Object.keys(containers).reduce( + ( + acc: { + identifier: string; + uuid: string; + contentletsId: string[]; + }[], + container + ) => { + const contentlets = containers[container].contentlets; // Get all contentlets from the container + + const contentletsKeys = Object.keys(contentlets); // This is the keys of uuids of the container + + contentletsKeys.forEach((key) => { + acc.push({ + identifier: + containers[container].container.path ?? + containers[container].container.identifier, + uuid: key.replace('uuid-', ''), + contentletsId: contentlets[key].map((contentlet) => contentlet.identifier) + }); + }); + + return acc; + }, + [] + ); +}; + +/** + * Get the host name for the request + * + * @export + * @param {boolean} isTraditionalPage + * @param {DotPageApiParams} params + * @return {*} {string} + */ +export const getRequestHostName = (isTraditionalPage: boolean, params: DotPageApiParams) => { + return !isTraditionalPage ? params.clientHost : window.location.origin; +}; + +/** + * Get the error payload + * @param errorCode + * @returns {{code: number; pageInfo: CommonErrorsInfo | null}} + */ +export const getErrorPayload = (errorCode: number) => + errorCode + ? { + code: errorCode, + pageInfo: COMMON_ERRORS[errorCode?.toString()] ?? null + } + : null; + +/** + * Get the editor states + * @param state + * @returns {{isDragging: boolean; dragIsActive: boolean; isScrolling: boolean}} + */ +export const getEditorStates = (state: EDITOR_STATE) => ({ + isDragging: state === EDITOR_STATE.DRAGGING, + dragIsActive: state === EDITOR_STATE.DRAGGING || state === EDITOR_STATE.SCROLL_DRAG, + isScrolling: state === EDITOR_STATE.SCROLL_DRAG || state === EDITOR_STATE.SCROLLING +}); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts index d85d1615e6cc..781e11bf4e69 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts @@ -1,12 +1,33 @@ +import { CurrentUser } from '@dotcms/dotcms-js'; +import { DotExperiment, DotExperimentStatus } from '@dotcms/dotcms-models'; + import { deleteContentletFromContainer, insertContentletInContainer, sanitizeURL, getPersonalization, createPageApiUrlWithQueryParams, - SDK_EDITOR_SCRIPT_SOURCE + SDK_EDITOR_SCRIPT_SOURCE, + computePageIsLocked, + computeCanEditPage, + mapContainerStructureToArrayOfContainers, + mapContainerStructureToDotContainerMap, + areContainersEquals } from '.'; +import { dotPageContainerStructureMock } from '../shared/mocks'; +import { DotPage } from '../shared/models'; + +const generatePageAndUser = ({ locked, lockedBy, userId }) => ({ + page: { + locked, + lockedBy + } as DotPage, + currentUser: { + userId + } as CurrentUser +}); + describe('utils functions', () => { describe('SDK Editor Script Source', () => { it('should return the correct script source', () => { @@ -344,4 +365,214 @@ describe('utils functions', () => { ); }); }); + + describe('computePageIsLocked', () => { + it('should return false when the page is unlocked', () => { + const { page, currentUser } = generatePageAndUser({ + locked: false, + lockedBy: '123', + userId: '123' + }); + + const result = computePageIsLocked(page, currentUser); + + expect(result).toBe(false); + }); + + it('should return false when the page is locked and is the same user', () => { + const { page, currentUser } = generatePageAndUser({ + locked: true, + lockedBy: '123', + userId: '123' + }); + + const result = computePageIsLocked(page, currentUser); + + expect(result).toBe(false); + }); + + it('should return true when the page is locked and is not the same user', () => { + const { page, currentUser } = generatePageAndUser({ + locked: true, + lockedBy: '123', + userId: '456' + }); + + const result = computePageIsLocked(page, currentUser); + + expect(result).toBe(true); + }); + }); + + describe('computeCanEditPage', () => { + it('should return true when the page can be edited, is not locked and does not have experiment', () => { + const { page, currentUser } = generatePageAndUser({ + locked: false, + lockedBy: '123', + userId: '123' + }); + + const result = computeCanEditPage({ ...page, canEdit: true }, currentUser); + + expect(result).toBe(true); + }); + + it('should return true when the page can be edited and does have an experiment that is not running or scheduled', () => { + const { page, currentUser } = generatePageAndUser({ + locked: false, + lockedBy: '123', + userId: '123' + }); + + const experiment = { + status: DotExperimentStatus.DRAFT + } as DotExperiment; + + const result = computeCanEditPage({ ...page, canEdit: true }, currentUser, experiment); + + expect(result).toBe(true); + }); + + it('should return false when the page can be edited and does have an experiment that is running', () => { + const { page, currentUser } = generatePageAndUser({ + locked: false, + lockedBy: '123', + userId: '123' + }); + + const experiment = { + status: DotExperimentStatus.RUNNING + } as DotExperiment; + + const result = computeCanEditPage({ ...page, canEdit: true }, currentUser, experiment); + + expect(result).toBe(false); + }); + + it('should return false when the page can be edited and does have an experiment that is scheduled', () => { + const { page, currentUser } = generatePageAndUser({ + locked: false, + lockedBy: '123', + userId: '123' + }); + + const experiment = { + status: DotExperimentStatus.SCHEDULED + } as DotExperiment; + + const result = computeCanEditPage({ ...page, canEdit: true }, currentUser, experiment); + + expect(result).toBe(false); + }); + + it('should return false when the page can be edited but is locked', () => { + const { page, currentUser } = generatePageAndUser({ + locked: true, + lockedBy: '123', + userId: '456' + }); + + const result = computeCanEditPage({ ...page, canEdit: true }, currentUser); + + expect(result).toBe(false); + }); + + it('should return false when the page cannot be edited', () => { + const { page, currentUser } = generatePageAndUser({ + locked: true, + lockedBy: '123', + userId: '456' + }); + + const result = computeCanEditPage({ ...page, canEdit: false }, currentUser); + + expect(result).toBe(false); + }); + }); + + describe('mapContainerStructureToArrayOfContainers', () => { + it('should map container structure to array', () => { + const result = mapContainerStructureToArrayOfContainers(dotPageContainerStructureMock); + + expect(result).toEqual([ + { + identifier: '123', + uuid: '123', + contentletsId: ['123', '456'] + }, + { + identifier: '123', + uuid: '456', + contentletsId: ['123'] + } + ]); + }); + }); + + describe('mapContainerStructureToDotContainerMap', () => { + it('should map container structure to dotContainerMap', () => { + const result = mapContainerStructureToDotContainerMap(dotPageContainerStructureMock); + + expect(result).toEqual({ + '123': dotPageContainerStructureMock['123'].container + }); + }); + }); + + describe('areContainersEquals', () => { + it('should return true when the containers are equal', () => { + expect( + areContainersEquals( + { + identifier: '123', + uuid: '123', + contentletsId: ['123', '456'] + }, + { + identifier: '123', + uuid: '123', + acceptTypes: 'test', + variantId: 'Default', + maxContentlets: 1 + } + ) + ).toBe(true); + }); + it('should return false when the containers dont have the same identifier', () => { + expect( + areContainersEquals( + { + identifier: '123', + uuid: '123', + contentletsId: ['123', '456'] + }, + { + identifier: '456', + uuid: '123', + acceptTypes: 'test', + variantId: 'Default', + maxContentlets: 1 + } + ) + ).toBe(false); + }); + it('should return false when the containers dont have the same uuid', () => { + expect( + areContainersEquals( + { + identifier: '123', + uuid: '123', + contentletsId: ['123', '456'] + }, + { + identifier: '123', + uuid: '456', + acceptTypes: 'test', + variantId: 'Default', + maxContentlets: 1 + } + ) + ).toBe(false); + }); + }); }); diff --git a/core-web/libs/utils-testing/src/lib/dot-page-render.mock.ts b/core-web/libs/utils-testing/src/lib/dot-page-render.mock.ts index ddf1d8000d54..fca0a0e4d997 100644 --- a/core-web/libs/utils-testing/src/lib/dot-page-render.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-page-render.mock.ts @@ -73,12 +73,204 @@ export const mockDotContainers = (): DotPageContainerStructure => { return { '/default/': { container: processedContainers[0].container, - containerStructures: [{ contentTypeVar: 'Banner' }] + containerStructures: [{ contentTypeVar: 'Banner' }], + contentlets: { + '123': [ + { + baseType: '123', + contentType: '123', + folder: '123', + dateCreated: '123', + host: '123', + identifier: '123', + dateModifed: '123', + content: 'something', + languageId: 123, + live: false, + inode: '123', + locked: false, + modDate: '123', + url: '123', + owner: '123', + modUser: '123', + image: '123', + working: false, + titleImage: '123', + stInode: '123', + deleted: false, + sortOrder: 123, + hostName: '123', + archived: false, + hasTitleImage: false, + modUserName: '123', + title: '123', + __icon__: '123' + }, + { + baseType: '456', + inode: '456', + identifier: '456', + contentType: '456', + content: 'something', + dateCreated: '456', + archived: false, + live: false, + host: '456', + folder: '456', + dateModifed: '456', + modDate: '456', + languageId: 456, + working: false, + title: '456', + url: '456', + owner: '456', + sortOrder: 456, + __icon__: '456', + modUser: '456', + locked: false, + deleted: false, + titleImage: '456', + hasTitleImage: false, + stInode: '456', + image: '456', + modUserName: '456', + hostName: '456' + } + ], + '456': [ + { + contentType: '123', + content: 'something', + dateCreated: '123', + folder: '123', + identifier: '123', + dateModifed: '123', + host: '123', + baseType: '123', + live: false, + inode: '123', + working: false, + languageId: 123, + owner: '123', + locked: false, + modDate: '123', + archived: false, + modUser: '123', + title: '123', + titleImage: '123', + image: '123', + hasTitleImage: false, + __icon__: '123', + deleted: false, + sortOrder: 123, + url: '123', + hostName: '123', + modUserName: '123', + stInode: '123' + } + ] + } }, '/banner/': { container: processedContainers[1].container, - containerStructures: [{ contentTypeVar: 'Contact' }] + containerStructures: [{ contentTypeVar: 'Contact' }], + contentlets: { + '123': [ + { + dateModifed: '123', + baseType: '123', + content: 'something', + contentType: '123', + identifier: '123', + folder: '123', + dateCreated: '123', + live: false, + inode: '123', + languageId: 123, + locked: false, + host: '123', + working: false, + modUser: '123', + owner: '123', + archived: false, + url: '123', + modDate: '123', + stInode: '123', + deleted: false, + hasTitleImage: false, + image: '123', + __icon__: '123', + hostName: '123', + sortOrder: 123, + titleImage: '123', + modUserName: '123', + title: '123' + }, + { + contentType: '456', + baseType: '456', + content: 'something', + dateCreated: '456', + inode: '456', + identifier: '456', + folder: '456', + live: false, + title: '456', + dateModifed: '456', + languageId: 456, + modDate: '456', + host: '456', + locked: false, + archived: false, + working: false, + owner: '456', + deleted: false, + modUser: '456', + __icon__: '456', + url: '456', + hasTitleImage: false, + hostName: '456', + titleImage: '456', + modUserName: '456', + sortOrder: 456, + stInode: '456', + image: '456' + } + ], + '456': [ + { + dateCreated: '123', + dateModifed: '123', + content: 'something', + baseType: '123', + inode: '123', + contentType: '123', + host: '123', + folder: '123', + identifier: '123', + modDate: '123', + live: false, + modUser: '123', + owner: '123', + languageId: 123, + locked: false, + working: false, + archived: false, + image: '123', + stInode: '123', + titleImage: '123', + hostName: '123', + title: '123', + deleted: false, + hasTitleImage: false, + __icon__: '123', + url: '123', + modUserName: '123', + sortOrder: 123 + } + ] + } } }; }; diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 8c7471c794d0..77f1e965c2af 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1341,6 +1341,11 @@ edit.ema.page.dropzone.one.max.contentlet=Only 1 item can be st edit.ema.page.executing.workflow.action=Executing workflow action... edit.ema.page.error.executing.workflow.action=Unable to execute workflow action edit.ema.page.no.workflow.action=No Workflow Actions +edit.ema.empty.palette=No contentlets available +edit.ema.page.unlock=Page Unlock +edit.ema.page.is.being.unlocked=Page is being unlocked +edit.ema.page.unlock.success=Page is unlocked +edit.ema.page.unlock.error=Page could not be unlocked editpage.action.cancel=Cancel editpage.action.delete=Delete editpage.action.save=Save @@ -5730,7 +5735,7 @@ editpage.file.uploading=Uploading {0} editpage.file.publishing=Publishing {0} editpage.locked-by=Locked by {0} -editpage.locked-contact-with=Locked.. Contact with {0} user +editpage.locked-contact-with=Locked. Contact with {0} user binary-field.settings.allow.type=Allowed File Type binary-field.settings.system.options.allow.url.import=Allow users to create a file by importing from URL diff --git a/examples/nextjs/package-lock.json b/examples/nextjs/package-lock.json index 3a75ffc458ef..146c2688ba56 100644 --- a/examples/nextjs/package-lock.json +++ b/examples/nextjs/package-lock.json @@ -8,9 +8,9 @@ "name": "nextjs-dotcms-ema", "version": "0.1.0", "dependencies": { - "@dotcms/client": "0.0.1-alpha.26", - "@dotcms/experiments": "0.0.1-alpha.26", - "@dotcms/react": "0.0.1-alpha.26", + "@dotcms/client": "0.0.1-alpha.28", + "@dotcms/experiments": "0.0.1-alpha.28", + "@dotcms/react": "0.0.1-alpha.28", "next": "14.1.1", "react": "^18", "react-dom": "^18" @@ -48,29 +48,29 @@ } }, "node_modules/@dotcms/client": { - "version": "0.0.1-alpha.26", - "resolved": "https://registry.npmjs.org/@dotcms/client/-/client-0.0.1-alpha.26.tgz", - "integrity": "sha512-eDcd22tdMO/KTXuALjM9sFBym0Kx1+akBOFIZD2T6OtM/ynWjDYQn/30YylmskYjDaQHsEfEg9ZSCqHEGAKdqw==" + "version": "0.0.1-alpha.28", + "resolved": "https://registry.npmjs.org/@dotcms/client/-/client-0.0.1-alpha.28.tgz", + "integrity": "sha512-T+wgunuu7iwQXpQSNncWfXc85HjlGHfxA1tgLtRthteSataKmY5oI/Ki0xNvxHLYnA1mklcLTRgFKzBHkjLoJw==" }, "node_modules/@dotcms/experiments": { - "version": "0.0.1-alpha.26", - "resolved": "https://registry.npmjs.org/@dotcms/experiments/-/experiments-0.0.1-alpha.26.tgz", - "integrity": "sha512-MU+jR3Ld05iYraFGDXEO35xsUF90WOpUK/Zz9JExNvl5wu58EZAnG2jYSnw5/ixsf987iEfn0UP3uNlCzPZGrQ==", + "version": "0.0.1-alpha.28", + "resolved": "https://registry.npmjs.org/@dotcms/experiments/-/experiments-0.0.1-alpha.28.tgz", + "integrity": "sha512-iTf6b3PlwH7P+4uHDpaV8ctIQeP7BH5DaplW/fhAczaf/s767OtnyS8liK3BBdikzVlE0UYuPcV9ZZ/u826pyg==", "dependencies": { "@jitsu/sdk-js": "^3.1.5" }, "peerDependencies": { - "@dotcms/client": "0.0.1-alpha.26", + "@dotcms/client": "0.0.1-alpha.28", "react": ">=18", "react-dom": ">=18" } }, "node_modules/@dotcms/react": { - "version": "0.0.1-alpha.26", - "resolved": "https://registry.npmjs.org/@dotcms/react/-/react-0.0.1-alpha.26.tgz", - "integrity": "sha512-vSNvt3H3aT61RLgzWp9iHN3hDrjfLvsM1cu3eqtAye483m/EZP/TkH1rztWzbQAQT9Yk65Uope7b9zk1wKjq8Q==", + "version": "0.0.1-alpha.28", + "resolved": "https://registry.npmjs.org/@dotcms/react/-/react-0.0.1-alpha.28.tgz", + "integrity": "sha512-GyVKQdhOyHk67xgbKkVOAXkpOtF3hQqYCWE9gRgHR0OdBbgJJkUhAsJqyUYKtasZ4krwt7jpG1qhlAr3woN5kA==", "peerDependencies": { - "@dotcms/client": "0.0.1-alpha.26", + "@dotcms/client": "0.0.1-alpha.28", "react": ">=18", "react-dom": ">=18" }