Skip to content

Commit

Permalink
implementation (category field): #29563 Change category selection fro…
Browse files Browse the repository at this point in the history
…m PrimeNG sidebar to dialog (#29770)

### Proposed Changes
* Category selection is displayed in a PrimeNG dialog instead of a
sidebar



### Screenshots

Before 


https://github.com/user-attachments/assets/e0549b6c-4992-4e1a-b5cb-fd4125566930


After 


https://github.com/user-attachments/assets/f992c914-5119-4923-9c78-c803a42daa48
  • Loading branch information
hmoreras authored Aug 29, 2024
1 parent 11358b0 commit 08c7899
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
}

.category-list__category-column {
flex: 0 0 30%;
flex: 0 0 38%;
border-right: 1px solid $color-palette-gray-400;
overflow-y: auto;
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
} from '../../models/dot-category-field.models';
import { DotCategoryFieldListSkeletonComponent } from '../dot-category-field-list-skeleton/dot-category-field-list-skeleton.component';

export const MINIMUM_CATEGORY_COLUMNS = 4;
export const MINIMUM_CATEGORY_COLUMNS = 3;

const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,33 @@
<p-sidebar
#sidebar
(onHide)="closedSidebar.emit()"
[(visible)]="visible"
[showCloseIcon]="false"
data-testId="sidebar"
position="right"
styleClass="w-9">
<ng-template pTemplate="header">
<div class="flex flex-row category-field__header">
<button
type="button"
(click)="sidebar.close($event)"
class="p-button-rounded p-button-text"
data-testId="back-btn"
icon="pi pi-angle-left"
pButton></button>
<div class="category-field__header-title" data-testId="sidebar-title">
{{ 'edit.content.category-field.sidebar.header.select-categories' | dm }}
</div>
</div>
</ng-template>

<p-dialog
(onHide)="closedDialog.emit()"
[(visible)]="$isVisible"
data-testId="dialog"
[draggable]="false"
[resizable]="false"
[maximizable]="false"
[modal]="true"
[header]="'edit.content.category-field.dialog.header.select-categories' | dm"
styleClass="category-field__dialog">
<div class="category-field__content h-full w-full">
<div class="category-field__left-pane flex flex-column">
<div class="category-field__search">
<dot-category-field-search
(changeMode)="store.setMode($event)"
(term)="store.search($event)"
@fadeAnimation
[isLoading]="store.isSearchLoading()" />
</div>
<div class="flex-grow-1 category-field__categories">
@if (store.mode() === 'list') {
<dot-category-field-category-list
@fadeAnimation
(itemChecked)="store.updateSelected($event.selected, $event.item)"
(rowClicked)="store.getCategories($event)"
[categories]="store.categoryList()"
[isLoading]="store.isListLoading()"
[isInitialState]="store.isInitSate()"
[selected]="store.selectedCategoriesValues()"
[selected]="store.confirmedCategoriesValues()"
[breadcrumbs]="store.breadcrumbMenu()" />
} @else {
<dot-category-field-search-list
@fadeAnimation
(itemChecked)="store.addSelected($event)"
(removeItem)="store.removeSelected($event)"
[status]="store.searchStatus()"
Expand All @@ -68,15 +52,23 @@
class="p-button p-button-link"
data-testId="clear_all-btn"
pButton>
{{ 'edit.content.category-field.sidebar.button.clear-all' | dm }}
{{ 'edit.content.category-field.dialog.button.clear-all' | dm }}
</button>
</div>
} @else {
<div class="category-field__empty-state" data-testId="category-field__empty-state">
<i class="pi pi-check-square"></i>
<p>{{ 'edit.content.category-field.sidebar.empty-state' | dm }}</p>
<p>{{ 'edit.content.category-field.dialog.empty-state' | dm }}</p>
</div>
}
</div>
</div>
</p-sidebar>
<ng-template pTemplate="footer">
<p-button
(click)="closedDialog.emit()"
[label]="'Cancel' | dm"
styleClass="p-button-outlined"
data-testId="dialog-cancel" />
<p-button (click)="confirmCategories()" [label]="'Apply' | dm" data-testId="dialog-apply" />
</ng-template>
</p-dialog>
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,17 @@
scrollbar-gutter: auto;
}

