Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor-content): implement breadcrumb navigation for categories #29394

Merged
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2686278
save
nicobytes Jul 25, 2024
fdc475b
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 25, 2024
fe40fc4
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 26, 2024
1370e06
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 29, 2024
76c255e
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 30, 2024
dad62a7
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 30, 2024
cb2587f
chore(edit-content): working on breadcrumbs navigation
nicobytes Jul 30, 2024
eeacac5
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 30, 2024
161e070
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Jul 30, 2024
7914312
chore(ui): working in collapse bredcrumb
nicobytes Jul 31, 2024
1402daf
chore(ui): working in collapse bredcrumb
nicobytes Jul 31, 2024
fd1a2ae
chore(ui): working in collapse bredcrumb
nicobytes Jul 31, 2024
e654847
Merge branch 'master' of github.com:dotCMS/core
nicobytes Jul 31, 2024
31b133c
chore(ui): working in collapse bredcrumb
nicobytes Jul 31, 2024
4044c91
chore(editor-content): fix colors
nicobytes Jul 31, 2024
f1e9d7c
chore(editor-content): working on tests
nicobytes Jul 31, 2024
715ef8a
chore(editor-content): apply format
nicobytes Jul 31, 2024
71822bd
chore(editor-content): add unit tests
nicobytes Jul 31, 2024
6c86622
chore(editor-content): add short animation
nicobytes Jul 31, 2024
9c9f32c
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Jul 31, 2024
fd4babd
chore(editor-content): change test name
nicobytes Jul 31, 2024
13ad2f0
Merge branch '29184-implement-breadcrumb-navigation-for-categories' o…
nicobytes Jul 31, 2024
b450315
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Jul 31, 2024
e8ae087
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 1, 2024
e89e3a4
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 1, 2024
05d5908
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 1, 2024
2794afa
chore(edit-content): working on breadcrumbs navigation
nicobytes Aug 1, 2024
f521628
chore(edit-content): regenerate yarn lock
nicobytes Aug 1, 2024
c6057b5
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 2, 2024
c35d878
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 2, 2024
4391278
Merge branch 'master' into 29184-implement-breadcrumb-navigation-for-…
nicobytes Aug 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@use "variables" as *;

