Skip to content

Commit

Permalink
feat(edit-content) unify add and update from search and list #28879
Browse files Browse the repository at this point in the history
  • Loading branch information
oidacra committed Jul 9, 2024
1 parent e31ffe3 commit 7745488
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,41 @@
[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.inode) {
<div
data-testId="category-item"
class="flex align-content-center align-items-center category-list__item"
[ngClass]="{ 'category-list__item--selected': item.checked }"
(click)="itemClicked.emit({ index, item })">
<p-checkbox
[(ngModel)]="itemsSelected"
[value]="item.key"
(onChange)="itemChecked.emit({ selected: $event.checked, item })" />
<!--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"
[ngClass]="{ 'cursor-pointer': item.childrenCount > 0 }"
[for]="item.key"
>{{ item.categoryName }}</label
>
<label
data-testId="category-item-label"
class="flex flex-grow-1 category-list__item-label"
[ngClass]="{ 'cursor-pointer': item.hasChildren }"
[for]="item.key"
>{{ item.value }}</label
>

@if (item.childrenCount > 0) {
<i
data-testId="category-item-with-child"
class="pi pi-chevron-right category-list__item-icon"></i>
}
</div>
@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 _) {
<div
class="flex-grow-1 category-list__category-column"
data-testId="category-column-empty"></div>
<div
class="flex-grow-1 category-list__category-column"
data-testId="category-column-empty"></div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('DotCategoryFieldCategoryListComponent', () => {
});

it('should emit the correct item when clicked', () => {
const emitSpy = jest.spyOn(spectator.component.itemClicked, 'emit');
const emitSpy = jest.spyOn(spectator.component.rowClicked, 'emit');
const items = spectator.queryAll(byTestId('category-item'));
spectator.click(items[0]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { TreeModule } from 'primeng/tree';

import { DotCategory } from '@dotcms/dotcms-models';

import { DotCategoryFieldCategory } from '../../models/dot-category-field.models';
import { DotCategoryFieldKeyValueObj } from '../../models/dot-category-field.models';

export const MINIMUM_CATEGORY_COLUMNS = 4;

Expand Down Expand Up @@ -54,7 +52,7 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
/**
* Represents the variable 'categories' which is of type 'DotCategoryFieldCategory[][]'.
*/
categories = input.required<DotCategoryFieldCategory[][]>();
categories = input.required<DotCategoryFieldKeyValueObj[][]>();

/**
* Represent the selected item saved in the contentlet
Expand All @@ -76,12 +74,15 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
/**
* Emit the item clicked to the parent component
*/
@Output() itemClicked = new EventEmitter<{ index: number; item: DotCategory }>();
@Output() rowClicked = new EventEmitter<{ index: number; item: DotCategoryFieldKeyValueObj }>();

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

/**
* Model of the items selected
Expand Down Expand Up @@ -115,12 +116,14 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
return;
}

const lastColumnIndex = columnsArray.length - 1;

if (
columnsArray[MINIMUM_CATEGORY_WITHOUT_SCROLLING - 1] &&
columnsArray[MINIMUM_CATEGORY_WITHOUT_SCROLLING - 1].nativeElement.children.length >
0
) {
columnsArray[columnsArray.length - 1].nativeElement.scrollIntoView({
columnsArray[lastColumnIndex].nativeElement.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'end'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<div #tableContainer class="category-field__search-list">
@if (!isLoading()) {
<p-table
(selectionChange)="selected.emit($event)"
[scrollHeight]="scrollHeight()"
[scrollable]="true"
[value]="categories()"
[value]="searchResults()"
dataKey="key"
selectionMode="multiple"
[(selection)]="itemsSelected"
(onHeaderCheckboxToggle)="onHeaderCheckboxToggle($event)"
(onRowSelect)="onSelectItem($event)"
(onRowUnselect)="onRemoveItem($event)"
styleClass="dotTable ">
<ng-template pTemplate="header">
<tr>
Expand All @@ -23,7 +26,7 @@
<td>
<p-tableCheckbox [value]="category"></p-tableCheckbox>
</td>
<td>{{ category.categoryName }}</td>
<td>{{ category.value }}</td>
<td>{{ category.path }}</td>
</tr>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Subject } from 'rxjs';

import { CommonModule } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
effect,
ElementRef,
EventEmitter,
inject,
Expand All @@ -12,18 +16,26 @@ import {
signal,
ViewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

import { SkeletonModule } from 'primeng/skeleton';
import { TableModule } from 'primeng/table';

import { debounceTime } from 'rxjs/operators';

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

import {
DotCategoryFieldCategory,
DotCategoryFieldCategorySearchedItems
DotCategoryFieldKeyValueObj,
DotTableHeaderCheckboxSelectEvent,
DotTableRowSelectEvent
} from '../../models/dot-category-field.models';
import { getParentPath } from '../../utils/category-field.utils';
import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skeleton.component';

const DELAY_FOR_LISTENER = 300;

@Component({
selector: 'dot-category-field-search-list',
standalone: true,
Expand All @@ -33,34 +45,131 @@ import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skele
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotCategoryFieldSearchListComponent implements AfterViewInit {
/**
* Represents a reference to a table container element in the DOM to calculate the
* 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 required categories input for DotCategoryFieldCategory.
*
* @typedef {DotCategoryFieldCategory[]} RequiredCategories
* Represents the categories found with the filter
*/
categories = input.required<DotCategoryFieldCategory[]>();

/**
* Represent the selected items in the store
*/
selected = input.required<DotCategoryFieldKeyValueObj[]>();

/**
* EventEmitter for emit the selected category(ies).
*/
categories = input.required<DotCategoryFieldCategorySearchedItems[]>();
@Output() itemChecked = new EventEmitter<
DotCategoryFieldKeyValueObj | DotCategoryFieldKeyValueObj[]
>();

@Output() selected = new EventEmitter<DotCategoryFieldCategorySearchedItems[]>();
/**
* EventEmitter that emits events to remove a selected item(s).
*/
@Output() removeItem = new EventEmitter<string | string[]>();

/**
* Represents the selected categories in the DotCategoryFieldCategory class.
* Represents a variable indicating if the component is in loading state.
*/
selectedCategories: DotCategoryFieldCategory;
isLoading = input.required<boolean>();

/**
* Computed variable to store the search results parsed.
*
*/
searchResults = computed<DotCategoryFieldKeyValueObj[]>(() => {
return this.categories().map((item) => {
const path = getParentPath(item);

return { key: item.key, value: item.categoryName, path: path, inode: item.inode };
});
});

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

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

#destroyRef = inject(DestroyRef);

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

private resizeSubject = new Subject();

ngAfterViewInit(): void {
this.calculateScrollHeight();
window.addEventListener('resize', this.calculateScrollHeight.bind(this));
this.resizeSubject
.pipe(takeUntilDestroyed(this.#destroyRef), debounceTime(DELAY_FOR_LISTENER))
.subscribe(() => this.calculateScrollHeight());

window.addEventListener('resize', () => this.resizeSubject.next());

this.#destroyRef.onDestroy(() => {
window.removeEventListener('resize', this.calculateScrollHeight.bind(this));
window.removeEventListener('resize', () => this.resizeSubject.next());
this.#effectRef.destroy();
});
}

/**
* This method is called when an item is selected.
*
* @param {$event: DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>} $event - The event object containing the selected item data.
* @return {void}
*/
onSelectItem({ data }: DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>): void {
this.itemChecked.emit(data);
}

/**
* Removes an item from the list.
*
* @param {DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>} $event - The event that triggered the item removal.
* @return {void}
*/
onRemoveItem({ data: { key } }: DotTableRowSelectEvent<DotCategoryFieldKeyValueObj>): void {
this.removeItem.emit(key);
}

/**
* Handles the event when the header checkbox is toggled.
*
* @param {DotTableHeaderCheckboxSelectEvent} event - The event triggered when the header checkbox is toggled.
*
* @return {void}
*/
onHeaderCheckboxToggle({ checked }: DotTableHeaderCheckboxSelectEvent): void {
if (checked) {
const values = this.searchResults().map((item) => item.key);
this.itemChecked.emit(this.searchResults());
this.temporarySelectedAll = [...values];
} else {
this.removeItem.emit(this.temporarySelectedAll);
this.temporarySelectedAll = [];
}
}

/**
* Calculate the high of the container for the virtual scroll
* @private
*/
private calculateScrollHeight(): void {
const containerHeight = this.tableContainer.nativeElement.offsetHeight;
this.scrollHeight.set(`${containerHeight}px`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,20 @@
</div>
<div class="flex-grow-1 category-field__categories">
@if (store.mode() === 'list') {
{{ store.searchCategories() | json }}
<dot-category-field-category-list
(itemChecked)="store.updateSelected($event.selected, $event.item)"
(itemClicked)="store.getCategories($event)"
(rowClicked)="store.getCategories($event)"
[categories]="store.categoryList()"
[selected]="store.selectedCategoriesValues()" />
} @else {
<dot-category-field-search-list
@fadeAnimation
(itemChecked)="store.addSelected($event)"
(removeItem)="store.removeSelected($event)"
[isLoading]="store.isSearchLoading()"
[categories]="store.getSearchedCategories()"
(selected)="selectedSearch($event)" />
[categories]="store.searchCategories()"
[selected]="store.selected()" />
}
</div>
</div>
Expand Down
Loading

0 comments on commit 7745488

Please sign in to comment.