From 78b1866bda8c2e4b99d4e130a7cf02941a62697e Mon Sep 17 00:00:00 2001 From: Rafael Velazco Date: Mon, 22 Apr 2024 18:21:07 -0400 Subject: [PATCH] feat(edit-contentlet): Implement new design in the edit content screen sidebar #27908 (#28175) * styles: update content sidebar style v1 * chore: code cleanup * chore: update placeholders with real data * chore: apply i18n * dev: show right tooltips * dev: i18n and lazyload for workflow status * refactor * test v1 * fix tests * test DotContentAsideWorkflow Component * feat: add loading state * fix tests * chore: code cleanup * feedback * Style: improve message in * chore: code cleanup --- .../src/lib/dot-router/dot-router.service.ts | 7 + .../lib/dot-workflow/dot-workflow.service.ts | 16 ++ .../libs/dotcms-scss/angular/_variables.scss | 1 + .../dotcms-theme/components/_chip.scss | 5 + .../dotcms-theme/components/_skeleton.scss | 4 +- .../dotcms-theme/components/_tooltip.scss | 20 +- .../components/messages/_message.scss | 24 ++- core-web/libs/dotcms-scss/shared/_fonts.scss | 2 + ...t-content-aside-information.component.html | 79 +++++++ ...t-content-aside-information.component.scss | 133 ++++++++++++ ...ontent-aside-information.component.spec.ts | 192 ++++++++++++++++++ ...dot-content-aside-information.component.ts | 41 ++++ .../dot-content-aside-workflow.component.html | 27 +++ .../dot-content-aside-workflow.component.scss | 36 ++++ ...t-content-aside-workflow.component.spec.ts | 101 +++++++++ .../dot-content-aside-workflow.component.ts | 72 +++++++ .../dot-edit-content-aside.component.html | 99 +++------ .../dot-edit-content-aside.component.scss | 72 +++++-- .../dot-edit-content-aside.component.spec.ts | 103 ++++------ .../dot-edit-content-aside.component.ts | 60 ++---- .../edit-content.layout.component.html | 36 ++-- .../edit-content.layout.component.scss | 10 +- .../edit-content.layout.component.spec.ts | 16 +- .../edit-content.layout.component.ts | 5 +- .../store/edit-content.store.spec.ts | 9 +- .../edit-content/store/edit-content.store.ts | 21 +- ...it-content-wysiwyg-field.component.spec.ts | 16 +- ...ot-edit-content-wysiwyg-field.component.ts | 8 +- .../models/dot-edit-content-form.interface.ts | 1 + .../src/lib/pipes/contentlet-status.pipe.ts | 5 +- .../libs/edit-content/src/lib/utils/mocks.ts | 3 +- .../dot-copy-button.component.html | 2 +- .../dot-copy-button.component.ts | 13 +- .../dot-relative-date.pipe.spec.ts | 10 +- .../dot-relative-date.pipe.ts | 26 ++- .../src/lib/dot-workflow-service.mock.ts | 73 +++++++ .../WEB-INF/messages/Language.properties | 3 + 37 files changed, 1065 insertions(+), 286 deletions(-) create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.html create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.scss create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.ts diff --git a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts index c05c3746aebb..f616611f91c8 100644 --- a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts +++ b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts @@ -201,6 +201,13 @@ export class DotRouterService { this.router.navigate([`/c/content/new/${variableName}`]); } + /** + * Redirect to edit the content type + * + * @param {string} id + * @param {string} portlet + * @memberof DotRouterService + */ goToEditContentType(id: string, portlet: string): void { this.router.navigate([`/${portlet}/edit/${id}`]); } diff --git a/core-web/libs/data-access/src/lib/dot-workflow/dot-workflow.service.ts b/core-web/libs/data-access/src/lib/dot-workflow/dot-workflow.service.ts index 9f25d60874c3..abcb3d078d9f 100644 --- a/core-web/libs/data-access/src/lib/dot-workflow/dot-workflow.service.ts +++ b/core-web/libs/data-access/src/lib/dot-workflow/dot-workflow.service.ts @@ -42,6 +42,22 @@ export class DotWorkflowService { ); } + /** + * Get the Workflow Schema for a ContentType given its inode + * + * @param {string} contentTypeId + * @return {*} + * @memberof DotWorkflowService + */ + getSchemaContentType(contentTypeId: string): Observable<{ + contentTypeSchemes: DotCMSWorkflow[]; + schemes: DotCMSWorkflow[]; + }> { + return this.httpClient + .get(`${this.WORKFLOW_URL}/schemes/schemescontenttypes/${contentTypeId}`) + .pipe(pluck('entity')); + } + /** * Get the current workflow status for Contentlet given its inode * diff --git a/core-web/libs/dotcms-scss/angular/_variables.scss b/core-web/libs/dotcms-scss/angular/_variables.scss index 66136246f6ac..c724a35a8d5a 100644 --- a/core-web/libs/dotcms-scss/angular/_variables.scss +++ b/core-web/libs/dotcms-scss/angular/_variables.scss @@ -47,6 +47,7 @@ $info: $color-palette-secondary; $field-tiny-height: 28px; $field-disabled-opacity: 0.38; $line-height: 1.5em; +$line-height-relative: 140%; // ICONS $md-icon-size-normal: 18px; diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_chip.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_chip.scss index 8bb1c2ee5479..89a62faa9e19 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_chip.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_chip.scss @@ -58,6 +58,11 @@ p-chip .p-chip { color: $color-accessible-text-fuchsia; } + &.p-chip-blue { + background-color: $color-accessible-text-blue-bg; + color: $color-accessible-text-blue; + } + &.p-chip-sm { gap: $spacing-0; padding: 0 $spacing-1; diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_skeleton.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_skeleton.scss index e118bca3b94c..73f9b8b272df 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_skeleton.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_skeleton.scss @@ -1,9 +1,9 @@ @use "variables" as *; .p-skeleton { - background-color: $color-palette-gray-100; + background-color: $color-palette-gray-300; border-radius: 3px; } .p-skeleton:after { - background: linear-gradient(90deg, $white, $color-palette-white-op-40, $white); + background: linear-gradient(90deg, $white, $color-palette-white-op-60, $white); } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tooltip.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tooltip.scss index 43420f138dcb..a031859fb98b 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tooltip.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/_tooltip.scss @@ -1,23 +1,23 @@ @use "variables" as *; .p-tooltip .p-tooltip-text { - background: $color-palette-gray-900; - color: $white; - padding: $spacing-2 $spacing-3; - font-size: 1rem; - box-shadow: none; - border-radius: $border-radius-xs; + background: $white; + box-shadow: $shadow-m; + padding: $spacing-1 $spacing-3; + font-size: $font-size-sm; + line-height: 140%; + border-radius: $border-radius-md; } .p-tooltip.p-tooltip-right .p-tooltip-arrow { - border-right-color: $color-palette-gray-900; + border-right-color: $white; } .p-tooltip.p-tooltip-left .p-tooltip-arrow { - border-left-color: $color-palette-gray-900; + border-left-color: $white; } .p-tooltip.p-tooltip-top .p-tooltip-arrow { - border-top-color: $color-palette-gray-900; + border-top-color: $white; } .p-tooltip.p-tooltip-bottom .p-tooltip-arrow { - border-bottom-color: $color-palette-gray-900; + border-bottom-color: $white; } diff --git a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/messages/_message.scss b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/messages/_message.scss index ae04a2b40125..bb639216eda5 100644 --- a/core-web/libs/dotcms-scss/angular/dotcms-theme/components/messages/_message.scss +++ b/core-web/libs/dotcms-scss/angular/dotcms-theme/components/messages/_message.scss @@ -52,7 +52,7 @@ } .p-message { - margin: $spacing-2 0; + margin: 0; border-radius: $border-radius-xs; } .p-message .p-message-wrapper { @@ -86,17 +86,31 @@ color: #01579b; } .p-message.p-message-success { - background: #c8e6c9; + background: rgba(245, 253, 248, 1); border: solid transparent; border-width: 0 0 0 0; - color: #1b5e20; + color: $black; + + .pi { + color: rgba(62, 217, 122, 1); + } +} +.p-message.p-message-success { + border: solid rgba(62, 217, 122, 1); + border-width: 0 0 0 0; } .p-message.p-message-success .p-message-icon { - color: #1b5e20; + color: rgba(62, 217, 122, 1); } .p-message.p-message-success .p-message-close { - color: #1b5e20; + color: rgba(62, 217, 122, 1); } +.p-messages.p-message-border-y:has(.p-message.p-message-success) { + border: 0 solid rgba(62, 217, 122, 1); + border-top-width: 1px; + border-bottom-width: 1px; +} + .p-message.p-message-warn { background: #ffecb3; border: solid transparent; diff --git a/core-web/libs/dotcms-scss/shared/_fonts.scss b/core-web/libs/dotcms-scss/shared/_fonts.scss index ef7c7d95b969..0c585cf39542 100644 --- a/core-web/libs/dotcms-scss/shared/_fonts.scss +++ b/core-web/libs/dotcms-scss/shared/_fonts.scss @@ -16,6 +16,7 @@ $font-size-xxxl: 3rem; // 48px $font-size-xxl: 1.75rem; // 28px $font-size-xl: 1.5rem; // 24px $font-size-lg: 1.25rem; // 20px +$font-size-lmd: 1.125rem; // 18px $font-size-md: $font-size-default; // 16px $font-size-smd: 0.875rem; // 14px $font-size-sm: 0.813rem; // 13px @@ -24,4 +25,5 @@ $font-size-xs: 0.625rem; // 10px $font-weight-regular-bold: 400; $font-weight-semi-bold: 500; +$font-weight-medium-bold: 600; $font-weight-bold: 700; diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.html new file mode 100644 index 000000000000..870dcbe6c9f0 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.html @@ -0,0 +1,79 @@ +
+
+ + +
+ +
diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.scss b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.scss new file mode 100644 index 000000000000..7cd6e6723a68 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.scss @@ -0,0 +1,133 @@ +@use "variables" as *; +@import "mixins"; + +.content-aside__information { + display: flex; + padding: $spacing-3; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: $spacing-1; + + .content-aside__status { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + } +} + +.content-aside__metadata { + display: flex; + flex-direction: column; + justify-content: center; + gap: $spacing-1; + width: 100%; + + .sidebar-card { + display: flex; + flex-direction: column; + background: $white; + padding: $spacing-1; + border-radius: $border-radius-lg; + gap: $spacing-0; + + &, + label { + user-select: none; + cursor: pointer; + } + + &:hover { + background: $color-palette-primary-100; + } + } + + .sidebar-card__header { + display: flex; + justify-content: space-between; + color: $black; + font-size: $font-size-smd; + font-weight: $font-weight-medium-bold; + line-height: $line-height-relative; + + i { + color: $color-palette-primary-500; + } + } + + .sidebar-card__label { + color: $color-palette-gray-800; + font-size: $font-size-smd; + font-weight: $font-weight-regular-bold; + line-height: $line-height-relative; + } +} + +.content-history__container { + display: flex; + align-items: center; + flex: 1 0 0; + align-self: stretch; + border-radius: $border-radius-lg; + background: $white; + + .content-history { + display: flex; + height: 100%; + padding: $spacing-1; + flex-direction: column; + align-items: flex-start; + gap: $spacing-0; + flex: 1 0 0; + position: relative; + user-select: none; + min-height: 5rem; + @include truncate-text; + + &, + & > * { + cursor: pointer; + } + + &:hover { + background: $color-palette-primary-100; + } + + label { + width: 100%; + } + + &:not(:first-child)::before { + content: ""; + position: absolute; + height: 39px; + border-left: 1px solid $color-palette-gray-300; + left: 0; + top: 0; + bottom: 0; + margin: auto; + } + } + + .content-history__title { + color: $black; + font-size: $font-size-smd; + font-weight: $font-weight-medium-bold; + line-height: $line-height-relative; + } + + .content-history__author { + color: $color-palette-gray-800; + font-size: $font-size-smd; + font-weight: $font-weight-regular-bold; + line-height: $line-height-relative; + } + + .content-history__date { + color: $color-palette-gray-700; + font-size: $font-size-xsm; + font-weight: $font-weight-regular-bold; + line-height: $line-height-relative; + } +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.spec.ts new file mode 100644 index 000000000000..1ec234c9bbab --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.spec.ts @@ -0,0 +1,192 @@ +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; + +import { fakeAsync, tick } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { Chip, ChipModule } from 'primeng/chip'; +import { TooltipModule } from 'primeng/tooltip'; + +import { DotFormatDateService, DotMessageService } from '@dotcms/data-access'; +import { + DotCopyButtonComponent, + DotLinkComponent, + DotMessagePipe, + DotRelativeDatePipe +} from '@dotcms/ui'; +import { + DotFormatDateServiceMock, + EMPTY_CONTENTLET, + MockDotMessageService +} from '@dotcms/utils-testing'; + +import { DotContentAsideInformationComponent } from './dot-content-aside-information.component'; + +import { ContentletStatusPipe } from '../../../../pipes/contentlet-status.pipe'; + +const messageServiceMock = new MockDotMessageService({ + Published: 'Published', + Modified: 'Modified', + Created: 'Created', + 'Content-Type': 'Content Type', + References: 'References' +}); + +const BASIC_CONTENTLET = { + ...EMPTY_CONTENTLET, + publishDate: '2021-01-01T00:00:00Z', + publishUserName: 'admin', + modDate: '2021-01-01T00:00:00Z', + modUserName: 'admin', + ownerName: 'admin', + createDate: '2021-01-01T00:00:00Z' +}; + +describe('DotContentAsideInformationComponent', () => { + let spectator: Spectator; + let dotRelativeDatePipe: DotRelativeDatePipe; + let router: Router; + + const createComponent = createComponentFactory({ + component: DotContentAsideInformationComponent, + imports: [ + RouterTestingModule.withRoutes([ + { + path: 'content-types-angular/edit/:contentType', + component: DotContentAsideInformationComponent + } + ]), + TooltipModule, + ChipModule, + ContentletStatusPipe, + DotMessagePipe, + DotRelativeDatePipe, + MockComponent(DotCopyButtonComponent), + MockComponent(DotLinkComponent) + ], + providers: [ + DotRelativeDatePipe, + { + provide: DotMessageService, + useValue: messageServiceMock + }, + { provide: DotFormatDateService, useClass: DotFormatDateServiceMock } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + detectChanges: false, + props: { + contentlet: BASIC_CONTENTLET, + contentTypeName: 'Blog', + loading: false + } + }); + + router = spectator.inject(Router); + dotRelativeDatePipe = spectator.inject(DotRelativeDatePipe, true); + }); + + it('should have a copy button', () => { + spectator.detectChanges(); + const dotCopyButton = spectator.query(DotCopyButtonComponent); + expect(dotCopyButton).not.toBeNull(); + expect(dotCopyButton.copy).toBe(BASIC_CONTENTLET.inode); + expect(dotCopyButton.label).toBe(`ID: ${BASIC_CONTENTLET.inode.slice(-10)}`); + }); + + it('should have a chip status', () => { + spectator.detectChanges(); + const chipComponent = spectator.query(Chip); + expect(chipComponent).not.toBeNull(); + expect(chipComponent.label).toBe('Revision'); + expect(chipComponent.styleClass).toBe('p-chip-sm p-chip-pink'); + }); + + it('should have a link to the content type', fakeAsync(() => { + spectator.detectChanges(); + + const linkElement = spectator.query(byTestId('content-type-link')); + const span = linkElement.querySelector('span'); + const label = linkElement.querySelector('label'); + expect(linkElement).not.toBeNull(); + expect(span.textContent.trim()).toBe('Content Type'); + expect(label.textContent.trim()).toBe('Blog'); + + linkElement.dispatchEvent(new Event('click')); + spectator.click(linkElement); + spectator.detectChanges(); + tick(); + expect(router.url).toBe(`/content-types-angular/edit/Blog`); + })); + + it('should have a references button', () => { + spectator.detectChanges(); + const referencesButton = spectator.query(byTestId('references-button')); + const referencesSpan = referencesButton.querySelector('span'); + expect(referencesButton).not.toBeNull(); + expect(referencesSpan.textContent.trim()).toBe('References'); + }); + + describe('history', () => { + it('should have publish history button', () => { + spectator.detectChanges(); + const publishButton = spectator.query(byTestId('publish-history')); + const publishLabel = publishButton.querySelector('.content-history__title'); + const publishAuthor = publishButton.querySelector('.content-history__author'); + const publishDate = publishButton.querySelector('.content-history__date'); + + expect(publishButton).not.toBeNull(); + expect(publishLabel.innerHTML.trim()).toBe('Published'); + expect(publishAuthor.textContent.trim()).toBe('admin'); + expect(publishDate.textContent.trim()).toBe( + dotRelativeDatePipe.transform(BASIC_CONTENTLET.publishDate) + ); + }); + + it('should have Modify history button', () => { + spectator.detectChanges(); + const modButton = spectator.query(byTestId('mod-history')); + const modLabel = modButton.querySelector('.content-history__title'); + const modAuthor = modButton.querySelector('.content-history__author'); + const modDate = modButton.querySelector('.content-history__date'); + + expect(modButton).not.toBeNull(); + expect(modLabel.innerHTML.trim()).toBe('Modified'); + expect(modAuthor.textContent.trim()).toBe('admin'); + expect(modDate.textContent.trim()).toBe( + dotRelativeDatePipe.transform(BASIC_CONTENTLET.modDate) + ); + }); + + it('should have Create history button', () => { + spectator.detectChanges(); + const createButton = spectator.query(byTestId('create-history')); + const createLabel = createButton.querySelector('.content-history__title'); + const createAuthor = createButton.querySelector('.content-history__author'); + const createDate = createButton.querySelector('.content-history__date'); + + expect(createButton).not.toBeNull(); + expect(createLabel.innerHTML.trim()).toBe('Created'); + expect(createAuthor.textContent.trim()).toBe('admin'); + expect(createDate.textContent.trim()).toBe( + dotRelativeDatePipe.transform(BASIC_CONTENTLET.createDate) + ); + }); + + it('should have history buttons', () => { + spectator.detectChanges(); + const historyContainer = spectator.query(byTestId('history-container')); + const publishButton = spectator.query(byTestId('publish-history')); + const modButton = spectator.query(byTestId('mod-history')); + const createHistory = spectator.query(byTestId('create-history')); + + expect(historyContainer).not.toBeNull(); + expect(publishButton).not.toBeNull(); + expect(modButton).not.toBeNull(); + expect(createHistory).not.toBeNull(); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.ts new file mode 100644 index 000000000000..9578ecd1f556 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-information/dot-content-aside-information.component.ts @@ -0,0 +1,41 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { ChipModule } from 'primeng/chip'; +import { SkeletonModule } from 'primeng/skeleton'; +import { TooltipModule } from 'primeng/tooltip'; + +import { DotCMSContentlet } from '@dotcms/dotcms-models'; +import { + DotCopyButtonComponent, + DotLinkComponent, + DotMessagePipe, + DotRelativeDatePipe +} from '@dotcms/ui'; + +import { ContentletStatusPipe } from '../../../../pipes/contentlet-status.pipe'; + +@Component({ + selector: 'dot-content-aside-information', + standalone: true, + imports: [ + CommonModule, + RouterLink, + TooltipModule, + ChipModule, + SkeletonModule, + DotCopyButtonComponent, + DotLinkComponent, + ContentletStatusPipe, + DotRelativeDatePipe, + DotMessagePipe + ], + templateUrl: './dot-content-aside-information.component.html', + styleUrl: './dot-content-aside-information.component.scss' +}) +export class DotContentAsideInformationComponent { + @Input() contentlet!: DotCMSContentlet; + @Input() contentTypeName!: string; + @Input() loading!: boolean; +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.html new file mode 100644 index 000000000000..853fd2b7ebe4 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.html @@ -0,0 +1,27 @@ +
+
+ {{ 'Workflow' | dm }} + {{ 'Step' | dm }} + {{ 'Content-Type' | dm }} + @if($workflowStatus()?.task ) { + {{ 'Assignee' | dm }} + } +
+
+ @if($workflowStatus()) { + + {{ $workflowStatus().scheme?.name }} + + + {{ $workflowStatus().step?.name || 'New' }} + + + {{ contentType.name }} + + @if($workflowStatus().task; as task ) { + + {{ task.assignedTo }} + + } } +
+
diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.scss b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.scss new file mode 100644 index 000000000000..1630879d0222 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.scss @@ -0,0 +1,36 @@ +@use "variables" as *; +@import "mixins"; + +.content-aside__workflow { + display: flex; + gap: $spacing-3; + padding: $spacing-1 $spacing-4; +} + +.content-aside__workflow-column { + display: flex; + flex: 1 0 0; + flex-direction: column; + align-items: flex-start; + gap: $spacing-1; + overflow: hidden; +} + +.workflow-column__title { + font-weight: $font-weight-medium-bold; +} + +.item__description { + color: $color-palette-gray-800; + margin: 0; +} + +.workflow-column__title, +.workflow-column__description { + color: $black; + height: 32px; + font-size: $font-size-smd; + line-height: $line-height; + width: 100%; + @include truncate-text; +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.spec.ts new file mode 100644 index 000000000000..9405501842b0 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.spec.ts @@ -0,0 +1,101 @@ +import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +import { SkeletonModule } from 'primeng/skeleton'; + +import { DotWorkflowService } from '@dotcms/data-access'; +import { + DotMessagePipe, + WORKFLOW_SCHEMA_MOCK, + WORKFLOW_STATUS_MOCK, + dotcmsContentTypeBasicMock +} from '@dotcms/utils-testing'; + +import { DotContentAsideWorkflowComponent } from './dot-content-aside-workflow.component'; + +const CONTENTTYPE_MOCK = { + ...dotcmsContentTypeBasicMock, + name: 'Blogs' +}; + +describe('DotContentAsideWorkflowComponent', () => { + let spectator: Spectator; + let dotWorkflowService: DotWorkflowService; + + const createComponent = createComponentFactory({ + component: DotContentAsideWorkflowComponent, + imports: [HttpClientTestingModule, DotMessagePipe, SkeletonModule], + componentProviders: [ + { + provide: DotWorkflowService, + useValue: { + getWorkflowStatus: () => of(WORKFLOW_STATUS_MOCK), + getSchemaContentType: () => of(WORKFLOW_SCHEMA_MOCK) + } + } + ] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + contentType: CONTENTTYPE_MOCK + }, + detectChanges: false + }); + + dotWorkflowService = spectator.inject(DotWorkflowService, true); + }); + + describe('New contentlet', () => { + it('should call getSchemaContentType', () => { + const spyWorkflow = jest.spyOn(dotWorkflowService, 'getSchemaContentType'); + spectator.detectChanges(); + expect(spyWorkflow).toHaveBeenCalledWith(CONTENTTYPE_MOCK.id); + }); + + it('should render aside workflow data', () => { + spectator.detectChanges(); + + expect(spectator.query(byTestId('workflow-name')).textContent.trim()).toBe('Blogs'); + expect(spectator.query(byTestId('workflow-step')).textContent.trim()).toBe('New'); + expect(spectator.query(byTestId('workflow-content-type')).textContent.trim()).toBe( + CONTENTTYPE_MOCK.name + ); + }); + + it('should not render assigned to', () => { + spectator.detectChanges(); + expect(spectator.query(byTestId('workflow-assigned'))).toBeNull(); + }); + }); + + describe('Existing contentlet', () => { + let spyWorkflow: jest.SpyInstance; + + beforeEach(() => { + spyWorkflow = jest.spyOn(dotWorkflowService, 'getWorkflowStatus'); + spectator.setInput('inode', '123'); + spectator.detectChanges(); + }); + + it('should call getWorkflowStatus', () => { + expect(spyWorkflow).toHaveBeenCalledWith('123'); + }); + + it('should render aside workflow data', () => { + expect(spectator.query(byTestId('workflow-name')).textContent.trim()).toBe( + 'System Workflow' + ); + expect(spectator.query(byTestId('workflow-step')).textContent.trim()).toBe('Published'); + expect(spectator.query(byTestId('workflow-assigned')).textContent.trim()).toBe( + 'Admin User' + ); + expect(spectator.query(byTestId('workflow-content-type')).textContent.trim()).toBe( + CONTENTTYPE_MOCK.name + ); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.ts new file mode 100644 index 000000000000..5d6a1f81c63f --- /dev/null +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/components/dot-content-aside-workflow/dot-content-aside-workflow.component.ts @@ -0,0 +1,72 @@ +import { Observable } from 'rxjs'; + +import { Component, Input, OnChanges, OnInit, SimpleChanges, inject, signal } from '@angular/core'; + +import { SkeletonModule } from 'primeng/skeleton'; + +import { map } from 'rxjs/operators'; + +import { DotWorkflowService } from '@dotcms/data-access'; +import { DotCMSContentType, DotCMSWorkflowStatus } from '@dotcms/dotcms-models'; +import { DotMessagePipe } from '@dotcms/ui'; + +@Component({ + selector: 'dot-content-aside-workflow', + standalone: true, + imports: [DotMessagePipe, SkeletonModule], + providers: [DotWorkflowService], + templateUrl: './dot-content-aside-workflow.component.html', + styleUrl: './dot-content-aside-workflow.component.scss' +}) +export class DotContentAsideWorkflowComponent implements OnInit, OnChanges { + @Input() inode: string; + @Input() contentType: DotCMSContentType; + + private readonly workflowService = inject(DotWorkflowService); + protected readonly $workflowStatus = signal(null); + + ngOnInit() { + this.setContentStatus(); + } + + ngOnChanges(SimpleChanges: SimpleChanges) { + if (SimpleChanges.inode?.currentValue) { + this.setContentStatus(); + } + } + + private setContentStatus() { + const obs$ = this.inode ? this.getWorkflowStatus() : this.getNewContentStatus(); + obs$.subscribe((workflowStatus) => this.$workflowStatus.set(workflowStatus)); + } + + /** + * Get the current workflow status + * + * @private + * @return {*} + * @memberof DotContentAsideWorkflowComponent + */ + private getWorkflowStatus(): Observable { + return this.workflowService.getWorkflowStatus(this.inode); + } + + /** + * Get the new content status + * + * @private + * @return {*} + * @memberof DotContentAsideWorkflowComponent + */ + private getNewContentStatus() { + return this.workflowService.getSchemaContentType(this.contentType.id).pipe( + map(({ contentTypeSchemes }) => { + return { + scheme: contentTypeSchemes[0], + step: null, + task: null + }; + }) + ); + } +} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.html index 832c3a2d4957..5f379455d70e 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.html @@ -1,73 +1,26 @@ - -
- - - {{ 'Workflow' | dm }} - - - -
- -
-
{{ 'Assignee' | dm }}
-
- {{ workflow.task?.assignedTo }} -
- -
{{ 'Step' | dm }}
-
- -
- -
{{ 'Workflow' | dm }}
-
- {{ workflow.scheme?.name }} -
-
-
- -
- - - {{ 'Information' | dm }} - - - -
- -
-
{{ 'Content State' | dm }}
- -
- -
- -
{{ 'Modified by' | dm }}
-
- - {{ contentLet?.modUserName }} - -
- -
{{ 'Last modified' | dm }}
-
- - {{ contentLet?.modDate | dotRelativeDate }} - -
- -
{{ 'Content Type' | dm }}
- {{ - contentType - }} -
+ diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.scss b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.scss index 6c57cba512b5..68b7234a7075 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.scss +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.scss @@ -1,34 +1,64 @@ @use "variables" as *; :host { - padding: 0 $spacing-3; - display: block; + display: flex; + width: 350px; + height: 100%; + padding-bottom: $spacing-0; + flex-direction: column; + align-items: flex-start; + border-left: 1px solid $color-palette-gray-400; + background: $color-palette-gray-100; + box-shadow: $shadow-m; } -.edit-content-aside--header { - display: flex; - justify-content: space-between; - margin-bottom: $spacing-1; +.content-aside { + overflow: auto; +} + +.content-aside, +.content-aside__section { + display: block; + width: 100%; - strong { + .content-aside__header { display: flex; + height: 48px; + padding: 0 $spacing-3; align-items: center; - gap: 0.3em; - } -} + gap: $spacing-1; + background: $color-palette-primary-200; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 10; -.edit-content-aside--item { - display: grid; - grid-template-columns: 1fr 1fr; - gap: $spacing-1; - grid-auto-rows: $spacing-5; + .content-aside__icon { + display: flex; + justify-content: center; + gap: $spacing-0; - p { - color: $color-palette-gray-800; - margin: 0; - } + label { + color: $color-palette-primary-500; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 140%; + text-transform: capitalize; + } + + i { + width: 1rem; + height: 1rem; + color: $color-palette-primary-500; + } + } - dd { - margin: 0; + label { + color: $black; + font-size: $font-size-lmd; + font-weight: $font-weight-bold; + line-height: $line-height; + } } } 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 2afc726a8def..4aa3d92c2087 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 @@ -1,12 +1,13 @@ -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator'; -import { mockProvider } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; +import { Spectator, createComponentFactory } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ActivatedRoute } from '@angular/router'; -import { DotMessageService, DotWorkflowService, DotFormatDateService } from '@dotcms/data-access'; +import { DotMessageService } from '@dotcms/data-access'; +import { MockDotMessageService, dotcmsContentTypeBasicMock } from '@dotcms/utils-testing'; +import { DotContentAsideInformationComponent } from './components/dot-content-aside-information/dot-content-aside-information.component'; +import { DotContentAsideWorkflowComponent } from './components/dot-content-aside-workflow/dot-content-aside-workflow.component'; import { DotEditContentAsideComponent } from './dot-edit-content-aside.component'; import { CONTENT_FORM_DATA_MOCK } from '../../utils/mocks'; @@ -16,87 +17,59 @@ describe('DotEditContentAsideComponent', () => { const createComponent = createComponentFactory({ component: DotEditContentAsideComponent, detectChanges: false, - imports: [HttpClientTestingModule], + imports: [ + HttpClientTestingModule, + DotContentAsideInformationComponent, + DotContentAsideWorkflowComponent + ], + declarations: [ + MockComponent(DotContentAsideInformationComponent), + MockComponent(DotContentAsideWorkflowComponent) + ], providers: [ - mockProvider(ActivatedRoute), // Needed, use RouterLink { provide: DotMessageService, - useValue: { - get() { - return 'Sample'; - } - } - }, - { - provide: DotFormatDateService, - useValue: { - differenceInCalendarDays: () => 10, - format: () => '11/07/2023', - getUTC: () => new Date() - } + useValue: new MockDotMessageService({ + Information: 'Information', + Workflow: 'Workflow', + 'show-all': 'Show all' + }) } ] }); beforeEach(() => { spectator = createComponent({ - providers: [ - { - provide: DotWorkflowService, - useValue: { - getWorkflowStatus: () => - of({ - scheme: { name: 'Test' }, - step: { name: 'Test' }, - task: { assignedTo: 'Admin User' } - }) - } - } - ] + props: { + contentlet: CONTENT_FORM_DATA_MOCK.contentlet, + contentType: dotcmsContentTypeBasicMock + } }); }); it('should render aside information data', () => { - spectator.setInput('contentLet', CONTENT_FORM_DATA_MOCK.contentlet); - 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'); - expect(spectator.query(byTestId('inode')).textContent.trim()).toBe( - CONTENT_FORM_DATA_MOCK.contentlet.inode.slice(0, 8) + const dotContentAsideInformationComponent = spectator.query( + DotContentAsideInformationComponent ); - }); - it('should not render aside information data', () => { - 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.contentType); - spectator.detectChanges(); - - expect(spectator.query(byTestId('modified-by')).textContent).toBe(''); - expect(spectator.query(byTestId('last-modified')).textContent).toBe(''); - expect(spectator.query(byTestId('inode'))).toBeFalsy(); + expect(dotContentAsideInformationComponent).toBeTruthy(); + expect(dotContentAsideInformationComponent.contentTypeName).toEqual( + dotcmsContentTypeBasicMock.variable + ); + expect(dotContentAsideInformationComponent.contentlet).toEqual( + CONTENT_FORM_DATA_MOCK.contentlet + ); }); it('should render aside workflow data', () => { - spectator.setInput('contentLet', CONTENT_FORM_DATA_MOCK.contentlet); - spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType.contentType); spectator.detectChanges(); + const dotContentAsideWorkflowComponent = spectator.query(DotContentAsideWorkflowComponent); - expect(spectator.component.workflow$).toBeDefined(); - expect(spectator.query(byTestId('workflow-name')).textContent.trim()).toBe('Test'); - expect(spectator.query(byTestId('workflow-step')).textContent.trim()).toBe('Test'); - expect(spectator.query(byTestId('workflow-assigned')).textContent.trim()).toBe( - 'Admin User' + expect(dotContentAsideWorkflowComponent).toBeTruthy(); + expect(dotContentAsideWorkflowComponent.inode).toEqual( + CONTENT_FORM_DATA_MOCK.contentlet.inode ); - }); - - it('should render New as status when dont have contentlet', () => { - spectator.setInput('contentLet', null); - spectator.setInput('contentType', CONTENT_FORM_DATA_MOCK.contentType.contentType); - spectator.detectChanges(); - - expect(spectator.query(byTestId('workflow-step')).textContent).toBe('New'); + expect(dotContentAsideWorkflowComponent.contentType).toEqual(dotcmsContentTypeBasicMock); }); }); diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.ts index 3ced60b06530..c6d3764824ce 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-aside/dot-edit-content-aside.component.ts @@ -1,22 +1,11 @@ -import { Observable, of } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { AsyncPipe, NgIf, NgSwitch, NgSwitchCase, SlicePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, OnInit, inject } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { DotCMSContentType, DotCMSContentlet } from '@dotcms/dotcms-models'; +import { DotMessagePipe } from '@dotcms/ui'; -import { ChipModule } from 'primeng/chip'; - -import { DotWorkflowService } from '@dotcms/data-access'; -import { DotCMSContentlet, DotCMSWorkflowStatus } from '@dotcms/dotcms-models'; -import { - DotApiLinkComponent, - DotCopyButtonComponent, - DotLinkComponent, - DotMessagePipe, - DotRelativeDatePipe -} from '@dotcms/ui'; - -import { ContentletStatusPipe } from '../../pipes/contentlet-status.pipe'; +import { DotContentAsideInformationComponent } from './components/dot-content-aside-information/dot-content-aside-information.component'; +import { DotContentAsideWorkflowComponent } from './components/dot-content-aside-workflow/dot-content-aside-workflow.component'; @Component({ selector: 'dot-edit-content-aside', @@ -25,35 +14,14 @@ import { ContentletStatusPipe } from '../../pipes/contentlet-status.pipe'; styleUrls: ['./dot-edit-content-aside.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - NgIf, - NgSwitch, - NgSwitchCase, - SlicePipe, - DotApiLinkComponent, - DotCopyButtonComponent, - DotRelativeDatePipe, - ChipModule, + CommonModule, DotMessagePipe, - ContentletStatusPipe, - RouterLink, - AsyncPipe, - DotLinkComponent - ], - providers: [DotWorkflowService] + DotContentAsideInformationComponent, + DotContentAsideWorkflowComponent + ] }) -export class DotEditContentAsideComponent implements OnInit { - @Input() contentLet!: DotCMSContentlet; - @Input() contentType!: string; - - private readonly workFlowService = inject(DotWorkflowService); - - workflow$!: Observable; - - ngOnInit() { - if (this.contentLet?.inode) { - this.workflow$ = this.workFlowService.getWorkflowStatus(this.contentLet.inode); - } else { - this.workflow$ = of({ scheme: null, step: null, task: null }); - } - } +export class DotEditContentAsideComponent { + @Input() contentlet!: DotCMSContentlet; + @Input() contentType!: DotCMSContentType; + @Input() loading!: boolean; } 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 7cdb62620186..ef7427e128f6 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,16 +1,24 @@ -
- - {{ 'edit.content.layout.beta.message.switch' | dm }} - {{ 'edit.content.layout.beta.message.needed' | dm }} -
+ + +
+ + {{ 'edit.content.layout.beta.message.switch' | dm }} + {{ ' ' }}{{ 'edit.content.layout.beta.message.needed' | dm }} +
+
+ + - +
{{ '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 05cf8adb7e39..ef08ea863574 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 @@ -4,7 +4,7 @@ display: grid; grid-template-areas: "topBar topBar" - "header header" + "header sidebar" "body sidebar"; grid-template-columns: 1fr 21.875rem; grid-template-rows: auto auto 1fr; @@ -15,12 +15,10 @@ .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; + .pi { + font-size: $font-size-lg; + margin-right: $spacing-1; } } 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 a0866adf23ca..d1ce256487be 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 @@ -8,6 +8,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ActivatedRoute } from '@angular/router'; import { MessageService } from 'primeng/api'; +import { MessagesModule } from 'primeng/messages'; import { DotContentTypeService, @@ -41,6 +42,7 @@ describe('EditContentLayoutComponent', () => { component: EditContentLayoutComponent, imports: [ HttpClientTestingModule, + MessagesModule, MockPipe(DotMessagePipe), MockComponent(DotEditContentFormComponent), MockComponent(DotEditContentToolbarComponent), @@ -60,7 +62,8 @@ describe('EditContentLayoutComponent', () => { const mockData: EditContentPayload = { actions: mockWorkflowsActions, contentType: CONTENT_TYPE_MOCK, - contentlet: BINARY_FIELD_CONTENTLET + contentlet: BINARY_FIELD_CONTENTLET, + loading: false }; beforeEach(async () => { @@ -123,8 +126,8 @@ describe('EditContentLayoutComponent', () => { spectator.detectChanges(); const asideComponent = spectator.query(DotEditContentAsideComponent); expect(asideComponent).toBeDefined(); - expect(asideComponent.contentLet).toEqual(mockData.contentlet); - expect(asideComponent.contentType).toEqual(mockData.contentType.variable); + expect(asideComponent.contentlet).toEqual(mockData.contentlet); + expect(asideComponent.contentType).toEqual(mockData.contentType); }); it('should fire workflow action', () => { @@ -165,7 +168,8 @@ describe('EditContentLayoutComponent', () => { const mockData: EditContentPayload = { actions: mockWorkflowsActions, contentType: CONTENT_TYPE_MOCK, - contentlet: null + contentlet: null, + loading: false }; beforeEach(async () => { @@ -229,8 +233,8 @@ describe('EditContentLayoutComponent', () => { spectator.detectChanges(); const asideComponent = spectator.query(DotEditContentAsideComponent); expect(asideComponent).toBeDefined(); - expect(asideComponent.contentLet).toEqual(mockData.contentlet); - expect(asideComponent.contentType).toEqual(mockData.contentType.variable); + expect(asideComponent.contentlet).toEqual(mockData.contentlet); + expect(asideComponent.contentType).toEqual(mockData.contentType); }); }); }); 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 f77f1ee4d668..7eec5b524838 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 @@ -6,6 +6,7 @@ import { ActivatedRoute, RouterLink } from '@angular/router'; import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; +import { MessagesModule } from 'primeng/messages'; import { ToastModule } from 'primeng/toast'; import { switchMap } from 'rxjs/operators'; @@ -36,6 +37,7 @@ import { DotEditContentService } from '../../services/dot-edit-content.service'; DotMessagePipe, ButtonModule, ToastModule, + MessagesModule, RouterLink, DotEditContentFormComponent, DotEditContentAsideComponent, @@ -76,7 +78,8 @@ export class EditContentLayoutComponent implements OnInit { this.store.setState({ contentType, actions, - contentlet + contentlet, + loading: false }); }); } 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 cf54beea5947..bbad9d9726bd 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 @@ -81,7 +81,8 @@ describe('DotEditContentStore', () => { spectator.service.setState({ actions: mockWorkflowsActions, contentType: CONTENT_TYPE_MOCK, - contentlet: BINARY_FIELD_CONTENTLET + contentlet: BINARY_FIELD_CONTENTLET, + loading: false }); }); @@ -96,14 +97,16 @@ describe('DotEditContentStore', () => { expect(state).toEqual({ actions: [], contentType: CONTENT_TYPE_MOCK, - contentlet: BINARY_FIELD_CONTENTLET + contentlet: BINARY_FIELD_CONTENTLET, + loading: false }); done(); }); spectator.service.updateState({ actions: [], contentType: CONTENT_TYPE_MOCK, - contentlet: BINARY_FIELD_CONTENTLET + contentlet: BINARY_FIELD_CONTENTLET, + loading: false }); }); 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 59e970b8b00d..8db79f123e16 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 @@ -22,6 +22,7 @@ interface EditContentState { actions: DotCMSWorkflowAction[]; contentType: DotCMSContentType; contentlet: DotCMSContentlet; + loading: boolean; } /** @@ -43,10 +44,11 @@ export class DotEditContentStore extends ComponentStore { private readonly dotMessageService = inject(DotMessageService); private readonly location = inject(Location); - readonly vm$ = this.select(({ actions, contentType, contentlet }) => ({ + readonly vm$ = this.select(({ actions, contentType, contentlet, loading }) => ({ actions, contentType, - contentlet + contentlet, + loading })); /** @@ -59,6 +61,16 @@ export class DotEditContentStore extends ComponentStore { ...newState })); + /** + * Update the loading state + * + * @memberof DotEditContentStore + */ + readonly updateLoading = this.updater((state, loading: boolean) => ({ + ...state, + loading + })); + /** * Update the contentlet and actions * @@ -70,7 +82,8 @@ export class DotEditContentStore extends ComponentStore { }>((state, { contentlet, actions }) => ({ ...state, contentlet, - actions + actions, + loading: false })); /** @@ -81,6 +94,7 @@ export class DotEditContentStore extends ComponentStore { readonly fireWorkflowActionEffect = this.effect( (data$: Observable>) => { return data$.pipe( + tap(() => this.updateLoading(true)), switchMap((options) => { return this.fireWorkflowAction(options).pipe( tapResponse( @@ -102,6 +116,7 @@ export class DotEditContentStore extends ComponentStore { }); }, ({ error }) => { + this.updateLoading(false); this.messageService.add({ severity: 'error', summary: this.dotMessageService.get('dot.common.message.error'), diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts index a55897c12db1..ce3b3500a924 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.spec.ts @@ -46,7 +46,7 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { { provide: HttpClient, useValue: { - get: () => of({}) + get: () => of(null) } }, { @@ -154,19 +154,18 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { describe('Systemwide TinyMCE prop', () => { it('should set the systemwide TinyMCE props', () => { - const globalConfig = { + const SYSTEM_WIDE_CONFIG = { toolbar1: 'undo redo | bold italic', theme: 'modern' }; - jest.spyOn(httpClient, 'get').mockReturnValue(of(globalConfig)); + jest.spyOn(httpClient, 'get').mockReturnValue(of(SYSTEM_WIDE_CONFIG)); spectator.detectChanges(); const editor = spectator.query(EditorComponent); expect(editor.init).toEqual({ - ...DEFAULT_CONFIG, - ...globalConfig, + ...SYSTEM_WIDE_CONFIG, theme: 'silver', setup: expect.any(Function) }); @@ -186,7 +185,7 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { }); it('should overwrite the systemwide TinyMCE props with the field variables', () => { - const globalConfig = { + const SYSTEM_WIDE_CONFIG = { toolbar1: 'undo redo | bold italic' }; @@ -200,7 +199,7 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { } ]; - jest.spyOn(httpClient, 'get').mockReturnValue(of(globalConfig)); + jest.spyOn(httpClient, 'get').mockReturnValue(of(SYSTEM_WIDE_CONFIG)); spectator.setInput('field', { ...WYSIWYG_MOCK, @@ -211,8 +210,7 @@ describe('DotEditContentWYSIWYGFieldComponent', () => { const editor = spectator.query(EditorComponent); expect(editor.init).toEqual({ - ...DEFAULT_CONFIG, - ...globalConfig, + ...SYSTEM_WIDE_CONFIG, theme: 'silver', toolbar1: 'undo redo', setup: expect.any(Function) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts index b27ff9b8ac9a..8cd1f9400bf8 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component.ts @@ -62,12 +62,12 @@ export class DotEditContentWYSIWYGFieldComponent implements OnInit { this.http .get(this.configPath) - .pipe(catchError(() => of({}))) - .subscribe((GLOBAL_CONFIG = {}) => { + .pipe(catchError(() => of(null))) + .subscribe((SYTEM_WIDE_CONFIG) => { + const CONFIG = SYTEM_WIDE_CONFIG || DEFAULT_CONFIG; this.init.set({ setup: (editor) => this.dotWysiwygPluginService.initializePlugins(editor), - ...DEFAULT_CONFIG, - ...GLOBAL_CONFIG, + ...CONFIG, ...variables, theme: 'silver' // In the new version, there is only one theme, which is the default one. Docs: https://www.tiny.cloud/docs/tinymce/latest/editor-theme/ }); 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 1913d1544364..2f386e9c9853 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 @@ -4,4 +4,5 @@ export interface EditContentPayload { contentType: DotCMSContentType; actions: DotCMSWorkflowAction[]; contentlet?: DotCMSContentlet; + loading: boolean; } diff --git a/core-web/libs/edit-content/src/lib/pipes/contentlet-status.pipe.ts b/core-web/libs/edit-content/src/lib/pipes/contentlet-status.pipe.ts index 32e87523bdad..a7c46d0fb9c4 100644 --- a/core-web/libs/edit-content/src/lib/pipes/contentlet-status.pipe.ts +++ b/core-web/libs/edit-content/src/lib/pipes/contentlet-status.pipe.ts @@ -13,7 +13,10 @@ export class ContentletStatusPipe implements PipeTransform { private readonly dotMessage = inject(DotMessageService); transform(contentlet?: DotCMSContentlet): { label: string; classes: string } { if (!contentlet) { - return null; + return { + label: this.dotMessage.get('New'), + classes: 'p-chip-blue' + }; } const contentletStatus = this.getContentletStatus(contentlet); 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 8fa94bc86b29..43c1e9ad46d4 100644 --- a/core-web/libs/edit-content/src/lib/utils/mocks.ts +++ b/core-web/libs/edit-content/src/lib/utils/mocks.ts @@ -944,7 +944,8 @@ export const CONTENT_FORM_DATA_MOCK: EditContentPayload = { __icon__: 'contentIcon', contentTypeIcon: 'event_note', variant: 'DEFAULT' - } + }, + loading: false }; /* CONTENT TYPE MOCKS */ diff --git a/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.html b/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.html index 0ea672c05216..855d9a3acc21 100644 --- a/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.html +++ b/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.html @@ -2,10 +2,10 @@ class="p-button-sm p-button-text" [pTooltip]="tooltipText" (click)="copyUrlToClipboard($event)" + hideDelay="800" pButton type="button" appendTo="body" - hideDelay="800" tooltipPosition="bottom"> {{ label }} diff --git a/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.ts b/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.ts index 8c4c65df6d2e..fb9accd0a77d 100644 --- a/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.ts +++ b/core-web/libs/ui/src/lib/components/dot-copy-button/dot-copy-button.component.ts @@ -1,5 +1,11 @@ import { NgIf } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnInit +} from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { TooltipModule } from 'primeng/tooltip'; @@ -32,7 +38,8 @@ export class DotCopyButtonComponent implements OnInit { constructor( private dotClipboardUtil: DotClipboardUtil, - private dotMessageService: DotMessageService + private dotMessageService: DotMessageService, + private readonly cd: ChangeDetectorRef ) {} ngOnInit() { @@ -55,10 +62,12 @@ export class DotCopyButtonComponent implements OnInit { setTimeout(() => { this.tooltipText = original; + this.cd.detectChanges(); }, 1000); }) .catch(() => { this.tooltipText = 'Error'; + this.cd.detectChanges(); }); } } diff --git a/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.spec.ts b/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.spec.ts index ba8c85108bbb..9fa3af2b1486 100644 --- a/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.spec.ts +++ b/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.spec.ts @@ -1,9 +1,9 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { DotFormatDateService } from '@dotcms/data-access'; +import { DotFormatDateService, DotMessageService } from '@dotcms/data-access'; import { DotcmsConfigService, LoginService } from '@dotcms/dotcms-js'; import { DotRelativeDatePipe } from '@dotcms/ui'; -import { DotcmsConfigServiceMock } from '@dotcms/utils-testing'; +import { DotcmsConfigServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; const ONE_DAY = 86400000; @@ -29,6 +29,10 @@ describe('DotRelativeDatePipe', () => { provide: DotcmsConfigService, useClass: DotcmsConfigServiceMock }, + { + provide: DotMessageService, + useValue: new MockDotMessageService({ new: 'New' }) + }, DotFormatDateService, DotRelativeDatePipe ] @@ -44,7 +48,7 @@ describe('DotRelativeDatePipe', () => { tick(1000); - expect(pipe.transform(date.getTime())).toEqual('1 second ago'); + expect(pipe.transform(date.getTime())).toEqual('Now'); })); it('should return relative date even if it is hardcoded date', fakeAsync(() => { diff --git a/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.ts b/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.ts index 7ef5233308ba..de3326c9c1a4 100644 --- a/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.ts +++ b/core-web/libs/ui/src/lib/pipes/dot-relative-date/dot-relative-date.pipe.ts @@ -1,18 +1,21 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { DotFormatDateService } from '@dotcms/data-access'; +import { DotFormatDateService, DotMessageService } from '@dotcms/data-access'; /* * Custom Pipe that returns the relative date. */ @Pipe({ name: 'dotRelativeDate', standalone: true }) export class DotRelativeDatePipe implements PipeTransform { - constructor(private dotFormatDateService: DotFormatDateService) {} + constructor( + private readonly dotFormatDateService: DotFormatDateService, + private readonly dotMessageService: DotMessageService + ) {} transform( time: string | number = new Date().getTime(), format = 'MM/dd/yyyy', - daysLimit = 7 + timeStampAfter = 7 ): string { const isMilliseconds = !isNaN(Number(time)); @@ -29,13 +32,16 @@ export class DotRelativeDatePipe implements PipeTransform { const finalDate = isMilliseconds ? this.dotFormatDateService.getUTC(cleanDate) : cleanDate; // Check how many days are between the final date and the current date - const showTimeStamp = - Math.abs( - this.dotFormatDateService.differenceInCalendarDays( - finalDate, - this.dotFormatDateService.getUTC() - ) - ) > daysLimit; + const diffTime = this.dotFormatDateService.differenceInCalendarDays( + finalDate, + this.dotFormatDateService.getUTC() + ); + + const showTimeStamp = timeStampAfter ? Math.abs(diffTime) > timeStampAfter : false; + + if (diffTime === 0 && !showTimeStamp) { + return this.dotMessageService.get('Now'); + } return showTimeStamp ? this.dotFormatDateService.format(finalDate, format) diff --git a/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts b/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts index 7d6df2653d66..55ae01194c69 100644 --- a/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts +++ b/core-web/libs/utils-testing/src/lib/dot-workflow-service.mock.ts @@ -42,6 +42,79 @@ export const mockWorkflows: DotCMSWorkflow[] = [ } ]; +export const WORKFLOW_SCHEMA_MOCK = { + contentTypeSchemes: [ + { + archived: false, + creationDate: 1713712903527, + defaultScheme: false, + description: '', + entryActionId: null, + id: '2a4e1d2e-5342-4b46-be3d-80d3a2d9c0dd', + mandatory: false, + modDate: 1713700998143, + name: 'Blogs', + system: false + } + ], + schemes: [ + { + archived: false, + creationDate: 1713718887000, + defaultScheme: false, + description: '', + entryActionId: null, + id: '2a4e1d2e-5342-4b46-be3d-80d3a2d9c0dd', + mandatory: false, + modDate: 1713700998143, + name: 'Blogs', + system: false + } + ] +}; + +export const WORKFLOW_STATUS_MOCK = { + scheme: { + archived: false, + creationDate: 1713718841367, + defaultScheme: false, + description: '', + entryActionId: null, + id: 'd61a59e1-a49c-46f2-a929-db2b4bfa88b2', + mandatory: false, + modDate: 1713700998153, + name: 'System Workflow', + system: true + }, + step: { + creationDate: 1713713102111, + enableEscalation: false, + escalationAction: null, + escalationTime: 0, + id: 'dc3c9cd0-8467-404b-bf95-cb7df3fbc293', + myOrder: 2, + name: 'Published', + resolved: true, + schemeId: 'd61a59e1-a49c-46f2-a929-db2b4bfa88b2' + }, + task: { + assignedTo: 'Admin User', + belongsTo: null, + createdBy: 'e7d4e34e-5127-45fc-8123-d48b62d510e3', + creationDate: 1564530075838, + description: '', + dueDate: null, + id: '26e58222-2c79-4879-93cb-982df8f84a7d', + inode: '26e58222-2c79-4879-93cb-982df8f84a7d', + languageId: 1, + modDate: 1700505024201, + new: false, + status: 'dc3c9cd0-8467-404b-bf95-cb7df3fbc293', + title: 'Snow', + webasset: '684a7b76-315a-48af-9ea8-967cce78ee98' + } +}; + export class DotWorkflowServiceMock { get(): Observable { return observableOf(_.cloneDeep(mockWorkflows)); diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 5b876934a632..ccfce3598f29 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5135,6 +5135,9 @@ api.sites.ruleengine.rules.contact.admin.error=Please contact an administrator.. Revision=Revision Draft=Draft Published=Published +Modified=Modified +Now=Now +Information=Information cluster-id=Cluster Id contenttypes.content.push_publish.filters=Filter download.bundle.header=Download Bundle