Skip to content

Commit

Permalink
implementation(Category Field): #29186 Handle Empty Category Message (#…
Browse files Browse the repository at this point in the history
…29388)

### Proposed Changes
* Display an empty category message component when no categories are
available.



### Screenshots
<img width="1337" alt="image"
src="https://github.com/user-attachments/assets/787e8ea6-6865-4096-b198-55618b692ff0">

<img width="1321" alt="image"
src="https://github.com/user-attachments/assets/bf570c3f-847a-4f79-8760-9b97c1d1681e">
  • Loading branch information
hmoreras authored Jul 30, 2024
1 parent 6343d2b commit c5f3961
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
<div class="w-full category-list__header">Root</div>
<div
[ngClass]="{ 'no-overflow-x-yet': $emptyColumns().length }"
class="flex-1 category-list__category-list">
@for (column of $categories(); let index = $index; track index) {
<!--dynamic columns-->
<div #categoryColumn class="category-list__category-column" data-testId="category-column">
@for (item of column; track item.key) {
<div
data-testId="category-item"
class="flex align-content-center align-items-center category-list__item"
[ngClass]="{ 'category-list__item--selected': item.clicked }"
(click)="rowClicked.emit({ index, item })">
<p-checkbox
[(ngModel)]="itemsSelected"
[value]="item.key"
(onChange)="itemChecked.emit({ selected: $event.checked, item })" />
@if ($categories().length) {
<div class="w-full category-list__header">Root</div>
<div
[ngClass]="{ 'no-overflow-x-yet': $emptyColumns().length }"
class="flex-1 category-list__category-list">
@for (column of $categories(); let index = $index; track index) {
<!--dynamic columns-->
<div
#categoryColumn
class="category-list__category-column"
data-testId="category-column">
@for (item of column; track item.key) {
<div
data-testId="category-item"
class="flex align-content-center align-items-center category-list__item"
[ngClass]="{ 'category-list__item--selected': item.clicked }"
(click)="rowClicked.emit({ index, item })">
<p-checkbox
[(ngModel)]="itemsSelected"
[value]="item.key"
(onChange)="itemChecked.emit({ selected: $event.checked, item })" />

<label
data-testId="category-item-label"
class="flex flex-grow-1 category-list__item-label"
[class.cursor-pointer]="item.hasChildren"
[for]="item.key">
{{ item.value }}
</label>
<label
data-testId="category-item-label"
class="flex flex-grow-1 category-list__item-label"
[class.cursor-pointer]="item.hasChildren"
[for]="item.key">
{{ item.value }}
</label>

@if (item.hasChildren) {
<i
data-testId="category-item-with-child"
class="pi pi-chevron-right category-list__item-icon"></i>
}
</div>
}
</div>
} @empty {
@if (!$isLoading()) {
No categories
@if (item.hasChildren) {
<i
data-testId="category-item-with-child"
class="pi pi-chevron-right category-list__item-icon"></i>
}
</div>
}
</div>
}
}

<!--Fake empty columns-->
@for (_ of $emptyColumns(); track $index) {
<div class="flex-grow-1 category-list__category-column" data-testId="category-column-empty">
@if ($isLoading() && 0 === $index) {
<dot-category-field-list-skeleton />
}
</div>
}
</div>
<!--Fake empty columns-->
@for (_ of $emptyColumns(); track $index) {
<div
class="flex-grow-1 category-list__category-column"
data-testId="category-column-empty">
@if ($isLoading() && 0 === $index) {
<dot-category-field-list-skeleton />
}
</div>
}
</div>
} @else {
<dot-empty-container [configuration]="emptyState" [hideContactUsLink]="true" />
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { TreeModule } from 'primeng/tree';

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

import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import { DotCategoryFieldKeyValueObj } from '../../models/dot-category-field.models';
import { DotCategoryFieldListSkeletonComponent } from '../dot-category-field-list-skeleton/dot-category-field-list-skeleton.component';

Expand All @@ -43,7 +47,8 @@ const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3;
CheckboxModule,
ButtonModule,
FormsModule,
DotCategoryFieldListSkeletonComponent
DotCategoryFieldListSkeletonComponent,
DotEmptyContainerComponent
],
templateUrl: './dot-category-field-category-list.component.html',
styleUrl: './dot-category-field-category-list.component.scss',
Expand Down Expand Up @@ -103,6 +108,7 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
*/
itemsSelected: string[];

readonly #messageService = inject(DotMessageService);
#cdr = inject(ChangeDetectorRef);
readonly #destroyRef = inject(DestroyRef);
readonly #effectRef = effect(() => {
Expand All @@ -112,6 +118,12 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
this.#cdr.markForCheck(); // force refresh
});

