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 240c48d3fcb3..2bc7b8c753eb 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 @@ -13,6 +13,7 @@ import { DotContentletEditorService } from '@components/dot-contentlet-editor/se import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService, + DotContentTypeService, DotCurrentUserService, DotEventsService, DotGenerateSecurePasswordService, @@ -42,7 +43,7 @@ import { StringUtils, UserModel } from '@dotcms/dotcms-js'; -import { FeaturedFlags } from '@dotcms/dotcms-models'; +import { DotCMSContentType, FeaturedFlags } from '@dotcms/dotcms-models'; import { DotLoadingIndicatorService } from '@dotcms/utils'; import { CoreWebServiceMock, @@ -67,6 +68,7 @@ describe('DotCustomEventHandlerService', () => { let dotWorkflowEventHandlerService: DotWorkflowEventHandlerService; let dotEventsService: DotEventsService; let dotLicenseService: DotLicenseService; + let dotContentTypeService: DotContentTypeService; let router: Router; const createFeatureFlagResponse = ( @@ -119,7 +121,8 @@ describe('DotCustomEventHandlerService', () => { LoginService, DotLicenseService, { provide: DotPropertiesService, useValue: dotPropertiesMock }, - Router + Router, + DotContentTypeService ], imports: [RouterTestingModule, HttpClientTestingModule] }); @@ -135,9 +138,15 @@ describe('DotCustomEventHandlerService', () => { dotWorkflowEventHandlerService = TestBed.inject(DotWorkflowEventHandlerService); dotEventsService = TestBed.inject(DotEventsService); dotLicenseService = TestBed.inject(DotLicenseService); + dotContentTypeService = TestBed.inject(DotContentTypeService); router = TestBed.inject(Router); }; + const metadata = {}; + const metadata2 = {}; + metadata[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = true; + metadata2[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = false; + beforeEach(() => { setup({ getKeys: () => of(createFeatureFlagResponse()) @@ -390,6 +399,9 @@ describe('DotCustomEventHandlerService', () => { }); spyOn(router, 'navigate'); + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata } as DotCMSContentType) + ); }); it('should create a contentlet', () => { @@ -449,6 +461,9 @@ describe('DotCustomEventHandlerService', () => { }); it('should create a contentlet', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata } as DotCMSContentType) + ); spyOn(dotContentletEditorService, 'create'); service.handle( @@ -464,6 +479,9 @@ describe('DotCustomEventHandlerService', () => { }); it('should edit a a workflow task', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata } as DotCMSContentType) + ); service.handle( new CustomEvent('ng-event', { detail: { @@ -480,6 +498,9 @@ describe('DotCustomEventHandlerService', () => { }); it('should edit a contentlet', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata } as DotCMSContentType) + ); service.handle( new CustomEvent('ng-event', { detail: { @@ -495,6 +516,9 @@ describe('DotCustomEventHandlerService', () => { }); it('should not create a contentlet', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata: metadata2 } as DotCMSContentType) + ); spyOn(dotContentletEditorService, 'create'); service.handle( @@ -510,6 +534,10 @@ describe('DotCustomEventHandlerService', () => { }); it('should not edit a a workflow task', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata: metadata2 } as DotCMSContentType) + ); + service.handle( new CustomEvent('ng-event', { detail: { @@ -526,6 +554,9 @@ describe('DotCustomEventHandlerService', () => { }); it('should not edit a contentlet', () => { + spyOn(dotContentTypeService, 'getContentType').and.returnValue( + of({ metadata: metadata2 } as DotCMSContentType) + ); service.handle( new CustomEvent('ng-event', { detail: { 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 b06dccf0ff60..ec58e7a05f44 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,10 +1,13 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; +import { take } from 'rxjs/operators'; + import { DotContentCompareEvent } from '@components/dot-content-compare/dot-content-compare.component'; import { DotCMSEditPageEvent } from '@components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component'; import { DotContentletEditorService } from '@components/dot-contentlet-editor/services/dot-contentlet-editor.service'; import { + DotContentTypeService, DotEventsService, DotGenerateSecurePasswordService, DotLicenseService, @@ -14,7 +17,7 @@ import { DotWorkflowEventHandlerService } from '@dotcms/data-access'; import { DotPushPublishDialogService, DotUiColors } from '@dotcms/dotcms-js'; -import { FeaturedFlags } from '@dotcms/dotcms-models'; +import { DotCMSContentType, 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'; @@ -32,8 +35,6 @@ export const COMPARE_CUSTOM_EVENT = 'compare-contentlet'; export class DotCustomEventHandlerService { private handlers: Record void>; - private contentTypesFeatureFlag: string[]; - constructor( private dotLoadingIndicatorService: DotLoadingIndicatorService, private dotRouterService: DotRouterService, @@ -48,22 +49,14 @@ export class DotCustomEventHandlerService { private dotEventsService: DotEventsService, private dotLicenseService: DotLicenseService, private router: Router, - private dotPropertiesService: DotPropertiesService + private dotPropertiesService: DotPropertiesService, + private dotContentTypeService: DotContentTypeService ) { this.dotPropertiesService - .getKeys([ - FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED, - FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_CONTENT_TYPE - ]) + .getKeys([FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]) .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 = { @@ -119,11 +112,16 @@ export class DotCustomEventHandlerService { } private createContentlet($event: CustomEvent): void { - if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { - return this.createContentletLegacy($event); - } + this.dotContentTypeService + .getContentType($event.detail.data.contentType) + .pipe(take(1)) + .subscribe((contentType) => { + if (this.shouldRedirectToOldContentEditor(contentType)) { + return this.createContentletLegacy($event); + } - this.router.navigate([`content/new/${$event.detail.data.contentType}`]); + this.router.navigate([`content/new/${$event.detail.data.contentType}`]); + }); } private goToEditPage($event: CustomEvent): void { @@ -140,11 +138,16 @@ export class DotCustomEventHandlerService { } private editContentlet($event: CustomEvent): void { - if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { - return this.editContentletLegacy($event); - } + this.dotContentTypeService + .getContentType($event.detail.data.contentType) + .pipe(take(1)) + .subscribe((contentType) => { + if (this.shouldRedirectToOldContentEditor(contentType)) { + return this.editContentletLegacy($event); + } - this.router.navigate([`content/${$event.detail.data.inode}`]); + this.router.navigate([`content/${$event.detail.data.inode}`]); + }); } private editTaskLegacy($event: CustomEvent): void { @@ -152,11 +155,16 @@ export class DotCustomEventHandlerService { } private editTask($event: CustomEvent): void { - if (this.shouldRedirectToOldContentEditor($event.detail.data.contentType)) { - return this.editTaskLegacy($event); - } + this.dotContentTypeService + .getContentType($event.detail.data.contentType) + .pipe(take(1)) + .subscribe((contentType) => { + if (this.shouldRedirectToOldContentEditor(contentType)) { + return this.editTaskLegacy($event); + } - this.router.navigate([`content/${$event.detail.data.inode}`]); + this.router.navigate([`content/${$event.detail.data.inode}`]); + }); } private setPersonalization($event: CustomEvent): void { @@ -199,17 +207,14 @@ export class DotCustomEventHandlerService { } /** - * Check if the content type is in the feature flag list + * Check if the content type have the feature flag in the metadata. * * @private - * @param {string} contentType + * @param {DotCMSContentType} contentType * @return {*} {boolean} * @memberof DotCustomEventHandlerService */ - private shouldRedirectToOldContentEditor(contentType: string): boolean { - return ( - !this.contentTypesFeatureFlag.includes('*') && - this.contentTypesFeatureFlag.indexOf(contentType) === -1 - ); + private shouldRedirectToOldContentEditor(contentType: DotCMSContentType): boolean { + return !contentType?.metadata?.[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]; } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.spec.ts index 8e9b536598af..f0a50c9622bc 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/content/dot-edit-content.component.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { mockProvider } from '@ngneat/spectator'; import { of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; @@ -30,6 +31,7 @@ import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/te import { DotAlertConfirmService, DotContentletLockerService, + DotContentTypeService, DotEditPageService, DotESContentService, DotEventsService, @@ -299,6 +301,7 @@ describe('DotEditContentComponent', () => { DotExperimentsService, DotSeoMetaTagsService, DotSeoMetaTagsUtilService, + mockProvider(DotContentTypeService), { provide: LoginService, useClass: LoginServiceMock diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-main/dot-edit-page-main.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-main/dot-edit-page-main.component.spec.ts index 59b55b47fda7..0131bb68f708 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-main/dot-edit-page-main.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-edit-page/main/dot-edit-page-main/dot-edit-page-main.component.spec.ts @@ -19,6 +19,7 @@ import { DotUiColorsService } from '@dotcms/app/api/services/dot-ui-colors/dot-u import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService, + DotContentTypeService, DotCurrentUserService, DotEventsService, DotGenerateSecurePasswordService, @@ -189,7 +190,8 @@ describe('DotEditPageMainComponent', () => { LoginService, DotLicenseService, Title, - mockProvider(DotSessionStorageService) + mockProvider(DotSessionStorageService), + mockProvider(DotContentTypeService) ] }); })); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts index a7626c6cb012..216d7b8cabdf 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-contentlets/dot-contentlets.component.spec.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { mockProvider } from '@ngneat/spectator'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DebugElement, Injectable } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -16,6 +18,7 @@ import { DotDownloadBundleDialogService } from '@dotcms/app/api/services/dot-dow import { DotUiColorsService } from '@dotcms/app/api/services/dot-ui-colors/dot-ui-colors.service'; import { DotAlertConfirmService, + DotContentTypeService, DotCurrentUserService, DotEventsService, DotGenerateSecurePasswordService, @@ -119,7 +122,8 @@ describe('DotContentletsComponent', () => { DotIframeService, LoginService, DotGenerateSecurePasswordService, - DotDownloadBundleDialogService + DotDownloadBundleDialogService, + mockProvider(DotContentTypeService) ] }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts index 34b926cb8b21..14a658610db1 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-portlet-detail.component.spec.ts @@ -1,3 +1,5 @@ +import { mockProvider } from '@ngneat/spectator'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; @@ -15,6 +17,7 @@ import { DotUiColorsService } from '@dotcms/app/api/services/dot-ui-colors/dot-u import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService, + DotContentTypeService, DotCurrentUserService, DotEventsService, DotGenerateSecurePasswordService, @@ -86,7 +89,8 @@ describe('DotPortletDetailComponent', () => { DotGlobalMessageService, DotEventsService, DotGenerateSecurePasswordService, - DotLicenseService + DotLicenseService, + mockProvider(DotContentTypeService) ], declarations: [DotPortletDetailComponent], imports: [ diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts index 2e3593879e62..3438c637338c 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-porlet-detail/dot-workflow-task/dot-workflow-task.component.spec.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { mockProvider } from '@ngneat/spectator'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { DebugElement, Injectable } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -19,6 +21,7 @@ import { DotUiColorsService } from '@dotcms/app/api/services/dot-ui-colors/dot-u import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService, + DotContentTypeService, DotCurrentUserService, DotEventsService, DotGenerateSecurePasswordService, @@ -136,7 +139,8 @@ describe('DotWorkflowTaskComponent', () => { DotWorkflowActionsFireService, DotGlobalMessageService, DotGenerateSecurePasswordService, - DotEventsService + DotEventsService, + mockProvider(DotContentTypeService) ] }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html index 2c3de52bb8db..2ca5a97e8cc3 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.html @@ -1,4 +1,17 @@ -
+ +
+ + + +
+ dotAutofocus /> + [field]="form.get('name')">
+ formControlName="icon">
@@ -35,8 +45,7 @@ pInputText type="text" name="description" - formControlName="description" - /> + formControlName="description" />
+ formControlName="publishDateVar">
@@ -127,13 +131,11 @@ + formControlName="detailPage">
+ [message]="'contenttypes.hint.URL.map.pattern.hint1' | dm"> @@ -143,7 +145,6 @@ pInputText type="text" name="urlMapPattern" - formControlName="urlMapPattern" - /> + formControlName="urlMapPattern" />
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.scss b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.scss index c1abbef507ce..ce9c6b3f76d8 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.scss +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.scss @@ -19,6 +19,9 @@ .content-type__form { width: 522px; + &.content-type__form-banner { + padding-top: 3.5rem; + } } .content-type__form-dates { @@ -36,3 +39,34 @@ margin-bottom: $spacing-3; margin-top: -0.25rem; } + +.content-type__new-content-banner { + background: $color-palette-primary-200; + color: $color-palette-primary-900; + padding: $spacing-2 $spacing-4; + font-size: $font-size-sm; + position: absolute; + width: 100%; + margin-left: -$spacing-5; + top: 5.5rem; + left: $spacing-5; + display: flex; + align-items: center; + gap: $spacing-1; + + a { + color: $color-palette-primary-900; + } + + /* Adjust the size of the checkbox */ + ::ng-deep .ui-chkbox-box { + width: 10px; + height: 10px; + } + + /* Adjust the size of the check icon inside the checkbox */ + ::ng-deep .ui-chkbox-icon { + width: 10px; + height: 10px; + } +} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts index a48d127bf723..3cb231efe500 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.spec.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import { mockProvider } from '@ngneat/spectator'; import { Observable, of } from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, DebugElement, forwardRef, Injectable, Input } from '@angular/core'; -import { ComponentFixture, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { AbstractControl, ControlValueAccessor, @@ -13,9 +14,12 @@ import { } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { ConfirmationService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; +import { CheckboxModule } from 'primeng/checkbox'; import { DropdownModule } from 'primeng/dropdown'; import { InputTextModule } from 'primeng/inputtext'; import { OverlayPanelModule } from 'primeng/overlaypanel'; @@ -25,19 +29,25 @@ import { DotPageSelectorModule } from '@components/_common/dot-page-selector/dot import { DotWorkflowsActionsSelectorFieldModule } from '@components/_common/dot-workflows-actions-selector-field/dot-workflows-actions-selector-field.module'; import { DotWorkflowsSelectorFieldModule } from '@components/_common/dot-workflows-selector-field/dot-workflows-selector-field.module'; import { DotFieldHelperModule } from '@components/dot-field-helper/dot-field-helper.module'; -import { DOTTestBed } from '@dotcms/app/test/dot-test-bed'; import { DotMdIconSelectorModule } from '@dotcms/app/view/components/_common/dot-md-icon-selector/dot-md-icon-selector.module'; import { + DotAlertConfirmService, DotContentTypesInfoService, + DotHttpErrorManagerService, DotLicenseService, DotMessageDisplayService, DotMessageService, DotWorkflowService } from '@dotcms/data-access'; -import { DotcmsConfigService, LoginService, SiteService } from '@dotcms/dotcms-js'; -import { DotCMSContentTypeLayoutRow, DotCMSSystemActionType } from '@dotcms/dotcms-models'; +import { CoreWebService, DotcmsConfigService, LoginService, SiteService } from '@dotcms/dotcms-js'; +import { + DotCMSContentTypeLayoutRow, + DotCMSSystemActionType, + FeaturedFlags +} from '@dotcms/dotcms-models'; import { DotFieldValidationMessageComponent, DotIconModule, DotMessagePipe } from '@dotcms/ui'; import { + CoreWebServiceMock, dotcmsContentTypeBasicMock, dotcmsContentTypeFieldBasicMock, DotMessageDisplayServiceMock, @@ -80,6 +90,16 @@ class MockDotLicenseService { } } +const mockActivatedRoute = { + snapshot: { + data: { + featuredFlags: { + [FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED]: true + } + } + } +}; + describe('ContentTypesFormComponent', () => { let comp: ContentTypesFormComponent; let fixture: ComponentFixture; @@ -119,6 +139,7 @@ describe('ContentTypesFormComponent', () => { ] } ]; + let activatedRoute: ActivatedRoute; beforeEach(waitForAsync(() => { const messageServiceMock = new MockDotMessageService({ @@ -154,7 +175,7 @@ describe('ContentTypesFormComponent', () => { const siteServiceMock = new SiteServiceMock(); - DOTTestBed.configureTestingModule({ + TestBed.configureTestingModule({ declarations: [ContentTypesFormComponent, DotSiteSelectorComponent], imports: [ RouterTestingModule.withRoutes([ @@ -171,6 +192,7 @@ describe('ContentTypesFormComponent', () => { DotWorkflowsSelectorFieldModule, DropdownModule, InputTextModule, + CheckboxModule, OverlayPanelModule, ReactiveFormsModule, RouterTestingModule, @@ -189,15 +211,24 @@ describe('ContentTypesFormComponent', () => { { provide: SiteService, useValue: siteServiceMock }, { provide: DotWorkflowService, useClass: DotWorkflowServiceMock }, { provide: DotLicenseService, useClass: MockDotLicenseService }, + { provide: CoreWebService, useClass: CoreWebServiceMock }, DotcmsConfigService, - DotContentTypesInfoService + DotContentTypesInfoService, + mockProvider(DotHttpErrorManagerService), + mockProvider(DotAlertConfirmService), + mockProvider(ConfirmationService), + { + provide: ActivatedRoute, + useValue: mockActivatedRoute + } ] }); - fixture = DOTTestBed.createComponent(ContentTypesFormComponent); + fixture = TestBed.createComponent(ContentTypesFormComponent); comp = fixture.componentInstance; de = fixture.debugElement; dotLicenseService = de.injector.get(DotLicenseService); + activatedRoute = de.injector.get(ActivatedRoute); })); it('should be invalid by default', () => { @@ -375,7 +406,7 @@ describe('ContentTypesFormComponent', () => { }; fixture.detectChanges(); - expect(Object.keys(comp.form.controls).length).toBe(13); + expect(Object.keys(comp.form.controls).length).toBe(14); expect(comp.form.get('icon')).not.toBeNull(); expect(comp.form.get('clazz')).not.toBeNull(); expect(comp.form.get('name')).not.toBeNull(); @@ -393,6 +424,7 @@ describe('ContentTypesFormComponent', () => { expect(comp.form.get('detailPage')).toBeNull(); expect(comp.form.get('urlMapPattern')).toBeNull(); + expect(comp.form.get('newEditContent')).not.toBeNull(); }); it('should render basic fields for non-content base types', () => { @@ -424,7 +456,7 @@ describe('ContentTypesFormComponent', () => { }; fixture.detectChanges(); - expect(Object.keys(comp.form.controls).length).toBe(15); + expect(Object.keys(comp.form.controls).length).toBe(16); expect(comp.form.get('clazz')).not.toBeNull(); expect(comp.form.get('name')).not.toBeNull(); expect(comp.form.get('icon')).not.toBeNull(); @@ -439,6 +471,7 @@ describe('ContentTypesFormComponent', () => { expect(comp.form.get('fixed')).not.toBeNull(); expect(comp.form.get('system')).not.toBeNull(); expect(comp.form.get('folder')).not.toBeNull(); + expect(comp.form.get('newEditContent')).not.toBeNull(); const workflowAction = comp.form.get('systemActionMappings'); expect(workflowAction.get(DotCMSSystemActionType.NEW)).not.toBeNull(); @@ -483,7 +516,8 @@ describe('ContentTypesFormComponent', () => { creationDate: jasmine.any(Date), modDate: jasmine.any(Date) } - ] + ], + newEditContent: false }); }); @@ -576,6 +610,44 @@ describe('ContentTypesFormComponent', () => { expect(comp.form.get('expireDateVar').disabled).toBe(true); }); + it('should render the new content banner when the feature flag is enabled', () => { + comp.data = { + ...dotcmsContentTypeBasicMock, + baseType: 'CONTENT', + id: '123' + }; + + activatedRoute.snapshot.data.featuredFlags[ + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ] = true; + + fixture.detectChanges(); + + const newContentBanner = de.query( + By.css('[data-test-id="content-type__new-content-banner"]') + ); + expect(newContentBanner).not.toBeNull(); + }); + + it('should hide the new content banner when the feature flag is disabled', () => { + comp.data = { + ...dotcmsContentTypeBasicMock, + baseType: 'CONTENT', + id: '123' + }; + + activatedRoute.snapshot.data.featuredFlags[ + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ] = false; + + fixture.detectChanges(); + + const newContentBanner = de.query( + By.css('[data-test-id="content-type__new-content-banner"]') + ); + expect(newContentBanner).toBeNull(); + }); + describe('fields dates enabled', () => { beforeEach(() => { comp.data = { @@ -675,6 +747,8 @@ describe('ContentTypesFormComponent', () => { }); it('should submit form correctly', () => { + const metadata = {}; + metadata[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = false; comp.submitForm(); expect(data).toEqual({ @@ -703,7 +777,8 @@ describe('ContentTypesFormComponent', () => { ], systemActionMappings: { NEW: '' }, detailPage: '', - urlMapPattern: '' + urlMapPattern: '', + metadata }); }); }); diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts index 811a8ba9770e..a75b2cccd4fd 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/form/content-types-form.component.ts @@ -17,6 +17,7 @@ import { UntypedFormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { SelectItem } from 'primeng/api'; @@ -30,7 +31,8 @@ import { DotCMSSystemAction, DotCMSSystemActionMappings, DotCMSSystemActionType, - DotCMSWorkflow + DotCMSWorkflow, + FeaturedFlags } from '@dotcms/dotcms-models'; import { FieldUtil } from '@dotcms/utils-testing'; @@ -63,6 +65,7 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { form: UntypedFormGroup; nameFieldLabel: string; workflowsSelected$: Observable; + newContentEditorEnabled: boolean; private originalValue: DotCMSContentType; private destroy$: Subject = new Subject(); @@ -71,7 +74,8 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { private fb: UntypedFormBuilder, private dotWorkflowService: DotWorkflowService, private dotLicenseService: DotLicenseService, - private dotMessageService: DotMessageService + private dotMessageService: DotMessageService, + private readonly route: ActivatedRoute ) {} ngOnInit(): void { @@ -81,6 +85,10 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { this.nameFieldLabel = this.setNameFieldLabel(); this.name.nativeElement.focus(); + this.newContentEditorEnabled = + this.route.snapshot?.data?.featuredFlags[ + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ]; } ngOnDestroy(): void { @@ -120,7 +128,7 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { */ submitForm(): void { if (this.canSave) { - this.send.emit(this.form.value); + this.send.emit(this.addMetadataToForm()); } } @@ -189,7 +197,10 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { disabled: true } ] - }) + }), + newEditContent: !!this.getMetaDataProperty( + FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED + ) }); this.setBaseTypeContentSpecificFields(); @@ -337,4 +348,21 @@ export class ContentTypesFormComponent implements OnInit, OnDestroy { publishDateVar.patchValue(''); } } + + private getMetaDataProperty(_prop: string): string | number | boolean { + return this.data?.metadata?.[_prop]; + } + + private addMetadataToForm(): DotCMSContentType { + const metadata = this.data.metadata || {}; + const newEditContent = this.form.get('newEditContent').value; + const form = this.form.value; + delete form.newEditContent; + metadata[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = newEditContent; + + return { + ...form, + metadata + }; + } } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit-routing.module.ts b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit-routing.module.ts index 39b09e1a100c..57f50423b084 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit-routing.module.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/dot-content-types-edit-routing.module.ts @@ -1,17 +1,27 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; +import { DotFeatureFlagResolver } from '@portlets/shared/resolvers'; + import { DotContentTypesEditComponent } from '.'; const routes: Routes = [ { component: DotContentTypesEditComponent, - path: '' + path: '', + resolve: { + featuredFlags: DotFeatureFlagResolver + }, + data: { + featuredFlagsToCheck: [FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] + } } ]; @NgModule({ exports: [RouterModule], - imports: [RouterModule.forChild(routes)] + imports: [RouterModule.forChild(routes)], + providers: [DotFeatureFlagResolver] }) export class DotContentTypesEditRoutingModule {} diff --git a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts index 342fa33a1237..6f474a29e54f 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/main-legacy/main-legacy.component.spec.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { mockProvider } from '@ngneat/spectator'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, DebugElement, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -16,6 +18,7 @@ import { DotUiColorsService } from '@dotcms/app/api/services/dot-ui-colors/dot-u import { dotEventSocketURLFactory, MockDotUiColorsService } from '@dotcms/app/test/dot-test-bed'; import { DotAlertConfirmService, + DotContentTypeService, DotEventsService, DotGenerateSecurePasswordService, DotHttpErrorManagerService, @@ -133,7 +136,8 @@ describe('MainLegacyComponent', () => { DotWorkflowActionsFireService, DotGlobalMessageService, DotEventsService, - DotGenerateSecurePasswordService + DotGenerateSecurePasswordService, + mockProvider(DotContentTypeService) ], declarations: [ MainComponentLegacyComponent, diff --git a/core-web/libs/dotcms-models/src/lib/dot-content-types.model.ts b/core-web/libs/dotcms-models/src/lib/dot-content-types.model.ts index d55963185b50..8e4bc8c36470 100644 --- a/core-web/libs/dotcms-models/src/lib/dot-content-types.model.ts +++ b/core-web/libs/dotcms-models/src/lib/dot-content-types.model.ts @@ -30,6 +30,7 @@ export interface DotCMSContentType { workflows: DotCMSWorkflow[]; workflow?: string[]; systemActionMappings?: DotCMSSystemActionMappings; + metadata?: { [key: string]: string | number | boolean }; } export interface DotCMSContentTypeField { @@ -58,6 +59,7 @@ export interface DotCMSContentTypeField { unique: boolean; values?: string; variable: string; + metadata?: { [key: string]: string | number | boolean }; } export interface DotCMSContentTypeLayoutTab { diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_checkbox.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_checkbox.scss index dac219f7c343..0457e19f7b9e 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_checkbox.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/form/_checkbox.scss @@ -9,6 +9,21 @@ p-checkbox.p-element { border-color: $color-alert-red; } } + + &.p-checkbox-sm { + .p-checkbox { + height: 1.25rem; + } + + .p-checkbox-box { + height: 1rem; + } + + .p-checkbox-icon { + width: $font-size-sm; + height: $font-size-sm; + } + } } .p-checkbox { diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.spec.ts index 58a0881d5d88..2afc726a8def 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.spec.ts @@ -58,7 +58,7 @@ describe('DotEditContentAsideComponent', () => { it('should render aside information data', () => { spectator.setInput('contentLet', CONTENT_FORM_DATA_MOCK.contentlet); - spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType); + spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType.contentType); spectator.detectChanges(); expect(spectator.query(byTestId('modified-by')).textContent.trim()).toBe('Admin User'); expect(spectator.query(byTestId('last-modified')).textContent.trim()).toBe('11/07/2023'); @@ -71,7 +71,7 @@ describe('DotEditContentAsideComponent', () => { const CONTENT_WITHOUT_CONTENTLET = { ...CONTENT_FORM_DATA_MOCK }; delete CONTENT_WITHOUT_CONTENTLET.contentlet; spectator.setInput('contentLet', CONTENT_WITHOUT_CONTENTLET.contentlet); - spectator.setInput('contentType', CONTENT_WITHOUT_CONTENTLET.contentType); + spectator.setInput('contentType', CONTENT_WITHOUT_CONTENTLET.contentType.contentType); spectator.detectChanges(); expect(spectator.query(byTestId('modified-by')).textContent).toBe(''); @@ -81,7 +81,7 @@ describe('DotEditContentAsideComponent', () => { it('should render aside workflow data', () => { spectator.setInput('contentLet', CONTENT_FORM_DATA_MOCK.contentlet); - spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType); + spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType.contentType); spectator.detectChanges(); expect(spectator.component.workflow$).toBeDefined(); @@ -94,7 +94,7 @@ describe('DotEditContentAsideComponent', () => { it('should render New as status when dont have contentlet', () => { spectator.setInput('contentLet', null); - spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType); + spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType.contentType); spectator.detectChanges(); expect(spectator.query(byTestId('workflow-step')).textContent).toBe('New'); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts index 26d1c6530ced..52a1eb7ea9aa 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.spec.ts @@ -162,7 +162,10 @@ describe('DotFormComponent', () => { props: { formData: { ...CONTENT_FORM_DATA_MOCK, - layout: [...LAYOUT_MOCK, TAB_DIVIDER_MOCK] + contentType: { + ...CONTENT_FORM_DATA_MOCK.contentType, + layout: [...LAYOUT_MOCK, TAB_DIVIDER_MOCK] + } } } }); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts index 4527a8c0d4ab..334e691c971a 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.ts @@ -83,7 +83,7 @@ export class DotEditContentFormComponent implements OnInit { this.onFormChange(value); }); - this.formData.fields.forEach((field) => { + this.formData.contentType.fields.forEach((field) => { if (Object.values(FILTERED_TYPES).includes(field.fieldType as FILTERED_TYPES)) { return; } @@ -133,7 +133,7 @@ export class DotEditContentFormComponent implements OnInit { * @memberof DotEditContentFormComponent */ onFormChange(value) { - this.formData.fields.forEach(({ variable, fieldType }) => { + this.formData.contentType.fields.forEach(({ variable, fieldType }) => { // Shorthand for conditional assignment if (FLATTENED_FIELD_TYPES.includes(fieldType as FIELD_TYPES)) { @@ -158,7 +158,7 @@ export class DotEditContentFormComponent implements OnInit { private setLayoutTabs() { this.tabs = transformLayoutToTabs( this.dotMessageService.get('Content'), - this.formData.layout + this.formData.contentType.layout ); } } diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.html b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.html index dfebffb18557..7cdb62620186 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.html +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.html @@ -1,4 +1,16 @@ +
+ + {{ 'edit.content.layout.beta.message.switch' | dm }} + {{ 'edit.content.layout.beta.message.needed' | dm }} +
-No content to show +{{ 'edit.content.layout.no.content.to.show ' | dm }} diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.scss b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.scss index ef4e6727ba17..05cf8adb7e39 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.scss +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.scss @@ -3,15 +3,27 @@ :host { display: grid; grid-template-areas: + "topBar topBar" "header header" "body sidebar"; grid-template-columns: 1fr 21.875rem; - grid-template-rows: auto 1fr; + grid-template-rows: auto auto 1fr; padding-bottom: 0; height: 100%; width: 100%; } +.topBar { + grid-area: topBar; + background: $color-palette-primary-200; + color: $color-palette-primary-900; + padding: $spacing-2 $spacing-4; + + a { + color: $color-palette-primary-900; + } +} + .header { grid-area: header; } diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.spec.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.spec.ts index cf2cb71435f7..a0866adf23ca 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.spec.ts @@ -1,4 +1,5 @@ import { expect } from '@jest/globals'; +import { byTestId } from '@ngneat/spectator'; import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; import { MockComponent, MockPipe } from 'ng-mocks'; import { of } from 'rxjs'; @@ -16,6 +17,7 @@ import { DotWorkflowsActionsService, DotFormatDateService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; import { mockWorkflowsActions } from '@dotcms/utils-testing'; @@ -57,9 +59,7 @@ describe('EditContentLayoutComponent', () => { describe('Existing content', () => { const mockData: EditContentPayload = { actions: mockWorkflowsActions, - contentType: BINARY_FIELD_CONTENTLET.contentType, - layout: CONTENT_TYPE_MOCK.layout, - fields: CONTENT_TYPE_MOCK.fields, + contentType: CONTENT_TYPE_MOCK, contentlet: BINARY_FIELD_CONTENTLET }; @@ -124,7 +124,7 @@ describe('EditContentLayoutComponent', () => { const asideComponent = spectator.query(DotEditContentAsideComponent); expect(asideComponent).toBeDefined(); expect(asideComponent.contentLet).toEqual(mockData.contentlet); - expect(asideComponent.contentType).toEqual(mockData.contentType); + expect(asideComponent.contentType).toEqual(mockData.contentType.variable); }); it('should fire workflow action', () => { @@ -141,14 +141,30 @@ describe('EditContentLayoutComponent', () => { } }); }); + + it('should hide the beta topBar if metadata is not present', () => { + spectator.detectChanges(); + + const betaTopbar = spectator.query(byTestId('topBar')); + expect(betaTopbar).toBeNull(); + }); + + it('should show the beta topBar when the metadata is present', () => { + spectator.detectChanges(); + const metadata = {}; + metadata[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = true; + + dotEditContentStore.patchState({ contentType: { ...CONTENT_TYPE_MOCK, metadata } }); + spectator.detectChanges(); + const betaTopbar = spectator.query(byTestId('topBar')); + expect(betaTopbar).not.toBeNull(); + }); }); describe('New content', () => { const mockData: EditContentPayload = { actions: mockWorkflowsActions, - contentType: CONTENT_TYPE_MOCK.contentType, - layout: CONTENT_TYPE_MOCK.layout, - fields: CONTENT_TYPE_MOCK.fields, + contentType: CONTENT_TYPE_MOCK, contentlet: null }; @@ -214,7 +230,7 @@ describe('EditContentLayoutComponent', () => { const asideComponent = spectator.query(DotEditContentAsideComponent); expect(asideComponent).toBeDefined(); expect(asideComponent.contentLet).toEqual(mockData.contentlet); - expect(asideComponent.contentType).toEqual(mockData.contentType); + expect(asideComponent.contentType).toEqual(mockData.contentType.variable); }); }); }); diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.ts index 301b74389803..f77f1ee4d668 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/edit-content.layout.component.ts @@ -2,7 +2,7 @@ import { Observable, forkJoin, of } from 'rxjs'; import { AsyncPipe, JsonPipe, NgIf } from '@angular/common'; import { ChangeDetectionStrategy, Component, OnInit, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; @@ -11,11 +11,11 @@ import { ToastModule } from 'primeng/toast'; import { switchMap } from 'rxjs/operators'; import { - DotMessageService, DotRenderMode, DotWorkflowActionsFireService, DotWorkflowsActionsService } from '@dotcms/data-access'; +import { FeaturedFlags } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; import { DotEditContentStore } from './store/edit-content.store'; @@ -36,6 +36,7 @@ import { DotEditContentService } from '../../services/dot-edit-content.service'; DotMessagePipe, ButtonModule, ToastModule, + RouterLink, DotEditContentFormComponent, DotEditContentAsideComponent, DotEditContentToolbarComponent @@ -47,7 +48,6 @@ import { DotEditContentService } from '../../services/dot-edit-content.service'; DotWorkflowsActionsService, DotWorkflowActionsFireService, DotEditContentService, - DotMessageService, MessageService, DotEditContentStore ] @@ -65,6 +65,7 @@ export class EditContentLayoutComponent implements OnInit { private formValue: Record; vm$: Observable = this.store.vm$; + featuredFlagContentKEY = FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED; ngOnInit(): void { const obs$ = !this.initialInode diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.spec.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.spec.ts index 4e95c13e56cc..cf54beea5947 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.spec.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.spec.ts @@ -95,9 +95,7 @@ describe('DotEditContentStore', () => { spectator.service.vm$.pipe(skip(1)).subscribe((state) => { expect(state).toEqual({ actions: [], - fields: CONTENT_TYPE_MOCK.fields, - layout: CONTENT_TYPE_MOCK.layout, - contentType: BINARY_FIELD_CONTENTLET.contentType, + contentType: CONTENT_TYPE_MOCK, contentlet: BINARY_FIELD_CONTENTLET }); done(); diff --git a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts index d46f23b30107..59e970b8b00d 100644 --- a/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts +++ b/core-web/libs/edit-content/src/lib/feature/edit-content/store/edit-content.store.ts @@ -43,15 +43,11 @@ export class DotEditContentStore extends ComponentStore { private readonly dotMessageService = inject(DotMessageService); private readonly location = inject(Location); - readonly vm$ = this.select( - ({ actions, contentType: { variable, layout, fields }, contentlet }) => ({ - actions, - contentType: contentlet?.contentType || variable, - layout: layout || [], - fields: fields || [], - contentlet - }) - ); + readonly vm$ = this.select(({ actions, contentType, contentlet }) => ({ + actions, + contentType, + contentlet + })); /** * Update the state diff --git a/core-web/libs/edit-content/src/lib/models/dot-edit-content-form.interface.ts b/core-web/libs/edit-content/src/lib/models/dot-edit-content-form.interface.ts index efe8fff0206c..1913d1544364 100644 --- a/core-web/libs/edit-content/src/lib/models/dot-edit-content-form.interface.ts +++ b/core-web/libs/edit-content/src/lib/models/dot-edit-content-form.interface.ts @@ -1,14 +1,7 @@ -import { - DotCMSContentTypeField, - DotCMSContentTypeLayoutRow, - DotCMSContentlet, - DotCMSWorkflowAction -} from '@dotcms/dotcms-models'; +import { DotCMSContentlet, DotCMSWorkflowAction, DotCMSContentType } from '@dotcms/dotcms-models'; export interface EditContentPayload { - layout: DotCMSContentTypeLayoutRow[]; - fields: DotCMSContentTypeField[]; + contentType: DotCMSContentType; actions: DotCMSWorkflowAction[]; - contentType: string; contentlet?: DotCMSContentlet; } diff --git a/core-web/libs/edit-content/src/lib/utils/mocks.ts b/core-web/libs/edit-content/src/lib/utils/mocks.ts index be379916be96..982c2af5f5ed 100644 --- a/core-web/libs/edit-content/src/lib/utils/mocks.ts +++ b/core-web/libs/edit-content/src/lib/utils/mocks.ts @@ -11,7 +11,8 @@ import { DotCMSContentlet, DotCMSContentType, DotCMSContentTypeField, - DotCMSContentTypeLayoutRow + DotCMSContentTypeLayoutRow, + FeaturedFlags } from '@dotcms/dotcms-models'; import { MockDotMessageService } from '@dotcms/utils-testing'; @@ -850,10 +851,17 @@ export const LAYOUT_FIELDS_VALUES_MOCK = { date: '2023-11-14 19:27:53' }; +const metadata = {}; +metadata[FeaturedFlags.FEATURE_FLAG_CONTENT_EDITOR2_ENABLED] = false; + export const CONTENT_FORM_DATA_MOCK: EditContentPayload = { actions: [], - layout: LAYOUT_MOCK, - fields: JUST_FIELDS_MOCKS, + contentType: { + metadata, + layout: LAYOUT_MOCK, + fields: JUST_FIELDS_MOCKS, + contentType: 'Test' + } as unknown as DotCMSContentType, contentlet: { // This contentlet is some random mock, if you need you can change the properties date: MOCK_DATE, // To add the value to the date field, defaultValue is string and I don't think we should change the whole type just for this @@ -886,8 +894,7 @@ export const CONTENT_FORM_DATA_MOCK: EditContentPayload = { __icon__: 'contentIcon', contentTypeIcon: 'event_note', variant: 'DEFAULT' - }, - contentType: 'Test' + } }; /* CONTENT TYPE MOCKS */ diff --git a/core-web/libs/utils-testing/src/lib/dot-content-types.mock.ts b/core-web/libs/utils-testing/src/lib/dot-content-types.mock.ts index 38162309a0a3..7cab5c1ee6c6 100644 --- a/core-web/libs/utils-testing/src/lib/dot-content-types.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-content-types.mock.ts @@ -3,6 +3,7 @@ import { DotCMSContentType, DotCMSContentTypeLayoutRow } from '@dotcms/dotcms-models'; + import { EMPTY_FIELD } from './field-util'; export const dotcmsContentTypeBasicMock: DotCMSContentType = { @@ -29,7 +30,8 @@ export const dotcmsContentTypeBasicMock: DotCMSContentType = { urlMapPattern: null, variable: null, versionable: false, - workflows: [] + workflows: [], + metadata: {} }; export const dotcmsContentTypeFieldBasicMock: DotCMSContentTypeField = { diff --git a/dotCMS/src/curl-test/ContentTypeResourceTests.json b/dotCMS/src/curl-test/ContentTypeResourceTests.json index 79582d0bb4c5..8ee86d68e2de 100644 --- a/dotCMS/src/curl-test/ContentTypeResourceTests.json +++ b/dotCMS/src/curl-test/ContentTypeResourceTests.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "4a7bb840-b421-4ff9-a020-4986dbd0dca1", + "_postman_id": "da18d7b4-3906-4765-8621-f8cf40de2482", "name": "ContentType Resource", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "27636414" + "_exporter_id": "5403727" }, "item": [ { @@ -2821,80 +2821,174 @@ "response": [] } ] - } - ], - "variable": [ - { - "key": "contentTypeVariable", - "value": "" }, { - "key": "contentTypeId", - "value": "" - }, + "name": "New metadata column", + "item": [ { - "key": "contentType.columnDivider", - "value": "" - }, + "name": "Create a test Content Type", + "event": [ { - "key": "contentType.divider", - "value": "" + "listen": "prerequest", + "script": { + "exec": [ + "const typeName = \"test-type-\" + Math.floor(Math.random() * 100);", + "pm.collectionVariables.set(\"typeNameWithMetadata\", typeName);" + ], + "type": "text/javascript" + } }, { - "key": "contentType.field1", - "value": "" - }, + "listen": "test", + "script": { + "exec": [ + "pm.test(\"HTTP Status code must be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Test Content Type created successfully\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors.length).to.eql(0, \"No errors should have been generated\");", + " pm.expect(jsonData.entity[0].metadata).to.not.eql(undefined, \"The 'metadata' field is missing\");", + " pm.expect(jsonData.entity[0].metadata.edit_mode).to.be.eql(true, \"The value of the 'edit_mode' attribute doesn't match the expected one\");", + " pm.collectionVariables.set(\"testContentTypeId\", jsonData.entity[0].id);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ { - "key": "contentType.field2", - "value": "" + "key": "username", + "value": "admin@dotcms.com", + "type": "string" }, { - "key": "contentType.field3", - "value": "" + "key": "password", + "value": "admin", + "type": "string" + } + ] }, - { - "key": "contentType.field4", - "value": "" + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"defaultType\": false,\n \"icon\": null,\n \"fixed\": false,\n \"system\": false,\n \"clazz\": \"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\": \"This is a test Content Type with a metadata field.\",\n \"host\": \"default\",\n \"name\": \"{{typeNameWithMetadata}}\",\n \"metadata\": {\n \"edit_mode\": true\n },\n \"systemActionMappings\": {\n \"NEW\": \"\"\n },\n \"workflow\": [\n \"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } }, - { - "key": "contentType.host", - "value": "" + "url": { + "raw": "{{serverURL}}/api/v1/contenttype", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype" + ] + } }, - { - "key": "contentType.workflow", - "value": "" + "response": [] }, { - "key": "contentTypeID", - "value": "" - }, + "name": "Update the test Content Type", + "event": [ { - "key": "contentTypeVAR", - "value": "" - }, + "listen": "test", + "script": { + "exec": [ + "pm.test('HTTP Status code must be 200', function () {", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Metadata value in test Content Type was updated successfully\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors.length).to.eql(0, \"No errors should have been generated\");", + " pm.expect(jsonData.entity.metadata).to.not.eql(undefined, \"The 'metadata' field is missing\");", + " pm.expect(jsonData.entity.metadata.edit_mode).to.be.eql(true, \"The value of the 'edit_mode' attribute doesn't match the expected one\");", + " pm.expect(jsonData.entity.metadata.my_new_prop).to.be.eql(123, \"The value of the new 'my_new_prop' attribute doesn't match the expected one\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ { - "key": "contentTypeFieldID", - "value": "" + "key": "username", + "value": "admin@dotcms.com", + "type": "string" }, { - "key": "contentTypeIdWithStoryBlock", - "value": "" + "key": "password", + "value": "admin", + "type": "string" + } + ] }, - { - "key": "folder", - "value": "" + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"defaultType\": false,\n \"icon\": \"event_note\",\n \"fixed\": false,\n \"system\": false,\n \"clazz\": \"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\": \"This is a test Content Type with a metadata field v2.\",\n \"host\": \"default\",\n \"folder\": \"SYSTEM_FOLDER\",\n \"name\": \"{{typeNameWithMetadata}}\",\n \"metadata\": {\n \"edit_mode\": true,\n \"my_new_prop\": 123\n },\n \"systemActionMappings\": {\n \"NEW\": \"\"\n },\n \"detailPage\": \"\",\n \"urlMapPattern\": \"\",\n \"id\": \"{{testContentTypeId}}\",\n \"workflow\": [\n \"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } }, - { - "key": "folder.identifier", - "value": "" + "url": { + "raw": "{{serverURL}}/api/v1/contenttype/id/{{testContentTypeId}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype", + "id", + "{{testContentTypeId}}" + ] + } }, + "response": [] + } + ], + "description": "This test case verifies that dotCMS is properly handling the new `metadata` column that has been added to the `structure` table. Such a new column is meant to store different configuration properties and/or temporary information without the need to create a brand new column again." + } + ], + "event": [ { - "key": "folder.hostId", - "value": "" + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } }, { - "key": "fieldTypesArr", - "value": "" + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } } ] } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactory.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactory.java index a70105930b38..2e2f9c3aea23 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactory.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactory.java @@ -16,6 +16,28 @@ */ public interface ContentTypeFactory { + String INODE_COLUMN = "inode"; + String NAME_COLUMN = "name"; + String DESCRIPTION_COLUMN = "description"; + String DEFAULT_STRUCTURE_COLUMN = "default_structure"; + String REVIEW_INTERVAL_COLUMN = "review_interval"; + String REVIEWER_ROLE_COLUMN = "reviewer_role"; + String PAGE_DETAIL_COLUMN = "page_detail"; + String STRUCTURE_TYPE_COLUMN = "structuretype"; + String SYSTEM_COLUMN = "system"; + String FIXED_COLUMN = "fixed"; + String VELOCITY_VAR_NAME_COLUMN = "velocity_var_name"; + String URL_MAP_PATTERN_COLUMN = "url_map_pattern"; + String HOST_COLUMN = "host"; + String FOLDER_COLUMN = "folder"; + String EXPIRE_DATE_VAR_COLUMN = "expire_date_var"; + String PUBLISH_DATE_VAR_COLUMN = "publish_date_var"; + String MOD_DATE_COLUMN = "mod_date"; + String SORT_ORDER_COLUMN = "sort_order"; + String ICON_COLUMN = "icon"; + String MARKED_FOR_DELETION_COLUMN = "marked_for_deletion"; + String METADATA_COLUMN = "metadata"; + default ContentTypeFactory instance(){ return new ContentTypeFactoryImpl(); } diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java index 065a5d27fbaa..14877a21468c 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/ContentTypeFactoryImpl.java @@ -3,14 +3,29 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.contenttype.business.sql.ContentTypeSql; import com.dotcms.contenttype.exception.NotFoundInDbException; -import com.dotcms.contenttype.model.field.*; -import com.dotcms.contenttype.model.type.*; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.FieldBuilder; +import com.dotcms.contenttype.model.field.FieldVariable; +import com.dotcms.contenttype.model.field.HostFolderField; +import com.dotcms.contenttype.model.field.ImmutableFieldVariable; +import com.dotcms.contenttype.model.type.BaseContentType; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.contenttype.model.type.ContentTypeBuilder; +import com.dotcms.contenttype.model.type.Expireable; +import com.dotcms.contenttype.model.type.FileAssetContentType; +import com.dotcms.contenttype.model.type.UrlMapable; import com.dotcms.contenttype.transform.contenttype.DbContentTypeTransformer; import com.dotcms.contenttype.transform.contenttype.ImplClassContentTypeTransformer; import com.dotcms.enterprise.license.LicenseManager; import com.dotcms.repackage.javax.validation.constraints.NotNull; import com.dotcms.util.DotPreconditions; -import com.dotmarketing.business.*; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.CacheLocator; +import com.dotmarketing.business.DeterministicIdentifierAPI; +import com.dotmarketing.business.DotStateException; +import com.dotmarketing.business.DotValidationException; +import com.dotmarketing.business.FactoryLocator; +import com.dotmarketing.business.RelationshipAPI; import com.dotmarketing.common.db.DotConnect; import com.dotmarketing.common.util.SQLUtil; import com.dotmarketing.exception.DotDataException; @@ -20,15 +35,27 @@ import com.dotmarketing.portlets.fileassets.business.FileAssetAPI; import com.dotmarketing.portlets.structure.model.Relationship; import com.dotmarketing.portlets.workflows.business.WorkFlowFactory; -import com.dotmarketing.util.*; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UUIDUtil; +import com.dotmarketing.util.UtilMethods; +import com.dotmarketing.util.VelocityUtil; import com.google.common.collect.ImmutableSet; import com.liferay.util.StringPool; import io.vavr.Lazy; import io.vavr.control.Try; import org.apache.commons.lang.time.DateUtils; +import java.util.ArrayList; import java.util.Calendar; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static com.dotcms.contenttype.business.ContentTypeAPIImpl.TYPES_AND_FIELDS_VALID_VARIABLE_REGEX; @@ -558,37 +585,44 @@ private void dbInodeInsert(final ContentType type) throws DotDataException { dc.loadResult(); } - private void dbUpdate(ContentType type) throws DotDataException { - DotConnect dc = new DotConnect(); - dc.setSQL(this.contentTypeSql.UPDATE_TYPE); - dc.addParam(type.name()); - dc.addParam(type.description()); - dc.addParam(type.defaultType()); - dc.addParam(type.detailPage()); - dc.addParam(type.baseType().getType()); - dc.addParam(type.system()); - dc.addParam(type.fixed()); - dc.addParam(type.variable()); - dc.addParam(new CleanURLMap(type.urlMapPattern()).toString()); - dc.addParam(type.host()); - dc.addParam(type.folder()); - dc.addParam(type.expireDateVar()); - dc.addParam(type.publishDateVar()); - dc.addParam(type.modDate()); - dc.addParam(type.icon()); - dc.addParam(type.sortOrder()); - dc.addParam(type.markedForDeletion()); + /** + * Updates the specified Content Type. + * + * @param type The {@link ContentType} to update. + * + * @throws DotDataException An error occurred when interacting with the database. + */ + private void dbUpdate(final ContentType type) throws DotDataException { + final DotConnect dc = new DotConnect(); + dc.setSQL(ContentTypeSql.UPDATE_TYPE); + updateContentTypeFields(dc, type); dc.addParam(type.id()); dc.loadResult(); } - private void dbInsert(ContentType type) throws DotDataException { - - - - DotConnect dc = new DotConnect(); - dc.setSQL(this.contentTypeSql.INSERT_TYPE); + /** + * Inserts the specified Content Type. + * + * @param type The {@link ContentType} to insert. + * + * @throws DotDataException An error occurred when interacting with the database. + */ + private void dbInsert(final ContentType type) throws DotDataException { + final DotConnect dc = new DotConnect(); + dc.setSQL(ContentTypeSql.INSERT_TYPE); dc.addParam(type.id()); + updateContentTypeFields(dc, type); + dc.loadResult(); + } + + /** + * Takes the "updateable" attributes of a Content Type and adds them to the {@link DotConnect} + * instance to save or update a Content Type. + * + * @param dc The {@link DotConnect} instance to add the parameters to. + * @param type The {@link ContentType} to get the attributes from. + */ + private void updateContentTypeFields(final DotConnect dc, final ContentType type) { dc.addParam(type.name()); dc.addParam(type.description()); dc.addParam(type.defaultType()); @@ -606,7 +640,7 @@ private void dbInsert(ContentType type) throws DotDataException { dc.addParam(type.icon()); dc.addParam(type.sortOrder()); dc.addParam(type.markedForDeletion()); - dc.loadResult(); + dc.addJSONParam(type.metadata()); } private boolean dbDelete(final ContentType type) throws DotDataException { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/sql/ContentTypeSql.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/sql/ContentTypeSql.java index 45883bd6276c..875e7a854f8f 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/sql/ContentTypeSql.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/sql/ContentTypeSql.java @@ -29,7 +29,7 @@ public static ContentTypeSql getInstance() { public static String SELECT_ALL_STRUCTURE_FIELDS = "select inode.inode as inode, owner, idate as idate, name, " + "description, default_structure, page_detail, structuretype, system, fixed, velocity_var_name , " - + "url_map_pattern , host, folder, expire_date_var , publish_date_var , mod_date, icon, marked_for_deletion, sort_order " + + "url_map_pattern , host, folder, expire_date_var , publish_date_var , mod_date, icon, marked_for_deletion, sort_order, metadata " + "from inode, structure where inode.type='structure' and inode.inode = structure.inode "; public static String SELECT_ALL_STRUCTURE_FIELDS_EXCLUDE_MARKED_FOR_DELETE = SELECT_ALL_STRUCTURE_FIELDS + NON_MARKED_FOR_DELETION; @@ -49,8 +49,8 @@ public static ContentTypeSql getInstance() { public static String INSERT_TYPE_INODE = "insert into inode (inode, idate, owner, type) values (?,?,?,'structure')"; public static String INSERT_TYPE = "insert into structure(inode,name,description,default_structure,page_detail," - + "structuretype,system,fixed,velocity_var_name,url_map_pattern,host,folder,expire_date_var,publish_date_var,mod_date,icon,sort_order,marked_for_deletion) " - + "values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + + "structuretype,system,fixed,velocity_var_name,url_map_pattern,host,folder,expire_date_var,publish_date_var,mod_date,icon,sort_order,marked_for_deletion,metadata) " + + "values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; public static String UPDATE_TYPE = "update structure set " + "name=?, " @@ -68,7 +68,8 @@ public static ContentTypeSql getInstance() { + "mod_date=?," + "icon=?," + "sort_order=?, " - + "marked_for_deletion=? " + + "marked_for_deletion=?, " + + "metadata=? " + "where inode=?"; public static String SELECT_QUERY_CONDITION = SELECT_ALL_STRUCTURE_FIELDS_EXCLUDE_MARKED_FOR_DELETE diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentType.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentType.java index 3d906c3679db..5e2fbcf00036 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentType.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentType.java @@ -10,7 +10,12 @@ import com.dotcms.util.CollectionsUtils; import com.dotmarketing.beans.Host; import com.dotmarketing.beans.PermissionableProxy; -import com.dotmarketing.business.*; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.DotStateException; +import com.dotmarketing.business.PermissionAPI; +import com.dotmarketing.business.PermissionSummary; +import com.dotmarketing.business.Permissionable; +import com.dotmarketing.business.RelatedPermissionableGroup; import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.portlets.contentlet.business.HostAPI; @@ -46,7 +51,11 @@ import javax.annotation.Nullable; import java.io.IOException; import java.io.Serializable; -import java.util.*; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @JsonTypeInfo( @@ -83,7 +92,7 @@ protected void check() { } } - static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; Boolean hasStoryBlockFields = null; @@ -223,6 +232,12 @@ public int sortOrder() { return 0; } + @Nullable + @Value.Default + public Map metadata() { + return null; + } + @JsonIgnore @Value.Lazy public List fields() { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentTypeBuilder.java b/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentTypeBuilder.java index 6393061f86a6..cab693d671f2 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentTypeBuilder.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/model/type/ContentTypeBuilder.java @@ -5,7 +5,14 @@ import java.lang.reflect.Method; import java.util.Date; - +import java.util.Map; + +/** + * This Builder interface allows dotCMS to create immutable Content Type objects. + * + * @author Will Ezell + * @since Jul 7th, 2016 + */ public interface ContentTypeBuilder { // Generated builders will implement this method @@ -25,8 +32,6 @@ public interface ContentTypeBuilder { ContentTypeBuilder defaultType(boolean variable); - //ContentTypeBuilder storageType(StorageType variable); - ContentTypeBuilder detailPage(String variable); ContentTypeBuilder fixed(boolean variable); @@ -69,11 +74,21 @@ public interface ContentTypeBuilder { */ ContentTypeBuilder markedForDeletion(boolean variable); - public static ContentTypeBuilder builder(ContentType type) throws DotStateException { + /** + * Allows a Content Type to store additional or temporary information associated to a given + * Content Type. Developers can save any attributes they deem necessary. + * + * @param metadata A Map of key/value pairs to be stored as metadata + * + * @return The current {@link ContentTypeBuilder} instance. + */ + ContentTypeBuilder metadata(final Map metadata); + + static ContentTypeBuilder builder(ContentType type) throws DotStateException { return builder(type.getClass()).from(type); } - public static ContentTypeBuilder builder(final Class clazz) throws DotStateException { + static ContentTypeBuilder builder(final Class clazz) throws DotStateException { try { String canon = clazz.getCanonicalName(); Class tryMe = clazz; @@ -96,7 +111,7 @@ public static ContentTypeBuilder builder(final Class clazz) throws DotStateExcep } - public static ContentType instanceOf(Class clazz) { + static ContentType instanceOf(Class clazz) { ContentTypeBuilder builder = builder(clazz); builder.name("INSTANCETYPE"); builder.variable("INSTANCETYPE"); diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DbContentTypeTransformer.java b/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DbContentTypeTransformer.java index 0643e8423368..2c9667a3604d 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DbContentTypeTransformer.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/transform/contenttype/DbContentTypeTransformer.java @@ -1,21 +1,33 @@ package com.dotcms.contenttype.transform.contenttype; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; - - +import com.dotcms.contenttype.business.ContentTypeFactory; import com.dotcms.contenttype.model.type.BaseContentType; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.contenttype.model.type.UrlMapable; import com.dotcms.enterprise.license.LicenseManager; -import com.google.common.collect.ImmutableList; +import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotmarketing.business.DotStateException; import com.dotmarketing.db.DbConnectionFactory; import com.dotmarketing.util.UtilMethods; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import io.vavr.control.Try; +import org.postgresql.util.PGobject; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This transformer takes a Map with attributes of a one or more Content Types, and transforms them + * into the {@link ContentType} objects. Such data Maps usually come directly from queries made to + * the database. + * + * @author Will Ezell + * @since Jun 29th, 2016 + */ public class DbContentTypeTransformer implements ContentTypeTransformer{ final List list; @@ -34,34 +46,43 @@ public DbContentTypeTransformer(List> initList){ this.list= ImmutableList.copyOf(newList); } + /** + * Transforms a map into a ContentType object. + * + * @param map The map containing the Content Type's attributes. + * + * @return A ContentType object. + * + * @throws DotStateException The Content Type's base type is not supported. + */ private static ContentType transform(final Map map) throws DotStateException { - - BaseContentType base = BaseContentType.getBaseContentType(DbConnectionFactory.getInt(map.get("structuretype").toString())); - + final BaseContentType base = BaseContentType.getBaseContentType(DbConnectionFactory.getInt(map.get(ContentTypeFactory.STRUCTURE_TYPE_COLUMN).toString())); final ContentType type = new ContentType() { - static final long serialVersionUID = 1L; + private final ObjectMapper jsonMapper = DotObjectMapperProvider.getInstance().getDefaultObjectMapper(); + + private static final long serialVersionUID = 1L; @Override public String variable() { - return (String) map.get("velocity_var_name"); + return (String) map.get(ContentTypeFactory.VELOCITY_VAR_NAME_COLUMN); } @Override public String urlMapPattern() { - String ret = (String) map.get("url_map_pattern"); + String ret = (String) map.get(ContentTypeFactory.URL_MAP_PATTERN_COLUMN); return (UrlMapable.class.isAssignableFrom(base.immutableClass()) && UtilMethods.isSet(ret)) ? ret : null; } @Override public String publishDateVar() { - String ret = (String) map.get("publish_date_var"); + String ret = (String) map.get(ContentTypeFactory.PUBLISH_DATE_VAR_COLUMN); return ( UtilMethods.isSet(ret)) ? ret : null; } @Override public String detailPage() { - String ret = (String) map.get("page_detail"); + String ret = (String) map.get(ContentTypeFactory.PAGE_DETAIL_COLUMN); return (UrlMapable.class.isAssignableFrom(base.immutableClass()) && UtilMethods.isSet(ret)) ? ret : null; } @@ -73,55 +94,55 @@ public String owner() { @Override public String name() { - String ret = (String) map.get("name"); + String ret = (String) map.get(ContentTypeFactory.NAME_COLUMN); return ( UtilMethods.isSet(ret)) ? ret : null; } @Override public String id() { - return (String) map.get("inode"); + return (String) map.get(ContentTypeFactory.INODE_COLUMN); } @Override public String host() { - return (String) map.get("host"); + return (String) map.get(ContentTypeFactory.HOST_COLUMN); } @Override public String folder() { - return (String) map.get("folder"); + return (String) map.get(ContentTypeFactory.FOLDER_COLUMN); } @Override public String expireDateVar() { - String ret = (String) map.get("expire_date_var"); + String ret = (String) map.get(ContentTypeFactory.EXPIRE_DATE_VAR_COLUMN); return ( UtilMethods.isSet(ret)) ? ret : null; } @Override public String description() { - String ret = (String) map.get("description"); + String ret = (String) map.get(ContentTypeFactory.DESCRIPTION_COLUMN); return ( UtilMethods.isSet(ret)) ? ret : null; } @Override public boolean fixed() { - return DbConnectionFactory.isDBTrue(map.get("fixed").toString()); + return DbConnectionFactory.isDBTrue(map.get(ContentTypeFactory.FIXED_COLUMN).toString()); } @Override public boolean system() { - return DbConnectionFactory.isDBTrue(map.get("system").toString()); + return DbConnectionFactory.isDBTrue(map.get(ContentTypeFactory.SYSTEM_COLUMN).toString()); } @Override public boolean defaultType() { - return DbConnectionFactory.isDBTrue(map.get("default_structure").toString()); + return DbConnectionFactory.isDBTrue(map.get(ContentTypeFactory.DEFAULT_STRUCTURE_COLUMN).toString()); } @Override public Date modDate() { - return convertSQLDate((Date) map.get("mod_date")); + return convertSQLDate((Date) map.get(ContentTypeFactory.MOD_DATE_COLUMN)); } @Override @@ -136,24 +157,32 @@ public BaseContentType baseType() { @Override public String icon() { - final String icon = (String) map.get("icon"); + final String icon = (String) map.get(ContentTypeFactory.ICON_COLUMN); return ( UtilMethods.isSet(icon)) ? icon : BaseContentType.iconFallbackMap.get(base); } @Override public int sortOrder() { - return UtilMethods.isSet(map.get("sort_order")) ? + return UtilMethods.isSet(map.get(ContentTypeFactory.SORT_ORDER_COLUMN)) ? DbConnectionFactory.getInt(map.get("sort_order").toString()) : 0; } @Override public boolean markedForDeletion() { - return Try.of(()->DbConnectionFactory.isDBTrue(map.get("marked_for_deletion").toString())).getOrElse(false); + return Try.of(()->DbConnectionFactory.isDBTrue(map.get(ContentTypeFactory.MARKED_FOR_DELETION_COLUMN).toString())).getOrElse(false); + } + + @Override + @SuppressWarnings("unchecked") + public Map metadata() { + return Try.of(() -> jsonMapper.readValue(((PGobject) map.get(ContentTypeFactory.METADATA_COLUMN)).getValue(), Map.class)).getOrElse(new HashMap<>()); } - private Date convertSQLDate(Date d){ - Date javaDate = new Date(); - if(d!=null) javaDate.setTime(d.getTime()); + private Date convertSQLDate(final Date d){ + final Date javaDate = new Date(); + if (d != null) { + javaDate.setTime(d.getTime()); + } return javaDate; } }; diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java index 805a488d7eb9..d1f2f2a51b42 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/contenttype/ContentTypeResource.java @@ -58,14 +58,31 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import static com.dotcms.util.DotPreconditions.checkNotEmpty; +import static com.dotcms.util.DotPreconditions.checkNotNull; import static com.liferay.util.StringPool.COMMA; /** @@ -118,7 +135,7 @@ public final Response copyType(@Context final HttpServletRequest req, final InitDataObject initData = this.webResource.init(null, req, res, true, null); final User user = initData.getUser(); - Response response = null; + Response response; try { @@ -146,20 +163,25 @@ public final Response copyType(@Context final HttpServletRequest req, session.removeAttribute(SELECTED_STRUCTURE_KEY); } - response = Response.ok(new ResponseEntityView(responseMap)).build(); - } catch (IllegalArgumentException e) { - Logger.error(this, e.getMessage(), e); - response = ExceptionMapperUtil - .createResponse(null, "Content-type is not valid (" + e.getMessage() + ")"); - }catch (DotStateException | DotDataException e) { - Logger.error(this, e.getMessage(), e); - response = ExceptionMapperUtil - .createResponse(null, "Content-type is not valid (" + e.getMessage() + ")"); - } catch (DotSecurityException e) { + response = Response.ok(new ResponseEntityView<>(responseMap)).build(); + } catch (final IllegalArgumentException e) { + final String errorMsg = String.format("Missing required information when copying Content Type " + + "'%s': %s", baseVariableName, ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + response = ExceptionMapperUtil.createResponse(null, errorMsg); + } catch (final DotStateException | DotDataException e) { + final String errorMsg = String.format("Failed to copy Content Type '%s': %s", + baseVariableName, ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + response = ExceptionMapperUtil.createResponse(null, errorMsg); + } catch (final DotSecurityException e) { + Logger.error(this, String.format("User '%s' does not have permission to copy Content Type " + + "'%s'", user.getUserId(), baseVariableName), e); throw new ForbiddenException(e); - - } catch (Exception e) { - Logger.error(this, e.getMessage(), e); + } catch (final Exception e) { + final String errorMsg = String.format("An error occurred when copying Content Type " + + "'%s': %s", baseVariableName, ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); response = ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); } @@ -227,6 +249,20 @@ private ImmutableMap copyContentTypeAndDependencies (final Conte .build(); } + /** + * Creates one or more Content Types specified in the Content Type Form parameter. This allows + * users to easily create more than one Content Type in a single request. + * + * @param req The current instance of the {@link HttpServletRequest}. + * @param res The current instance of the {@link HttpServletResponse}. + * @param form The {@link ContentTypeForm} containing the required information to create the + * Content Type(s). + * + * @return The JSON response with the Content Type(s) created. + * + * @throws DotDataException An error occurs when persisting the Content Type(s) in the + * database. + */ @POST @JSONP @NoCache @@ -236,68 +272,79 @@ public final Response createType(@Context final HttpServletRequest req, @Context final HttpServletResponse res, final ContentTypeForm form) throws DotDataException { - final InitDataObject initData = this.webResource.init(null, req, res, true, null); + final InitDataObject initData = + new WebResource.InitBuilder(webResource) + .requestAndResponse(req, res) + .requiredBackendUser(false) + .requiredFrontendUser(false) + .rejectWhenNoUser(true) + .init(); final User user = initData.getUser(); - - Response response = null; - try { - - Logger.debug(this, ()->String.format("Saving new content type '%s' ", form.getRequestJson())); + checkNotNull(form, "The 'form' parameter is required"); + Logger.debug(this, ()->String.format("Creating Content Type(s): %s", form.getRequestJson())); final HttpSession session = req.getSession(false); final Iterable typesToSave = form.getIterable(); - final List> retTypes = new ArrayList<>(); + final List> savedContentTypes = new ArrayList<>(); - // Validate input for (final ContentTypeForm.ContentTypeFormEntry entry : typesToSave) { - final ContentType type = entry.contentType; final Set workflowsIds = new HashSet<>(entry.workflowsIds); if (UtilMethods.isSet(type.id()) && !UUIDUtil.isUUID(type.id())) { - return ExceptionMapperUtil.createResponse(null, "ContentType 'id' if set, should be a uuid"); + return ExceptionMapperUtil.createResponse(null, String.format("Content Type ID " + + "'%s' is either not set, or is not a valid UUID", type.id())); } final Tuple2> tuple2 = this.saveContentTypeAndDependencies(type, initData.getUser(), workflowsIds, form.getSystemActions(), APILocator.getContentTypeAPI(user, true), true); final ContentType contentTypeSaved = tuple2._1; - - ImmutableMap responseMap = ImmutableMap.builder() + final ImmutableMap responseMap = ImmutableMap.builder() .putAll(new JsonContentTypeTransformer(contentTypeSaved).mapObject()) .put("workflows", this.workflowHelper.findSchemesByContentType(contentTypeSaved.id(), initData.getUser())) .put("systemActionMappings", tuple2._2.stream() - .collect(Collectors.toMap(mapping-> mapping.getSystemAction(), mapping->mapping))) + .collect(Collectors.toMap(SystemActionWorkflowActionMapping::getSystemAction, mapping->mapping))) .build(); - retTypes.add(responseMap); - + savedContentTypes.add(responseMap); // save the last one to the session to be compliant with #13719 if(null != session){ session.removeAttribute(SELECTED_STRUCTURE_KEY); } } - - response = Response.ok(new ResponseEntityView<>(retTypes)).build(); - } catch (IllegalArgumentException e) { - Logger.error(this, e.getMessage(), e); - response = ExceptionMapperUtil - .createResponse(null, "Content-type is not valid (" + e.getMessage() + ")"); - }catch (DotStateException | DotDataException e) { - Logger.error(this, e.getMessage(), e); - response = ExceptionMapperUtil - .createResponse(null, "Content-type is not valid (" + e.getMessage() + ")"); - } catch (DotSecurityException e) { + return Response.ok(new ResponseEntityView<>(savedContentTypes)).build(); + } catch (final IllegalArgumentException e) { + final String errorMsg = String.format("Missing required information when creating Content Type(s): " + + "%s", ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + return ExceptionMapperUtil.createResponse(null, errorMsg); + }catch (final DotStateException | DotDataException e) { + final String errorMsg = String.format("Failed to create Content Type(s): %s", ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + return ExceptionMapperUtil.createResponse(null, errorMsg); + } catch (final DotSecurityException e) { + Logger.error(this, String.format("User '%s' does not have permission to create " + + "Content Type(s)", user.getUserId()), e); throw new ForbiddenException(e); - - } catch (Exception e) { - Logger.error(this, e.getMessage(), e); - response = ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); + } catch (final Exception e) { + final String errorMsg = String.format("An error occurred when creating Content Type(s): " + + "%s", ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + return ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); } - - return response; } - + /** + * Updates the Content Type based on the given ID or Velocity variable name. + * + * @param idOrVar The ID or Velocity variable name of the Content Type to update. + * @param form The {@link ContentTypeForm} containing the required information to update the + * Content Type. + * @param req The current instance of the {@link HttpServletRequest}. + * @param res The current instance of the {@link HttpServletResponse}. + * + * @return The JSON response with the updated information of the Content Type. + */ @PUT @Path("/id/{idOrVar}") @JSONP @@ -305,31 +352,25 @@ public final Response createType(@Context final HttpServletRequest req, @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON, "application/javascript" }) public Response updateType(@PathParam("idOrVar") final String idOrVar, final ContentTypeForm form, - @Context final HttpServletRequest req, @Context final HttpServletResponse res) throws DotDataException { - - final InitDataObject initData = this.webResource.init(null, req, res, false, null); + @Context final HttpServletRequest req, @Context final HttpServletResponse res) { + final InitDataObject initData = + new WebResource.InitBuilder(webResource) + .requestAndResponse(req, res) + .requiredBackendUser(false) + .requiredFrontendUser(false) + .rejectWhenNoUser(true) + .init(); final User user = initData.getUser(); final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(user, true); - - Response response = null; - try { + checkNotNull(form, "The 'form' parameter is required"); final ContentType contentType = form.getContentType(); - - Logger.debug(this, String.format("Updating content type '%s' ", form.getRequestJson())); - - if (!UtilMethods.isSet(contentType.id())) { - - response = ExceptionMapperUtil.createResponse(null, "Field 'id' should be set"); - - } else { - + Logger.debug(this, String.format("Updating content type: '%s'", form.getRequestJson())); + checkNotEmpty(contentType.id(), BadRequestException.class, "Content Type 'id' attribute must be set"); final ContentType currentContentType = contentTypeAPI.find(idOrVar); if (!currentContentType.id().equals(contentType.id())) { - - response = ExceptionMapperUtil.createResponse(null, "Field id '"+ idOrVar +"' does not match a content-type with id '"+ contentType.id() +"'"); - + return ExceptionMapperUtil.createResponse(null, "Field id '"+ idOrVar +"' does not match the Content Type with id '"+ contentType.id() +"'"); } else { final Tuple2> tuple2 = this.saveContentTypeAndDependencies(contentType, user, @@ -339,30 +380,50 @@ public Response updateType(@PathParam("idOrVar") final String idOrVar, final Con .putAll(new JsonContentTypeTransformer(contentTypeAPI.find(tuple2._1.variable())).mapObject()) .put("workflows", this.workflowHelper.findSchemesByContentType(contentType.id(), initData.getUser())) .put("systemActionMappings", tuple2._2.stream() - .collect(Collectors.toMap(mapping-> mapping.getSystemAction(), mapping->mapping))); - - response = Response.ok(new ResponseEntityView(builderMap.build())).build(); + .collect(Collectors.toMap(SystemActionWorkflowActionMapping::getSystemAction, mapping->mapping))); + return Response.ok(new ResponseEntityView<>(builderMap.build())).build(); } - } - } catch (NotFoundInDbException e) { - - response = ExceptionMapperUtil.createResponse(e, Response.Status.NOT_FOUND); - - } catch ( DotStateException | DotDataException e) { - - response = ExceptionMapperUtil.createResponse(null, "Content-type is not valid ("+ e.getMessage() +")"); - - } catch (DotSecurityException e) { + } catch (final NotFoundInDbException e) { + Logger.error(this, String.format("Content Type with ID or var name '%s' was not found", idOrVar), e); + return ExceptionMapperUtil.createResponse(e, Response.Status.NOT_FOUND); + } catch (final DotStateException | DotDataException e) { + final String errorMsg = String.format("Failed to update Content Type with ID or var name " + + "'%s': %s", idOrVar, ExceptionUtil.getErrorMessage(e)); + Logger.error(this, errorMsg, e); + return ExceptionMapperUtil.createResponse(null, errorMsg); + } catch (final DotSecurityException e) { + Logger.error(this, String.format("User '%s' does not have permission to update Content Type with ID or var name " + + "'%s'", user.getUserId(), idOrVar), e); throw new ForbiddenException(e); - - } catch (Exception e) { - - response = ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); + } catch (final Exception e) { + Logger.error(this, String.format("An error occurred when updating Content Type with ID or var name " + + "'%s': %s", idOrVar, ExceptionUtil.getErrorMessage(e)), e); + return ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); } - - return response; } + /** + * Saves the specified Content Type and properly handles additional data associated to it, such + * as Workflow information. + * + * @param contentType The {@link ContentType} to save. + * @param user The {@link User} executing this action. + * @param workflowsIds The {@link Set} of Workflow IDs to associate to the Content + * Type. + * @param systemActionMappings The {@link List} of {@link Tuple2} containing the + * {@link WorkflowAPI.SystemAction} and the {@link String} + * representing the Workflow Action ID. + * @param contentTypeAPI The {@link ContentTypeAPI} instance to use. + * @param isNew A {@link Boolean} indicating if the Content Type is new or not. + * + * @return A {@link Tuple2} containing the saved {@link ContentType} and the {@link List} of + * {@link SystemActionWorkflowActionMapping} associated to it. + * + * @throws DotSecurityException The specified User doesn't have the required permissions to + * perform this action. + * @throws DotDataException An error occurs when persisting the Content Type in the + * database. + */ @WrapInTransaction private Tuple2> saveContentTypeAndDependencies (final ContentType contentType, final User user, @@ -509,16 +570,15 @@ private void handleUpdateFieldAndFieldVariables(final User user, @NoCache @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) public Response deleteType(@PathParam("idOrVar") final String idOrVar, @Context final HttpServletRequest req, @Context final HttpServletResponse res) - throws DotDataException, JSONException { + throws JSONException { final InitDataObject initData = this.webResource.init(null, req, res, true, null); final User user = initData.getUser(); - ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(user, true); + final ContentTypeAPI contentTypeAPI = APILocator.getContentTypeAPI(user, true); try { - - ContentType type = null; + ContentType type; try { type = contentTypeAPI.find(idOrVar); } catch (NotFoundInDbException nfdb) { @@ -530,12 +590,10 @@ public Response deleteType(@PathParam("idOrVar") final String idOrVar, @Context JSONObject joe = new JSONObject(); joe.put("deleted", type.id()); - Response response = Response.ok(new ResponseEntityView(joe.toString())).build(); - return response; - - } catch (DotSecurityException e) { + return Response.ok(new ResponseEntityView<>(joe.toString())).build(); + } catch (final DotSecurityException e) { throw new ForbiddenException(e); - } catch (Exception e) { + } catch (final Exception e) { Logger.error(this, String.format("Error deleting content type identified by (%s) ",idOrVar), e); return ExceptionMapperUtil.createResponse(e, Response.Status.INTERNAL_SERVER_ERROR); } @@ -558,8 +616,7 @@ public Response getType( final User user = initData.getUser(); ContentTypeAPI tapi = APILocator.getContentTypeAPI(user, true); Response response = Response.status(404).build(); - final Map resultMap = new HashMap<>(); - final HttpSession session = req.getSession(false); + final HttpSession session = req.getSession(false); try { Logger.debug(this, ()-> "Getting the Type: " + idOrVar); @@ -571,12 +628,12 @@ public Response getType( } final boolean live = paramLive == null ? - (PageMode.get(Try.of(() -> HttpServletRequestThreadLocal.INSTANCE.getRequest()).getOrNull())).showLive + (PageMode.get(Try.of(HttpServletRequestThreadLocal.INSTANCE::getRequest).getOrNull())).showLive : paramLive; final ContentTypeInternationalization contentTypeInternationalization = languageId != null ? new ContentTypeInternationalization(languageId, live, user) : null; - resultMap.putAll(new JsonContentTypeTransformer(type, contentTypeInternationalization).mapObject()); + final Map resultMap = new HashMap<>(new JsonContentTypeTransformer(type, contentTypeInternationalization).mapObject()); resultMap.put("workflows", this.workflowHelper.findSchemesByContentType(type.id(), initData.getUser())); resultMap.put("systemActionMappings", @@ -584,11 +641,11 @@ public Response getType( .stream().collect(Collectors.toMap(mapping -> mapping.getSystemAction(),mapping -> mapping))); response = ("true".equalsIgnoreCase(req.getParameter("include_permissions")))? - Response.ok(new ResponseEntityView(resultMap, PermissionsUtil.getInstance().getPermissionsArray(type, initData.getUser()))).build(): - Response.ok(new ResponseEntityView(resultMap)).build(); - } catch (DotSecurityException e) { + Response.ok(new ResponseEntityView<>(resultMap, PermissionsUtil.getInstance().getPermissionsArray(type, initData.getUser()))).build(): + Response.ok(new ResponseEntityView<>(resultMap)).build(); + } catch (final DotSecurityException e) { throw new ForbiddenException(e); - } catch (NotFoundInDbException nfdb2) { + } catch (final NotFoundInDbException nfdb2) { // nothing to do here, will throw a 404 } @@ -668,9 +725,7 @@ public final Response filteredContentTypes(@Context final HttpServletRequest req @NoCache @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) public final Response getRecentBaseTypes(@Context final HttpServletRequest request) { - - Response response = null; - + Response response; try { final List types = contentTypeHelper.getTypes(request); response = Response.ok(new ResponseEntityView<>(types)).build(); @@ -727,9 +782,7 @@ public final Response getContentTypes(@Context final HttpServletRequest httpRequ @QueryParam(ContentTypesPaginator.HOST_PARAMETER_ID) final String hostId) throws DotDataException { final InitDataObject initData = webResource.init(null, httpRequest, httpResponse, true, null); - - Response response = null; - + Response response; final String orderBy = getOrderByRealName(orderbyParam); final User user = initData.getUser(); diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/structure/model/Structure.java b/dotCMS/src/main/java/com/dotmarketing/portlets/structure/model/Structure.java index 8f8892ce2982..bd810d6ebd45 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/structure/model/Structure.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/structure/model/Structure.java @@ -1,13 +1,10 @@ package com.dotmarketing.portlets.structure.model; -import static com.dotcms.util.CollectionsUtils.map; - import com.dotcms.contenttype.model.type.BaseContentType; import com.dotcms.contenttype.model.type.ContentTypeIf; +import com.dotcms.exception.ExceptionUtil; import com.dotcms.publisher.util.PusheableAsset; import com.dotcms.publishing.manifest.ManifestItem; -import com.dotcms.publishing.manifest.ManifestItem.ManifestInfo; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.dotmarketing.beans.Host; import com.dotmarketing.beans.Inode; import com.dotmarketing.business.APILocator; @@ -20,18 +17,27 @@ import com.dotmarketing.exception.DotHibernateException; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.folders.model.Folder; import com.dotmarketing.portlets.structure.factories.FieldFactory; import com.dotmarketing.portlets.structure.factories.StructureFactory; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.apache.commons.lang.builder.ToStringBuilder; + import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.commons.lang.builder.ToStringBuilder; - - +/** + * This class is an old representation a Content Type in dotCMS. It's still used in many places for + * backwards compatibility, but it should not be used in new code. + * + * @author root + * @since Mar 22nd, 2012 + */ public class Structure extends Inode implements Permissionable, Treeable,ContentTypeIf, ManifestItem { @@ -39,37 +45,37 @@ public class Structure extends Inode implements Permissionable, Treeable,Content /** - * @deprecated As of 2016-05-16, replaced by {@link Type#CONTENT} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#CONTENT} */ @Deprecated public static final int STRUCTURE_TYPE_CONTENT = 1; /** - * @deprecated As of 2016-05-16, replaced by {@link Type#WIDGET} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#WIDGET} */ @Deprecated public static final int STRUCTURE_TYPE_WIDGET = 2; /** - * @deprecated As of 2016-05-16, replaced by {@link Type#FORM} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#FORM} */ @Deprecated public static final int STRUCTURE_TYPE_FORM = 3; /** - * @deprecated As of 2016-05-16, replaced by {@link Type#FILEASSET} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#FILEASSET */ @Deprecated public static final int STRUCTURE_TYPE_FILEASSET = 4; /** - * @deprecated As of 2016-05-16, replaced by {@link Type#HTMLPAGE} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#HTMLPAGE} */ @Deprecated public static final int STRUCTURE_TYPE_HTMLPAGE = 5; /** - * @deprecated As of 2016-05-16, replaced by {@link Type#PERSONA} + * @deprecated As of 2016-05-16, replaced by {@link BaseContentType#PERSONA} */ @Deprecated public static final int STRUCTURE_TYPE_PERSONA = 6; @@ -87,15 +93,14 @@ public class Structure extends Inode implements Permissionable, Treeable,Content private boolean system; private String velocityVarName; private String urlMapPattern; - private String host="SYSTEM_HOST"; - private String folder="SYSTEM_FOLDER"; + private String host = Host.SYSTEM_HOST; + private String folder = Folder.SYSTEM_FOLDER; private String publishDateVar; private String expireDateVar; private Date modDate; private String icon; private int sortOrder; - - + private Map metadata = new HashMap<>(); public String getDetailPage() { return pagedetail; @@ -152,14 +157,13 @@ public void delete() throws DotHibernateException, DotDataException delete(recursive); } - public void delete(boolean recursive) throws DotHibernateException, DotDataException - { + public void delete(final boolean recursive) throws DotDataException { if(recursive) { - List list = FieldFactory.getFieldsByStructure(inode); + final List list = FieldFactory.getFieldsByStructure(inode); for(int i = 0;i < list.size();i++) { - Field field = (Field) list.get(i); + final Field field = list.get(i); field.delete(); } } @@ -310,11 +314,11 @@ public List acceptedPermissions() { public Permissionable getParentPermissionable() throws DotDataException { try { - if(UtilMethods.isSet(getFolder()) && !getFolder().equals("SYSTEM_FOLDER")){ + if(UtilMethods.isSet(getFolder()) && !getFolder().equals(Folder.SYSTEM_FOLDER)){ return APILocator.getFolderAPI().find(getFolder(), APILocator.getUserAPI().getSystemUser(), false); - }else if(UtilMethods.isSet(getHost()) && !getHost().equals("SYSTEM_HOST")){ + }else if(UtilMethods.isSet(getHost()) && !getHost().equals(Host.SYSTEM_HOST)){ try { return APILocator.getHostAPI().find(getHost(), APILocator.getUserAPI().getSystemUser(), false); @@ -323,8 +327,9 @@ public Permissionable getParentPermissionable() throws DotDataException { } } return APILocator.getHostAPI().findSystemHost(); - } catch (Exception e) { - throw new DotRuntimeException(e.getMessage(), e); + } catch (final Exception e) { + throw new DotRuntimeException(String.format("Failed to get the parent permissionable from Content Type " + + "'%s' [ %s ]: %s", this.name, this.identifier, ExceptionUtil.getErrorMessage(e)), e); } } @@ -439,4 +444,24 @@ public String getIcon() { public int getSortOrder() { return sortOrder; } + + /** + * Returns the metadata for this Content Type. + * + * @return A Map with the Content Type's metadata. + */ + public Map getMetadata(){ + return this.metadata; + } + + /** + * Sets the metadata for this Content Type, which may include different configuration + * properties or simple common-use attributes for the type in a single column. + * + * @param metadata A Map with the Content Type's metadata. + */ + public void setMetadata(final Map metadata){ + this.metadata = metadata; + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTable.java b/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTable.java new file mode 100644 index 000000000000..befbf93d5189 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTable.java @@ -0,0 +1,29 @@ +package com.dotmarketing.startup.runonce; + +import com.dotmarketing.common.db.DotConnect; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.startup.StartupTask; + +/** + * Adds the {@code metadata} column to the {@code structure} table. This JSONB column is meant + * to store any sort of additional configuration properties for a Content Type that may not be + * strictly related to their core functionality or for temporary behavior. + * + * @author Jose Castro + * @since Jan 11th, 2024 + */ +public class Task240112AddMetadataColumnToStructureTable implements StartupTask { + + @Override + public boolean forceRun() { + return true; + } + + @Override + public void executeUpgrade() throws DotDataException, DotRuntimeException { + final DotConnect dc = new DotConnect().setSQL("ALTER TABLE structure ADD COLUMN IF NOT EXISTS metadata JSONB NULL"); + dc.loadResult(); + } + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/util/TaskLocatorUtil.java b/dotCMS/src/main/java/com/dotmarketing/util/TaskLocatorUtil.java index 4376c7fad0a3..95f023312d41 100644 --- a/dotCMS/src/main/java/com/dotmarketing/util/TaskLocatorUtil.java +++ b/dotCMS/src/main/java/com/dotmarketing/util/TaskLocatorUtil.java @@ -238,10 +238,11 @@ import com.dotmarketing.startup.runonce.Task230707CreateSystemTable; import com.dotmarketing.startup.runonce.Task230713IncreaseDisabledWysiwygColumnSize; import com.dotmarketing.startup.runonce.Task231109AddPublishDateToContentletVersionInfo; -import com.google.common.collect.ImmutableList; -import com.dotmarketing.startup.runonce.Task240111AddInodeAndIdentifierLeftIndexes; import com.dotmarketing.startup.runonce.Task231207AddMetadataColumnToWorkflowAction; import com.dotmarketing.startup.runonce.Task240102AlterVarcharLengthOfRelationType; +import com.dotmarketing.startup.runonce.Task240111AddInodeAndIdentifierLeftIndexes; +import com.dotmarketing.startup.runonce.Task240112AddMetadataColumnToStructureTable; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Comparator; @@ -556,6 +557,7 @@ public static List> getStartupRunOnceTaskClasses() { .add(Task231207AddMetadataColumnToWorkflowAction.class) .add(Task240102AlterVarcharLengthOfRelationType.class) .add(Task240111AddInodeAndIdentifierLeftIndexes.class) + .add(Task240112AddMetadataColumnToStructureTable.class) .build(); return ret.stream().sorted(classNameComparator).collect(Collectors.toList()); } diff --git a/dotCMS/src/main/resources/postgres.sql b/dotCMS/src/main/resources/postgres.sql index 83f86ce70d02..9cf10a57aa72 100644 --- a/dotCMS/src/main/resources/postgres.sql +++ b/dotCMS/src/main/resources/postgres.sql @@ -730,6 +730,7 @@ create table structure ( sort_order int4, icon varchar(255), marked_for_deletion bool not null default false, + metadata JSONB NULL, primary key (inode) ); create table cms_role ( diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 11870f8e1a2c..fbb3ce236f1a 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1142,7 +1142,7 @@ Do-you-want-to-delete-assets=Are you sure you want to delete pushed assets histo Do-you-want-to-delete-this-contentlet-s=Do you want to delete this content? Do-you-want-to-drop-all-old-assets=Do you want to drop all old assets? (This operation cannot be undone) Do-you-want-to-fix-assets-inconsistencies=Do you want to fix assets inconsistencies? (This operation cannot be undone) -Do-you-want-to-replace-the-existing-asset-name=Do you want to replace the existing asset-name +Do-you-want-to-replace-the-existing-asset-name=Do you want to replace the existing asset-name doesn-t-match-any-structure-field-this-column-of-data-will-be-ignored=doesn't match any Content Type field; this column of data will be ignored. dont-have-permissions-msg=You don't have the required permissions to perform this action dotcms.api.error.bad_request=Bad Request @@ -5622,3 +5622,9 @@ block-editor.extension.ai-image.api-error.no-choice-returned=No choices returned editpage.language-change-missing-lang-populate.confirm.header=Create New Language Version? editpage.language-change-missing-lang-populate.confirm.message=Page does not exist in the selected language. Create a new version in {0}? + +edit.content.layout.beta.message=Explore the Edit Content Beta which provides new editing functionalities. Your experience matters to us, and you can easily +edit.content.layout.beta.message.switch=switch back +edit.content.layout.beta.message.needed=if needed. +edit.content.layout.no.content.to.show = No content to show. +content.type.form.banner.message= Enable Edit Content Beta for a fresh editing experience (roll back anytime). \ No newline at end of file diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite.java index 239434239833..0226488fd2c5 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite.java @@ -219,6 +219,7 @@ import com.dotmarketing.startup.runonce.Task231109AddPublishDateToContentletVersionInfoTest; import com.dotmarketing.startup.runonce.Task240102AlterVarcharLengthOfRelationTypeTest; import com.dotmarketing.startup.runonce.Task240111AddInodeAndIdentifierLeftIndexesTest; +import com.dotmarketing.startup.runonce.Task240112AddMetadataColumnToStructureTableTest; import com.dotmarketing.util.HashBuilderTest; import com.dotmarketing.util.ITConfigTest; import com.dotmarketing.util.MaintenanceUtilTest; @@ -672,7 +673,8 @@ Task240111AddInodeAndIdentifierLeftIndexesTest.class, AnnouncementsHelperIntegrationTest.class, RemoteAnnouncementsLoaderIntegrationTest.class, - JsEngineTest.class + JsEngineTest.class, + Task240112AddMetadataColumnToStructureTableTest.class }) public class MainSuite { diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java index 29b51f156cb6..30954afc161a 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java @@ -148,12 +148,13 @@ import com.dotmarketing.startup.runonce.Task230328AddMarkedForDeletionColumnTest; import com.dotmarketing.startup.runonce.Task230426AlterVarcharLengthOfLockedByColTest; import com.dotmarketing.startup.runonce.Task230523CreateVariantFieldInContentletIntegrationTest; -import com.dotmarketing.startup.runonce.Task231109AddPublishDateToContentletVersionInfoTest; import com.dotmarketing.startup.runonce.Task230701AddHashIndicesToWorkflowTablesTest; import com.dotmarketing.startup.runonce.Task230707CreateSystemTableTest; import com.dotmarketing.startup.runonce.Task230713IncreaseDisabledWysiwygColumnSizeTest; +import com.dotmarketing.startup.runonce.Task231109AddPublishDateToContentletVersionInfoTest; import com.dotmarketing.startup.runonce.Task240102AlterVarcharLengthOfRelationTypeTest; import com.dotmarketing.startup.runonce.Task240111AddInodeAndIdentifierLeftIndexesTest; +import com.dotmarketing.startup.runonce.Task240112AddMetadataColumnToStructureTableTest; import com.dotmarketing.util.MaintenanceUtilTest; import com.dotmarketing.util.ResourceCollectorUtilTest; import com.dotmarketing.util.UtilMethodsITest; @@ -329,7 +330,8 @@ Task240102AlterVarcharLengthOfRelationTypeTest.class, Task240111AddInodeAndIdentifierLeftIndexesTest.class, AnnouncementsHelperIntegrationTest.class, - RemoteAnnouncementsLoaderIntegrationTest.class + RemoteAnnouncementsLoaderIntegrationTest.class, + Task240112AddMetadataColumnToStructureTableTest.class }) public class MainSuite2b { diff --git a/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTableTest.java b/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTableTest.java new file mode 100644 index 000000000000..91db391c7199 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotmarketing/startup/runonce/Task240112AddMetadataColumnToStructureTableTest.java @@ -0,0 +1,31 @@ +package com.dotmarketing.startup.runonce; + +import com.dotcms.util.IntegrationTestInitService; +import com.dotmarketing.exception.DotDataException; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Verifies that the {@link Task240112AddMetadataColumnToStructureTable} Upgrade Task works as + * expected. + * + * @author Jose Castro + * @since Jan 11th, 2024 + */ +public class Task240112AddMetadataColumnToStructureTableTest { + + @BeforeClass + public static void prepare() throws Exception { + IntegrationTestInitService.getInstance().init(); + } + + @Test + public void TestUpgradeTask() throws DotDataException { + final Task240112AddMetadataColumnToStructureTable task = new Task240112AddMetadataColumnToStructureTable(); + task.executeUpgrade(); + assertTrue(task.forceRun()); + } + +} diff --git a/tools/dotcms-cli/api-data-model/src/main/java/com/dotcms/contenttype/model/type/ContentType.java b/tools/dotcms-cli/api-data-model/src/main/java/com/dotcms/contenttype/model/type/ContentType.java index 71e82f0c30ba..8c58f888d3ef 100644 --- a/tools/dotcms-cli/api-data-model/src/main/java/com/dotcms/contenttype/model/type/ContentType.java +++ b/tools/dotcms-cli/api-data-model/src/main/java/com/dotcms/contenttype/model/type/ContentType.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import org.immutables.value.Value; import org.immutables.value.Value.Default; @@ -163,6 +164,12 @@ public Boolean system() { @Nullable public abstract String urlMapPattern(); + @Nullable + @Value.Default + public Map metadata() { + return null; + } + @Value.Default public List workflows() { return Collections.emptyList();