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(edit-content) add selected component in sidebar, remove item and clear all button #29237

Merged
merged 12 commits into from
Jul 18, 2024
Merged
4 changes: 2 additions & 2 deletions core-web/libs/dotcms-models/src/lib/dot-categories.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export interface DotCategory {
parentList?: DotCategoryParent[];
}

type DotCategoryParent = {
categoryName: string;
export type DotCategoryParent = {
name: string;
key: string;
inode: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { By } from '@angular/platform-browser';

import { BlockEditorModule, DotBlockEditorComponent } from '@dotcms/block-editor';
import {
DotHttpErrorManagerService,
DotLicenseService,
DotMessageDisplayService,
DotMessageService
Expand Down Expand Up @@ -169,7 +170,7 @@ describe.each([...FIELDS_MOCK])('DotEditContentFieldComponent all fields', (fiel
useValue: createFormGroupDirectiveMock()
}
],
providers: [FormGroupDirective]
providers: [FormGroupDirective, mockProvider(DotHttpErrorManagerService)]
});

beforeEach(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

import {
CATEGORY_LIST_MOCK,
CATEGORY_LIST_MOCK_TRANSFORMED,
CATEGORY_LIST_MOCK_TRANSFORMED_MATRIX,
CATEGORY_MOCK_TRANSFORMED,
SELECTED_LIST_MOCK
} from '../../mocks/category-field.mocks';
Expand All @@ -26,7 +26,7 @@ describe('DotCategoryFieldCategoryListComponent', () => {
beforeEach(() => {
spectator = createComponent({
props: {
categories: CATEGORY_LIST_MOCK_TRANSFORMED,
categories: CATEGORY_LIST_MOCK_TRANSFORMED_MATRIX,
selected: SELECTED_LIST_MOCK
}
});
Expand Down Expand Up @@ -73,14 +73,14 @@ describe('DotCategoryFieldCategoryListComponent', () => {

expect(emitSpy).toHaveBeenCalledWith({
index: 0,
item: CATEGORY_LIST_MOCK_TRANSFORMED[0][0]
item: CATEGORY_LIST_MOCK_TRANSFORMED_MATRIX[0][0]
});
});

it('should apply selected class to the correct item', () => {
spectator = createComponent({
props: {
categories: CATEGORY_MOCK_TRANSFORMED,
categories: [CATEGORY_MOCK_TRANSFORMED],
selected: SELECTED_LIST_MOCK
}
});
Expand All @@ -95,7 +95,7 @@ describe('DotCategoryFieldCategoryListComponent', () => {

it('should not render any empty columns when there are enough categories', () => {
const minColumns = 4;
const testCategories = Array(minColumns).fill(CATEGORY_LIST_MOCK_TRANSFORMED[0]);
const testCategories = Array(minColumns).fill(CATEGORY_LIST_MOCK_TRANSFORMED_MATRIX[0]);

spectator = createComponent({
props: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
DestroyRef,
Expand Down Expand Up @@ -89,12 +90,15 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit {
*/
itemsSelected: string[];

#cdr = inject(ChangeDetectorRef);

readonly #destroyRef = inject(DestroyRef);

readonly #effectRef = effect(() => {
// Todo: find a better way to update this
// 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
oidacra marked this conversation as resolved.
Show resolved Hide resolved
});

ngAfterViewInit() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="flex gap-3 align-items-center flex-wrap" data-testId="category-list">
<div class="flex gap-2 align-items-center flex-wrap" data-testId="category-list">
@for (category of $categoriesToShow(); track category.key) {
<p-chip
[pTooltip]="category.value"
[pTooltip]="category.path || category.value"
[removable]="true"
[label]="category.value"
tooltipPosition="top"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@
<p-tableCheckbox [value]="category"></p-tableCheckbox>
</td>
<td>{{ category.value }}</td>
<td>{{ category.path }}</td>
<td>
<span [pTooltip]="category.path" tooltipPosition="top">
{{ category.path || 'edit.content.category-field.category.root-name' | dm }}
</span>
</td>
</tr>
</ng-template>
</p-table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

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

import { debounceTime } from 'rxjs/operators';

Expand All @@ -33,7 +34,14 @@ import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skele
@Component({
selector: 'dot-category-field-search-list',
standalone: true,
imports: [CommonModule, TableModule, SkeletonModule, DotTableSkeletonComponent, DotMessagePipe],
imports: [
CommonModule,
TableModule,
SkeletonModule,
DotTableSkeletonComponent,
DotMessagePipe,
TooltipModule
],
templateUrl: './dot-category-field-search-list.component.html',
styleUrl: './dot-category-field-search-list.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
data-testId="search-input"
pInputText
type="text" />
@if (searchControl.value && !$isLoading()) {
@if (!searchControl.pristine && !$isLoading()) {
<span
data-testId="search-icon-clear"
(click)="clearInput()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';

import { InputTextModule } from 'primeng/inputtext';

import { debounceTime, filter } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter, tap } from 'rxjs/operators';

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

Expand Down Expand Up @@ -41,10 +41,17 @@ export class DotCategoryFieldSearchComponent {
$isLoading = input<boolean>(false, { alias: 'isLoading' });

constructor() {
// Emit the term to search, if the input is empty hide the result.
this.searchControl.valueChanges
.pipe(
takeUntilDestroyed(),
debounceTime(DEBOUNCE_TIME),
distinctUntilChanged(),
tap((value: string) => {
oidacra marked this conversation as resolved.
Show resolved Hide resolved
if (value.length === 0) {
this.clearInput();
}
}),
filter((value: string) => value.length >= MINIMUM_CHARACTERS)
)
.subscribe((value: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<ul class="category-list">
@for (category of $categories(); track category.key) {
<li class="category-list__item" @fadeAnimation data-testId="category-item">
<div class="category-list__item-content">
<div class="category-list__title" data-testId="category-title">
{{ category.value }}
</div>
<div class="category-list__path">
<span
[pTooltip]="category.path"
tooltipPosition="top"
class="category-list__path-content"
data-testId="category-path">
{{ category.path || 'edit.content.category-field.category.root-name' | dm }}
</span>
</div>
</div>

<div class="category-list__remove">
<button
(click)="removeItem.emit(category.key)"
icon="pi pi-times"
class="p-button-sm p-button-text p-button-secondary"
data-testId="category-remove-btn"
pButton></button>
</div>
</li>

} @empty {
<li data-testId="category-list-empty">No Categories selected</li>
oidacra marked this conversation as resolved.
Show resolved Hide resolved
}
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@use "variables" as *;

.category-list {
list-style: none;
margin: 0;
padding: 0;
}

.category-list__item {
display: grid;
grid-template-columns: 1fr $spacing-5;
align-items: center;
padding: $spacing-1 0;
gap: $spacing-1;
border-bottom: 1px solid $color-palette-gray-300;
}

.category-list__item-content {
display: flex;
justify-content: space-between;
width: 100%;
overflow: hidden;
gap: $spacing-1;
flex-direction: column;
}

.category-list__title {
color: $black;
font-size: $font-size-smd;
font-weight: $font-weight-medium-bold;
flex-shrink: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.category-list__path {
flex-grow: 1;
overflow: hidden;
}

.category-list__path-content {
font-size: $font-size-smd;
color: $color-palette-gray-700;
font-weight: $font-weight-regular-bold;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
}

.category-list__remove {
display: flex;
align-items: center;
justify-content: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';

import { DotMessageService } from '@dotcms/data-access';
import { DotMessagePipe } from '@dotcms/utils-testing';

import { DotCategoryFieldSelectedComponent } from './dot-category-field-selected.component';

import { CATEGORY_MOCK_TRANSFORMED } from '../../mocks/category-field.mocks';

describe('DotCategoryFieldSelectedComponent', () => {
let spectator: Spectator<DotCategoryFieldSelectedComponent>;
const createComponent = createComponentFactory({
component: DotCategoryFieldSelectedComponent,
imports: [DotMessagePipe],
providers: [mockProvider(DotMessageService)]
});

beforeEach(() => {
spectator = createComponent();
spectator.setInput('categories', CATEGORY_MOCK_TRANSFORMED);
spectator.detectChanges();
});

it('should render the list of categories', () => {
expect(spectator.queryAll(byTestId('category-item')).length).toBe(
CATEGORY_MOCK_TRANSFORMED.length
);
});

it('should display category name and path', () => {
const items = spectator.queryAll(byTestId('category-item'));

items.forEach((item, index) => {
const title = item.querySelector('[data-testId="category-title"]');
const path = item.querySelector('[data-testId="category-path"]');

expect(title).toContainText(CATEGORY_MOCK_TRANSFORMED[index].value);
expect(path?.getAttribute('ng-reflect-text')).toBe(
CATEGORY_MOCK_TRANSFORMED[index].path
);
});
});

it('should display remove button', () => {
const buttons = spectator.queryAll(byTestId('category-remove-btn'));
expect(buttons.length).toBe(CATEGORY_MOCK_TRANSFORMED.length);
});

it('should emit an event when remove button is clicked', () => {
const removeSpy = jest.spyOn(spectator.component.removeItem, 'emit');
const button = spectator.query(byTestId('category-remove-btn'));
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
@@ -0,0 +1,60 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, input, Output } from '@angular/core';

import { ButtonModule } from 'primeng/button';
import { ChipModule } from 'primeng/chip';
import { TooltipModule } from 'primeng/tooltip';

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

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

/**
* Represents the Dot Category Field Selected Component.
* @class
* @classdesc The Dot Category Field Selected Component is responsible for rendering the selected categories
* in the Dot Category Field Component.
*/
@Component({
selector: 'dot-category-field-selected',
standalone: true,
imports: [
CommonModule,
ButtonModule,
DotMessagePipe,
DotCategoryFieldSearchListComponent,
ChipModule,
TooltipModule
],
templateUrl: './dot-category-field-selected.component.html',
styleUrl: './dot-category-field-selected.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('fadeAnimation', [
state(
'void',
style({
opacity: 0
})
),
transition(':enter, :leave', [animate('50ms ease-in-out')])
])
]
})
export class DotCategoryFieldSelectedComponent {
/**
* Represents the array of selected categories.
*/
$categories = input<DotCategoryFieldKeyValueObj[]>([], {
alias: 'categories'
});

/**
* Represents an EventEmitter used for removing items. Emit the key
* of the category
*/
@Output()
removeItem = new EventEmitter<string>();
}
Loading