diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts index f16532fb6983..eaba95cf5d7e 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts @@ -2,8 +2,11 @@ import { describe, expect } from '@jest/globals'; import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; -import { DotLicenseService } from '@dotcms/data-access'; +import { MessageService } from 'primeng/api'; + +import { DotLicenseService, DotMessageService } from '@dotcms/data-access'; import { + MockDotMessageService, mockDotContainers, mockDotLayout, mockDotTemplate, @@ -54,11 +57,16 @@ describe('EditEmaStore', () => { service: EditEmaStore, mocks: [DotPageApiService, DotActionUrlService], providers: [ + MessageService, { provide: DotLicenseService, useValue: { isEnterprise: () => of(true) } + }, + { + provide: DotMessageService, + useValue: new MockDotMessageService({}) } ] }); @@ -85,120 +93,6 @@ describe('EditEmaStore', () => { }); }); - describe('url sanitize', () => { - it('should remove the slash from the start', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: '/cool', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/cool?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it("should remove the slash from the end if it's not the only character", (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: 'super-cool/', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/super-cool?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it('should remove the slash from the end and the beggining', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: '/hello-there/', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/hello-there?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it('should remove the index if a nested path', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: 'i-have-the-high-ground/index', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/i-have-the-high-ground?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it('should remove the index if a nested path with slash', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: 'no-index-please/index/', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/no-index-please?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it('should leave as it is for valid url', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: 'this-is-where-the-fun-begins', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/this-is-where-the-fun-begins?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - - it('should leave as it is for a nested valid url', (done) => { - spectator.service.load({ - clientHost: 'http://localhost:3000', - language_id: '1', - url: 'hello-there/general-kenobi', - 'com.dotmarketing.persona.id': '123' - }); - - spectator.service.editorState$.subscribe((state) => { - expect(state.apiURL).toEqual( - 'http://localhost/api/v1/page/json/hello-there/general-kenobi?language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona' - ); - done(); - }); - }); - }); - describe('selectors', () => { it('should return editorState', (done) => { spectator.service.editorState$.subscribe((state) => { @@ -513,5 +407,58 @@ describe('EditEmaStore', () => { pageId: 'page-identifier-123' }); }); + + it('should not add form to page when the form is dupe and triggers a message', () => { + const messageService = spectator.inject(MessageService); + + const addMessageSpy = jest.spyOn(messageService, 'add'); + + const payload: ActionPayload = { + pageId: 'page-identifier-123', + language_id: '1', + container: { + identifier: 'container-identifier-123', + uuid: '123', + acceptTypes: 'test', + maxContentlets: 1, + contentletsId: ['existing-contentlet-123', 'form-identifier-123'] + }, + pageContainers: [ + { + identifier: 'container-identifier-123', + uuid: '123', + contentletsId: ['existing-contentlet-123', 'form-identifier-123'] + } + ], + contentlet: { + identifier: 'existing-contentlet-123', + inode: 'existing-contentlet-inode-456', + title: 'Hello World' + } + }; + const dotPageApiService = spectator.inject(DotPageApiService); + dotPageApiService.save.andReturn(of({})); + dotPageApiService.getFormIndetifier.andReturn(of('form-identifier-123')); + + spectator.service.load({ + clientHost: 'http://localhost:3000', + language_id: 'en', + url: 'test-url', + 'com.dotmarketing.persona.id': '123' + }); + spectator.service.saveFormToPage({ + payload, + formId: 'form-identifier-789', + // eslint-disable-next-line @typescript-eslint/no-empty-function + whenSaved: () => {} + }); + + expect(addMessageSpy).toHaveBeenCalledWith({ + severity: 'info', + summary: 'editpage.content.add.already.title', + detail: 'editpage.content.add.already.message', + life: 2000 + }); + }); }); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts index 71b14d39806c..e035a28c6db7 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts @@ -4,9 +4,11 @@ import { EMPTY, Observable, forkJoin } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { MessageService } from 'primeng/api'; + import { catchError, map, shareReplay, switchMap, take, tap } from 'rxjs/operators'; -import { DotLicenseService } from '@dotcms/data-access'; +import { DotLicenseService, DotMessageService } from '@dotcms/data-access'; import { DotContainerMap, DotLayout, DotPageContainerStructure } from '@dotcms/dotcms-models'; import { DotActionUrlService } from '../../services/dot-action-url/dot-action-url.service'; @@ -18,7 +20,7 @@ import { import { DEFAULT_PERSONA, EDIT_CONTENTLET_URL, ADD_CONTENTLET_URL } from '../../shared/consts'; import { EDITOR_STATE } from '../../shared/enums'; import { ActionPayload, SavePagePayload } from '../../shared/models'; -import { insertContentletInContainer } from '../../utils'; +import { insertContentletInContainer, sanitizeURL } from '../../utils'; type DialogType = 'content' | 'form' | 'widget' | 'shell' | null; @@ -34,7 +36,7 @@ export interface EditEmaState { editorState: EDITOR_STATE; } -function getFormId(dotPageApiService) { +function getFormId(dotPageApiService: DotPageApiService) { return (source: Observable) => source.pipe( switchMap(({ payload, formId, whenSaved }) => { @@ -60,7 +62,9 @@ export class EditEmaStore extends ComponentStore { constructor( private dotPageApiService: DotPageApiService, private dotActionUrl: DotActionUrlService, - private dotLicenseService: DotLicenseService + private dotLicenseService: DotLicenseService, + private messageService: MessageService, + private dotMessageService: DotMessageService ) { super(); } @@ -212,7 +216,26 @@ export class EditEmaStore extends ComponentStore { }), getFormId(this.dotPageApiService), switchMap(({ whenSaved, payload }) => { - const pageContainers = insertContentletInContainer(payload); + const { pageContainers, didInsert } = insertContentletInContainer(payload); + + // This should not be called here but since here is where we get the form contentlet + // we need to do it here, we need to refactor editor and will fix there. + if (!didInsert) { + this.messageService.add({ + severity: 'info', + summary: this.dotMessageService.get( + 'editpage.content.add.already.title' + ), + detail: this.dotMessageService.get( + 'editpage.content.add.already.message' + ), + life: 2000 + }); + + this.updateEditorState(EDITOR_STATE.LOADED); + + return EMPTY; + } return this.dotPageApiService .save({ @@ -409,13 +432,7 @@ export class EditEmaStore extends ComponentStore { } private createPageURL(params: DotPageApiParams): string { - const url = params.url - .replace(/^\/|\/$/g, '') // Remove slashes from the beginning and end of the url - .split('/') - .filter((part, i) => { - return !i || part !== 'index'; // Filter the index from the url if it is at the last position - }) - .join('/'); + const url = sanitizeURL(params.url); return `${url}?language_id=${params.language_id}&com.dotmarketing.persona.id=${params['com.dotmarketing.persona.id']}`; } 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 cf33f895c18a..5767267c72f1 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 @@ -179,7 +179,7 @@ [style]="{ width: '400px' }" - [rejectIcon]="null" - [acceptIcon]="null" + rejectIcon="hidden" + acceptIcon="hidden" rejectButtonStyleClass="p-button-outlined" data-testId="confirm-dialog"> 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 0c42b36bd340..267f89416785 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 @@ -58,7 +58,9 @@ const messagesMock = { 'editpage.content.contentlet.remove.confirmation_message.message': 'Are you sure you want to remove this content?', 'dot.common.dialog.accept': 'Accept', - 'dot.common.dialog.reject': 'Reject' + 'dot.common.dialog.reject': 'Reject', + 'editpage.content.add.already.title': 'Content already added', + 'editpage.content.add.already.message': 'This content is already added to this container' }; const dragEventMock = { @@ -217,6 +219,8 @@ describe('EditEmaEditorComponent', () => { let spectator: SpectatorRouting; let store: EditEmaStore; let confirmationService: ConfirmationService; + let messageService: MessageService; + let addMessageSpy: jest.SpyInstance; const createComponent = createRouting({ canEdit: true, canRead: true }); @@ -232,6 +236,8 @@ describe('EditEmaEditorComponent', () => { store = spectator.inject(EditEmaStore, true); confirmationService = spectator.inject(ConfirmationService, true); + messageService = spectator.inject(MessageService, true); + addMessageSpy = jest.spyOn(messageService, 'add'); store.load({ clientHost: 'http://localhost:3000', @@ -836,6 +842,106 @@ describe('EditEmaEditorComponent', () => { spectator.detectChanges(); }); + it('should not add contentlet after backend emit SAVE_CONTENTLET and contentlet is dupe', () => { + spectator.detectChanges(); + + const initAddIframeDialogMock = jest.spyOn(store, 'initActionAdd'); + + const payload: ActionPayload = { + pageContainers: [ + { + identifier: 'test', + uuid: 'test', + contentletsId: ['456', '123'] + } + ], + container: { + identifier: 'test', + acceptTypes: 'test', + uuid: 'test', + maxContentlets: 1, + contentletsId: ['123', '456'] + }, + contentlet: { + inode: '123', + title: 'Hello World', + identifier: '123' + }, + pageId: 'test1', + language_id: 'test', + position: 'before' + }; + + spectator.setInput('contentlet', { + x: 100, + y: 100, + width: 500, + height: 500, + payload + }); + + spectator.detectComponentChanges(); + + spectator.triggerEventHandler( + EmaContentletToolsComponent, + 'addContent', + payload + ); + + spectator.detectComponentChanges(); + + const dialog = spectator.query(byTestId('dialog')); + + expect(dialog.getAttribute('ng-reflect-visible')).toBe('true'); + expect(initAddIframeDialogMock).toHaveBeenCalledWith({ + containerId: 'test', + acceptTypes: 'test', + language_id: 'test' + }); + + const dialogIframe = spectator.debugElement.query( + By.css('[data-testId="dialog-iframe"]') + ); + + spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load + + dialogIframe.nativeElement.contentWindow.document.dispatchEvent( + new CustomEvent('ng-event', { + detail: { + name: NG_CUSTOM_EVENTS.CREATE_CONTENTLET, + data: { + url: 'test/url', + contentType: 'test' + } + } + }) + ); + + spectator.detectChanges(); + + expect(dialogIframe.nativeElement.src).toBe('http://localhost/test/url'); + + spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load + + dialogIframe.nativeElement.contentWindow.document.dispatchEvent( + new CustomEvent('ng-event', { + detail: { + name: NG_CUSTOM_EVENTS.SAVE_PAGE, + payload: { + contentletIdentifier: '456' + } + } + }) + ); + + expect(addMessageSpy).toHaveBeenCalledWith({ + severity: 'info', + summary: 'Content already added', + detail: 'This content is already added to this container', + life: 2000 + }); + }); + it('should add contentlet after backend emit CONTENT_SEARCH_SELECT', () => { const saveMock = jest.spyOn(store, 'savePage'); @@ -920,6 +1026,77 @@ describe('EditEmaEditorComponent', () => { expect(saveMock).toHaveBeenCalled(); }); + it('should not add contentlet after backend emit CONTENT_SEARCH_SELECT and contentlet is dupe', () => { + spectator.detectChanges(); + + const payload: 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' + }, + container: { + identifier: 'container-identifier-123', + acceptTypes: 'test', + uuid: 'uuid-123', + maxContentlets: 1, + contentletsId: ['contentlet-identifier-123'] + }, + pageId: 'test' + }; + + spectator.setInput('contentlet', { + x: 100, + y: 100, + width: 500, + height: 500, + payload + }); + + spectator.detectComponentChanges(); + + spectator.triggerEventHandler( + EmaContentletToolsComponent, + 'addContent', + payload + ); + + spectator.detectComponentChanges(); + + const dialogIframe = spectator.debugElement.query( + By.css('[data-testId="dialog-iframe"]') + ); + + spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load + + dialogIframe.nativeElement.contentWindow.document.dispatchEvent( + new CustomEvent('ng-event', { + detail: { + name: NG_CUSTOM_EVENTS.CONTENT_SEARCH_SELECT, + data: { + identifier: 'contentlet-identifier-123', + inode: '123' + } + } + }) + ); + + expect(addMessageSpy).toHaveBeenCalledWith({ + severity: 'info', + summary: 'Content already added', + detail: 'This content is already added to this container', + life: 2000 + }); + }); + it('should add widget after backend emit CONTENT_SEARCH_SELECT', () => { const saveMock = jest.spyOn(store, 'savePage'); const actionAdd = jest.spyOn(store, 'initActionAdd'); @@ -1010,6 +1187,85 @@ describe('EditEmaEditorComponent', () => { expect(saveMock).toHaveBeenCalled(); }); + + it('should not add widget after backend emit CONTENT_SEARCH_SELECT and widget is dupe', () => { + const actionAdd = jest.spyOn(store, 'initActionAdd'); + + spectator.detectChanges(); + + const payload: 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' + }, + container: { + identifier: 'container-identifier-123', + acceptTypes: 'test', + uuid: 'uuid-123', + maxContentlets: 1, + contentletsId: ['contentlet-identifier-123'] + }, + pageId: 'test' + }; + + spectator.setInput('contentlet', { + x: 100, + y: 100, + width: 500, + height: 500, + payload + }); + + spectator.detectComponentChanges(); + + spectator.triggerEventHandler( + EmaContentletToolsComponent, + 'addWidget', + payload + ); + + spectator.detectComponentChanges(); + + expect(actionAdd).toHaveBeenCalledWith({ + containerId: 'container-identifier-123', + acceptTypes: 'WIDGET', + language_id: '1' + }); + + const dialogIframe = spectator.debugElement.query( + By.css('[data-testId="dialog-iframe"]') + ); + + spectator.triggerEventHandler(dialogIframe, 'load', {}); // There's no way we can load the iframe, because we are setting a real src and will not load + + dialogIframe.nativeElement.contentWindow.document.dispatchEvent( + new CustomEvent('ng-event', { + detail: { + name: NG_CUSTOM_EVENTS.CONTENT_SEARCH_SELECT, + data: { + identifier: 'contentlet-identifier-123', + inode: '123' + } + } + }) + ); + + expect(addMessageSpy).toHaveBeenCalledWith({ + severity: 'info', + summary: 'Content already added', + detail: 'This content is already added to this container', + life: 2000 + }); + }); }); describe('misc', () => { @@ -1352,8 +1608,8 @@ describe('EditEmaEditorComponent', () => { const confirmDialog = spectator.query(byTestId('confirm-dialog')); - expect(confirmDialog.getAttribute('acceptIcon')).toBeNull(); - expect(confirmDialog.getAttribute('rejectIcon')).toBeNull(); + expect(confirmDialog.getAttribute('acceptIcon')).toBe('hidden'); + expect(confirmDialog.getAttribute('rejectIcon')).toBe('hidden'); }); }); 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 453d230aeae0..fc2449c23cf3 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 @@ -337,11 +337,17 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { */ onPlaceItem(event: ActionPayload): void { if (this.draggedPayload.type === 'contentlet') { - const pageContainers = insertContentletInContainer({ + const { pageContainers, didInsert } = insertContentletInContainer({ ...event, newContentletId: this.draggedPayload.item.identifier }); + if (!didInsert) { + this.handleDuplicatedContentlet(); + + return; + } + this.store.savePage({ pageContainers, pageId: event.pageId, @@ -478,11 +484,17 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { /* */ }, [NG_CUSTOM_EVENTS.CONTENT_SEARCH_SELECT]: () => { - const pageContainers = insertContentletInContainer({ + const { pageContainers, didInsert } = insertContentletInContainer({ ...this.savePayload, newContentletId: detail.data.identifier }); + if (!didInsert) { + this.handleDuplicatedContentlet(); + + return; + } + // Save when selected this.store.savePage({ pageContainers, @@ -497,11 +509,17 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { }, [NG_CUSTOM_EVENTS.SAVE_PAGE]: () => { if (this.savePayload) { - const pageContainers = insertContentletInContainer({ + const { pageContainers, didInsert } = insertContentletInContainer({ ...this.savePayload, newContentletId: detail.payload.contentletIdentifier }); + if (!didInsert) { + this.handleDuplicatedContentlet(); + + return; + } + // Save when created this.store.savePage({ pageContainers, @@ -625,4 +643,15 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy { this.rows = []; this.contentlet = null; } + + private handleDuplicatedContentlet() { + this.messageService.add({ + severity: 'info', + summary: this.dotMessageService.get('editpage.content.add.already.title'), + detail: this.dotMessageService.get('editpage.content.add.already.message'), + life: 2000 + }); + + this.store.updateEditorState(EDITOR_STATE.LOADED); + } } 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 e751fdc800ad..2e4a10568112 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 @@ -77,6 +77,45 @@ describe('EditEmaGuard', () => { }); }); + it('should navigate to "edit-ema" and sanitize url', (done) => { + jest.spyOn(emaAppConfigurationService, 'get').mockReturnValue( + of({ + pattern: 'some-pattern', + url: 'https://example.com', + options: { + authenticationToken: '12345', + additionalOption1: 'value1', + additionalOption2: 'value2' + } + }) + ); + + const route: ActivatedRouteSnapshot = { + firstChild: { + url: [{ path: 'content' }] + }, + queryParams: { url: '/some-url/with-index/index' } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + const result = TestBed.runInInjectionContext( + () => editEmaGuard(route, state) as Observable + ); + + result.subscribe((canActivate) => { + expect(router.navigate).toHaveBeenCalledWith(['/edit-ema/content'], { + queryParams: { + 'com.dotmarketing.persona.id': 'modes.persona.no.persona', + language_id: 1, + url: 'some-url/with-index' + }, + replaceUrl: true + }); + expect(canActivate).toBe(true); + done(); + }); + }); + it('should not update the queryParams on navigate', (done) => { jest.spyOn(emaAppConfigurationService, 'get').mockReturnValue( of({ 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 cc9f3a7dd6c7..a829677bc9b0 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 @@ -8,6 +8,8 @@ import { map } from 'rxjs/operators'; import { EmaAppConfigurationService } from '@dotcms/data-access'; +import { sanitizeURL } from '../../utils'; + type EmaQueryParams = { url: string; language_id: number; @@ -57,6 +59,13 @@ 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' && + /index$/g.test(queryParams[curr.key]) + ) { + acc[curr.key] = sanitizeURL(queryParams[curr.key]); + acc.missing = true; } return acc; 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 e7f8f558e5ea..5d009dee1204 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 @@ -5,27 +5,41 @@ import { ActionPayload, ContainerPayload, PageContainer } from '../shared/models * * @export * @param {ActionPayload} action - * @return {*} {PageContainer[]} + * @return {*} {{ + * pageContainers: PageContainer[]; + * didInsert: boolean; + * }} */ -export function insertContentletInContainer(action: ActionPayload): PageContainer[] { +export function insertContentletInContainer(action: ActionPayload): { + pageContainers: PageContainer[]; + didInsert: boolean; +} { if (action.position) { return insertPositionedContentletInContainer(action); } + let didInsert = false; + const { pageContainers, container, personaTag, newContentletId } = action; - return pageContainers.map((pageContainer) => { + const newPageContainers = pageContainers.map((pageContainer) => { if ( areContainersEquals(pageContainer, container) && !pageContainer.contentletsId.includes(newContentletId) ) { pageContainer.contentletsId.push(newContentletId); + didInsert = true; } pageContainer.personaTag = personaTag; return pageContainer; }); + + return { + pageContainers: newPageContainers, + didInsert + }; } /** @@ -75,21 +89,35 @@ function areContainersEquals( * * @export * @param {ActionPayload} payload - * @return {*} {PageContainer[]} + * @return {*} {{ + * pageContainers: PageContainer[]; + * didInsert: boolean; + * }} */ -function insertPositionedContentletInContainer(payload: ActionPayload): PageContainer[] { +function insertPositionedContentletInContainer(payload: ActionPayload): { + pageContainers: PageContainer[]; + didInsert: boolean; +} { + let didInsert = false; + const { pageContainers, container, contentlet, personaTag, newContentletId, position } = payload; - return pageContainers.map((pageContainer) => { - if (areContainersEquals(pageContainer, container)) { + const newPageContainers = pageContainers.map((pageContainer) => { + if ( + areContainersEquals(pageContainer, container) && + !pageContainer.contentletsId.includes(newContentletId) + ) { const index = pageContainer.contentletsId.indexOf(contentlet.identifier); if (index !== -1) { const offset = position === 'before' ? index : index + 1; pageContainer.contentletsId.splice(offset, 0, newContentletId); + + didInsert = true; } else { pageContainer.contentletsId.push(newContentletId); + didInsert = true; } } @@ -97,4 +125,25 @@ function insertPositionedContentletInContainer(payload: ActionPayload): PageCont return pageContainer; }); + + return { + pageContainers: newPageContainers, + didInsert + }; +} + +/** + * Remove the index from the end of the url if it's nested and also remove extra slashes + * + * @param {string} url + * @return {*} {string} + */ +export function sanitizeURL(url: string): string { + return url + .replace(/^\/|\/$/g, '') // Remove slashes from the beginning and end of the url + .split('/') + .filter((part, i) => { + return !i || part !== 'index'; // Filter the index from the url if it is at the last position + }) + .join('/'); } 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 079c1d8c49e8..58a3627ec6ee 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,4 +1,4 @@ -import { deleteContentletFromContainer, insertContentletInContainer } from '.'; +import { deleteContentletFromContainer, insertContentletInContainer, sanitizeURL } from '.'; describe('utils functions', () => { describe('delete contentlet from container', () => { @@ -121,14 +121,17 @@ describe('utils functions', () => { newContentletId: 'new-contentlet-id-123' }); - expect(result).toEqual([ - { - identifier: 'container-identifier-123', - uuid: 'container-uui-123', - contentletsId: ['contentlet-mark-123', 'new-contentlet-id-123'], - personaTag: undefined - } - ]); + expect(result).toEqual({ + didInsert: true, + pageContainers: [ + { + identifier: 'container-identifier-123', + uuid: 'container-uui-123', + contentletsId: ['contentlet-mark-123', 'new-contentlet-id-123'], + personaTag: undefined + } + ] + }); }); it('should insert in specific position', () => { @@ -167,14 +170,17 @@ describe('utils functions', () => { newContentletId: '000' }); - expect(result).toEqual([ - { - identifier: 'test', - uuid: 'test', - contentletsId: ['test', 'test123', '000', 'test1234'], - personaTag: undefined - } - ]); + expect(result).toEqual({ + didInsert: true, + pageContainers: [ + { + identifier: 'test', + uuid: 'test', + contentletsId: ['test', 'test123', '000', 'test1234'], + personaTag: undefined + } + ] + }); }); it('should not insert contentlet if already exist', () => { @@ -207,17 +213,53 @@ describe('utils functions', () => { pageContainers, container, contentlet, + newContentletId: 'test', language_id: 'test', pageId: 'test' }); - expect(result).toEqual([ - { - identifier: 'test', - uuid: 'test', - contentletsId: ['test'] - } - ]); + expect(result).toEqual({ + didInsert: false, + pageContainers: [ + { + identifier: 'test', + uuid: 'test', + contentletsId: ['test'] + } + ] + }); + }); + }); + + describe('url sanitize', () => { + it('should remove the slash from the start', () => { + expect(sanitizeURL('/cool')).toEqual('cool'); + }); + + it("should remove the slash from the end if it's not the only character", () => { + expect(sanitizeURL('super-cool/')).toEqual('super-cool'); + }); + + it('should remove the slash from the end and the beggining', () => { + expect(sanitizeURL('/hello-there/')).toEqual('hello-there'); + }); + + it('should remove the index if a nested path', () => { + expect(sanitizeURL('i-have-the-high-ground/index')).toEqual('i-have-the-high-ground'); + }); + + it('should remove the index if a nested path with slash', () => { + expect(sanitizeURL('no-index-please/index/')).toEqual('no-index-please'); + }); + + it('should leave as it is for valid url', () => { + expect(sanitizeURL('this-is-where-the-fun-begins')).toEqual( + 'this-is-where-the-fun-begins' + ); + }); + + it('should leave as it is for a nested valid url', () => { + expect(sanitizeURL('hello-there/general-kenobi')).toEqual('hello-there/general-kenobi'); }); }); });