diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts index 754826e13cb7..cbd57ff0a723 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.spec.ts @@ -1,7 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { of } from 'rxjs'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { ConfirmationService } from 'primeng/api'; @@ -18,6 +21,7 @@ import { DotEventsService, DotGenerateSecurePasswordService, DotLicenseService, + DotPropertiesService, DotWorkflowActionsFireService } from '@dotcms/data-access'; import { @@ -33,6 +37,7 @@ import { StringUtils, UserModel } from '@dotcms/dotcms-js'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotLoadingIndicatorService } from '@dotcms/utils'; import { CoreWebServiceMock, @@ -62,9 +67,18 @@ describe('DotCustomEventHandlerService', () => { let dotWorkflowEventHandlerService: DotWorkflowEventHandlerService; let dotEventsService: DotEventsService; let dotLicenseService: DotLicenseService; + let router: Router; - beforeEach(() => { - TestBed.configureTestingModule({ + const createFeatureFlagResponse = ( + enabled: string = 'NOT_FOUND', + contentType: string = '*' + ) => ({ + [FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]: enabled, + [FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE]: contentType + }); + + const setup = (dotPropertiesMock: unknown) => { + TestBed.resetTestingModule().configureTestingModule({ providers: [ DotCustomEventHandlerService, DotLoadingIndicatorService, @@ -100,7 +114,9 @@ describe('DotCustomEventHandlerService', () => { DotDownloadBundleDialogService, DotGenerateSecurePasswordService, LoginService, - DotLicenseService + DotLicenseService, + { provide: DotPropertiesService, useValue: dotPropertiesMock }, + Router ], imports: [RouterTestingModule, HttpClientTestingModule] }); @@ -116,6 +132,13 @@ describe('DotCustomEventHandlerService', () => { dotWorkflowEventHandlerService = TestBed.inject(DotWorkflowEventHandlerService); dotEventsService = TestBed.inject(DotEventsService); dotLicenseService = TestBed.inject(DotLicenseService); + router = TestBed.inject(Router); + }; + + beforeEach(() => { + setup({ + getKeys: () => of(createFeatureFlagResponse()) + }); }); it('should show loading indicator and go to edit page when event is emited by iframe', () => { @@ -144,6 +167,7 @@ describe('DotCustomEventHandlerService', () => { it('should create a contentlet', () => { spyOn(dotContentletEditorService, 'create'); + service.handle( new CustomEvent('ng-event', { detail: { @@ -160,6 +184,43 @@ describe('DotCustomEventHandlerService', () => { }); }); + it('should create a host', () => { + spyOn(dotContentletEditorService, 'create'); + + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'create-host', + data: { url: 'hello.world.com' } + } + }) + ); + + expect(dotContentletEditorService.create).toHaveBeenCalledWith({ + data: { + url: 'hello.world.com' + } + }); + }); + + it('should create a contentlet from edit page', () => { + spyOn(dotContentletEditorService, 'create'); + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'create-contentlet-from-edit-page', + data: { url: 'hello.world.com' } + } + }) + ); + + expect(dotContentletEditorService.create).toHaveBeenCalledWith({ + data: { + url: 'hello.world.com' + } + }); + }); + it('should edit a contentlet', () => { service.handle( new CustomEvent('ng-event', { @@ -174,6 +235,20 @@ describe('DotCustomEventHandlerService', () => { expect(dotRouterService.goToEditContentlet).toHaveBeenCalledWith('123'); }); + it('should edit a host', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-host', + data: { + inode: '123' + } + } + }) + ); + expect(dotRouterService.goToEditContentlet).toHaveBeenCalledWith('123'); + }); + it('should edit a a workflow task', () => { service.handle( new CustomEvent('ng-event', { @@ -304,4 +379,162 @@ describe('DotCustomEventHandlerService', () => { ); expect(dotLicenseService.updateLicense).toHaveBeenCalled(); }); + + describe('edit content 2 is enabled and contentTypes are catchall', () => { + beforeEach(() => { + setup({ + getKeys: () => of(createFeatureFlagResponse('true')) + }); + + spyOn(router, 'navigate'); + }); + + it('should create a contentlet', () => { + spyOn(dotContentletEditorService, 'create'); + + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'create-contentlet', + data: { contentType: 'test' } + } + }) + ); + + expect(router.navigate).toHaveBeenCalledWith(['content/new/test']); + }); + + it('should edit a a workflow task', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-task', + data: { + inode: '123', + contentType: 'test' + } + } + }) + ); + + expect(router.navigate).toHaveBeenCalledWith(['content/123']); + }); + + it('should edit a contentlet', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-contentlet', + data: { + inode: '123', + contentType: 'test' + } + } + }) + ); + expect(router.navigate).toHaveBeenCalledWith(['content/123']); + }); + }); + + describe('edit content 2 is enabled and contentTypes are limited', () => { + beforeEach(() => { + setup({ + getKeys: () => of(createFeatureFlagResponse('true', 'test,test2')) + }); + + spyOn(router, 'navigate'); + }); + + it('should create a contentlet', () => { + spyOn(dotContentletEditorService, 'create'); + + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'create-contentlet', + data: { contentType: 'test' } + } + }) + ); + + expect(router.navigate).toHaveBeenCalledWith(['content/new/test']); + }); + + it('should edit a a workflow task', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-task', + data: { + inode: '123', + contentType: 'test2' + } + } + }) + ); + + expect(router.navigate).toHaveBeenCalledWith(['content/123']); + }); + + it('should edit a contentlet', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-contentlet', + data: { + inode: '123', + contentType: 'test2' + } + } + }) + ); + expect(router.navigate).toHaveBeenCalledWith(['content/123']); + }); + + it('should not create a contentlet', () => { + spyOn(dotContentletEditorService, 'create'); + + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'create-contentlet', + data: { contentType: 'not in the list' } + } + }) + ); + + expect(router.navigate).not.toHaveBeenCalledWith(['content/new/test']); + }); + + it('should not edit a a workflow task', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-task', + data: { + inode: '123', + contentType: 'not in the list' + } + } + }) + ); + + expect(router.navigate).not.toHaveBeenCalledWith(['content/123']); + }); + + it('should not edit a contentlet', () => { + service.handle( + new CustomEvent('ng-event', { + detail: { + name: 'edit-contentlet', + data: { + inode: '123', + contentType: 'not in the list' + } + } + }) + ); + expect(router.navigate).not.toHaveBeenCalledWith(['content/123']); + }); + }); }); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts index 807fe869f2ca..af6f25a9d55f 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-custom-event-handler/dot-custom-event-handler.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; import { DotIframeService } from '@components/_common/iframe/service/dot-iframe/dot-iframe.service'; import { DotContentCompareEvent } from '@components/dot-content-compare/dot-content-compare.component'; @@ -7,9 +8,11 @@ import { DotContentletEditorService } from '@components/dot-contentlet-editor/se import { DotEventsService, DotGenerateSecurePasswordService, - DotLicenseService + DotLicenseService, + DotPropertiesService } from '@dotcms/data-access'; import { DotPushPublishDialogService, DotUiColors } from '@dotcms/dotcms-js'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotLoadingIndicatorService } from '@dotcms/utils'; import { DotDownloadBundleDialogService } from '@services/dot-download-bundle-dialog/dot-download-bundle-dialog.service'; import { DotNavLogoService } from '@services/dot-nav-logo/dot-nav-logo.service'; @@ -27,7 +30,9 @@ export const COMPARE_CUSTOM_EVENT = 'compare-contentlet'; */ @Injectable() export class DotCustomEventHandlerService { - private readonly handlers; + private handlers: Record void>; + + private contentTypesFeatureFlag: string[]; constructor( private dotLoadingIndicatorService: DotLoadingIndicatorService, @@ -41,23 +46,52 @@ export class DotCustomEventHandlerService { private dotWorkflowEventHandlerService: DotWorkflowEventHandlerService, private dotGenerateSecurePasswordService: DotGenerateSecurePasswordService, private dotEventsService: DotEventsService, - private dotLicenseService: DotLicenseService + private dotLicenseService: DotLicenseService, + private router: Router, + private dotPropertiesService: DotPropertiesService ) { - if (!this.handlers) { - this.handlers = { - 'edit-page': this.goToEditPage.bind(this), - 'edit-contentlet': this.editContentlet.bind(this), - 'edit-task': this.editTask.bind(this), - 'create-contentlet': this.createContentlet.bind(this), - 'company-info-updated': this.setPersonalization.bind(this), - 'push-publish': this.pushPublishDialog.bind(this), - 'download-bundle': this.downloadBundleDialog.bind(this), - 'workflow-wizard': this.executeWorkflowWizard.bind(this), - 'generate-secure-password': this.generateSecurePassword.bind(this), - 'compare-contentlet': this.openCompareDialog.bind(this), - 'license-changed': this.updateLicense.bind(this) - }; - } + this.dotPropertiesService + .getKeys([ + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED, + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE + ]) + .subscribe((response) => { + const contentEditorFeatureFlag = + response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] === 'true'; + const contentTypeFeatureFlag = + response[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE]; + + this.contentTypesFeatureFlag = contentTypeFeatureFlag + .split(',') + .map((item) => item.trim()); + + if (!this.handlers) { + this.handlers = { + 'edit-page': this.goToEditPage.bind(this), + 'edit-contentlet': contentEditorFeatureFlag + ? this.editContentlet.bind(this) + : this.editContentletLegacy.bind(this), + 'edit-task': contentEditorFeatureFlag + ? this.editTask.bind(this) + : this.editTaskLegacy.bind(this), + 'create-contentlet': contentEditorFeatureFlag + ? this.createContentlet.bind(this) + : this.createContentletLegacy.bind(this), + 'create-contentlet-from-edit-page': this.createContentletLegacy.bind(this), + 'company-info-updated': this.setPersonalization.bind(this), + 'push-publish': this.pushPublishDialog.bind(this), + 'download-bundle': this.downloadBundleDialog.bind(this), + 'workflow-wizard': this.executeWorkflowWizard.bind(this), + 'generate-secure-password': this.generateSecurePassword.bind(this), + 'compare-contentlet': this.openCompareDialog.bind(this), + 'license-changed': this.updateLicense.bind(this), + + // THIS NEEDS TESTING + 'edit-host': this.editContentletLegacy.bind(this), + 'create-host': this.createContentletLegacy.bind(this) + }; + } + }); } /** @@ -76,7 +110,7 @@ export class DotCustomEventHandlerService { this.dotGenerateSecurePasswordService.open($event.detail.data); } - private createContentlet($event: CustomEvent): void { + private createContentletLegacy($event: CustomEvent): void { this.dotContentletEditorService.create({ data: $event.detail.data }); @@ -84,6 +118,14 @@ export class DotCustomEventHandlerService { // this.dotRouterService.goToCreateContent($event.detail.data); } + private createContentlet($event: CustomEvent): void { + if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { + return this.createContentletLegacy($event); + } + + this.router.navigate([`content/new/${$event.detail.data.contentType}`]); + } + private goToEditPage($event: CustomEvent): void { this.dotLoadingIndicatorService.show(); this.dotRouterService.goToEditPage({ @@ -93,14 +135,30 @@ export class DotCustomEventHandlerService { }); } - private editContentlet($event: CustomEvent): void { + private editContentletLegacy($event: CustomEvent): void { this.dotRouterService.goToEditContentlet($event.detail.data.inode); } - private editTask($event: CustomEvent): void { + private editContentlet($event: CustomEvent): void { + if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { + return this.editContentletLegacy($event); + } + + this.router.navigate([`content/${$event.detail.data.inode}`]); + } + + private editTaskLegacy($event: CustomEvent): void { this.dotRouterService.goToEditTask($event.detail.data.inode); } + private editTask($event: CustomEvent): void { + if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { + return this.editTaskLegacy($event); + } + + this.router.navigate([`content/${$event.detail.data.inode}`]); + } + private setPersonalization($event: CustomEvent): void { this.dotNavLogoService.setLogo($event.detail.payload.navBarLogo); @@ -139,4 +197,19 @@ export class DotCustomEventHandlerService { private updateLicense(): void { this.dotLicenseService.updateLicense(); } + + /** + * Check if the content type is in the feature flag list + * + * @private + * @param {string} contentType + * @return {*} {boolean} + * @memberof DotCustomEventHandlerService + */ + private shouldRedirectToOldContentEditor(contentType: string): boolean { + return ( + !this.contentTypesFeatureFlag.includes('*') && + this.contentTypesFeatureFlag.indexOf(contentType) === -1 + ); + } } diff --git a/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.spec.ts new file mode 100644 index 000000000000..8e48a21a7baa --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.spec.ts @@ -0,0 +1,63 @@ +import { of } from 'rxjs'; + +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { DotPropertiesService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; + +import { editContentGuard } from './edit-content.guard'; + +describe('EditContentGuard', () => { + let dotPropertiesService: DotPropertiesService; + + const setup = (dotPropertiesServiceMock: unknown) => { + TestBed.configureTestingModule({ + providers: [ + editContentGuard, + { + provide: DotPropertiesService, + useValue: dotPropertiesServiceMock + }, + HttpClient + ], + imports: [HttpClientTestingModule] + }); + + dotPropertiesService = TestBed.inject(DotPropertiesService); + spyOn(dotPropertiesService, 'getFeatureFlag').and.callThrough(); + + return TestBed.runInInjectionContext(editContentGuard); + }; + + it('should allow access to Edit Content new form', (done) => { + const guard = setup({ + getFeatureFlag: () => of(true) + }); + + expect(dotPropertiesService.getFeatureFlag).toHaveBeenCalledWith( + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ); + + guard.subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it('should deny access to Edit Content new form', (done) => { + const guard = setup({ + getFeatureFlag: () => of(false) + }); + + expect(dotPropertiesService.getFeatureFlag).toHaveBeenCalledWith( + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ); + + guard.subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); +}); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.ts b/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.ts new file mode 100644 index 000000000000..adc483126219 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/api/services/guards/edit-content.guard.ts @@ -0,0 +1,13 @@ +import { Observable } from 'rxjs'; + +import { inject } from '@angular/core'; + +import { DotPropertiesService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; + +/** + * Check if the Edit Content new form is enabled + * @returns Observable + */ +export const editContentGuard = (): Observable => + inject(DotPropertiesService).getFeatureFlag(FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.spec.ts index 2a5606141cdb..bdbf6d625a6e 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.spec.ts @@ -26,9 +26,9 @@ describe('PagesGuardService', () => { it('should allow access to Pages Portlets', () => { let result: boolean; - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('true')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); pagesGuardService.canActivate().subscribe((res) => (result = res)); - expect(dotPropertiesService.getKey).toHaveBeenCalledWith( + expect(dotPropertiesService.getFeatureFlag).toHaveBeenCalledWith( FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE ); expect(result).toBe(true); @@ -36,9 +36,9 @@ describe('PagesGuardService', () => { it('should deny access to Pages Portlets', () => { let result: boolean; - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('false')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(false)); pagesGuardService.canActivate().subscribe((res) => (result = res)); - expect(dotPropertiesService.getKey).toHaveBeenCalledWith( + expect(dotPropertiesService.getFeatureFlag).toHaveBeenCalledWith( FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE ); expect(result).toBe(false); diff --git a/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts index 64c73c8467be..a5aa1dc20547 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/guards/pages-guard.service.ts @@ -3,8 +3,6 @@ import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; -import { map, take } from 'rxjs/operators'; - import { DotPropertiesService } from '@dotcms/data-access'; import { FeaturedFlags } from '@dotcms/dotcms-models'; @@ -20,13 +18,8 @@ export class PagesGuardService implements CanActivate { * @returns Observable */ canActivate(): Observable { - return this.dotConfigurationService - .getKey(FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE) - .pipe( - take(1), - map((enabled: string) => { - return enabled === 'true'; - }) - ); + return this.dotConfigurationService.getFeatureFlag( + FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE + ); } } diff --git a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts index e8bde63f8c7d..a3ce8f28a8f0 100644 --- a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts +++ b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts @@ -13,6 +13,7 @@ import { DotCustomReuseStrategyService } from '@shared/dot-custom-reuse-strategy import { AuthGuardService } from './api/services/guards/auth-guard.service'; import { ContentletGuardService } from './api/services/guards/contentlet-guard.service'; import { DefaultGuardService } from './api/services/guards/default-guard.service'; +import { editContentGuard } from './api/services/guards/edit-content.guard'; import { MenuGuardService } from './api/services/guards/menu-guard.service'; import { PagesGuardService } from './api/services/guards/pages-guard.service'; import { PublicAuthGuardService } from './api/services/guards/public-auth-guard.service'; @@ -94,6 +95,17 @@ const PORTLETS_ANGULAR = [ loadChildren: () => import('@portlets/dot-edit-page/dot-edit-page.module').then((m) => m.DotEditPageModule) }, + { + canActivate: [editContentGuard], + path: 'content', + loadChildren: () => import('@dotcms/edit-content').then((m) => m.DotEditContentRoutes) + }, + { + canActivate: [MenuGuardService, PagesGuardService], + path: 'pages', + loadChildren: () => + import('@portlets/dot-pages/dot-pages.module').then((m) => m.DotPagesModule) + }, { path: '', canActivate: [MenuGuardService], @@ -137,12 +149,6 @@ const PORTLETS_IFRAME = [ } ] }, - { - canActivate: [MenuGuardService, PagesGuardService], - path: 'pages', - loadChildren: () => - import('@portlets/dot-pages/dot-pages.module').then((m) => m.DotPagesModule) - }, { canActivateChild: [ContentletGuardService], path: 'add', diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts index 88da0174d3b2..282e1ad5a305 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.spec.ts @@ -174,7 +174,7 @@ describe('DotEditPageStateControllerComponent', () => { spyOn(component.modeChange, 'emit'); spyOn(dotPageStateService, 'setLock'); spyOn(personalizeService, 'personalized').and.returnValue(of(null)); - featFlagMock = spyOn(propertiesService, 'getKey').and.returnValue(of('false')); + featFlagMock = spyOn(propertiesService, 'getFeatureFlag').and.returnValue(of(false)); }); describe('elements', () => { @@ -533,7 +533,7 @@ describe('DotEditPageStateControllerComponent', () => { describe('feature flag edit URLContentMap is on', () => { beforeEach(() => { - featFlagMock.and.returnValue(of('true')); + featFlagMock.and.returnValue(of(true)); const pageRenderStateMocked: DotPageRenderState = new DotPageRenderState( { ...mockUser(), userId: '457' }, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.ts index da8d16fffce6..b105a440702a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-state-controller/dot-edit-page-state-controller.component.ts @@ -94,10 +94,9 @@ export class DotEditPageStateControllerComponent implements OnChanges, OnInit { ngOnInit(): void { this.dotPropertiesService - .getKey(this.featureFlagEditURLContentMap) - .pipe(take(1)) + .getFeatureFlag(this.featureFlagEditURLContentMap) .subscribe((result) => { - this.featureFlagEditURLContentMapIsOn = result && result === 'true'; + this.featureFlagEditURLContentMapIsOn = result; if (this.featureFlagEditURLContentMapIsOn && this.pageState.params.urlContentMap) { this.menuItems = this.getMenuItems(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.ts index d7d16350d454..52f5e962c2ee 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/components/dot-edit-page-toolbar/dot-edit-page-toolbar.component.ts @@ -10,8 +10,6 @@ import { Output } from '@angular/core'; -import { take } from 'rxjs/operators'; - import { DotLicenseService, DotPropertiesService } from '@dotcms/data-access'; import { DotCMSContentlet, @@ -55,10 +53,9 @@ export class DotEditPageToolbarComponent implements OnInit, OnChanges, OnDestroy ngOnInit() { // TODO: Remove next line when total functionality of Favorite page is done for release this.dotConfigurationService - .getKey(FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE) - .pipe(take(1)) - .subscribe((enabled: string) => { - this.showFavoritePageStar = enabled === 'true'; + .getFeatureFlag(FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE) + .subscribe((enabled) => { + this.showFavoritePageStar = enabled; }); this.isEnterpriseLicense$ = this.dotLicenseService.isEnterprise(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts index 6eccf82a56eb..c8f946b921ec 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/layout/dot-edit-layout/dot-edit-layout.component.spec.ts @@ -151,7 +151,7 @@ describe('DotEditLayoutComponent', () => { { provide: DotPropertiesService, useValue: { - getKey: () => of('false') + getFeatureFlag: () => of(false) } } ] @@ -344,7 +344,7 @@ describe('DotEditLayoutComponent', () => { describe('New Template Builder', () => { beforeEach(() => { - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('true')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); fixture.detectChanges(); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts index b4e657678afa..8c06be86daad 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-nav/dot-edit-page-nav.component.spec.ts @@ -60,6 +60,9 @@ export class MockDotPropertiesService { getKey(): Observable { return observableOf(true); } + getFeatureFlag(): Observable { + return observableOf(true); + } } @Component({ diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts index 4a7f4b66026e..3042972df6e2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.spec.ts @@ -192,7 +192,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { spyOn(component.modeChange, 'emit'); spyOn(dotPageStateService, 'setLock'); spyOn(personalizeService, 'personalized').and.returnValue(of(null)); - featFlagMock = spyOn(propertiesService, 'getKey').and.returnValue(of('false')); + featFlagMock = spyOn(propertiesService, 'getFeatureFlag').and.returnValue(of(false)); deDotTabButtons = de.query(By.css('[data-testId="dot-tabs-buttons"]')); dotTabButtons = deDotTabButtons.componentInstance; @@ -539,7 +539,7 @@ describe('DotEditPageStateControllerSeoComponent', () => { }); describe('page does not have URLContentMap and feature flag is on', () => { beforeEach(() => { - featFlagMock.and.returnValue(of('true')); + featFlagMock.and.returnValue(of(true)); const pageRenderStateMocked: DotPageRenderState = new DotPageRenderState( { ...mockUser(), userId: '486' }, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.ts index b54ac813a5cc..2b72709ae91a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-state-controller-seo/dot-edit-page-state-controller-seo.component.ts @@ -133,10 +133,10 @@ export class DotEditPageStateControllerSeoComponent implements OnInit, OnChanges ngOnInit(): void { this.dotPropertiesService - .getKey(this.featureFlagEditURLContentMap) - .pipe(take(1)) + .getFeatureFlag(this.featureFlagEditURLContentMap) + .subscribe((result) => { - this.featureFlagEditURLContentMapIsOn = result && result === 'true'; + this.featureFlagEditURLContentMapIsOn = result; if (this.featureFlagEditURLContentMapIsOn && this.pageState.params.urlContentMap) { this.menuItems = this.getMenuItems(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.spec.ts index 9f718e6e0dd4..4876a0dad617 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.spec.ts @@ -117,7 +117,7 @@ class MockDotPageStateService { @Injectable() export class MockDotPropertiesService { - getKey(): Observable { + getFeatureFlag(): Observable { return of(true); } } @@ -238,7 +238,7 @@ describe('DotEditPageToolbarSeoComponent', () => { dotMessageDisplayService = de.injector.get(DotMessageDisplayService); dotDialogService = de.injector.get(DialogService); dotPropertiesService = TestBed.inject(DotPropertiesService); - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('true')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); }); describe('elements', () => { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.ts index 7ce3bfec8ff7..76a615c61119 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/seo/components/dot-edit-page-toolbar-seo/dot-edit-page-toolbar-seo.component.ts @@ -20,8 +20,6 @@ import { TagModule } from 'primeng/tag'; import { ToolbarModule } from 'primeng/toolbar'; import { TooltipModule } from 'primeng/tooltip'; -import { take } from 'rxjs/operators'; - import { DotSecondaryToolbarModule } from '@components/dot-secondary-toolbar'; import { DotGlobalMessageModule } from '@dotcms/app/view/components/_common/dot-global-message/dot-global-message.module'; import { DotLicenseService, DotPropertiesService } from '@dotcms/data-access'; @@ -101,10 +99,9 @@ export class DotEditPageToolbarSeoComponent implements OnInit, OnChanges, OnDest ngOnInit() { // TODO: Remove next line when total functionality of Favorite page is done for release this.dotConfigurationService - .getKey(FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE) - .pipe(take(1)) - .subscribe((enabled: string) => { - this.showFavoritePageStar = enabled === 'true'; + .getFeatureFlag(FeaturedFlags.DOTFAVORITEPAGE_FEATURE_ENABLE) + .subscribe((enabled) => { + this.showFavoritePageStar = enabled; }); this.isEnterpriseLicense$ = this.dotLicenseService.isEnterprise(); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.spec.ts index acd23062212f..4e0017e86e13 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.spec.ts @@ -44,6 +44,54 @@ const mockContentType: DotCMSContentType = { workflows: [] }; +const mockContentType2: DotCMSContentType = { + baseType: 'CONTENT', + nEntries: 23, + clazz: 'com.dotcms.contenttype.model.type.ImmutableSimpleContentType', + defaultType: false, + fields: [], + fixed: false, + folder: 'SYSTEM_FOLDER', + host: 'SYSTEM_HOST', + iDate: 1667904275000, + icon: 'event_note', + id: 'ce930143870e11569f93f8a9fff5da19', + layout: [], + modDate: 1667904276000, + multilingualable: false, + name: 'Dot Favorite Page', + system: false, + systemActionMappings: {}, + variable: 'test', + versionable: true, + workflows: [] +}; + +const mockContentType3: DotCMSContentType = { + baseType: 'CONTENT', + nEntries: 23, + clazz: 'com.dotcms.contenttype.model.type.ImmutableSimpleContentType', + defaultType: false, + fields: [], + fixed: false, + folder: 'SYSTEM_FOLDER', + host: 'SYSTEM_HOST', + iDate: 1667904275000, + icon: 'event_note', + id: 'ce930143870e11569f93f8a9fff5da19', + layout: [], + modDate: 1667904276000, + multilingualable: false, + name: 'Dot Favorite Page', + system: false, + systemActionMappings: {}, + variable: 'notAvailable', + versionable: true, + workflows: [] +}; + +const mockContentTypes = [{ ...mockContentType }, { ...mockContentType2 }, { ...mockContentType3 }]; + class storeMock { get vm$() { return of({ @@ -70,7 +118,7 @@ class storeMock { { label: 'ES-es', value: 2 } ], languageLabels: { 1: 'En-en', 2: 'Es-es' }, - pageTypes: [mockContentType] + pageTypes: mockContentTypes }); } @@ -86,40 +134,49 @@ describe('DotPagesCreatePageDialogComponent', () => { let dotRouterService: DotRouterService; let store: DotPageStore; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DotPagesCreatePageDialogComponent, HttpClientTestingModule], - providers: [ - { provide: CoreWebService, useClass: CoreWebServiceMock }, - { - provide: DynamicDialogRef, - useValue: { - close: jasmine.createSpy() - } - }, - { - provide: DynamicDialogConfig, - useValue: { - data: { - contentTypeVariable: 'contentType', - onSave: jasmine.createSpy() + const setupTestingModule = async ( + isContentEditor2Enabled = false, + availableContentTypes = ['*'] + ) => { + await TestBed.resetTestingModule() + .configureTestingModule({ + imports: [DotPagesCreatePageDialogComponent, HttpClientTestingModule], + providers: [ + { provide: CoreWebService, useClass: CoreWebServiceMock }, + { + provide: DynamicDialogRef, + useValue: { + close: jasmine.createSpy() + } + }, + { + provide: DynamicDialogConfig, + useValue: { + data: { + contentTypeVariable: 'contentType', + onSave: jasmine.createSpy() + } + } + }, + { + provide: DynamicDialogConfig, + useValue: { + data: { + pageTypes: mockContentTypes, + isContentEditor2Enabled, + availableContentTypes + } } - } - }, - { - provide: DynamicDialogConfig, - useValue: { - data: [{ ...mockContentType }] - } - }, - { provide: DotPageStore, useClass: storeMock }, - { - provide: ActivatedRoute, - useClass: ActivatedRouteMock - }, - { provide: DotRouterService, useClass: MockDotRouterService } - ] - }).compileComponents(); + }, + { provide: DotPageStore, useClass: storeMock }, + { + provide: ActivatedRoute, + useClass: ActivatedRouteMock + }, + { provide: DotRouterService, useClass: MockDotRouterService } + ] + }) + .compileComponents(); store = TestBed.inject(DotPageStore); spyOn(store, 'getPageTypes'); @@ -127,8 +184,11 @@ describe('DotPagesCreatePageDialogComponent', () => { de = fixture.debugElement; dotRouterService = TestBed.inject(DotRouterService); dialogRef = TestBed.inject(DynamicDialogRef); + fixture.detectChanges(); - }); + }; + + beforeEach(async () => await setupTestingModule()); it('should have html components with attributes', () => { expect( @@ -160,7 +220,7 @@ describe('DotPagesCreatePageDialogComponent', () => { it('should set pages types data when init', () => { fixture.componentInstance.pageTypes$.subscribe((data) => { - expect(data).toEqual([{ ...mockContentType }]); + expect(data).toEqual(mockContentTypes); }); }); @@ -180,7 +240,47 @@ describe('DotPagesCreatePageDialogComponent', () => { input.nativeElement.value = 'Dot Favorite Page'; input.nativeElement.dispatchEvent(new Event('keyup')); fixture.componentInstance.pageTypes$.subscribe((data) => { - expect(data).toEqual([{ ...mockContentType }]); + expect(data).toEqual(mockContentTypes); + }); + }); + + describe("when it's content editor 2 enabled", () => { + beforeEach(async () => await setupTestingModule(true)); + + it('should redirect url when click on page', () => { + const pageType = de.query(By.css(`.dot-pages-create-page-dialog__page-item`)); + pageType.triggerEventHandler('click', mockContentType.variable); + expect(dotRouterService.goToURL).toHaveBeenCalledWith( + `content/new/${mockContentType.variable}` + ); + expect(dialogRef.close).toHaveBeenCalled(); + }); + }); + + describe("when it's content editor 2 enabled and limited content types", () => { + beforeEach(async () => await setupTestingModule(true, [mockContentType.variable, 'test'])); + + it('should redirect url when click on page', () => { + const pageType = de.query(By.css(`.dot-pages-create-page-dialog__page-item`)); + pageType.triggerEventHandler('click', mockContentType.variable); + expect(dotRouterService.goToURL).toHaveBeenCalledWith( + `content/new/${mockContentType.variable}` + ); + expect(dialogRef.close).toHaveBeenCalled(); + }); + + it('should redirect url when click on page for content type test', () => { + const pageType = de.queryAll(By.css(`.dot-pages-create-page-dialog__page-item`))[1]; + pageType.triggerEventHandler('click'); + expect(dotRouterService.goToURL).toHaveBeenCalledWith(`content/new/test`); + expect(dialogRef.close).toHaveBeenCalled(); + }); + + it('should not redirect to new edit content url when click on page', () => { + const pageType = de.queryAll(By.css(`.dot-pages-create-page-dialog__page-item`))[2]; + pageType.triggerEventHandler('click'); + expect(dotRouterService.goToURL).toHaveBeenCalledWith('/pages/new/notAvailable'); + expect(dialogRef.close).toHaveBeenCalled(); }); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.ts index 80a86b313cf7..8dad51d3b547 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-create-page-dialog/dot-pages-create-page-dialog.component.ts @@ -52,7 +52,15 @@ export class DotPagesCreatePageDialogComponent implements OnInit, OnDestroy { */ goToCreatePage(variableName: string): void { this.ref.close(); - this.dotRouterService.goToURL(`/pages/new/${variableName}`); + + // Get the feature flag from the store and change the routing + const url = + this.config.data.isContentEditor2Enabled && + !this.shouldRedirectToOldContentEditor(variableName) + ? `content/new/${variableName}` + : `/pages/new/${variableName}`; + + this.dotRouterService.goToURL(url); } ngOnInit(): void { @@ -63,17 +71,17 @@ export class DotPagesCreatePageDialogComponent implements OnInit, OnDestroy { switchMap((searchValue: string) => { if (searchValue.length) { return of( - this.config.data.filter((pageType: DotCMSContentType) => + this.config.data.pageTypes.filter((pageType: DotCMSContentType) => pageType.name .toLocaleLowerCase() .includes(searchValue.toLocaleLowerCase()) ) ); } else { - return of(this.config.data); + return of(this.config.data.pageTypes); } }), - startWith(this.config.data) + startWith(this.config.data.pageTypes) ); } @@ -81,4 +89,19 @@ export class DotPagesCreatePageDialogComponent implements OnInit, OnDestroy { this.destroy$.next(true); this.destroy$.complete(); } + + /** + * Check if the content type is in the feature flag list + * + * @private + * @param {string} contentType + * @return {*} {boolean} + * @memberof DotCustomEventHandlerService + */ + private shouldRedirectToOldContentEditor(contentType: string): boolean { + return ( + !this.config.data.availableContentTypes.includes('*') && + this.config.data.availableContentTypes.indexOf(contentType) === -1 + ); + } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-favorite-panel.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-favorite-panel.component.spec.ts index 3d79af5dc024..e708ed42a15e 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-favorite-panel.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-favorite-panel/dot-pages-favorite-panel.component.spec.ts @@ -107,7 +107,8 @@ describe('DotPagesFavoritePanelComponent', () => { actionMenuDomId: '', items: [], addToBundleCTId: 'test1' - } + }, + isContentEditor2Enabled: false }); } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.spec.ts index a4beba7d5c99..385d095e309b 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-listing-panel/dot-pages-listing-panel.component.spec.ts @@ -84,7 +84,8 @@ describe('DotPagesListingPanelComponent', () => { { label: 'En-en', value: 1 }, { label: 'ES-es', value: 2 } ], - languageLabels: { 1: 'En-en', 2: 'Es-es' } + languageLabels: { 1: 'En-en', 2: 'Es-es' }, + isContentEditor2Enabled: false }); } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts index 9fb4d5a2b8f0..6e0a0a4082a2 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.spec.ts @@ -28,6 +28,7 @@ import { DotLocalstorageService, DotPageTypesService, DotPageWorkflowsActionsService, + DotPropertiesService, DotRenderMode, DotWorkflowActionsFireService, DotWorkflowsActionsService, @@ -112,6 +113,7 @@ describe('DotPageStore', () => { let dotHttpErrorManagerService: DotHttpErrorManagerService; let dotFavoritePageService: DotFavoritePageService; let dotLocalstorageService: DotLocalstorageService; + let dotPropertiesService: DotPropertiesService; beforeEach(() => { TestBed.configureTestingModule({ @@ -131,6 +133,7 @@ describe('DotPageStore', () => { StringUtils, DotFavoritePageService, DotLocalstorageService, + DotPropertiesService, { provide: DialogService, useClass: DialogServiceMock }, { provide: DotcmsEventsService, useClass: DotcmsEventsServiceMock }, { provide: CoreWebService, useClass: CoreWebServiceMock }, @@ -157,10 +160,13 @@ describe('DotPageStore', () => { dotWorkflowActionsFireService = TestBed.inject(DotWorkflowActionsFireService); dotFavoritePageService = TestBed.inject(DotFavoritePageService); dotLocalstorageService = TestBed.inject(DotLocalstorageService); + dotPropertiesService = TestBed.inject(DotPropertiesService); spyOn(dialogService, 'open').and.callThrough(); spyOn(dotHttpErrorManagerService, 'handle'); spyOn(dotLocalstorageService, 'getItem').and.returnValue(`true`); + spyOn(dotPropertiesService, 'getKey').and.returnValue(of('*')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(false)); dotPageStore.setInitialStateData(5); dotPageStore.setKeyword('test'); @@ -424,7 +430,11 @@ describe('DotPageStore', () => { expect(dialogService.open).toHaveBeenCalledWith(DotPagesCreatePageDialogComponent, { header: 'create.page', width: '58rem', - data: expectedInputArray + data: { + pageTypes: expectedInputArray, + isContentEditor2Enabled: false, + availableContentTypes: ['*'] + } }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.ts index 890b3cab8f35..ee0ff377e82f 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-pages/dot-pages-store/dot-pages.store.ts @@ -26,6 +26,7 @@ import { DotMessageService, DotPageTypesService, DotPageWorkflowsActionsService, + DotPropertiesService, DotRenderMode, DotWorkflowActionsFireService, DotWorkflowsActionsService, @@ -43,6 +44,7 @@ import { DotLanguage, DotPermissionsType, ESContent, + FeaturedFlags, PermissionsType, UserPermissions } from '@dotcms/dotcms-models'; @@ -82,6 +84,8 @@ export interface DotPagesState { pages?: DotPagesInfo; pageTypes?: DotCMSContentType[]; portletStatus: ComponentStatus; + isContentEditor2Enabled: boolean; + availableContentTypes: string[]; } export interface DotSessionStorageFilter { @@ -125,6 +129,14 @@ export class DotPageStore extends ComponentStore { state.pages.status === ComponentStatus.INIT ); + readonly isContentEditor2Enabled$: Observable = this.select( + (state) => state.isContentEditor2Enabled + ); + + readonly availableContentTypes$: Observable = this.select( + (state) => state.availableContentTypes + ); + readonly isPortletLoading$: Observable = this.select( (state) => state.portletStatus === ComponentStatus.LOADING || @@ -334,7 +346,11 @@ export class DotPageStore extends ComponentStore { this.dialogService.open(DotPagesCreatePageDialogComponent, { header: this.dotMessageService.get('create.page'), width: '58rem', - data: pageTypes + data: { + pageTypes, + isContentEditor2Enabled: this.get().isContentEditor2Enabled, + availableContentTypes: this.get().availableContentTypes + } }); }, (error: HttpErrorResponse) => { @@ -518,6 +534,8 @@ export class DotPageStore extends ComponentStore { this.languageIdValue$, this.showArchivedValue$, this.pageTypes$, + this.isContentEditor2Enabled$, + this.availableContentTypes$, ( { favoritePages, @@ -536,7 +554,9 @@ export class DotPageStore extends ComponentStore { keywordValue, languageIdValue, showArchivedValue, - pageTypes + pageTypes, + isContentEditor2Enabled, + availableContentTypes ) => ({ favoritePages, isEnterprise, @@ -553,7 +573,9 @@ export class DotPageStore extends ComponentStore { keywordValue, languageIdValue, showArchivedValue, - pageTypes + pageTypes, + isContentEditor2Enabled, + availableContentTypes }) ); @@ -758,7 +780,10 @@ export class DotPageStore extends ComponentStore { ? this.dotMessageService.get('Edit') : this.dotMessageService.get('View'), command: () => { - this.dotRouterService.goToEditContentlet(item.inode); + this.get().isContentEditor2Enabled && + !this.shouldRedirectToOldContentEditor(item.contentType) + ? this.dotRouterService.goToURL(`content/${item.inode}`) + : this.dotRouterService.goToEditContentlet(item.inode); } }); } @@ -840,7 +865,8 @@ export class DotPageStore extends ComponentStore { private pushPublishService: PushPublishService, private siteService: SiteService, private dotFavoritePageService: DotFavoritePageService, - private dotLocalstorageService: DotLocalstorageService + private dotLocalstorageService: DotLocalstorageService, + private dotPropertiesService: DotPropertiesService ) { super(null); } @@ -860,7 +886,13 @@ export class DotPageStore extends ComponentStore { .getEnvironments() .pipe(map((environments: DotEnvironment[]) => !!environments.length)), this.getSessionStorageFilterParams(), - this.getLocalStorageFavoritePanelParams() + this.getLocalStorageFavoritePanelParams(), + this.dotPropertiesService.getFeatureFlag( + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ), + this.dotPropertiesService + .getKey(FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE) + .pipe(map((contentTypes) => contentTypes.split(','))) ]) .pipe( take(1), @@ -872,7 +904,9 @@ export class DotPageStore extends ComponentStore { isEnterprise, environments, filterParams, - collapsedParam + collapsedParam, + isContentEditor2Enabled, + availableContentTypes ]: [ ESContent, DotCurrentUser, @@ -880,7 +914,9 @@ export class DotPageStore extends ComponentStore { boolean, boolean, DotSessionStorageFilter, - boolean + boolean, + boolean, + string[] ]) => { return this.dotCurrentUser .getUserPermissions( @@ -899,7 +935,9 @@ export class DotPageStore extends ComponentStore { environments, permissionsType, filterParams, - collapsedParam + collapsedParam, + isContentEditor2Enabled, + availableContentTypes ]; }) ); @@ -915,7 +953,9 @@ export class DotPageStore extends ComponentStore { environments, permissions, filterParams, - collapsedParam + collapsedParam, + isContentEditor2Enabled, + availableContentTypes ]: [ ESContent, DotCurrentUser, @@ -924,7 +964,9 @@ export class DotPageStore extends ComponentStore { boolean, DotPermissionsType, DotSessionStorageFilter, - boolean + boolean, + boolean, + string[] ]): void => { this.setState({ favoritePages: { @@ -956,7 +998,9 @@ export class DotPageStore extends ComponentStore { archived: filterParams?.archived || false, status: ComponentStatus.INIT }, - portletStatus: ComponentStatus.LOADED + portletStatus: ComponentStatus.LOADED, + isContentEditor2Enabled, + availableContentTypes }); }, () => { @@ -986,7 +1030,9 @@ export class DotPageStore extends ComponentStore { keyword: '', status: ComponentStatus.INIT }, - portletStatus: ComponentStatus.LOADED + portletStatus: ComponentStatus.LOADED, + isContentEditor2Enabled: false, + availableContentTypes: ['*'] }); } ); @@ -1065,4 +1111,19 @@ export class DotPageStore extends ComponentStore { (error) => this.httpErrorManagerService.handle(error, true) ); } + + /** + * Check if the content type is in the feature flag list + * + * @private + * @param {string} contentType + * @return {*} {boolean} + * @memberof DotCustomEventHandlerService + */ + private shouldRedirectToOldContentEditor(contentType: string): boolean { + return ( + !this.get().availableContentTypes.includes('*') && + this.get().availableContentTypes.indexOf(contentType) === -1 + ); + } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts index df2655fcd908..700cc9cf3352 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.spec.ts @@ -181,7 +181,7 @@ describe('DotTemplateBuilderComponent', () => { { provide: DotPropertiesService, useValue: { - getKey: () => of('false') + getFeatureFlag: () => of(false) } }, DotEventsService, @@ -264,7 +264,7 @@ describe('DotTemplateBuilderComponent', () => { theme: '123', live: true }; - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('true')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(true)); fixture.detectChanges(); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts index 69c0c15b8b8c..402f2960c737 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-templates/dot-template-create-edit/dot-template-builder/dot-template-builder.component.ts @@ -10,7 +10,7 @@ import { ViewChild } from '@angular/core'; -import { debounceTime, map, takeUntil } from 'rxjs/operators'; +import { debounceTime, takeUntil } from 'rxjs/operators'; import { IframeComponent } from '@components/_common/iframe/iframe-component'; import { DotPropertiesService } from '@dotcms/data-access'; @@ -38,9 +38,8 @@ export class DotTemplateBuilderComponent implements OnInit, OnDestroy { permissionsUrl = ''; historyUrl = ''; readonly featureFlag = FeaturedFlags.FEATURE_FLAG_TEMPLATE_BUILDER; - featureFlagIsOn$ = this.propertiesService - .getKey(this.featureFlag) - .pipe(map((result) => result && result === 'true')); + featureFlagIsOn$ = this.propertiesService.getFeatureFlag(this.featureFlag); + templateUpdate$ = new Subject(); destroy$: Subject = new Subject(); lastTemplate: DotTemplateItem; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.spec.ts index ed9ea498d37b..217118482b6c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.spec.ts @@ -1,4 +1,4 @@ -import { of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { HttpClientModule } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; @@ -22,7 +22,7 @@ describe('DotFeatureFlagResolver', () => { dotConfigurationService = TestBed.inject(DotPropertiesService); }); - it('should return an observable of boolean values', () => { + it('should return an observable of boolean values', (done) => { const route: ActivatedRouteSnapshot = { data: { featuredFlagsToCheck: ['flag1', 'flag2'] @@ -44,20 +44,23 @@ describe('DotFeatureFlagResolver', () => { queryParamMap: undefined }; - const expectedFlags = { - flag1: 'true', - flag2: 'false' - }; - - const expectedFlagsResult = { + const expectedFlagsResult: Record = { flag1: true, flag2: false }; - spyOn(dotConfigurationService, 'getKeys').and.returnValue(of(expectedFlags)); + spyOn(dotConfigurationService, 'getFeatureFlags').and.returnValue(of(expectedFlagsResult)); - resolver.resolve(route).subscribe((result) => { - expect(result).toEqual(expectedFlagsResult); - }); + (resolver.resolve(route) as Observable>).subscribe( + (result: Record) => { + expect(dotConfigurationService.getFeatureFlags).toHaveBeenCalledWith([ + 'flag1', + 'flag2' + ]); + + expect(result).toEqual(expectedFlagsResult); + done(); + } + ); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.ts index 1bfda8b494cb..25a8f59ac999 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/resolvers/dot-feature-flag-resolver.service.ts @@ -3,8 +3,6 @@ import { Observable, of } from 'rxjs'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; -import { map } from 'rxjs/operators'; - import { DotPropertiesService } from '@dotcms/data-access'; /** @@ -21,28 +19,14 @@ import { DotPropertiesService } from '@dotcms/data-access'; * } */ @Injectable() -export class DotFeatureFlagResolver implements Resolve>> { +export class DotFeatureFlagResolver + implements Resolve> | Observable> +{ constructor(private readonly dotConfigurationService: DotPropertiesService) {} resolve(route: ActivatedRouteSnapshot) { if (route.data.featuredFlagsToCheck) { - return this.dotConfigurationService.getKeys(route.data.featuredFlagsToCheck).pipe( - map((result) => - route.data.featuredFlagsToCheck.reduce( - ( - acc: { - [key: string]: boolean; - }, - key: string - ) => { - acc[key] = result && result[key] === 'true'; - - return acc; - }, - {} - ) - ) - ); + return this.dotConfigurationService.getFeatureFlags(route.data.featuredFlagsToCheck); } return of(false); diff --git a/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.spec.ts b/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.spec.ts index 48dc8d4ab1f9..633ca480f313 100644 --- a/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.spec.ts @@ -32,7 +32,7 @@ describe('DotShowHideFeatureDirective', () => { providers: [ ViewContainerRef, TemplateRef, - { provide: DotPropertiesService, useValue: { getKey: () => of('true') } } + { provide: DotPropertiesService, useValue: { getFeatureFlag: () => of(true) } } ] }); @@ -54,7 +54,7 @@ describe('DotShowHideFeatureDirective', () => { describe('with feature flag disabled', () => { beforeEach(() => { - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('false')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(false)); fixture.detectChanges(); }); @@ -79,8 +79,7 @@ describe('DotShowHideFeatureDirective', () => { + [ngTemplateOutlet]="enabledComponent"> ` }) class TestWithAlternateTemplateComponent { @@ -98,7 +97,10 @@ describe('DotShowHideFeatureDirective with alternate template', () => { providers: [ ViewContainerRef, TemplateRef, - { provide: DotPropertiesService, useValue: { getKey: () => of('true') } } + { + provide: DotPropertiesService, + useValue: { getFeatureFlag: () => of(true) } + } ] }); @@ -128,7 +130,7 @@ describe('DotShowHideFeatureDirective with alternate template', () => { describe('with feature flag disabled', () => { beforeEach(() => { - spyOn(dotPropertiesService, 'getKey').and.returnValue(of('false')); + spyOn(dotPropertiesService, 'getFeatureFlag').and.returnValue(of(false)); fixture.detectChanges(); }); diff --git a/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.ts b/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.ts index 445a8ff56482..cd81bc207df4 100644 --- a/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.ts +++ b/core-web/apps/dotcms-ui/src/app/shared/directives/dot-show-hide-feature/dot-show-hide-feature.directive.ts @@ -1,7 +1,5 @@ import { Component, Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; -import { take } from 'rxjs/operators'; - import { DotPropertiesService } from '@dotcms/data-access'; import { FeaturedFlags } from '@dotcms/dotcms-models'; @@ -65,22 +63,18 @@ export class DotShowHideFeatureDirective implements OnInit { ) {} ngOnInit() { - this.dotPropertiesService - .getKey(this._featureFlag) - .pipe(take(1)) - .subscribe((value) => { - const isEnabled = value && value === 'true'; - this.viewContainer.clear(); + this.dotPropertiesService.getFeatureFlag(this._featureFlag).subscribe((isEnabled) => { + this.viewContainer.clear(); - if (isEnabled) { - this.viewContainer.createEmbeddedView(this.templateRef); - } else if (this.alternateTemplateRef) { - this.viewContainer.createEmbeddedView(this.alternateTemplateRef); - } else { - console.warn( - `Feature flag "${this._featureFlag}" doesn't exist or is disabled and no alternate template was provided` - ); - } - }); + if (isEnabled) { + this.viewContainer.createEmbeddedView(this.templateRef); + } else if (this.alternateTemplateRef) { + this.viewContainer.createEmbeddedView(this.alternateTemplateRef); + } else { + console.warn( + `Feature flag "${this._featureFlag}" doesn't exist or is disabled and no alternate template was provided` + ); + } + }); } } diff --git a/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.spec.ts b/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.spec.ts index 251c4f02b1ef..37830176fe71 100644 --- a/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.spec.ts @@ -10,7 +10,8 @@ import { DotPropertiesService } from './dot-properties.service'; const fakeResponse = { entity: { key1: 'data', - list: ['1', '2'] + list: ['1', '2'], + featureFlag: 'true' } }; @@ -74,6 +75,38 @@ describe('DotPropertiesService', () => { req.flush(apiResponse); }); + it('should get feature flag value', (done) => { + const featureFlag = 'featureFlag'; + expect(service).toBeTruthy(); + + service.getFeatureFlag(featureFlag).subscribe((response) => { + expect(response).toEqual(true); + done(); + }); + const req = httpMock.expectOne(`/api/v1/configuration/config?keys=${featureFlag}`); + expect(req.request.method).toBe('GET'); + req.flush(fakeResponse); + }); + + it('should get feature flag values', (done) => { + const featureFlags = ['featureFlag', 'featureFlag2']; + const apiResponse = { + entity: { + featureFlag: 'true', + featureFlag2: 'NOT_FOUND' + } + }; + + service.getFeatureFlags(featureFlags).subscribe((response) => { + expect(response['featureFlag']).toBe(true); + expect(response['featureFlag2']).toBe(false); + done(); + }); + const req = httpMock.expectOne(`/api/v1/configuration/config?keys=${featureFlags.join()}`); + expect(req.request.method).toBe('GET'); + req.flush(apiResponse); + }); + describe('getKey', () => { it('should return the value of a key if it exists in featureConfig', (done) => { const key = 'existingKey'; diff --git a/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts b/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts index 301af16fd45d..c1282d9c3bfc 100644 --- a/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts +++ b/core-web/libs/data-access/src/lib/dot-properties/dot-properties.service.ts @@ -3,7 +3,7 @@ import { Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { pluck, take } from 'rxjs/operators'; +import { map, pluck, take } from 'rxjs/operators'; import { FeaturedFlags } from '@dotcms/dotcms-models'; @@ -67,6 +67,36 @@ export class DotPropertiesService { .pipe(take(1), pluck('entity', key)); } + /** + * Get the value of specific feature flag + * + * @param {string} key + * @return {*} {Observable} + * @memberof DotPropertiesService + */ + getFeatureFlag(key: string): Observable { + return this.getKey(key).pipe(map((value) => value === 'true')); + } + + /** + * Get the values of specific feature flags + * + * @param {string[]} keys + * @return {*} {Observable>} + * @memberof DotPropertiesService + */ + getFeatureFlags(keys: string[]): Observable> { + return this.getKeys(keys).pipe( + map((flags) => { + return Object.keys(flags).reduce((acc, key) => { + acc[key] = flags[key] === 'true'; + + return acc; + }, {} as Record); + }) + ); + } + /** * Loads the configuration for the feature flags by calling the `getKeys` method and subscribing to the result. * @returns An observable that emits the feature configuration object. diff --git a/core-web/libs/dotcms-models/src/lib/shared-models.ts b/core-web/libs/dotcms-models/src/lib/shared-models.ts index 97722d88af51..e8c2cbb6067d 100644 --- a/core-web/libs/dotcms-models/src/lib/shared-models.ts +++ b/core-web/libs/dotcms-models/src/lib/shared-models.ts @@ -21,7 +21,9 @@ export enum FeaturedFlags { FEATURE_FLAG_TEMPLATE_BUILDER = 'FEATURE_FLAG_TEMPLATE_BUILDER_2', FEATURE_FLAG_SEO_IMPROVEMENTS = 'FEATURE_FLAG_SEO_IMPROVEMENTS', FEATURE_FLAG_SEO_PAGE_TOOLS = 'FEATURE_FLAG_SEO_PAGE_TOOLS', - FEATURE_FLAG_EDIT_URL_CONTENT_MAP = 'FEATURE_FLAG_EDIT_URL_CONTENT_MAP' + FEATURE_FLAG_EDIT_URL_CONTENT_MAP = 'FEATURE_FLAG_EDIT_URL_CONTENT_MAP', + FEATURE_FLAG_CONTENT_EDITOR2_ENABLED = 'CONTENT_EDITOR2_ENABLED', + FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE = 'CONTENT_EDITOR2_CONTENT_TYPE' } export type DotDropdownGroupSelectOption = { diff --git a/core-web/libs/edit-content/.eslintrc.json b/core-web/libs/edit-content/.eslintrc.json new file mode 100644 index 000000000000..8ddd5c22164e --- /dev/null +++ b/core-web/libs/edit-content/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "dot", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "dot", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nrwl/nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": {} + } + ] +} diff --git a/core-web/libs/edit-content/README.md b/core-web/libs/edit-content/README.md new file mode 100644 index 000000000000..9a4afe550479 --- /dev/null +++ b/core-web/libs/edit-content/README.md @@ -0,0 +1,7 @@ +# edit-content + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test edit-content` to execute the unit tests. diff --git a/core-web/libs/edit-content/jest.config.ts b/core-web/libs/edit-content/jest.config.ts new file mode 100644 index 000000000000..657ede88d120 --- /dev/null +++ b/core-web/libs/edit-content/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'edit-content', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$' + } + }, + coverageDirectory: '../../coverage/libs/edit-content', + transform: { + '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular' + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment' + ] +}; diff --git a/core-web/libs/edit-content/project.json b/core-web/libs/edit-content/project.json new file mode 100644 index 000000000000..b89646f2d8b9 --- /dev/null +++ b/core-web/libs/edit-content/project.json @@ -0,0 +1,31 @@ +{ + "name": "edit-content", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/edit-content/src", + "prefix": "dotcms", + "targets": { + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/edit-content/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/edit-content/**/*.ts", "libs/edit-content/**/*.html"] + } + } + }, + "tags": [] +} diff --git a/core-web/libs/edit-content/src/index.ts b/core-web/libs/edit-content/src/index.ts new file mode 100644 index 000000000000..6f686d9312b6 --- /dev/null +++ b/core-web/libs/edit-content/src/index.ts @@ -0,0 +1 @@ +export * from './lib/edit-content.routes'; diff --git a/core-web/libs/edit-content/src/lib/edit-content.routes.ts b/core-web/libs/edit-content/src/lib/edit-content.routes.ts new file mode 100644 index 000000000000..d473280c66c3 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/edit-content.routes.ts @@ -0,0 +1,15 @@ +import { Route } from '@angular/router'; + +import { EditContentShellComponent } from './edit-content.shell.component'; +import { FormComponent } from './feature/form/form.component'; + +export const DotEditContentRoutes: Route[] = [ + { + path: '', + component: EditContentShellComponent, + children: [ + { path: 'new/:contentType', title: 'Create Content', component: FormComponent }, + { path: ':id', title: 'Edit Content', component: FormComponent } + ] + } +]; diff --git a/core-web/libs/edit-content/src/lib/edit-content.shell.component.html b/core-web/libs/edit-content/src/lib/edit-content.shell.component.html new file mode 100644 index 000000000000..0680b43f9c6a --- /dev/null +++ b/core-web/libs/edit-content/src/lib/edit-content.shell.component.html @@ -0,0 +1 @@ + diff --git a/core-web/libs/edit-content/src/lib/edit-content.shell.component.scss b/core-web/libs/edit-content/src/lib/edit-content.shell.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/core-web/libs/edit-content/src/lib/edit-content.shell.component.spec.ts b/core-web/libs/edit-content/src/lib/edit-content.shell.component.spec.ts new file mode 100644 index 000000000000..edbc17647afc --- /dev/null +++ b/core-web/libs/edit-content/src/lib/edit-content.shell.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditContentShellComponent } from './edit-content.shell.component'; + +describe('EditContentComponent', () => { + let component: EditContentShellComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditContentShellComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(EditContentShellComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/edit-content.shell.component.ts b/core-web/libs/edit-content/src/lib/edit-content.shell.component.ts new file mode 100644 index 000000000000..7fa0e9de2fe3 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/edit-content.shell.component.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'dot-edit-content', + standalone: true, + imports: [CommonModule, RouterModule], + templateUrl: './edit-content.shell.component.html', + styleUrls: ['./edit-content.shell.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditContentShellComponent {} diff --git a/core-web/libs/edit-content/src/lib/feature/form/form.component.html b/core-web/libs/edit-content/src/lib/feature/form/form.component.html new file mode 100644 index 000000000000..3721d8ef64f3 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/feature/form/form.component.html @@ -0,0 +1 @@ +

form works for {{ contentType ?? identifier }}

diff --git a/core-web/libs/edit-content/src/lib/feature/form/form.component.scss b/core-web/libs/edit-content/src/lib/feature/form/form.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/core-web/libs/edit-content/src/lib/feature/form/form.component.spec.ts b/core-web/libs/edit-content/src/lib/feature/form/form.component.spec.ts new file mode 100644 index 000000000000..bcb0b5b1a787 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/feature/form/form.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { FormComponent } from './form.component'; + +describe('FormComponent', () => { + let component: FormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormComponent, RouterTestingModule] + }).compileComponents(); + + fixture = TestBed.createComponent(FormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/feature/form/form.component.ts b/core-web/libs/edit-content/src/lib/feature/form/form.component.ts new file mode 100644 index 000000000000..c0a5f2092328 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/feature/form/form.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'dot-edit-content-form', + standalone: true, + imports: [CommonModule], + templateUrl: './form.component.html', + styleUrls: ['./form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormComponent { + private activatedRoute = inject(ActivatedRoute); + + public contentType = this.activatedRoute.snapshot.params['contentType']; + public identifier = this.activatedRoute.snapshot.params['id']; +} diff --git a/core-web/libs/edit-content/src/test-setup.ts b/core-web/libs/edit-content/src/test-setup.ts new file mode 100644 index 000000000000..1100b3e8a6ed --- /dev/null +++ b/core-web/libs/edit-content/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular/setup-jest'; diff --git a/core-web/libs/edit-content/tsconfig.json b/core-web/libs/edit-content/tsconfig.json new file mode 100644 index 000000000000..d789346cd607 --- /dev/null +++ b/core-web/libs/edit-content/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/core-web/libs/edit-content/tsconfig.lib.json b/core-web/libs/edit-content/tsconfig.lib.json new file mode 100644 index 000000000000..ba8e6019d0ae --- /dev/null +++ b/core-web/libs/edit-content/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/test-setup.ts", "src/**/*.spec.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/core-web/libs/edit-content/tsconfig.spec.json b/core-web/libs/edit-content/tsconfig.spec.json new file mode 100644 index 000000000000..ee34e10d079a --- /dev/null +++ b/core-web/libs/edit-content/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/core-web/tsconfig.base.json b/core-web/tsconfig.base.json index 31ae64895219..9a2f3485da4e 100644 --- a/core-web/tsconfig.base.json +++ b/core-web/tsconfig.base.json @@ -30,6 +30,7 @@ "@dotcms/dotcms-models": ["libs/dotcms-models/src/index.ts"], "@dotcms/dotcms-webcomponents": ["libs/dotcms-webcomponents/src/index.ts"], "@dotcms/dotcms-webcomponents/loader": ["dist/libs/dotcms-webcomponents/loader"], + "@dotcms/edit-content": ["libs/edit-content/src/index.ts"], "@dotcms/portlets/dot-experiments/data-access": [ "libs/portlets/dot-experiments/data-access/src/index.ts" ], diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index 6e722b0e8091..614ee6127f21 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -65,7 +65,7 @@ public class ConfigurationResource implements Serializable { new String[] {"EMAIL_SYSTEM_ADDRESS", "CHARSET","CONTENT_PALETTE_HIDDEN_CONTENT_TYPES", "FEATURE_FLAG_EXPERIMENTS", "DOTFAVORITEPAGE_FEATURE_ENABLE", "FEATURE_FLAG_TEMPLATE_BUILDER_2", "SHOW_VIDEO_THUMBNAIL", "EXPERIMENTS_MIN_DURATION", "EXPERIMENTS_MAX_DURATION", "FEATURE_FLAG_SEO_IMPROVEMENTS", - "FEATURE_FLAG_SEO_PAGE_TOOLS", "FEATURE_FLAG_EDIT_URL_CONTENT_MAP", "FEATURE_FLAG_NEW_BINARY_FIELD"})); + "FEATURE_FLAG_SEO_PAGE_TOOLS", "FEATURE_FLAG_EDIT_URL_CONTENT_MAP", "CONTENT_EDITOR2_ENABLED", "CONTENT_EDITOR2_CONTENT_TYPE", "FEATURE_FLAG_NEW_BINARY_FIELD" })); private boolean isOnBlackList(final String key) { diff --git a/dotCMS/src/main/webapp/html/ng-contentlet-selector.jsp b/dotCMS/src/main/webapp/html/ng-contentlet-selector.jsp index 9852111e69e8..49ce9b1e5bc7 100644 --- a/dotCMS/src/main/webapp/html/ng-contentlet-selector.jsp +++ b/dotCMS/src/main/webapp/html/ng-contentlet-selector.jsp @@ -116,13 +116,30 @@