From 3f30e5399e7b817693c804a80e43ec78a15f31b7 Mon Sep 17 00:00:00 2001 From: Arcadio Quintero Date: Mon, 10 Jun 2024 14:25:48 -0400 Subject: [PATCH] feat(edit-content) change from dialog to sidebar #28764 --- ...ntent-category-field-dialog.component.html | 18 ----- ...ntent-category-field-dialog.component.scss | 31 -------- ...nt-category-field-dialog.component.spec.ts | 50 ------------ ...content-category-field-dialog.component.ts | 19 ----- ...tent-category-field-sidebar.component.html | 38 +++++++++ ...tent-category-field-sidebar.component.scss | 43 +++++++++++ ...t-category-field-sidebar.component.spec.ts | 47 +++++++++++ ...ontent-category-field-sidebar.component.ts | 33 ++++++++ ...edit-content-category-field.component.html | 15 ++-- ...t-content-category-field.component.spec.ts | 77 ++++++++++++++++--- ...t-edit-content-category-field.component.ts | 56 ++++++++++---- 11 files changed, 278 insertions(+), 149 deletions(-) delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.html deleted file mode 100644 index 2fcadac5ba69..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
-
Search & category tree
-
-
Selected Categories
-
- - -
-
-
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.scss deleted file mode 100644 index ec948410b733..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use "variables" as *; - -.category-field__dialog { - display: grid; - grid-template-columns: 2fr 1fr; - margin: auto; -} - -.category-field__left-pane, -.category-field__right-pane { - padding: $spacing-3; - gap: $spacing-3; -} - -.category-field__left-pane { - background-color: $color-palette-gray-300; -} - -.category-field__selected-categories { - border: $field-border-size solid $color-palette-gray-400; - border-radius: $border-radius-sm; - padding: $spacing-3; -} - -.category-field__actions { - gap: $spacing-1; -} - -::ng-deep .category-field__dialog .p-dialog-content { - padding: 0; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.spec.ts deleted file mode 100644 index 306f32403408..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, it } from '@jest/globals'; -import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; -import { mockProvider } from '@ngneat/spectator/jest'; - -import { DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { DotMessageService } from '@dotcms/data-access'; - -import { DotEditContentCategoryFieldDialogComponent } from './dot-edit-content-category-field-dialog.component'; - -describe('DotEditContentCategoryFieldDialogComponent', () => { - let spectator: Spectator; - let dialogRef: DynamicDialogRef; - - const createComponent = createComponentFactory({ - component: DotEditContentCategoryFieldDialogComponent, - providers: [ - mockProvider(DynamicDialogRef, { - close: jest.fn() - }), - mockProvider(DotMessageService) - ] - }); - - beforeEach(() => { - spectator = createComponent(); - dialogRef = spectator.inject(DynamicDialogRef); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should have a cancel button', () => { - expect(spectator.query(byTestId('cancel-btn'))).not.toBeNull(); - }); - it('should have a apply button', () => { - expect(spectator.query(byTestId('apply-btn'))).not.toBeNull(); - }); - - it('should close the dialog when you click cancel', () => { - const cancelBtn = spectator.query(byTestId('cancel-btn')); - expect(cancelBtn).not.toBeNull(); - - expect(dialogRef.close).not.toHaveBeenCalled(); - - spectator.click(cancelBtn); - expect(dialogRef.close).toHaveBeenCalled(); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.ts deleted file mode 100644 index 17778c4b54c3..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { DotMessagePipe } from '@dotcms/ui'; - -@Component({ - selector: 'dot-edit-content-category-field-dialog', - standalone: true, - imports: [DialogModule, ButtonModule, DotMessagePipe], - templateUrl: './dot-edit-content-category-field-dialog.component.html', - styleUrl: './dot-edit-content-category-field-dialog.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotEditContentCategoryFieldDialogComponent { - protected dialogRef: DynamicDialogRef = inject(DynamicDialogRef); -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html new file mode 100644 index 000000000000..c82c728ac64c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html @@ -0,0 +1,38 @@ + + +
+ +
+ {{ 'edit.content.category-field.sidebar.header.select-categories' | dm }} +
+
+
+ +
+
+ +
Categories
+
+ +
+
Selected Categories
+
+ +
+
+
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss new file mode 100644 index 000000000000..0c6023307745 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss @@ -0,0 +1,43 @@ +@use "variables" as *; + +.category-field__header { + height: 60px; + align-items: center; + flex-wrap: wrap; + gap: $spacing-0; + padding: 0 $spacing-4; +} + +.category-field__header-title { + font-size: $font-size-lg; + font-weight: $font-weight-medium-bold; + margin: 0; +} + +.category-field__content { + display: grid; + grid-template-columns: 2fr 1fr; + margin: auto; + background-color: $color-palette-gray-300; + padding: $spacing-4; + gap: $spacing-1; +} + +.category-field__left-pane, +.category-field__right-pane { + gap: $spacing-1; +} + +.category-field__search, +.category-field__categories, +.category-field__right-pane { + border: $field-border-size solid $color-palette-gray-400; + border-radius: $border-radius-md; + background-color: $white; + + padding: $spacing-3; +} + +:host ::ng-deep .p-sidebar-content { + padding: 0; +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts new file mode 100644 index 000000000000..2c5f6015740c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts @@ -0,0 +1,47 @@ +import { expect, it } from '@jest/globals'; +import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; +import { mockProvider } from '@ngneat/spectator/jest'; + +import { DotMessageService } from '@dotcms/data-access'; + +import { DotEditContentCategoryFieldSidebarComponent } from './dot-edit-content-category-field-sidebar.component'; + +describe('DotEditContentCategoryFieldSidebarComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotEditContentCategoryFieldSidebarComponent, + providers: [mockProvider(DotMessageService)] + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should have `visible` property by default `true`', () => { + expect(spectator.component.visible).toBe(true); + }); + + it('should have a sidebar', () => { + expect(spectator.query(byTestId('sidebar'))).not.toBeNull(); + }); + + it('should have a clear all button', () => { + expect(spectator.query(byTestId('clear_all-btn'))).not.toBeNull(); + }); + + it('should close the sidebar when you click back', () => { + const closedSidebarSpy = jest.spyOn(spectator.component.closedSidebar, 'emit'); + const cancelBtn = spectator.query(byTestId('back-btn')); + expect(cancelBtn).not.toBeNull(); + + expect(closedSidebarSpy).not.toHaveBeenCalled(); + + spectator.click(cancelBtn); + expect(closedSidebarSpy).toHaveBeenCalled(); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts new file mode 100644 index 000000000000..d2fc74edd96c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component, EventEmitter } from '@angular/core'; + +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { SidebarModule } from 'primeng/sidebar'; + +import { DotMessagePipe } from '@dotcms/ui'; + +/** + * Component for the sidebar that appears when editing content category field. + */ +@Component({ + selector: 'dot-edit-content-category-field-sidebar', + standalone: true, + imports: [DialogModule, ButtonModule, DotMessagePipe, SidebarModule], + templateUrl: './dot-edit-content-category-field-sidebar.component.html', + styleUrl: './dot-edit-content-category-field-sidebar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotEditContentCategoryFieldSidebarComponent { + /** + * Indicates whether the sidebar is visible or not. + * + */ + visible = true; + + /** + * The event is fired whenever the sidebar is closed either by hitting 'Escape', + * clicking on the overlay, or on the back button. + * + */ + closedSidebar = new EventEmitter(); +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html index 08f5331f45f2..a5c488488fe5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html @@ -12,10 +12,13 @@ }
- +
+ + diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts index 371924f7e70e..62dcda1cebf0 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts @@ -4,6 +4,7 @@ import { mockProvider } from '@ngneat/spectator/jest'; import { DotMessageService } from '@dotcms/data-access'; +import { DotEditContentCategoryFieldSidebarComponent } from './components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component'; import { DotEditContentCategoryFieldComponent } from './dot-edit-content-category-field.component'; describe('DotEditContentCategoryFieldComponent', () => { @@ -22,17 +23,75 @@ describe('DotEditContentCategoryFieldComponent', () => { jest.resetAllMocks(); }); - it('should have a select categories button', () => { - expect(spectator.query(byTestId('show-dialog-btn'))).not.toBeNull(); + describe('Elements', () => { + it('should render a button for selecting categories', () => { + expect(spectator.query(byTestId('show-sidebar-btn'))).not.toBeNull(); + }); + + it('should display the category list with chips when there are categories', () => { + spectator.component.values = []; + spectator.detectComponentChanges(); + expect(spectator.query(byTestId('category-chip-list'))).toBeNull(); + + spectator.component.values = [{ id: 1, value: 'Streetwear' }]; + spectator.detectComponentChanges(); + expect(spectator.query(byTestId('category-chip-list'))).not.toBeNull(); + }); }); - it('should show the category list wrapper', () => { - spectator.component.values = []; - spectator.detectComponentChanges(); - expect(spectator.query(byTestId('category-chip-list'))).toBeNull(); + describe('Interactions', () => { + it('should invoke `showCategoriesSidebar` method when the select button is clicked', () => { + const selectBtn = spectator.query(byTestId('show-sidebar-btn')); + const showCategoriesSidebarSpy = jest.spyOn( + spectator.component, + 'showCategoriesSidebar' + ); + expect(selectBtn).not.toBeNull(); + + spectator.click(selectBtn); + + expect(showCategoriesSidebarSpy).toHaveBeenCalled(); + }); + + it('should disable the `Select` button after `showCategoriesSidebar` method is invoked', () => { + const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement; + expect(selectBtn).not.toBeNull(); + + spectator.click(selectBtn); + + expect(selectBtn.disabled).toBe(true); + }); + + it('should create a DotEditContentCategoryFieldSidebarComponent instance when the `Select` button is clicked', () => { + const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement; + expect(selectBtn).not.toBeNull(); + expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).toBeNull(); + + spectator.click(selectBtn); + expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).not.toBeNull(); + }); + + it('should remove DotEditContentCategoryFieldSidebarComponent when `closedSidebar` emit', async () => { + const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement; + expect(selectBtn).not.toBeNull(); + spectator.click(selectBtn); + + const sidebarComponentRef = spectator.query( + DotEditContentCategoryFieldSidebarComponent + ); + expect(sidebarComponentRef).not.toBeNull(); + + sidebarComponentRef.closedSidebar.emit(); + + spectator.detectComponentChanges(); + + // Due to a delay in the pipe of the subscription + await new Promise((resolve) => setTimeout(resolve, 400)); + + expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).toBeNull(); - spectator.component.values = [{ id: 1, value: 'Streetwear' }]; - spectator.detectComponentChanges(); - expect(spectator.query(byTestId('category-chip-list'))).not.toBeNull(); + spectator.detectComponentChanges(); + expect(selectBtn.disabled).toBe(false); + }); }); }); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts index ecc49e7d7465..a2ce3380e897 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts @@ -1,17 +1,30 @@ import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ComponentRef, + DestroyRef, + inject, + input, + signal, + ViewChild +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { ButtonModule } from 'primeng/button'; import { ChipModule } from 'primeng/chip'; import { ChipsModule } from 'primeng/chips'; -import { DialogService } from 'primeng/dynamicdialog'; import { TooltipModule } from 'primeng/tooltip'; +import { delay } from 'rxjs/operators'; + import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; -import { DotMessagePipe } from '@dotcms/ui'; +import { DotDynamicDirective, DotMessagePipe } from '@dotcms/ui'; + +import { DotEditContentCategoryFieldSidebarComponent } from './components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component'; -import { DotEditContentCategoryFieldDialogComponent } from './components/dot-edit-content-category-field-dialog/dot-edit-content-category-field-dialog.component'; +const CLOSE_SIDEBAR_CSS_DELAY_MS = 300; /** * Component for editing content category field. @@ -29,7 +42,8 @@ import { DotEditContentCategoryFieldDialogComponent } from './components/dot-edi ChipModule, NgClass, TooltipModule, - DotMessagePipe + DotMessagePipe, + DotDynamicDirective ], templateUrl: './dot-edit-content-category-field.component.html', styleUrl: './dot-edit-content-category-field.component.scss', @@ -40,7 +54,6 @@ import { DotEditContentCategoryFieldDialogComponent } from './components/dot-edi useFactory: () => inject(ControlContainer, { skipSelf: true }) } ], - providers: [DialogService], // eslint-disable-next-line @angular-eslint/no-host-metadata-property host: { '[class.dot-category-field__container--has-categories]': 'hasCategories()', @@ -48,6 +61,9 @@ import { DotEditContentCategoryFieldDialogComponent } from './components/dot-edi } }) export class DotEditContentCategoryFieldComponent { + disableSelectCategoriesButton = signal(false); + @ViewChild(DotDynamicDirective, { static: true }) sidebarHost!: DotDynamicDirective; + /** * The `field` variable is of type `DotCMSContentTypeField` and is a required input. * @@ -58,8 +74,8 @@ export class DotEditContentCategoryFieldComponent { // TODO: Replace with the content of the selected categories values = []; - - #dialogService = inject(DialogService); + readonly #destroyRef = inject(DestroyRef); + #componentRef: ComponentRef; /** * Checks if the object has categories. @@ -74,13 +90,21 @@ export class DotEditContentCategoryFieldComponent { * * @returns {void} */ - showCategories(): void { - this.#dialogService.open(DotEditContentCategoryFieldDialogComponent, { - showHeader: false, - styleClass: 'category-field__dialog', - width: '1000px', - height: '600px', - position: 'center' - }); + showCategoriesSidebar(): void { + this.disableSelectCategoriesButton.set(true); + this.#componentRef = this.sidebarHost.viewContainerRef.createComponent( + DotEditContentCategoryFieldSidebarComponent + ); + + this.setSidebarListener(); + } + + private setSidebarListener() { + this.#componentRef.instance.closedSidebar + .pipe(takeUntilDestroyed(this.#destroyRef), delay(CLOSE_SIDEBAR_CSS_DELAY_MS)) + .subscribe(() => { + this.disableSelectCategoriesButton.set(false); + this.sidebarHost.viewContainerRef.clear(); + }); } }