From 80c3d621f8a302494d6d9be4d526f993cc82fcc6 Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Mon, 13 Jan 2025 12:36:03 -0300 Subject: [PATCH] chore(FTM): Add Unlock button to new Toolbar (#31096) This pull request introduces a new unlock button feature to the `DotUveToolbarComponent` and includes various related changes across multiple files. The most important changes include the addition of the unlock button in the HTML template, corresponding styles, service injections, and comprehensive unit tests to ensure the functionality works as expected. ### Unlock Button Feature: * [`dot-uve-toolbar.component.html`](diffhunk://#diff-9937556e73b051b878ba22ad1ce971a70019a617d7979b3e0bcc814801ad350bR31-R43): Added the unlock button with various properties and event binding for unlocking a page. * [`dot-uve-toolbar.component.scss`](diffhunk://#diff-dd2bc3ab605f3a154f1a4b3b5de1f8e364e7bbc5a3fb4b078bba74aaedcad312R18-R22): Added styles for the unlock button, including handling the disabled state. * [`dot-uve-toolbar.component.ts`](diffhunk://#diff-217a9e619d6590c4f652e85353b9637ba5e464ddeb0424be35aef39bb8dceb30L29-R36): Injected `DotContentletLockerService` and added the `unlockPage` method to handle the unlocking logic. [[1]](diffhunk://#diff-217a9e619d6590c4f652e85353b9637ba5e464ddeb0424be35aef39bb8dceb30L29-R36) [[2]](diffhunk://#diff-217a9e619d6590c4f652e85353b9637ba5e464ddeb0424be35aef39bb8dceb30R93-R102) [[3]](diffhunk://#diff-217a9e619d6590c4f652e85353b9637ba5e464ddeb0424be35aef39bb8dceb30R312-R347) * [`dot-uve-toolbar.component.spec.ts`](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R14): Added unit tests for the unlock button, including tests for visibility, state, and service call verification. [[1]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R14) [[2]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1L112-R114) [[3]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R123) [[4]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R141-R143) [[5]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R206) [[6]](diffhunk://#diff-3eaa147616a5d1ff374a5fa27b0f38f0159a9039ef7e8d672dec43631f48a9e1R227-R302) ### Additional Changes: * [`models.ts`](diffhunk://#diff-4b111bf572c55ee766710e9b4ac256c014112a06ad7e08baa8302b290458338bL9-R23): Updated interfaces to include `UnlockOptions` for the new unlock button feature. * [`withUVEToolbar.spec.ts`](diffhunk://#diff-7a5de702ac1dc81304f4c31816f5c0363aa56141f8afa583a87856e7a0d8482dL32-R32): Adjusted the initial state to include `mockCurrentUser` for testing purposes. [[1]](diffhunk://#diff-7a5de702ac1dc81304f4c31816f5c0363aa56141f8afa583a87856e7a0d8482dL32-R32) [[2]](diffhunk://#diff-7a5de702ac1dc81304f4c31816f5c0363aa56141f8afa583a87856e7a0d8482dL112-L128) --- .../dot-uve-toolbar.component.html | 13 ++ .../dot-uve-toolbar.component.scss | 5 + .../dot-uve-toolbar.component.spec.ts | 85 +++++++- .../dot-uve-toolbar.component.ts | 48 ++++- .../edit-ema/portlet/src/lib/shared/models.ts | 17 +- .../editor/toolbar/withUVEToolbar.spec.ts | 201 ++++++++++-------- .../features/editor/toolbar/withUVEToolbar.ts | 60 +++--- .../WEB-INF/messages/Language.properties | 1 + 8 files changed, 303 insertions(+), 127 deletions(-) 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 1edee249735..c825d80e68a 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 @@ -28,6 +28,19 @@ }
+ @if ($unlockButton(); as unlockButton) { + + } + @if (runningExperiment) { device.inode === 'default')) + device: signal(DEFAULT_DEVICES.find((device) => device.inode === 'default')), + $unlockButton: signal(null) }; describe('DotUveToolbarComponent', () => { @@ -118,6 +120,7 @@ describe('DotUveToolbarComponent', () => { let messageService: MessageService; let confirmationService: ConfirmationService; let devicesService: DotDevicesService; + let dotContentletLockerService: DotContentletLockerService; const fixedDate = new Date('2024-01-01'); jest.spyOn(global, 'Date').mockImplementation(() => fixedDate); @@ -135,6 +138,9 @@ describe('DotUveToolbarComponent', () => { providers: [ UVEStore, provideHttpClientTesting(), + mockProvider(DotContentletLockerService, { + unlock: jest.fn().mockReturnValue(of({})) + }), mockProvider(ConfirmationService, { confirm: jest.fn() }), @@ -197,6 +203,7 @@ describe('DotUveToolbarComponent', () => { messageService = spectator.inject(MessageService, true); devicesService = spectator.inject(DotDevicesService); confirmationService = spectator.inject(ConfirmationService, true); + dotContentletLockerService = spectator.inject(DotContentletLockerService); }); it('should have a dot-uve-workflow-actions component', () => { @@ -217,6 +224,82 @@ describe('DotUveToolbarComponent', () => { }); }); + describe('unlock button', () => { + it('should be null', () => { + expect(spectator.query(byTestId('uve-toolbar-unlock-button'))).toBeNull(); + }); + + it('should be true', () => { + baseUVEState.$unlockButton.set({ + inode: '123', + disabled: false, + loading: false, + info: { + message: 'editpage.toolbar.page.release.lock.locked.by.user', + args: ['John Doe'] + } + }); + spectator.detectChanges(); + + expect(spectator.query(byTestId('uve-toolbar-unlock-button'))).toBeTruthy(); + }); + + it('should be disabled', () => { + baseUVEState.$unlockButton.set({ + disabled: true, + loading: false, + info: { + message: 'editpage.toolbar.page.release.lock.locked.by.user', + args: ['John Doe'] + }, + inode: '123' + }); + spectator.detectChanges(); + expect( + spectator + .query(byTestId('uve-toolbar-unlock-button')) + .getAttribute('ng-reflect-disabled') + ).toEqual('true'); + }); + + it('should be loading', () => { + baseUVEState.$unlockButton.set({ + loading: true, + disabled: false, + inode: '123', + info: { + message: 'editpage.toolbar.page.release.lock.locked.by.user', + args: ['John Doe'] + } + }); + spectator.detectChanges(); + expect( + spectator + .query(byTestId('uve-toolbar-unlock-button')) + .getAttribute('ng-reflect-loading') + ).toEqual('true'); + }); + + it('should call dotContentletLockerService.unlockPage', () => { + const spy = jest.spyOn(dotContentletLockerService, 'unlock'); + + baseUVEState.$unlockButton.set({ + loading: true, + disabled: false, + inode: '123', + info: { + message: 'editpage.toolbar.page.release.lock.locked.by.user', + args: ['John Doe'] + } + }); + spectator.detectChanges(); + + spectator.click(byTestId('uve-toolbar-unlock-button')); + + expect(spy).toHaveBeenCalledWith('123'); + }); + }); + describe('dot-ema-bookmarks', () => { it('should have attr', () => { const bookmarks = spectator.query(DotEmaBookmarksComponent); 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 10f59f3d330..eccaf8c6e9e 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,3 +1,5 @@ +import { tapResponse } from '@ngrx/operators'; + import { ClipboardModule } from '@angular/cdk/clipboard'; import { NgClass, NgTemplateOutlet } from '@angular/common'; import { @@ -26,7 +28,12 @@ import { ToolbarModule } from 'primeng/toolbar'; import { map } from 'rxjs/operators'; import { UVE_MODE } from '@dotcms/client'; -import { DotDevicesService, DotMessageService, DotPersonalizeService } from '@dotcms/data-access'; +import { + DotContentletLockerService, + DotDevicesService, + DotMessageService, + DotPersonalizeService +} from '@dotcms/data-access'; import { DotPersona, DotLanguage, DotDeviceListItem } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; @@ -83,6 +90,7 @@ export class DotUveToolbarComponent { readonly #confirmationService = inject(ConfirmationService); readonly #personalizeService = inject(DotPersonalizeService); readonly #deviceService = inject(DotDevicesService); + readonly #dotContentletLockerService = inject(DotContentletLockerService); readonly $toolbar = this.#store.$uveToolbar; readonly $showWorkflowActions = this.#store.$showWorkflowsActions; @@ -90,6 +98,8 @@ export class DotUveToolbarComponent { readonly $apiURL = this.#store.$apiURL; readonly $personaSelectorProps = this.#store.$personaSelector; readonly $infoDisplayProps = this.#store.$infoDisplayProps; + readonly $unlockButton = this.#store.$unlockButton; + readonly $devices: Signal = toSignal( this.#deviceService.get().pipe(map((devices = []) => [...DEFAULT_DEVICES, ...devices])), { @@ -299,4 +309,40 @@ export class DotUveToolbarComponent { } }); } + + /** + * Unlocks a page with the specified inode. + * + * @param {string} inode + * @memberof EditEmaToolbarComponent + */ + unlockPage(inode: string) { + 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.#store.reloadCurrentPage()); + } } 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 30824fc98d1..bc0036ec362 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 @@ -6,12 +6,21 @@ import { CommonErrors, DialogStatus, FormStatus } from './enums'; import { DotPageApiParams } from '../services/dot-page-api.service'; +export interface MessagePipeOptions { + message: string; + args: string[]; +} + +export interface UnlockOptions { + inode: string; + loading: boolean; + info: MessagePipeOptions; + disabled: boolean; +} + export interface InfoOptions { icon: string; - info: { - message: string; - args: string[]; - }; + info: MessagePipeOptions; id: string; actionIcon?: string; } diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.spec.ts index 5aa633b6bfa..b5c23f0b8c8 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/store/features/editor/toolbar/withUVEToolbar.spec.ts @@ -29,7 +29,7 @@ const initialState: UVEState = { isEnterprise: true, languages: [], pageAPIResponse: MOCK_RESPONSE_HEADLESS, - currentUser: null, + currentUser: mockCurrentUser, experiment: null, errorCode: null, pageParams, @@ -109,23 +109,6 @@ describe('withEditor', () => { it('should return null', () => { expect(store.$infoDisplayProps()).toBe(null); }); - - describe('socialMedia', () => { - it('should text for current social media', () => { - store.setSEO('facebook'); - - expect(store.$infoDisplayProps()).toEqual({ - icon: 'pi pi-facebook', - id: 'socialMedia', - info: { - message: 'Viewing facebook social media preview', - args: [] - }, - actionIcon: 'pi pi-times' - }); - }); - }); - describe('variant', () => { it('should show have text for variant', () => { const currentExperiment = getRunningExperimentMock(); @@ -172,75 +155,6 @@ describe('withEditor', () => { } }); }); - it('should show label and icon when page is lock for editing and has unlock permission', () => { - patchState(store, { - pageAPIResponse: { - ...MOCK_RESPONSE_HEADLESS, - page: { - ...MOCK_RESPONSE_HEADLESS.page, - locked: true, - canLock: true, - lockedByName: 'John Doe', - lockedBy: '456' - } - }, - currentUser: mockCurrentUser - }); - expect(store.$infoDisplayProps()).toEqual({ - icon: 'pi pi-lock', - id: 'locked', - info: { - message: 'editpage.locked-by', - args: ['John Doe'] - } - }); - }); - - it("should show a different message for that can't be locked", () => { - patchState(store, { - pageAPIResponse: { - ...MOCK_RESPONSE_HEADLESS, - page: { - ...MOCK_RESPONSE_HEADLESS.page, - locked: true, - canLock: false, - lockedByName: mockCurrentUser.givenName, - lockedBy: mockCurrentUser.userId - } - }, - currentUser: { - ...mockCurrentUser, - userId: '123' - } - }); - - expect(store.$infoDisplayProps()).toEqual({ - icon: 'pi pi-lock', - id: 'locked', - info: { - message: 'editpage.locked-contact-with', - args: ['Test'] - } - }); - }); - - it('should be null when locked by the same user', () => { - patchState(store, { - pageAPIResponse: { - ...MOCK_RESPONSE_HEADLESS, - page: { - ...MOCK_RESPONSE_HEADLESS.page, - locked: true, - canLock: true, - lockedByName: mockCurrentUser.givenName, - lockedBy: mockCurrentUser.userId - } - }, - currentUser: mockCurrentUser - }); - - expect(store.$infoDisplayProps()).toBe(null); - }); }); describe('$showWorkflowsActions', () => { @@ -285,6 +199,119 @@ describe('withEditor', () => { }); }); }); + + describe('$unlockButton', () => { + it('should be null if the page is not locked', () => { + patchState(store, { + pageAPIResponse: { + ...store.pageAPIResponse(), + page: { + ...store.pageAPIResponse().page, + locked: false + } + } + }); + + expect(store.$unlockButton()).toBe(null); + }); + + it('should be null if the page is locked by the current user', () => { + patchState(store, { + pageAPIResponse: { + ...store.pageAPIResponse(), + page: { + ...store.pageAPIResponse().page, + locked: true, + lockedBy: mockCurrentUser.userId + } + }, + pageParams: { + ...store.pageParams(), + editorMode: UVE_MODE.EDIT + } + }); + + expect(store.$unlockButton()).toBe(null); + }); + + it('should be null if the page is locked but mode is preview', () => { + patchState(store, { + pageAPIResponse: { + ...store.pageAPIResponse(), + page: { + ...store.pageAPIResponse().page, + locked: true, + lockedBy: '123' + } + }, + pageParams: { + ...store.pageParams(), + editorMode: UVE_MODE.PREVIEW + } + }); + + expect(store.$unlockButton()).toBe(null); + }); + + it('should show label and icon when page is lock for editing and has unlock permission', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page, + locked: true, + canLock: true, + lockedByName: 'John Doe', + lockedBy: '456' + } + }, + pageParams: { + ...store.pageParams(), + editorMode: UVE_MODE.EDIT + }, + status: UVE_STATUS.LOADED + }); + expect(store.$unlockButton()).toEqual({ + inode: store.pageAPIResponse().page.inode, + disabled: false, + loading: false, + info: { + message: 'editpage.toolbar.page.release.lock.locked.by.user', + args: ['John Doe'] + } + }); + }); + + it('should be disabled if the page is locked by another user and cannot be unlocked', () => { + patchState(store, { + pageAPIResponse: { + ...MOCK_RESPONSE_HEADLESS, + page: { + ...MOCK_RESPONSE_HEADLESS.page, + locked: true, + lockedBy: '123', + lockedByName: 'John Doe', + canLock: false + } + }, + pageParams: { + ...store.pageParams(), + editorMode: UVE_MODE.EDIT + }, + status: UVE_STATUS.LOADED + }); + + expect(store.$unlockButton()).toEqual({ + disabled: true, + info: { + message: 'editpage.locked-by', + args: ['John Doe'] + }, + inode: store.pageAPIResponse().page.inode, + loading: false + }); + }); + }); }); describe('methods', () => { 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 index 64d2209fe8c..e13a1a1b044 100644 --- 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 @@ -14,7 +14,7 @@ import { DotDevice, DotExperimentStatus, SeoMetaTagsResult } from '@dotcms/dotcm import { DEFAULT_DEVICE, DEFAULT_PERSONA } from '../../../../shared/consts'; import { UVE_STATUS } from '../../../../shared/enums'; -import { InfoOptions } from '../../../../shared/models'; +import { InfoOptions, UnlockOptions } from '../../../../shared/models'; import { computePageIsLocked, createFavoritePagesURL, @@ -110,6 +110,30 @@ export function withUVEToolbar() { unlockButton: shouldShowUnlock ? unlockButton : null }; }), + + $unlockButton: computed(() => { + const pageAPIResponse = store.pageAPIResponse(); + const currentUser = store.currentUser(); + const isPreviewMode = store.pageParams()?.editorMode === UVE_MODE.PREVIEW; + const isLocked = computePageIsLocked(pageAPIResponse.page, currentUser); + const info = { + message: pageAPIResponse.page.canLock + ? 'editpage.toolbar.page.release.lock.locked.by.user' + : 'editpage.locked-by', + args: [pageAPIResponse.page.lockedByName] + }; + + const disabled = !pageAPIResponse.page.canLock; + + return !isPreviewMode && isLocked + ? { + inode: pageAPIResponse.page.inode, + loading: store.status() === UVE_STATUS.LOADING, + info, + disabled + } + : null; + }), $personaSelector: computed(() => { const pageAPIResponse = store.pageAPIResponse(); @@ -130,21 +154,6 @@ export function withUVEToolbar() { $infoDisplayProps: computed(() => { const pageAPIResponse = store.pageAPIResponse(); const canEditPage = store.canEditPage(); - const socialMedia = store.socialMedia(); - const currentUser = store.currentUser(); - const isPreview = store.pageParams()?.editorMode === UVE_MODE.PREVIEW; - - if (socialMedia && !isPreview) { - return { - icon: `pi pi-${socialMedia.toLowerCase()}`, - id: 'socialMedia', - info: { - message: `Viewing ${socialMedia} social media preview`, - args: [] - }, - actionIcon: 'pi pi-times' - }; - } if (!getIsDefaultVariant(pageAPIResponse?.viewAs.variantId)) { const variantId = pageAPIResponse.viewAs.variantId; @@ -169,24 +178,7 @@ export function withUVEToolbar() { }; } - if (computePageIsLocked(pageAPIResponse.page, currentUser)) { - 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) { + if (!pageAPIResponse.page.locked && !canEditPage) { return { icon: 'pi pi-exclamation-circle warning', id: 'no-permission', diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 66d00b8244b..820465e90cc 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1483,6 +1483,7 @@ editpage.toolbar.nav.experiments=A/B editpage.toolbar.nav.page.tools=Page Tools editpage.toolbar.page.cant.edit=You don't have permission to edit this Page editpage.toolbar.page.locked.by.user=Locked by {0} +editpage.toolbar.page.release.lock.locked.by.user=Unlock
Locked by {0} editpage.toolbar.preview.page=Preview editpage.toolbar.edit.url.map.content=Edit {0} editpage.toolbar.preview.page.clipboard=Preview Page