:host ::ng-deep .p-sidebar-content {
padding: 0;
height: 100%;
overflow: hidden;
:host ::ng-deep {
.category-field__dialog.p-dialog {
min-height: 90%;
height: 100%;
width: 75vw;

.p-dialog-content {
padding: 0;
height: 100%;
}
}
}

.category-field__empty-state {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,37 @@ import { expect, it } from '@jest/globals';
import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { Sidebar } from 'primeng/sidebar';
import { Dialog } from 'primeng/dialog';

import { DotHttpErrorManagerService, DotMessageService } from '@dotcms/data-access';

import { DotCategoryFieldSidebarComponent } from './dot-category-field-sidebar.component';
import { DotCategoryFieldDialogComponent } from './dot-category-field-dialog.component';

import { CATEGORY_LIST_MOCK } from '../../mocks/category-field.mocks';
import { CategoriesService } from '../../services/categories.service';
import { CategoryFieldStore } from '../../store/content-category-field.store';
import { DotCategoryFieldCategoryListComponent } from '../dot-category-field-category-list/dot-category-field-category-list.component';
import { DotCategoryFieldSelectedComponent } from '../dot-category-field-selected/dot-category-field-selected.component';

describe('DotEditContentCategoryFieldSidebarComponent', () => {
let spectator: Spectator<DotCategoryFieldSidebarComponent>;
describe('DotCategoryFieldDialogComponent', () => {
let spectator: Spectator<DotCategoryFieldDialogComponent>;
let store: InstanceType<typeof CategoryFieldStore>;

const createComponent = createComponentFactory({
component: DotCategoryFieldSidebarComponent,
component: DotCategoryFieldDialogComponent,
providers: [mockProvider(DotMessageService), CategoryFieldStore]
});

beforeEach(() => {
spectator = createComponent({
props: {
visible: true
},
providers: [
mockProvider(CategoriesService, {
getChildren: jest.fn().mockReturnValue(of(CATEGORY_LIST_MOCK))
}),
mockProvider(DotHttpErrorManagerService)
]
});
spectator.setInput('isVisible', true);

store = spectator.inject(CategoryFieldStore, true);

Expand All @@ -46,8 +44,8 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => {
});

it('should have `visible` property set to `true` by default', () => {
expect(spectator.component.visible).toBe(true);
expect(spectator.query(Sidebar)).not.toBeNull();
expect(spectator.component.$isVisible()).toBe(true);
expect(spectator.query(Dialog)).not.toBeNull();
});

it('should render the selected categories list when there are selected categories', () => {
Expand All @@ -64,15 +62,42 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => {
expect(spectator.query(byTestId('category-field__empty-state'))).not.toBeNull();
});

it('should emit event to close sidebar when "back" button is clicked', () => {
const closedSidebarSpy = jest.spyOn(spectator.component.closedSidebar, 'emit');
const cancelBtn = spectator.query(byTestId('back-btn'));
expect(cancelBtn).not.toBeNull();
it('should have the correct configuration for the dialog.', () => {
const closedDialogSpy = jest.spyOn(spectator.component.closedDialog, 'emit');
const dialog = spectator.query(Dialog);

expect(closedSidebarSpy).not.toHaveBeenCalled();
expect(dialog.draggable).toBe(false);
expect(dialog.resizable).toBe(false);
expect(dialog.modal).toBe(true);
expect(dialog.modal).toBe(true);

spectator.click(cancelBtn);
expect(closedSidebarSpy).toHaveBeenCalled();
dialog.onHide.emit();

expect(closedDialogSpy).toHaveBeenCalled();
});

it('should close the dialog when the close button is clicked', () => {
const closedDialogSpy = jest.spyOn(spectator.component.closedDialog, 'emit');
spectator.click(byTestId('dialog-cancel'));

expect(closedDialogSpy).toHaveBeenCalled();
});

it('should save the changes and apply the categories when the apply button is clicked', () => {
const closedDialogSpy = jest.spyOn(spectator.component.closedDialog, 'emit');
const addConfirmedCategoriesSky = jest.spyOn(store, 'addConfirmedCategories');
const categories = { key: '1234', value: 'test' };
store.addSelected({ key: '1234', value: 'test' });
spectator.detectChanges();

expect(store.selected()).toEqual([categories]);
expect(store.confirmedCategories()).toEqual([]);

spectator.click(byTestId('dialog-apply'));

expect(store.confirmedCategories()).toEqual([categories]);
expect(closedDialogSpy).toHaveBeenCalled();
expect(addConfirmedCategoriesSky).toHaveBeenCalled();
});

it('should render the CategoryFieldCategoryList component', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
EventEmitter,
inject,
Input,
model,
OnDestroy,
OnInit,
Output
output
} from '@angular/core';

import { ButtonModule } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { InputTextModule } from 'primeng/inputtext';
import { SidebarModule } from 'primeng/sidebar';

import { DotMessagePipe } from '@dotcms/ui';

Expand All @@ -26,60 +23,48 @@ import { DotCategoryFieldSearchListComponent } from '../dot-category-field-searc
import { DotCategoryFieldSelectedComponent } from '../dot-category-field-selected/dot-category-field-selected.component';

/**
* The DotCategoryFieldSidebarComponent is a sidebar panel that allows editing of content category field.
* The DotCategoryFieldDialogComponent is a dialog panel that allows editing of content category field.
* It provides interfaces for item selection and click handling, and communicates with a store
* to fetch and update the categories' data.
*
* @property {boolean} visible - Indicates the visibility of the sidebar. Default is `true`.
* @property {EventEmitter<void>} closedSidebar - Event emitted when the sidebar is closed.
* @property {boolean} visible - Indicates the visibility of the dialog. Default is `true`.
* @property {output<void>} closedDialog - Output emitted when the dialog is closed.
*/
@Component({
selector: 'dot-category-field-sidebar',
selector: 'dot-category-field-dialog',
standalone: true,
imports: [
DialogModule,
ButtonModule,
DotMessagePipe,
SidebarModule,
DotCategoryFieldCategoryListComponent,
InputTextModule,
DotCategoryFieldSearchComponent,
DotCategoryFieldSearchListComponent,
DotCategoryFieldSelectedComponent,
NgClass
],
templateUrl: './dot-category-field-sidebar.component.html',
styleUrl: './dot-category-field-sidebar.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('fadeAnimation', [
state(
'void',
style({
opacity: 0
})
),
transition(':enter, :leave', [animate('50ms ease-in-out')])
])
]
templateUrl: './dot-category-field-dialog.component.html',
styleUrl: './dot-category-field-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy {
export class DotCategoryFieldDialogComponent implements OnInit, OnDestroy {
/**
* Indicates the visibility of the sidebar.
* Indicates the visibility of the dialog.
*
* @memberof DotCategoryFieldSidebarComponent
* @memberof DotCategoryFieldDialogComponent
*/
@Input() visible = false;
$isVisible = model<boolean>(false, { alias: 'isVisible' });

/**
* Output that emit if the sidebar is closed
* Output that emit if the Dialog is closed
*/
@Output() closedSidebar = new EventEmitter<void>();
closedDialog = output<void>();

/**
* Store based on the `CategoryFieldStore`.
*
* @memberof DotCategoryFieldSidebarComponent
* @memberof DotCategoryFieldDialogComponent
*/
readonly store = inject(CategoryFieldStore);

Expand All @@ -95,4 +80,9 @@ export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.store.clean();
}

confirmCategories(): void {
this.store.addConfirmedCategories();
this.closedDialog.emit();
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
@if (store.selected().length) {
@if (store.confirmedCategories().length) {
<dot-category-field-chips
data-testId="category-chip-list"
[categories]="store.selected()"
(remove)="store.removeSelected($event)" />
[categories]="store.confirmedCategories()"
(remove)="store.removeConfirmedCategories($event)" />
}

<div class="dot-category-field__select">
<button
type="button"
(click)="openCategoriesSidebar()"
[disabled]="$showCategoriesSidebar()"
(click)="openCategoriesDialog()"
[disabled]="$showCategoriesDialog()"
[label]="'edit.content.category-field.show-categories-dialog' | dm"
class="p-button-sm p-button-text p-button-secondary"
data-testId="show-sidebar-btn"
data-testId="show-dialog-btn"
pButton></button>
</div>

@if ($showCategoriesSidebar()) {
@defer (when $showCategoriesSidebar()) {
<dot-category-field-sidebar
[visible]="$showCategoriesSidebar()"
(closedSidebar)="closeCategoriesSidebar()"
data-testId="sidebar-placeholder" />
@if ($showCategoriesDialog()) {
@defer (when $showCategoriesDialog()) {
<dot-category-field-dialog
[isVisible]="$showCategoriesDialog()"
(closedDialog)="closeCategoriesDialog()"
data-testId="dialog-placeholder" />
}
}
Loading

0 comments on commit 08c7899

Please sign in to comment.