::ng-deep dot-crumbtrail .p-breadcrumb {
:host ::ng-deep .p-breadcrumb {
ul li {
&:first-child {
font-size: $font-size-lg;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@if ($categories().length) {
<div class="w-full category-list__header">Root</div>
<div class="w-full category-list__header">
<dot-collapse-breadcrumb data-testId="breadcrumb-menu" [model]="$breadcrumbsMenu()" />
</div>
<div
[ngClass]="{ 'no-overflow-x-yet': $emptyColumns().length }"
class="flex-1 category-list__category-list">
Expand All @@ -16,7 +18,7 @@
[ngClass]="{ 'category-list__item--selected': item.clicked }"
(click)="rowClicked.emit({ index, item })">
<p-checkbox
[(ngModel)]="itemsSelected"
[(ngModel)]="$selected"
[value]="item.key"
(onChange)="itemChecked.emit({ selected: $event.checked, item })" />

Expand Down Expand Up @@ -50,5 +52,5 @@
}
</div>
} @else {
<dot-empty-container [configuration]="emptyState" [hideContactUsLink]="true" />
<dot-empty-container [configuration]="$emptyState()" [hideContactUsLink]="true" />
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('DotCategoryFieldCategoryListComponent', () => {
spectator.setInput('categories', CATEGORY_LIST_MOCK_TRANSFORMED_MATRIX);
spectator.setInput('selected', SELECTED_LIST_MOCK);
spectator.setInput('isLoading', false);
spectator.setInput('breadcrumbs', []);

spectator.detectChanges();
});
Expand Down Expand Up @@ -77,8 +78,6 @@ describe('DotCategoryFieldCategoryListComponent', () => {
});

it('should apply selected class to the correct item', () => {
// spectator = createComponent();

spectator.setInput('categories', [CATEGORY_MOCK_TRANSFORMED]);
spectator.setInput('selected', SELECTED_LIST_MOCK);
spectator.setInput('isLoading', false);
Expand Down Expand Up @@ -115,4 +114,54 @@ describe('DotCategoryFieldCategoryListComponent', () => {

expect(spectator.query(DotCategoryFieldListSkeletonComponent)).not.toBeNull();
});

describe('with breadcrumbs', () => {
it('should generate the breadcrumb menu according to the breadcrumb input', () => {
spectator.setInput('breadcrumbs', CATEGORY_MOCK_TRANSFORMED);
spectator.detectChanges();

expect(spectator.component.$breadcrumbsMenu().length).toBe(
CATEGORY_MOCK_TRANSFORMED.length + 1
);
});

it('should render the breadcrumbs menu', () => {
spectator.setInput('breadcrumbs', []);
spectator.detectChanges();

const breadcrumbs = spectator.queryAll('dot-collapse-breadcrumb .p-menuitem-link');

expect(breadcrumbs.length).toBe(1);
});

it('should emit the correct item when breadcrumb clicked', () => {
spectator.setInput('breadcrumbs', []);
spectator.detectChanges();

const emitSpy = jest.spyOn(spectator.component.rowClicked, 'emit');
const breadcrumbs = spectator.queryAll('dot-collapse-breadcrumb .p-menuitem-link');
spectator.click(breadcrumbs[0]);

expect(emitSpy).toHaveBeenCalledWith({ index: 0 });
});

it('should render the correct number of breadcrumbs', () => {
spectator.setInput('breadcrumbs', CATEGORY_MOCK_TRANSFORMED);
spectator.detectChanges();
const breadcrumbs = spectator.queryAll('dot-collapse-breadcrumb .p-menuitem-link');

expect(breadcrumbs.length).toBe(CATEGORY_MOCK_TRANSFORMED.length + 1);
});

it('should emit the correct item when breadcrumb clicked', () => {
nicobytes marked this conversation as resolved.
Show resolved Hide resolved
spectator.setInput('breadcrumbs', CATEGORY_MOCK_TRANSFORMED);
spectator.detectChanges();

const emitSpy = jest.spyOn(spectator.component.rowClicked, 'emit');
const breadcrumbs = spectator.queryAll('dot-collapse-breadcrumb .p-menuitem-link');
spectator.click(breadcrumbs[1]);

expect(emitSpy).toHaveBeenCalledWith({ index: 0, item: CATEGORY_MOCK_TRANSFORMED[0] });
});
});
});
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
DestroyRef,
effect,
ElementRef,
EventEmitter,
inject,
input,
Output,
QueryList,
ViewChildren
model,
output,
signal,
viewChildren
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';

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 {
DotEmptyContainerComponent,
PrincipalConfiguration,
DotCollapseBreadcrumbComponent
} from '@dotcms/ui';

import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import { DotCategoryFieldKeyValueObj } from '../../models/dot-category-field.models';
import {
DotCategoryFieldItem,
DotCategoryFieldKeyValueObj
} 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;
Expand All @@ -36,7 +39,6 @@ const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3;
/**
* Represents the Dot Category Field Category List component.
* @class
* @implements {AfterViewInit}
*/
@Component({
selector: 'dot-category-field-category-list',
Expand All @@ -48,6 +50,7 @@ const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3;
ButtonModule,
FormsModule,
DotCategoryFieldListSkeletonComponent,
DotCollapseBreadcrumbComponent,
DotEmptyContainerComponent
],
templateUrl: './dot-category-field-category-list.component.html',
Expand All @@ -57,11 +60,15 @@ const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3;
class: 'category-list__wrapper'
}
})
export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
export class DotCategoryFieldCategoryListComponent {
/**
* Represents the DotMessageService instance.
*/
readonly #dotMessageService = inject(DotMessageService);
/**
* Represent the columns of categories
*/
@ViewChildren('categoryColumn') categoryColumns: QueryList<ElementRef>;
$categoryColumns = viewChildren<ElementRef<HTMLDivElement>>('categoryColumn');

/**
* Represents the variable 'categories' which is of type 'DotCategoryFieldCategory[][]'.
Expand All @@ -71,7 +78,7 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
/**
* Represent the selected item saved in the contentlet
*/
$selected = input<string[]>([], { alias: 'selected' });
$selected = model<string[]>([], { alias: 'selected' });

/**
* Generate the empty columns
Expand All @@ -90,55 +97,67 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
*/
$isLoading = input<boolean>(true, { alias: 'isLoading' });

/**
* Represents the breadcrumbs to display
*/
$breadcrumbs = input<DotCategoryFieldKeyValueObj[]>([], { alias: 'breadcrumbs' });

/**
* Represents the breadcrumbs menu to display
*
* @memberof DotCategoryFieldCategoryListComponent
*/
$breadcrumbsMenu = computed(() => {
const currentItems = this.$breadcrumbs().map((item, index) => {
return {
label: item.value,
command: () => {
this.rowClicked.emit({ index, item });
}
};
});

return [
{
label: this.#dotMessageService.get(
'edit.content.category-field.category.root-name'
),
command: () => {
this.rowClicked.emit({ index: 0 });
}
},
...currentItems
];
});

/**
* Emit the item clicked to the parent component
*/
@Output() rowClicked = new EventEmitter<{ index: number; item: DotCategoryFieldKeyValueObj }>();
rowClicked = output<DotCategoryFieldItem>();

/**
* Emit the item checked or selected to the parent component
*/
@Output() itemChecked = new EventEmitter<{
itemChecked = output<{
selected: string[];
item: DotCategoryFieldKeyValueObj;
}>();

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

readonly #messageService = inject(DotMessageService);
#cdr = inject(ChangeDetectorRef);
readonly #destroyRef = inject(DestroyRef);
readonly #effectRef = effect(() => {
// Todo: change itemsSelected to use model when update Angular to >17.3
// Initial selected items from the contentlet
this.itemsSelected = this.$selected();
this.#cdr.markForCheck(); // force refresh
});

emptyState: PrincipalConfiguration = {
title: this.#messageService.get(CATEGORY_FIELD_EMPTY_MESSAGES.empty.title),
$emptyState = signal<PrincipalConfiguration>({
title: this.#dotMessageService.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(() => {
this.scrollHandler();
});
subtitle: this.#dotMessageService.get(CATEGORY_FIELD_EMPTY_MESSAGES.empty.subtitle)
});