emptyState: PrincipalConfiguration = {
title: this.#messageService.get(CATEGORY_FIELD_EMPTY_MESSAGES.empty.title),
icon: CATEGORY_FIELD_EMPTY_MESSAGES.empty.icon,
subtitle: this.#messageService.get(CATEGORY_FIELD_EMPTY_MESSAGES.empty.subtitle)
};

ngAfterViewInit() {
// Handle the horizontal scroll to make visible the last column
this.categoryColumns.changes.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ describe('DotCategoryFieldSearchListComponent', () => {
expect(rows.length).toBe(CATEGORY_MOCK_TRANSFORMED.length);
});

it('should render `dot-empty-container` with `empty` configuration ', () => {
const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES.empty;
it('should render `dot-empty-container` with `no results` configuration ', () => {
const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES.noResults;
spectator.setInput('status', ComponentStatus.LOADED);
spectator.setInput('categories', []);
spectator.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
*/
private getMessageConfig(): PrincipalConfiguration | null {
const configKey =
this.$status() === ComponentStatus.ERROR ? ComponentStatus.ERROR : 'empty';
this.$status() === ComponentStatus.ERROR ? ComponentStatus.ERROR : 'noResults';
const { title, icon, subtitle } = CATEGORY_FIELD_EMPTY_MESSAGES[configKey];

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,5 @@
pButton></button>
</div>
</li>
} @empty {
<li data-testId="category-list-empty">No Categories selected</li>
}
</ul>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.category-list {
list-style: none;
margin: 0;
padding: 0;
padding: 0 $spacing-3 0 0;
}

.category-list__item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,4 @@ describe('DotCategoryFieldSelectedComponent', () => {
spectator.click(button);
expect(removeSpy).toHaveBeenCalledWith(CATEGORY_MOCK_TRANSFORMED[0].key);
});

it('should display "No Categories selected" when there are no categories', () => {
spectator.setInput('categories', []);
const emptyMessage = spectator.query(byTestId('category-list-empty'));
expect(emptyMessage).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,30 @@
</div>
</div>

<div class="category-field__right-pane flex flex-column">
<div class="category-field__selected-categories-list flex-1">
<dot-category-field-selected
(removeItem)="store.removeSelected($event)"
[categories]="store.selected()" />
</div>
<div class="category-field__actions flex justify-content-end">
<button
(click)="store.removeSelected($allCategoryKeys())"
class="p-button p-button-link"
data-testId="clear_all-btn"
pButton>
{{ 'edit.content.category-field.sidebar.button.clear-all' | dm }}
</button>
</div>
<div
class="category-field__right-pane flex flex-column"
[ngClass]="{ empty: !store.selected().length }">
@if (store.selected().length) {
<div class="category-field__selected-categories-list flex-1">
<dot-category-field-selected
(removeItem)="store.removeSelected($event)"
[categories]="store.selected()" />
</div>
<div class="category-field__actions flex justify-content-end">
<button
(click)="store.removeSelected($allCategoryKeys())"
class="p-button p-button-link"
data-testId="clear_all-btn"
pButton>
{{ 'edit.content.category-field.sidebar.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>
</div>
}
</div>
</div>
</p-sidebar>
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
@use "variables" as *;

$default-scroll-width: 17px;

.category-field__header {
align-items: center;
flex-wrap: wrap;
Expand Down Expand Up @@ -43,7 +41,10 @@ $default-scroll-width: 17px;
flex: 0 0 25%;
gap: $spacing-1;
padding: $spacing-3;
padding-right: calc($spacing-3 - $default-scroll-width);
padding-right: 0;
&.empty {
padding-left: 0;
}
}

.category-field__categories,
Expand All @@ -64,3 +65,17 @@ $default-scroll-width: 17px;
height: 100%;
overflow: hidden;
}

.category-field__empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: $spacing-3;

p {
text-align: center;
font-size: $font-size-smd;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ 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>;
let store: InstanceType<typeof CategoryFieldStore>;

const createComponent = createComponentFactory({
component: DotCategoryFieldSidebarComponent,
Expand All @@ -34,6 +36,8 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => {
]
});

store = spectator.inject(CategoryFieldStore, true);

spectator.detectChanges();
});

Expand All @@ -46,9 +50,18 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => {
expect(spectator.query(Sidebar)).not.toBeNull();
});

it('should render a "clear all" button', () => {
it('should render the selected categories list when there are selected categories', () => {
store.addSelected({ key: '1234', value: 'test' });

spectator.detectChanges();
expect(spectator.query(byTestId('clear_all-btn'))).not.toBeNull();
expect(spectator.query(DotCategoryFieldSelectedComponent)).not.toBeNull();
});

it('should render the empty state when there are no selected categories', () => {
spectator.detectChanges();
expect(spectator.query(byTestId('clear_all-btn'))).toBeNull();
expect(spectator.query(byTestId('category-field__empty-state'))).not.toBeNull();
});

it('should emit event to close sidebar when "back" button is clicked', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { NgClass } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -44,7 +45,8 @@ import { DotCategoryFieldSelectedComponent } from '../dot-category-field-selecte
InputTextModule,
DotCategoryFieldSearchComponent,
DotCategoryFieldSearchListComponent,
DotCategoryFieldSelectedComponent
DotCategoryFieldSelectedComponent,
NgClass
],
templateUrl: './dot-category-field-sidebar.component.html',
styleUrl: './dot-category-field-sidebar.component.scss',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
clearCategoriesAfterIndex,
clearParentPathAfterIndex,
getSelectedFromContentlet,
removeEmptyArrays,
removeItemByKey,
transformCategories,
transformToSelectedObject,
Expand Down Expand Up @@ -276,7 +277,10 @@ export const CategoryFieldStore = signalStore(
next: (newCategories) => {
if (event) {
patchState(store, {
categories: [...store.categories(), newCategories],
categories: removeEmptyArrays([
...store.categories(),
newCategories
]),
state: ComponentStatus.LOADED,
keyParentPath: [
...store.keyParentPath(),
Expand All @@ -285,7 +289,10 @@ export const CategoryFieldStore = signalStore(
});
} else {
patchState(store, {
categories: [...store.categories(), newCategories],
categories: removeEmptyArrays([
...store.categories(),
newCategories
]),
state: ComponentStatus.LOADED
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
clearCategoriesAfterIndex,
clearParentPathAfterIndex,
getSelectedFromContentlet,
removeEmptyArrays,
removeItemByKey,
transformCategories,
updateChecked
Expand Down Expand Up @@ -584,5 +585,20 @@ describe('CategoryFieldUtils', () => {
);
expect(result).toEqual([]);
});

it('should return the same array if there are no empty arrays', () => {
const array: DotCategory[][] = [
[{ key: '1', categoryName: 'Category 1' } as DotCategory],
[{ key: '2', categoryName: 'Category 2' } as DotCategory]
];
const result = removeEmptyArrays(array);
expect(result).toEqual(array);
});

it('should return an empty array if all arrays are empty', () => {
const array: DotCategory[][] = [[], [], []];
const result = removeEmptyArrays(array);
expect(result).toEqual([]);
});
});
});
Loading

0 comments on commit c5f3961

Please sign in to comment.