diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.html index 91541de64d57..ff181a2141ef 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.html @@ -1,7 +1,16 @@ - +@if (store.$previewMode()) { + +} @else { + +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.spec.ts index 351c62a312e3..fd2e7290f3cc 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.spec.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from '@jest/globals'; -import { Spectator, createComponentFactory } from '@ngneat/spectator/jest'; +import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator'; import { of } from 'rxjs'; import { AsyncPipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { signal } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ButtonModule } from 'primeng/button'; @@ -17,6 +18,8 @@ import { LoginServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; import { DotEmaBookmarksComponent } from './dot-ema-bookmarks.component'; +import { UVEStore } from '../../../store/dot-uve.store'; + describe('DotEmaBookmarksComponent', () => { let spectator: Spectator; @@ -26,6 +29,13 @@ describe('DotEmaBookmarksComponent', () => { providers: [ DialogService, HttpClient, + // { + // provide: UVEStore, + // useValue: { + // $previewMode: signal(false) + // } + // }, + mockProvider(UVEStore, { $previewMode: signal(false) }), { provide: LoginService, useClass: LoginServiceMock @@ -116,4 +126,16 @@ describe('DotEmaBookmarksComponent', () => { }) ); }); + + describe('preview mode', () => { + it('should render the bookmark button with new UVE toolbar style when preview mode is true', () => { + const store = spectator.inject(UVEStore, true); + jest.spyOn(store, '$previewMode').mockReturnValue(signal(true)); + + spectator.detectChanges(); + const button = spectator.debugElement.query(By.css('[data-testId="bookmark-button"]')); + + expect(button.componentInstance.textContent).toBe(undefined); + }); + }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.ts index cc88d46883b2..785da5309689 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-ema-bookmarks/dot-ema-bookmarks.component.ts @@ -11,6 +11,8 @@ import { DotCMSContentlet } from '@dotcms/dotcms-models'; import { DotFavoritePageComponent } from '@dotcms/portlets/dot-ema/ui'; import { DotMessagePipe } from '@dotcms/ui'; +import { UVEStore } from '../../../store/dot-uve.store'; + @Component({ selector: 'dot-ema-bookmarks', standalone: true, @@ -25,6 +27,7 @@ export class DotEmaBookmarksComponent implements OnInit { private readonly dotFavoritePageService = inject(DotFavoritePageService); private readonly dialogService = inject(DialogService); private readonly dotMessageService = inject(DotMessageService); + protected readonly store = inject(UVEStore); favoritePage: DotCMSContentlet; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.html index 3bafc923771a..50d6b45e839c 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.html @@ -1,18 +1,27 @@ - + @if ($toolbar().editor) { + - + - + - + + } @else if ($toolbar().preview) { + PREVIEW MODE CONTENT + Back + } @@ -22,3 +31,7 @@ Workflows + +@if ($toolbar().showInfoDisplay) { + +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.spec.ts index 8546edd3bfc5..0d18751a0a1b 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.spec.ts @@ -1,47 +1,157 @@ -import { byTestId, Spectator } from '@ngneat/spectator'; +import { byTestId, mockProvider, Spectator } from '@ngneat/spectator'; import { createComponentFactory } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { HttpClientTestingModule, provideHttpClientTesting } from '@angular/common/http/testing'; +import { signal } from '@angular/core'; + +import { DotExperimentsService, DotLanguagesService, DotLicenseService } from '@dotcms/data-access'; +import { LoginService } from '@dotcms/dotcms-js'; +import { + DotExperimentsServiceMock, + DotLanguagesServiceMock, + DotLicenseServiceMock +} from '@dotcms/utils-testing'; import { DotUveToolbarComponent } from './dot-uve-toolbar.component'; +import { DotPageApiService } from '../../../services/dot-page-api.service'; +import { DEFAULT_PERSONA } from '../../../shared/consts'; +import { + HEADLESS_BASE_QUERY_PARAMS, + MOCK_RESPONSE_HEADLESS, + MOCK_RESPONSE_VTL +} from '../../../shared/mocks'; +import { UVEStore } from '../../../store/dot-uve.store'; +import { + createFavoritePagesURL, + createFullURL, + createPageApiUrlWithQueryParams, + sanitizeURL +} from '../../../utils'; +import { DotEmaBookmarksComponent } from '../dot-ema-bookmarks/dot-ema-bookmarks.component'; + describe('DotUveToolbarComponent', () => { let spectator: Spectator; const createComponent = createComponentFactory({ - component: DotUveToolbarComponent + component: DotUveToolbarComponent, + imports: [HttpClientTestingModule, MockComponent(DotEmaBookmarksComponent)], + providers: [ + UVEStore, + provideHttpClientTesting(), + { + provide: DotLanguagesService, + useValue: new DotLanguagesServiceMock() + }, + { + provide: DotExperimentsService, + useValue: DotExperimentsServiceMock + }, + { + provide: DotLicenseService, + useValue: new DotLicenseServiceMock() + }, + { + provide: DotPageApiService, + useValue: { + get: () => of(MOCK_RESPONSE_HEADLESS) + } + }, + { + provide: LoginService, + useValue: { + getCurrentUser: () => of({}) + } + } + ] }); - beforeEach(() => { - spectator = createComponent(); - }); + const params = HEADLESS_BASE_QUERY_PARAMS; + const url = sanitizeURL(params?.url); - it('should have preview button', () => { - expect(spectator.query(byTestId('uve-toolbar-preview'))).toBeTruthy(); - }); + const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params); + const pageAPIResponse = MOCK_RESPONSE_HEADLESS; - it('should have bookmark button', () => { - expect(spectator.query(byTestId('uve-toolbar-bookmark'))).toBeTruthy(); - }); + const pageAPI = `/api/v1/page/${'json'}/${pageAPIQueryParams}`; - it('should have copy url button', () => { - expect(spectator.query(byTestId('uve-toolbar-copy-url'))).toBeTruthy(); - }); + const shouldShowInfoDisplay = false || pageAPIResponse?.page.locked || false || false; - it('should have api link button', () => { - expect(spectator.query(byTestId('uve-toolbar-api-link'))).toBeTruthy(); + const bookmarksUrl = createFavoritePagesURL({ + languageId: Number(params?.language_id), + pageURI: url, + siteId: pageAPIResponse?.site.identifier }); - it('should have experiments button', () => { - expect(spectator.query(byTestId('uve-toolbar-running-experiment'))).toBeTruthy(); - }); + describe('base state', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(UVEStore, { + $uveToolbar: signal({ + editor: { + bookmarksUrl, + copyUrl: createFullURL(params, pageAPIResponse?.site.identifier), + apiUrl: `${'http://localhost'}${pageAPI}` + }, + preview: null, - it('should have language selector', () => { - expect(spectator.query(byTestId('uve-toolbar-language-selector'))).toBeTruthy(); - }); + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: null, + runningExperiment: null, + workflowActionsInode: pageAPIResponse?.page.inode, + unlockButton: null, + showInfoDisplay: shouldShowInfoDisplay, + personaSelector: { + pageId: pageAPIResponse?.page.identifier, + value: pageAPIResponse?.viewAs.persona ?? DEFAULT_PERSONA + } + }), + setDevice: jest.fn(), + setSocialMedia: jest.fn(), + pageParams: signal(params), + pageAPIResponse: signal(MOCK_RESPONSE_VTL), + reloadCurrentPage: jest.fn(), + loadPageAsset: jest.fn() + }) + ] + }); + }); - it('should have persona selector', () => { - expect(spectator.query(byTestId('uve-toolbar-persona-selector'))).toBeTruthy(); - }); + describe('dot-ema-bookmarks', () => { + it('should have attr', () => { + const bookmarks = spectator.query(DotEmaBookmarksComponent); + + expect(bookmarks.url).toBe('/test-url?host_id=123-xyz-567-xxl&language_id=1'); + }); + }); + + it('should have preview button', () => { + expect(spectator.query(byTestId('uve-toolbar-preview'))).toBeTruthy(); + }); + + it('should have copy url button', () => { + expect(spectator.query(byTestId('uve-toolbar-copy-url'))).toBeTruthy(); + }); + + it('should have api link button', () => { + expect(spectator.query(byTestId('uve-toolbar-api-link'))).toBeTruthy(); + }); + + it('should have experiments button', () => { + expect(spectator.query(byTestId('uve-toolbar-running-experiment'))).toBeTruthy(); + }); + + it('should have language selector', () => { + expect(spectator.query(byTestId('uve-toolbar-language-selector'))).toBeTruthy(); + }); + + it('should have persona selector', () => { + expect(spectator.query(byTestId('uve-toolbar-persona-selector'))).toBeTruthy(); + }); - it('should have workflows button', () => { - expect(spectator.query(byTestId('uve-toolbar-workflow-actions'))).toBeTruthy(); + it('should have workflows button', () => { + expect(spectator.query(byTestId('uve-toolbar-workflow-actions'))).toBeTruthy(); + }); }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.ts index e8b7f4940d69..348396d3a509 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/dot-uve-toolbar/dot-uve-toolbar.component.ts @@ -1,16 +1,26 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { ToolbarModule } from 'primeng/toolbar'; -import { DotMessagePipe } from '@dotcms/ui'; +import { UVEStore } from '../../../store/dot-uve.store'; +import { DotEmaBookmarksComponent } from '../dot-ema-bookmarks/dot-ema-bookmarks.component'; +import { DotEmaInfoDisplayComponent } from '../dot-ema-info-display/dot-ema-info-display.component'; @Component({ selector: 'dot-uve-toolbar', standalone: true, - imports: [ButtonModule, DotMessagePipe, ToolbarModule], + imports: [ButtonModule, ToolbarModule, DotEmaBookmarksComponent, DotEmaInfoDisplayComponent], templateUrl: './dot-uve-toolbar.component.html', styleUrl: './dot-uve-toolbar.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class DotUveToolbarComponent {} +export class DotUveToolbarComponent { + #store = inject(UVEStore); + + readonly $toolbar = this.#store.$uveToolbar; + + togglePreviewMode(preview: boolean) { + this.#store.togglePreviewMode(preview); + } +} 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 index 54458d0ce6ca..a51fb31413c4 100644 --- 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 @@ -27,6 +27,7 @@ export interface EditorToolbarState { device?: DotDeviceWithIcon; socialMedia?: string; isEditState: boolean; + isPreviewModeActive?: boolean; } export interface PageDataContainer { @@ -107,3 +108,36 @@ export interface ToolbarProps { hideSocialMedia: boolean; }; } + +/** + * This is used for model the props of + * the New UVE Toolbar with Preview Mode and Future Time Machine + * + * @export + * @interface UVEToolbarProps + */ +export interface UVEToolbarProps { + editor: { + bookmarksUrl: string; + copyUrl: string; + apiUrl: string; + }; + preview?: { + deviceSelector: { + apiLink: string; + hideSocialMedia: boolean; + }; + }; + personaSelector: { + pageId: string; + value: DotPersona; + }; + runningExperiment?: DotExperiment; + currentLanguage: DotLanguage; + workflowActionsInode?: string; + unlockButton?: { + inode: string; + loading: boolean; + }; + showInfoDisplay?: boolean; +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.ts new file mode 100644 index 000000000000..ce67a7ae9ffb --- /dev/null +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.ts @@ -0,0 +1,131 @@ +import { + signalStoreFeature, + withMethods, + withComputed, + withState, + type, + patchState +} from '@ngrx/signals'; + +import { computed } from '@angular/core'; + +import { DotExperimentStatus } from '@dotcms/dotcms-models'; + +import { DEFAULT_PERSONA } from '../../../../shared/consts'; +import { UVE_STATUS } from '../../../../shared/enums'; +import { + computePageIsLocked, + createFavoritePagesURL, + createFullURL, + createPageApiUrlWithQueryParams, + getIsDefaultVariant, + sanitizeURL +} from '../../../../utils'; +import { UVEState } from '../../../models'; +import { EditorToolbarState, UVEToolbarProps } from '../models'; + +/** + * The initial state for the editor toolbar. + * + * @property {EditorToolbarState} initialState - The initial state object for the editor toolbar. + * @property {string | null} initialState.device - The current device being used, or null if not set. + * @property {string | null} initialState.socialMedia - The current social media platform being used, or null if not set. + * @property {boolean} initialState.isEditState - Flag indicating whether the editor is in edit mode. + * @property {boolean} initialState.isPreviewModeActive - Flag indicating whether the preview mode is active. + */ +const initialState: EditorToolbarState = { + device: null, + socialMedia: null, + isEditState: true, + isPreviewModeActive: false +}; + +export function withUVEToolbar() { + return signalStoreFeature( + { + state: type() + }, + withState(initialState), + withComputed((store) => ({ + $uveToolbar: computed(() => { + const params = store.pageParams(); + const url = sanitizeURL(params?.url); + + const experiment = store.experiment?.(); + const pageAPIResponse = store.pageAPIResponse(); + const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params); + + const pageAPI = `/api/v1/page/${ + store.isTraditionalPage() ? 'render' : 'json' + }/${pageAPIQueryParams}`; + + const bookmarksUrl = createFavoritePagesURL({ + languageId: Number(params?.language_id), + pageURI: url, + siteId: pageAPIResponse?.site?.identifier + }); + + const isPageLocked = computePageIsLocked( + pageAPIResponse?.page, + store.currentUser() + ); + const shouldShowUnlock = isPageLocked && pageAPIResponse?.page.canLock; + const isExperimentRunning = experiment?.status === DotExperimentStatus.RUNNING; + + const unlockButton = { + inode: pageAPIResponse?.page.inode, + loading: store.status() === UVE_STATUS.LOADING + }; + + const shouldShowInfoDisplay = + !getIsDefaultVariant(pageAPIResponse?.viewAs.variantId) || + !store.canEditPage() || + isPageLocked || + !!store.device() || + !!store.socialMedia(); + + const siteId = pageAPIResponse?.site?.identifier; + const clientHost = `${params?.clientHost ?? window.location.origin}`; + + return { + editor: store.isPreviewModeActive() + ? null + : { + bookmarksUrl, + copyUrl: createFullURL(params, siteId), + apiUrl: pageAPI + }, + preview: store.isPreviewModeActive() + ? { + deviceSelector: { + apiLink: `${clientHost}${pageAPI}`, + hideSocialMedia: !store.isTraditionalPage() + } + } + : null, + currentLanguage: pageAPIResponse?.viewAs.language, + urlContentMap: store.isEditState() + ? (pageAPIResponse?.urlContentMap ?? null) + : null, + runningExperiment: isExperimentRunning ? experiment : null, + workflowActionsInode: store.canEditPage() ? pageAPIResponse?.page.inode : null, + personaSelector: { + pageId: pageAPIResponse?.page.identifier, + value: pageAPIResponse?.viewAs.persona ?? DEFAULT_PERSONA + }, + unlockButton: shouldShowUnlock ? unlockButton : null, + showInfoDisplay: shouldShowInfoDisplay + }; + }) + })), + withMethods((store) => ({ + // Fake method to toggle preview mode + // This method should be implemented in the real application + togglePreviewMode: (preview) => { + patchState(store, { + isPreviewModeActive: preview + }); + } + })) + ); +} diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts index 6e13612cfe36..60e16bc29bdb 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/withEditor.spec.ts @@ -525,6 +525,34 @@ describe('withEditor', () => { }); }); + describe('withUVEToolbar', () => { + describe('withComputed', () => { + describe('$toolbarProps', () => { + it('should return the base info', () => { + expect(store.$uveToolbar()).toEqual({ + editor: { + apiUrl: '/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&host_id=123-xyz-567-xxl' + }, + preview: null, + currentLanguage: MOCK_RESPONSE_HEADLESS.viewAs.language, + urlContentMap: null, + runningExperiment: null, + workflowActionsInode: MOCK_RESPONSE_HEADLESS.page.inode, + personaSelector: { + pageId: MOCK_RESPONSE_HEADLESS.page.identifier, + value: MOCK_RESPONSE_HEADLESS.viewAs.persona ?? DEFAULT_PERSONA + }, + unlockButton: null, + showInfoDisplay: false + }); + }); + }); + }); + }); + describe('withSave', () => { describe('withMethods', () => { describe('savePage', () => { 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 index 61fb207283fd..66be972ae8e2 100644 --- 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 @@ -20,6 +20,7 @@ import { } from './models'; import { withSave } from './save/withSave'; import { withEditorToolbar } from './toolbar/withEditorToolbar'; +import { withUVEToolbar } from './toolbar/withUVEToolbar'; import { Container, @@ -65,6 +66,7 @@ export function withEditor() { state: type() }, withState(initialState), + withUVEToolbar(), withEditorToolbar(), withSave(), withClient(), 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 index ebc0bf165807..bb7eeb96926a 100644 --- 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 @@ -38,7 +38,7 @@ export interface TranslateProps { } export interface DotUveViewParams { - preview: false; + preview: boolean; orientation: string; device: string; seo: string;