this.#destroyRef.onDestroy(() => {
this.#effectRef.destroy();
constructor() {
effect(() => {
const columnsArray = this.$categoryColumns();
this.scrollHandler(columnsArray);
});
}

private scrollHandler() {
private scrollHandler(columnsArray: readonly ElementRef<HTMLDivElement>[]) {
try {
const columnsArray = this.categoryColumns.toArray();

if (columnsArray.length === 0) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SkeletonModule } from 'primeng/skeleton';
standalone: true,
imports: [CommonModule, SkeletonModule],
template: `
<ul class="m-0 p-1 list-none">
<ul class="m-0 p-1 list-none fadein animation-duration-500">
@for (_ of $rows(); track $index) {
<li class="flex">
<p-skeleton size="1rem" styleClass="mr-2"></p-skeleton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
(rowClicked)="store.getCategories($event)"
[categories]="store.categoryList()"
[isLoading]="store.isListLoading()"
[selected]="store.selectedCategoriesValues()" />
[selected]="store.selectedCategoriesValues()"
[breadcrumbs]="store.breadcrumbMenu()" />
} @else {
<dot-category-field-search-list
@fadeAnimation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
@use "variables" as *;

:host ::ng-deep .p-breadcrumb {
> ul > li {
&:last-child a {
pointer-events: none;
.p-menuitem-text {
color: $black;
font-weight: $font-weight-bold;
}
}
}
}

.category-field__header {
align-items: center;
flex-wrap: wrap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type HierarchyParent = Pick<DotCategory, 'inode' | 'parentList' | 'key'>
/**
* Represents an clicked item in a DotCategoryField.
*/
export type DotCategoryFieldItem = { index: number; item: DotCategoryFieldKeyValueObj };
export type DotCategoryFieldItem = { index: number; item?: DotCategoryFieldKeyValueObj };

/**
* Represents an event when a row is selected in a table.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
removeItemByKey,
transformCategories,
transformToSelectedObject,
updateChecked
updateChecked,
getMenuItemsFromKeyParentPath
} from '../utils/category-field.utils';

export type CategoryFieldState = {
Expand Down Expand Up @@ -117,7 +118,17 @@ export const CategoryFieldStore = signalStore(
store
.searchCategories()
.map((column) => transformCategories(column, store.keyParentPath()))
)
),

/**
* Transform the selected categories to a breadcrumb menu
*/
breadcrumbMenu: computed(() => {
const categories = store.categories();
const keyParentPath = store.keyParentPath();

return getMenuItemsFromKeyParentPath(categories, keyParentPath);
})
})),
withMethods(
(
Expand Down Expand Up @@ -263,13 +274,13 @@ export const CategoryFieldStore = signalStore(
filter(
(event) =>
!event ||
(event.item.hasChildren &&
!store.keyParentPath().includes(event.item.key))
(event?.item?.hasChildren &&
!store.keyParentPath().includes(event?.item?.key))
),
tap(() => patchState(store, { state: ComponentStatus.LOADING })),
switchMap((event) => {
const categoryInode: string = event
? event.item.inode
? event?.item.inode
: store.rootCategoryInode();

return categoryService.getChildren(categoryInode).pipe(
Expand Down
Loading
Loading