Skip to content

Commit

Permalink
feat(edit-content) empty and error placeholder #29185
Browse files Browse the repository at this point in the history
  • Loading branch information
oidacra committed Jul 27, 2024
1 parent 0f53ef2 commit 5b23783
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 59 deletions.
Binary file removed .nx/cache/18.3.5-nx.darwin-arm64.node
Binary file not shown.
4 changes: 3 additions & 1 deletion core-web/libs/dotcms-models/src/lib/shared-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
* |-> IDLE = Finished Loading or Saving
* SAVING = Status of an action of the component loaded (delete, saving, editing)
* |-> IDLE = Finished delete, saving, editing
* ERROR = Error state for the component
**/
export enum ComponentStatus {
INIT = 'INIT',
LOADING = 'LOADING',
LOADED = 'LOADED',
SAVING = 'SAVING',
IDLE = 'IDLE'
IDLE = 'IDLE',
ERROR = 'ERROR'
}

export const enum FeaturedFlags {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div #tableContainer class="category-field__search-list">
@if (!isLoading()) {
@if (!$isLoading()) {
<p-table
data-testId="categories-table"
[scrollHeight]="$scrollHeight()"
Expand All @@ -11,7 +11,7 @@
(onHeaderCheckboxToggle)="onHeaderCheckboxToggle($event)"
(onRowSelect)="onSelectItem($event)"
(onRowUnselect)="onRemoveItem($event)"
styleClass="dotTable ">
styleClass="dotTable">
<ng-template pTemplate="header">
<tr data-testId="table-header">
<th id="checkbox" scope="col" data-testId="table-header-checkbox">
Expand Down Expand Up @@ -42,6 +42,16 @@
</td>
</tr>
</ng-template>

<ng-template pTemplate="emptymessage">
<tr>
<td colspan="3">
<dot-empty-container
[configuration]="$emptyOrErrorMessage()"
[hideContactUsLink]="true" />
</td>
</tr>
</ng-template>
</p-table>
} @else {
<dot-table-skeleton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
@use "variables" as *;

.category-field__search-list {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}

:host ::ng-deep .dotTable.p-datatable > .p-datatable-wrapper {
border-radius: 0;
border: none;
:host {
::ng-deep {
.dotTable.p-datatable > .p-datatable-wrapper {
border-radius: 0;
border: none;
}
}

&.category-field__search-list--empty ::ng-deep {
p-table,
.dotTable.p-datatable,
.p-datatable-wrapper,
table {
height: 100%;

tr:not(.p-highlight):hover {
background: $white;
cursor: auto;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest';

import { DotMessageService } from '@dotcms/data-access';
import { ComponentStatus } from '@dotcms/dotcms-models';
import { DotEmptyContainerComponent } from '@dotcms/ui';

import { DotCategoryFieldSearchListComponent } from './dot-category-field-search-list.component';

import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import { MockResizeObserver } from '../../../../utils/mocks';
import { CATEGORY_MOCK_TRANSFORMED } from '../../mocks/category-field.mocks';

const mockMessageService = {
get: jest.fn((key: string) => `${key}`)
};

describe('DotCategoryFieldSearchListComponent', () => {
let spectator: Spectator<DotCategoryFieldSearchListComponent>;

const createComponent = createComponentFactory({
component: DotCategoryFieldSearchListComponent,
providers: [mockProvider(DotMessageService)]
providers: [{ provide: DotMessageService, useValue: mockMessageService }]
});

beforeEach(() => {
spectator = createComponent({
detectChanges: false,
props: {
categories: CATEGORY_MOCK_TRANSFORMED,
selected: CATEGORY_MOCK_TRANSFORMED,
isLoading: false
status: ComponentStatus.LOADED
}
});

Expand All @@ -30,19 +39,15 @@ describe('DotCategoryFieldSearchListComponent', () => {
global.ResizeObserver = MockResizeObserver;
});

afterEach(() => {
jest.resetAllMocks();
});

it('should show the skeleton if the component is loading', () => {
spectator.setInput('isLoading', true);
spectator.setInput('status', ComponentStatus.LOADING);
spectator.detectChanges();
expect(spectator.query(byTestId('categories-skeleton'))).not.toBeNull();
expect(spectator.query(byTestId('categories-table'))).toBeNull();
});

it('should show the table if the component is not loading', () => {
spectator.setInput('isLoading', false);
spectator.setInput('status', ComponentStatus.LOADED);
spectator.detectChanges();
expect(spectator.query(byTestId('categories-table'))).not.toBeNull();
expect(spectator.query(byTestId('categories-skeleton'))).toBeNull();
Expand All @@ -63,4 +68,24 @@ describe('DotCategoryFieldSearchListComponent', () => {
const rows = spectator.queryAll(byTestId('table-row'));
expect(rows.length).toBe(CATEGORY_MOCK_TRANSFORMED.length);
});

it('should render `dot-empty-container` with `empty` configuration ', () => {
const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES.empty;
spectator.setInput('status', ComponentStatus.LOADED);
spectator.setInput('categories', []);
spectator.detectChanges();

expect(spectator.query(DotEmptyContainerComponent)).not.toBeNull();
expect(spectator.component.$emptyOrErrorMessage()).toEqual(expectedConfig);
});

it('should render `dot-empty-container` with `error` configuration ', () => {
const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES[ComponentStatus.ERROR];
spectator.setInput('status', ComponentStatus.ERROR);
spectator.setInput('categories', []);

spectator.detectChanges();
expect(spectator.query(DotEmptyContainerComponent)).not.toBeNull();
expect(spectator.component.$emptyOrErrorMessage()).toEqual(expectedConfig);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
EventEmitter,
inject,
input,
OnDestroy,
Output,
output,
signal,
ViewChild
} from '@angular/core';
Expand All @@ -22,15 +23,21 @@ import { TooltipModule } from 'primeng/tooltip';

import { debounceTime } from 'rxjs/operators';

import { DotMessagePipe } from '@dotcms/ui';
import { DotMessageService } from '@dotcms/data-access';
import { ComponentStatus } from '@dotcms/dotcms-models';
import { DotEmptyContainerComponent, DotMessagePipe, PrincipalConfiguration } from '@dotcms/ui';

import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import {
DotCategoryFieldKeyValueObj,
DotTableHeaderCheckboxSelectEvent,
DotTableRowSelectEvent
} from '../../models/dot-category-field.models';
import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skeleton.component';

/**
* Represents a search list component for category field.
*/
@Component({
selector: 'dot-category-field-search-list',
standalone: true,
Expand All @@ -40,10 +47,14 @@ import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skele
SkeletonModule,
DotTableSkeletonComponent,
DotMessagePipe,
TooltipModule
TooltipModule,
DotEmptyContainerComponent
],
templateUrl: './dot-category-field-search-list.component.html',
styleUrl: './dot-category-field-search-list.component.scss',
host: {
'[class.category-field__search-list--empty]': '$tableIsEmpty()'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDestroy {
Expand All @@ -52,11 +63,13 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* viewport to use in the virtual scroll
*/
@ViewChild('tableContainer', { static: false }) tableContainer!: ElementRef;

/**
* The scrollHeight variable represents a signal with a default value of '0px'.
* It can be used to track and manipulate the height of a scrollable element.
*/
$scrollHeight = signal<string>('0px');

/**
* Represents the categories found with the filter
*/
Expand All @@ -66,35 +79,55 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* Represent the selected items in the store
*/
selected = input.required<DotCategoryFieldKeyValueObj[]>();

/**
* EventEmitter for emit the selected category(ies).
* Represents the current state of the component.
*/
@Output() itemChecked = new EventEmitter<
DotCategoryFieldKeyValueObj | DotCategoryFieldKeyValueObj[]
>();
status = input.required<ComponentStatus>();

/**
* EventEmitter that emits events to remove a selected item(s).
* Output for emit the selected category(ies).
*/
@Output() removeItem = new EventEmitter<string | string[]>();
$itemChecked = output<DotCategoryFieldKeyValueObj | DotCategoryFieldKeyValueObj[]>({
alias: 'itemChecked'
});

/**
* Represents a variable indicating if the component is in loading state.
* Output that emits events to remove a selected item(s).
*/
isLoading = input.required<boolean>();
$removeItem = output<string | string[]>({ alias: 'removeItem' });

/**
* Model of the items selected
*/
itemsSelected: DotCategoryFieldKeyValueObj[];

/**
* Represents an array of temporary selected items.
*/
temporarySelectedAll: string[] = [];

/**
* Flag indicating whether the table is empty.
*/
$tableIsEmpty = computed(() => !this.$isLoading() && this.categories().length === 0);

/**
* A computed variable that represents the loading status of a component.
*/
$isLoading = computed(() => this.status() === ComponentStatus.LOADING);

/**
* Gets the computed value of $emptyOrErrorMessage.
*/
$emptyOrErrorMessage = computed(() => this.getMessageConfig());

#messageService = inject(DotMessageService);

readonly #effectRef = effect(() => {
// Todo: find a better way to update this
this.itemsSelected = this.selected();
});

readonly #resize$ = new Subject<ResizeObserverEntry>();
readonly #resizeObserver = new ResizeObserver((entries) => this.#resize$.next(entries[0]));

Expand All @@ -111,7 +144,7 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* @return {void}
*/
onSelectItem({ data }: DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>): void {
this.itemChecked.emit(data);
this.$itemChecked.emit(data);
}

/**
Expand All @@ -121,7 +154,7 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* @return {void}
*/
onRemoveItem({ data: { key } }: DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>): void {
this.removeItem.emit(key);
this.$removeItem.emit(key);
}

/**
Expand All @@ -134,10 +167,10 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
onHeaderCheckboxToggle({ checked }: DotTableHeaderCheckboxSelectEvent): void {
if (checked) {
const values = this.categories().map((item) => item.key);
this.itemChecked.emit(this.categories());
this.$itemChecked.emit(this.categories());
this.temporarySelectedAll = [...values];
} else {
this.removeItem.emit(this.temporarySelectedAll);
this.$removeItem.emit(this.temporarySelectedAll);
this.temporarySelectedAll = [];
}
}
Expand All @@ -162,4 +195,21 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
this.$scrollHeight.set(`${containerHeight}px`);
}
}

/**
* Retrieves the message configuration based on the current component status.
*
* @private
* @returns {PrincipalConfiguration | null} Returns the message configuration, or null if no configuration is found.
*/
private getMessageConfig(): PrincipalConfiguration | null {
const configKey = this.status() === ComponentStatus.ERROR ? ComponentStatus.ERROR : 'empty';
const { title, icon, subtitle } = CATEGORY_FIELD_EMPTY_MESSAGES[configKey];

return {
title: this.#messageService.get(title),
icon: icon,
subtitle: this.#messageService.get(subtitle)
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@fadeAnimation
(itemChecked)="store.addSelected($event)"
(removeItem)="store.removeSelected($event)"
[isLoading]="store.isSearchLoading()"
[status]="store.searchStatus()"
[categories]="store.searchCategoryList()"
[selected]="store.selected()" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,19 @@ export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy {
* @memberof DotCategoryFieldSidebarComponent
*/
@Input() visible = false;

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

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

/**
* Computed property for retrieving all category keys.
*/
Expand Down
Loading

0 comments on commit 5b23783

Please sign in to comment.