From 2b6105d2db1a33294f25265009b3339d7d4e5c57 Mon Sep 17 00:00:00 2001 From: Arcadio Quintero Date: Wed, 26 Jun 2024 22:15:56 -0400 Subject: [PATCH] feat(edit-content) show and navigate categories #28831 (#28977) ### Proposed Changes * Part of Custom Component Category-Field. Get, show and navigate categories ### Checklist - [x] Tests - [x] Translations - [ ] Security Implications Contemplated (add notes if applicable) ### Additional Info ### Walkthrough video https://github.com/dotCMS/core/assets/1909643/0793a077-62e3-4d11-bd46-f72d7f2dcd31 --------- Co-authored-by: Freddy Montes <751424+fmontes@users.noreply.github.com> --- core-web/apps/dotcms-ui/proxy-dev.conf.mjs | 3 +- .../dot-categories.service.spec.ts | 11 +- .../dot-categories/dot-categories.service.ts | 2 +- .../dot-categories-list.component.spec.ts | 18 +- .../dot-categories-list.component.ts | 3 +- .../store/dot-categories-list-store.ts | 50 ++--- core-web/libs/dotcms-models/src/index.ts | 1 + .../src/lib}/dot-categories.model.ts | 9 +- .../src/lib/dot-content-types.model.ts | 1 + core-web/libs/edit-content/.eslintrc.json | 11 +- .../dot-edit-content-field.component.html | 37 +--- ...ategory-field-category-list.component.html | 40 ++++ ...ategory-field-category-list.component.scss | 83 +++++++ ...gory-field-category-list.component.spec.ts | 96 ++++++++ ...-category-field-category-list.component.ts | 130 +++++++++++ ...dot-category-field-sidebar.component.html} | 13 +- ...dot-category-field-sidebar.component.scss} | 23 +- ...t-category-field-sidebar.component.spec.ts | 64 ++++++ .../dot-category-field-sidebar.component.ts | 69 ++++++ ...t-category-field-sidebar.component.spec.ts | 47 ---- ...ontent-category-field-sidebar.component.ts | 33 --- ...edit-content-category-field.component.html | 27 ++- ...edit-content-category-field.component.scss | 2 +- ...t-content-category-field.component.spec.ts | 54 +++-- ...t-edit-content-category-field.component.ts | 43 ++-- .../mocks/category-field.mocks.ts | 205 ++++++++++++++++++ .../models/dot-category-field.models.ts | 25 +++ .../services/categories.service.spec.ts | 20 ++ .../services/categories.service.ts | 44 ++++ .../content-category-field.store.spec.ts | 124 +++++++++++ .../store/content-category-field.store.ts | 151 +++++++++++++ .../utils/category-field.utils.spec.ts | 200 +++++++++++++++++ .../utils/category-field.utils.ts | 90 ++++++++ .../ema-contentlet-tools.component.html | 5 +- .../template-builder-box.component.spec.ts | 20 +- .../ai-image-prompt-form.component.spec.ts | 45 +--- .../dot-workflow-actions.component.spec.ts | 11 +- core-web/package.json | 2 +- core-web/yarn.lock | 95 ++------ .../WEB-INF/messages/Language.properties | 4 +- 40 files changed, 1573 insertions(+), 338 deletions(-) rename core-web/{apps/dotcms-ui/src/app/shared/models/dot-categories => libs/dotcms-models/src/lib}/dot-categories.model.ts (73%) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.html create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.scss create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.ts rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/{dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html => dot-category-field-sidebar/dot-category-field-sidebar.component.html} (79%) rename core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/{dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss => dot-category-field-sidebar/dot-category-field-sidebar.component.scss} (67%) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/mocks/category-field.mocks.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/models/dot-category-field.models.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.spec.ts create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.ts diff --git a/core-web/apps/dotcms-ui/proxy-dev.conf.mjs b/core-web/apps/dotcms-ui/proxy-dev.conf.mjs index d4a4ceaf676c..94d95890ff00 100644 --- a/core-web/apps/dotcms-ui/proxy-dev.conf.mjs +++ b/core-web/apps/dotcms-ui/proxy-dev.conf.mjs @@ -12,7 +12,8 @@ export default [ '/application', '/assets', '/dotcms-block-editor', - '/dotcms-binary-field-builder' + '/dotcms-binary-field-builder', + '/categoriesServlet' ], target: 'http://localhost:8080', secure: false, diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.spec.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.spec.ts index ae298f7644e0..f9ae1505b4e7 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.spec.ts @@ -3,12 +3,9 @@ import { of } from 'rxjs'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { - CATEGORY_SOURCE, - DotCategory -} from '@dotcms/app/shared/models/dot-categories/dot-categories.model'; import { DotHttpErrorManagerService } from '@dotcms/data-access'; import { CoreWebService } from '@dotcms/dotcms-js'; +import { CATEGORY_SOURCE, DotCategory } from '@dotcms/dotcms-models'; import { CoreWebServiceMock } from '@dotcms/utils-testing'; import { @@ -18,6 +15,12 @@ import { } from './dot-categories.service'; const mockCategory: DotCategory = { + active: false, + childrenCount: 0, + description: '', + iDate: 0, + keywords: '', + owner: '', categoryId: '1222', categoryName: 'Test', key: 'adsdsd', diff --git a/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.ts b/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.ts index d263c34e1866..bc7192098af8 100644 --- a/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.ts +++ b/core-web/apps/dotcms-ui/src/app/api/services/dot-categories/dot-categories.service.ts @@ -4,9 +4,9 @@ import { Injectable } from '@angular/core'; import { LazyLoadEvent } from 'primeng/api'; -import { DotCategory } from '@dotcms/app/shared/models/dot-categories/dot-categories.model'; import { OrderDirection, PaginatorService } from '@dotcms/data-access'; import { CoreWebService } from '@dotcms/dotcms-js'; +import { DotCategory } from '@dotcms/dotcms-models'; export const CATEGORY_API_URL = 'v1/categories'; diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.spec.ts index e16609799616..f30babb78201 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.spec.ts @@ -21,9 +21,9 @@ import { TableModule } from 'primeng/table'; import { DotEmptyStateModule } from '@components/_common/dot-empty-state/dot-empty-state.module'; import { DotPortletBaseModule } from '@components/dot-portlet-base/dot-portlet-base.module'; import { DotCategoriesService } from '@dotcms/app/api/services/dot-categories/dot-categories.service'; -import { DotCategory } from '@dotcms/app/shared/models/dot-categories/dot-categories.model'; import { DotMessageService } from '@dotcms/data-access'; import { CoreWebService } from '@dotcms/dotcms-js'; +import { DotCategory } from '@dotcms/dotcms-models'; import { DotActionMenuButtonComponent, DotMenuComponent, @@ -58,7 +58,13 @@ xdescribe('DotCategoriesListingTableComponent', () => { working: false, name: 'dsdsd', friendlyName: 'dfdf', - type: 'ASD' + type: 'ASD', + active: false, + childrenCount: 0, + description: '', + iDate: 0, + keywords: '', + owner: '' }, { categoryId: '9e882f2a-ada2-47e3-a441-bdf9a7254216', @@ -72,7 +78,13 @@ xdescribe('DotCategoriesListingTableComponent', () => { working: false, name: 'dsdsd', friendlyName: 'dfdf', - type: 'ASD' + type: 'ASD', + active: false, + childrenCount: 0, + description: '', + iDate: 0, + keywords: '', + owner: '' } ]; beforeEach(() => { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.ts index 669fe2c98aaa..cd7323685b90 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/dot-categories-list.component.ts @@ -5,7 +5,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; import { LazyLoadEvent } from 'primeng/api'; import { Table } from 'primeng/table'; -import { DotCategory } from '@dotcms/app/shared/models/dot-categories/dot-categories.model'; +import { DotCategory } from '@dotcms/dotcms-models'; import { DotCategoriesListState, DotCategoriesListStore } from './store/dot-categories-list-store'; @@ -24,6 +24,7 @@ export class DotCategoriesListComponent { dataTable: Table; @ViewChild('gf') globalSearch: ElementRef; + constructor(private store: DotCategoriesListStore) {} /** diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts index 92fcb0d559f3..a5304036297a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-categories/dot-categories-list/store/dot-categories-list-store.ts @@ -8,9 +8,8 @@ import { LazyLoadEvent, MenuItem } from 'primeng/api'; import { map, take } from 'rxjs/operators'; import { DotCategoriesService } from '@dotcms/app/api/services/dot-categories/dot-categories.service'; -import { DotCategory } from '@dotcms/app/shared/models/dot-categories/dot-categories.model'; import { DotMessageService, OrderDirection } from '@dotcms/data-access'; -import { DotActionMenuItem } from '@dotcms/dotcms-models'; +import { DotActionMenuItem, DotCategory } from '@dotcms/dotcms-models'; import { DataTableColumn } from '@models/data-table'; export interface DotCategoriesListState { @@ -29,27 +28,7 @@ export interface DotCategoriesListState { @Injectable() export class DotCategoriesListStore extends ComponentStore { - constructor( - private dotMessageService: DotMessageService, - private categoryService: DotCategoriesService - ) { - super(); - this.setState({ - categoriesBulkActions: this.getCategoriesBulkActions(), - categoriesActions: this.getCategoriesActions(), - tableColumns: this.getCategoriesColumns(), - selectedCategories: [], - categories: [], - categoryBreadCrumbs: [], - currentPage: this.categoryService.currentPage, - paginationPerPage: this.categoryService.paginationPerPage, - totalRecords: this.categoryService.totalRecords, - sortField: null, - sortOrder: OrderDirection.ASC - }); - } readonly vm$ = this.select((state: DotCategoriesListState) => state); - /** * Get categories breadcrumbs * @memberof DotCategoriesListStore @@ -61,7 +40,6 @@ export class DotCategoriesListStore extends ComponentStore This function returns an observable of an array of DotCategory objects * @returns Observable @@ -150,6 +123,7 @@ export class DotCategoriesListStore extends ComponentStore This function returns an observable of an array of DotCategory objects * @returns Observable @@ -169,6 +143,26 @@ export class DotCategoriesListStore extends ComponentStore + } @case (fieldTypes.RADIO) { - + } @case (fieldTypes.TEXT) { - + } @case (fieldTypes.TEXTAREA) { - + } @case (fieldTypes.CHECKBOX) { - + } @case (fieldTypes.MULTI_SELECT) { } @case (calendarTypes.includes(field.fieldType) ? field.fieldType : '') { - + } @case (fieldTypes.TAG) { - + } @case (fieldTypes.JSON) { - + } @case (fieldTypes.BINARY) { } @case (fieldTypes.WYSIWYG) { - + } @case (fieldTypes.HOST_FOLDER) { } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.html new file mode 100644 index 000000000000..f0812ca8f83c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.html @@ -0,0 +1,40 @@ +
Root
+
+ @for (column of categories(); let index = $index; track index) { + +
+ @for (item of column; track item.inode) { +
+ + + + + @if (item.childrenCount > 0) { + + } +
+ } +
+ } + + + @for (_ of emptyColumns(); track _) { +
+ } +
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.scss new file mode 100644 index 000000000000..2e20396ccc5a --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.scss @@ -0,0 +1,83 @@ +@use "variables" as *; + +:host { + overflow: auto; + height: 100%; + display: flex; + flex-direction: column; +} + +.category-list__wrapper { + border-radius: $border-radius-md; + display: flex; + flex-direction: column; + height: 100%; +} + +.category-list__header { + padding: $spacing-1 $spacing-4; + font-size: $font-size-lg; + font-weight: $font-weight-medium-bold; + border-bottom: 1px solid $color-palette-gray-400; +} + +.category-list__category-list { + display: flex; + flex: 1 1 auto; + overflow-x: auto; + overflow-y: hidden; + + &.no-overflow-x-yet { + overflow-x: hidden; + } +} + +.category-list__category-column { + flex: 0 0 30%; + border-right: 1px solid $color-palette-gray-400; + overflow-y: auto; + height: 100%; + padding: $spacing-1; + + &:last-child { + border-right: none; + } +} + +.category-list__item { + gap: $spacing-3; + padding: 0 $spacing-1 0 $spacing-1; + transition: background-color $basic-speed; + height: 40px; + flex-wrap: nowrap; + opacity: 0; + animation: fadeIn $basic-speed forwards; + + &:hover { + background-color: $color-palette-primary-100; + } +} + +.category-list__item--selected { + background-color: $color-palette-primary-200; +} + +.category-list__item-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.category-list__item-icon { + color: $color-palette-primary; + cursor: pointer; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.spec.ts new file mode 100644 index 000000000000..e3b11fb4ec6c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.spec.ts @@ -0,0 +1,96 @@ +import { expect } from '@jest/globals'; +import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; + +import { DotMessageService } from '@dotcms/data-access'; + +import { + DotCategoryFieldCategoryListComponent, + MINIMUM_CATEGORY_COLUMNS +} from './dot-category-field-category-list.component'; + +import { CATEGORY_LIST_MOCK, SELECTED_LIST_MOCK } from '../../mocks/category-field.mocks'; + +describe('DotCategoryFieldCategoryListComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotCategoryFieldCategoryListComponent, + providers: [mockProvider(DotMessageService)] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + categories: CATEGORY_LIST_MOCK, + selected: SELECTED_LIST_MOCK + } + }); + + spectator.detectChanges(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should render correct number of category columns', () => { + expect(spectator.queryAll(byTestId('category-column')).length).toBe( + CATEGORY_LIST_MOCK.length + ); + }); + + it('should render correct number of category items', () => { + expect(spectator.queryAll(byTestId('category-item')).length).toBe( + CATEGORY_LIST_MOCK.flat().length + ); + }); + + it('should render correct number of category item labels', () => { + expect(spectator.queryAll(byTestId('category-item-label')).length).toBe( + CATEGORY_LIST_MOCK.flat().length + ); + }); + + it('should render correct number of empty columns', () => { + expect(spectator.queryAll(byTestId('category-column-empty')).length).toBe( + MINIMUM_CATEGORY_COLUMNS - CATEGORY_LIST_MOCK.length + ); + }); + + it('should render one category item with child indicator', () => { + expect(spectator.queryAll(byTestId('category-item-with-child')).length).toBe(1); + }); + + it('should emit the correct item when clicked', () => { + const emitSpy = jest.spyOn(spectator.component.itemClicked, 'emit'); + const items = spectator.queryAll(byTestId('category-item')); + spectator.click(items[0]); + + expect(emitSpy).toHaveBeenCalledWith({ + index: 0, + item: CATEGORY_LIST_MOCK[0][0] + }); + }); + + it('should apply selected class to the correct item', () => { + const items = spectator.queryAll(byTestId('category-item')); + expect(items[1].className).toContain('category-list__item--selected'); + expect(items[2].className).toContain('category-list__item--selected'); + }); + + it('should not render any empty columns when there are enough categories', () => { + const minColumns = 4; + const testCategories = Array(minColumns).fill(CATEGORY_LIST_MOCK[0]); + + spectator = createComponent({ + props: { + categories: testCategories, + selected: SELECTED_LIST_MOCK + } + }); + + spectator.detectChanges(); + + expect(spectator.queryAll(byTestId('category-column-empty')).length).toBe(0); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.ts new file mode 100644 index 000000000000..b9509c738018 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-category-list/dot-category-field-category-list.component.ts @@ -0,0 +1,130 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + ElementRef, + EventEmitter, + inject, + input, + OnDestroy, + Output, + QueryList, + 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 { DotCategory } from '@dotcms/dotcms-models'; + +import { DotCategoryFieldCategory } from '../../models/dot-category-field.models'; + +export const MINIMUM_CATEGORY_COLUMNS = 4; + +const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3; + +/** + * Represents the Dot Category Field Category List component. + * @class + * @implements {AfterViewInit} + */ +@Component({ + selector: 'dot-category-field-category-list', + standalone: true, + imports: [CommonModule, TreeModule, CheckboxModule, FormsModule, ButtonModule], + templateUrl: './dot-category-field-category-list.component.html', + styleUrl: './dot-category-field-category-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'category-list__wrapper' + } +}) +export class DotCategoryFieldCategoryListComponent implements AfterViewInit, OnDestroy { + @ViewChildren('categoryColumn') categoryColumns: QueryList; + + /** + * Represents the variable 'categories' which is of type 'DotCategoryFieldCategory[][]'. + */ + categories = input.required(); + /** + * Represent the selected item saved in the contentlet + */ + selected = input.required(); + + /** + * Generate the empty columns + */ + emptyColumns = computed(() => { + const numberOfEmptyColumnsNeeded = Math.max( + MINIMUM_CATEGORY_COLUMNS - this.categories().length, + 0 + ); + + return Array(numberOfEmptyColumnsNeeded).fill(null); + }); + + /** + * Emit the item clicked to the parent component + */ + @Output() itemClicked = new EventEmitter<{ index: number; item: DotCategory }>(); + + /** + * Model of the items selected + */ + itemsSelected: string[]; + + readonly #destroyRef = inject(DestroyRef); + + #effectRef = effect(() => { + this.itemsSelected = this.selected(); + }); + + ngAfterViewInit() { + // Handle the horizontal scroll to make visible the last column + this.categoryColumns.changes.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe(() => { + this.scrollHandler(); + }); + } + + ngOnDestroy(): void { + this.#effectRef.destroy(); + } + + private scrollHandler() { + try { + const columnsArray = this.categoryColumns.toArray(); + + if (columnsArray.length === 0) { + return; + } + + if ( + columnsArray[MINIMUM_CATEGORY_WITHOUT_SCROLLING - 1] && + columnsArray[MINIMUM_CATEGORY_WITHOUT_SCROLLING - 1].nativeElement.children.length > + 0 + ) { + columnsArray[columnsArray.length - 1].nativeElement.scrollIntoView({ + behavior: 'smooth', + block: 'end', + inline: 'end' + }); + } else { + // scroll to the first column + columnsArray[0].nativeElement.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'start' + }); + } + } catch (error) { + console.error('Error during scrollHandler execution:', error); + } + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.html similarity index 79% rename from core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.html index c82c728ac64c..689c548d7356 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.html @@ -1,8 +1,8 @@ @@ -23,14 +23,19 @@
-
Categories
+
+ +
Selected Categories
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.scss similarity index 67% rename from core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss rename to core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.scss index d92ce35afaa6..6b9a0937bd67 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.scss +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.scss @@ -14,8 +14,8 @@ } .category-field__content { - display: grid; - grid-template-columns: 2fr 1fr; + display: flex; + flex-direction: row; margin: auto; background-color: $color-palette-gray-300; padding: $spacing-4; @@ -27,15 +27,32 @@ gap: $spacing-1; } +.category-field__left-pane { + display: flex; + flex-direction: row; + flex: 1; + gap: $spacing-1; + max-width: 75vw; + overflow: hidden; + height: 100%; +} + +.category-field__right-pane { + flex: 0 0 25%; // Ajusta segĂșn sea necesario + gap: $spacing-1; +} + .category-field__search, .category-field__categories, .category-field__right-pane { border: $field-border-size solid $color-palette-gray-400; border-radius: $border-radius-md; background-color: $white; - padding: $spacing-3; + overflow: hidden; } :host ::ng-deep .p-sidebar-content { padding: 0; + height: 100%; + overflow: hidden; } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.spec.ts new file mode 100644 index 000000000000..529a39f7315c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.spec.ts @@ -0,0 +1,64 @@ +import { expect, it } from '@jest/globals'; +import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; + +import { Sidebar } from 'primeng/sidebar'; + +import { DotMessageService } from '@dotcms/data-access'; + +import { DotCategoryFieldSidebarComponent } from './dot-category-field-sidebar.component'; + +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'; + +describe('DotEditContentCategoryFieldSidebarComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: DotCategoryFieldSidebarComponent, + providers: [mockProvider(DotMessageService), CategoryFieldStore] + }); + + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(CategoriesService, { + getChildren: jest.fn().mockReturnValue(of(CATEGORY_LIST_MOCK)) + }) + ] + }); + + spectator.detectChanges(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should have `visible` property set to `true` by default', () => { + expect(spectator.component.visible).toBe(true); + expect(spectator.query(Sidebar)).not.toBeNull(); + }); + + it('should render a "clear all" button', () => { + spectator.detectChanges(); + expect(spectator.query(byTestId('clear_all-btn'))).not.toBeNull(); + }); + + it('should emit event to close sidebar when "back" button is clicked', () => { + const closedSidebarSpy = jest.spyOn(spectator.component.closedSidebar, 'emit'); + const cancelBtn = spectator.query(byTestId('back-btn')); + expect(cancelBtn).not.toBeNull(); + + expect(closedSidebarSpy).not.toHaveBeenCalled(); + + spectator.click(cancelBtn); + expect(closedSidebarSpy).toHaveBeenCalled(); + }); + + it('should render the CategoryFieldCategoryList component', () => { + expect(spectator.query(DotCategoryFieldCategoryListComponent)).not.toBeNull(); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.ts new file mode 100644 index 000000000000..a1ac137dbf38 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-sidebar/dot-category-field-sidebar.component.ts @@ -0,0 +1,69 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + OnDestroy, + OnInit, + Output +} from '@angular/core'; + +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { SidebarModule } from 'primeng/sidebar'; + +import { DotMessagePipe } from '@dotcms/ui'; + +import { DotCategoryFieldItem } from '../../models/dot-category-field.models'; +import { CategoryFieldStore } from '../../store/content-category-field.store'; +import { DotCategoryFieldCategoryListComponent } from '../dot-category-field-category-list/dot-category-field-category-list.component'; + +/** + * Component for the sidebar that appears when editing content category field. + */ +@Component({ + selector: 'dot-category-field-sidebar', + standalone: true, + imports: [ + DialogModule, + ButtonModule, + DotMessagePipe, + SidebarModule, + DotCategoryFieldCategoryListComponent + ], + templateUrl: './dot-category-field-sidebar.component.html', + styleUrl: './dot-category-field-sidebar.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy { + /** + * Indicates whether the sidebar is visible or not. + */ + visible = true; + + /** + * Output that emit if the sidebar is closed + */ + @Output() closedSidebar = new EventEmitter(); + + readonly store = inject(CategoryFieldStore); + + ngOnInit(): void { + this.store.getCategories(); + } + + /** + * Handles the click event on an item. + * + * @param {number} index - The index of the item being clicked. + * @param {DotCategory} item - The item being clicked. + * @returns {void} + */ + itemClicked({ index, item }: DotCategoryFieldItem): void { + this.store.getCategories({ index, item }); + } + + ngOnDestroy(): void { + this.store.clean(); + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts deleted file mode 100644 index 2c5f6015740c..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, it } from '@jest/globals'; -import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; -import { mockProvider } from '@ngneat/spectator/jest'; - -import { DotMessageService } from '@dotcms/data-access'; - -import { DotEditContentCategoryFieldSidebarComponent } from './dot-edit-content-category-field-sidebar.component'; - -describe('DotEditContentCategoryFieldSidebarComponent', () => { - let spectator: Spectator; - - const createComponent = createComponentFactory({ - component: DotEditContentCategoryFieldSidebarComponent, - providers: [mockProvider(DotMessageService)] - }); - - beforeEach(() => { - spectator = createComponent(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should have `visible` property by default `true`', () => { - expect(spectator.component.visible).toBe(true); - }); - - it('should have a sidebar', () => { - expect(spectator.query(byTestId('sidebar'))).not.toBeNull(); - }); - - it('should have a clear all button', () => { - expect(spectator.query(byTestId('clear_all-btn'))).not.toBeNull(); - }); - - it('should close the sidebar when you click back', () => { - const closedSidebarSpy = jest.spyOn(spectator.component.closedSidebar, 'emit'); - const cancelBtn = spectator.query(byTestId('back-btn')); - expect(cancelBtn).not.toBeNull(); - - expect(closedSidebarSpy).not.toHaveBeenCalled(); - - spectator.click(cancelBtn); - expect(closedSidebarSpy).toHaveBeenCalled(); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts deleted file mode 100644 index d2fc74edd96c..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ChangeDetectionStrategy, Component, EventEmitter } from '@angular/core'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { SidebarModule } from 'primeng/sidebar'; - -import { DotMessagePipe } from '@dotcms/ui'; - -/** - * Component for the sidebar that appears when editing content category field. - */ -@Component({ - selector: 'dot-edit-content-category-field-sidebar', - standalone: true, - imports: [DialogModule, ButtonModule, DotMessagePipe, SidebarModule], - templateUrl: './dot-edit-content-category-field-sidebar.component.html', - styleUrl: './dot-edit-content-category-field-sidebar.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotEditContentCategoryFieldSidebarComponent { - /** - * Indicates whether the sidebar is visible or not. - * - */ - visible = true; - - /** - * The event is fired whenever the sidebar is closed either by hitting 'Escape', - * clicking on the overlay, or on the back button. - * - */ - closedSidebar = new EventEmitter(); -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html index a5c488488fe5..963c6e0eda2a 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.html @@ -1,24 +1,23 @@ -@if (values.length) { -
- @for (category of values; track category.id) { - - } -
+@if (store.selectedCategories().length) { +
+ @for (category of store.categoriesValue(); track category.key) { + + } +
}
- - + diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.scss index 397cfe8c5bde..64d5a700a738 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.scss +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.scss @@ -7,6 +7,7 @@ &.dot-category-field__container--has-categories .dot-category-field__categories { border-bottom: none; + padding: $spacing-1; } } @@ -19,7 +20,6 @@ } .dot-category-field__categories { - padding: $spacing-1; gap: $spacing-1; border-radius: $border-radius-md $border-radius-md 0 0; } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts index 718afe8267cd..b58cecf9f74b 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.spec.ts @@ -1,23 +1,41 @@ import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { HttpClient } from '@angular/common/http'; import { fakeAsync } from '@angular/core/testing'; import { DotMessageService } from '@dotcms/data-access'; -import { DotEditContentCategoryFieldSidebarComponent } from './components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component'; +import { DotCategoryFieldSidebarComponent } from './components/dot-category-field-sidebar/dot-category-field-sidebar.component'; import { DotEditContentCategoryFieldComponent } from './dot-edit-content-category-field.component'; import { CLOSE_SIDEBAR_CSS_DELAY_MS } from './dot-edit-content-category-field.const'; +import { CATEGORY_FIELD_CONTENTLET_MOCK, CATEGORY_FIELD_MOCK } from './mocks/category-field.mocks'; +import { CategoriesService } from './services/categories.service'; +import { CategoryFieldStore } from './store/content-category-field.store'; describe('DotEditContentCategoryFieldComponent', () => { let spectator: Spectator; const createComponent = createComponentFactory({ component: DotEditContentCategoryFieldComponent, - providers: [mockProvider(DotMessageService)] + imports: [MockComponent(DotCategoryFieldSidebarComponent)], + componentViewProviders: [CategoryFieldStore], + providers: [ + mockProvider(DotMessageService), + mockProvider(CategoriesService), + mockProvider(HttpClient) + ] }); beforeEach(() => { - spectator = createComponent(); + spectator = createComponent({ + props: { + contentlet: CATEGORY_FIELD_CONTENTLET_MOCK, + field: CATEGORY_FIELD_MOCK + } + }); + + spectator.detectChanges(); }); afterEach(() => { @@ -29,13 +47,20 @@ describe('DotEditContentCategoryFieldComponent', () => { expect(spectator.query(byTestId('show-sidebar-btn'))).not.toBeNull(); }); - it('should display the category list with chips when there are categories', () => { - spectator.component.values = []; - spectator.detectComponentChanges(); + it('should not display the category list with chips when there are no categories', () => { + spectator = createComponent({ + props: { + contentlet: [], + field: CATEGORY_FIELD_MOCK + } + }); + + spectator.detectChanges(); + expect(spectator.query(byTestId('category-chip-list'))).toBeNull(); + }); - spectator.component.values = [{ id: 1, value: 'Streetwear' }]; - spectator.detectComponentChanges(); + it('should display the category list with chips when there are categories', () => { expect(spectator.query(byTestId('category-chip-list'))).not.toBeNull(); }); }); @@ -60,16 +85,19 @@ describe('DotEditContentCategoryFieldComponent', () => { spectator.click(selectBtn); + spectator.detectChanges(); + expect(selectBtn.disabled).toBe(true); }); it('should create a DotEditContentCategoryFieldSidebarComponent instance when the `Select` button is clicked', () => { const selectBtn = spectator.query(byTestId('show-sidebar-btn')) as HTMLButtonElement; expect(selectBtn).not.toBeNull(); - expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).toBeNull(); + expect(spectator.query(DotCategoryFieldSidebarComponent)).toBeNull(); spectator.click(selectBtn); - expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).not.toBeNull(); + + expect(spectator.query(DotCategoryFieldSidebarComponent)).not.toBeNull(); }); it('should remove DotEditContentCategoryFieldSidebarComponent when `closedSidebar` emit', fakeAsync(() => { @@ -77,9 +105,7 @@ describe('DotEditContentCategoryFieldComponent', () => { expect(selectBtn).not.toBeNull(); spectator.click(selectBtn); - const sidebarComponentRef = spectator.query( - DotEditContentCategoryFieldSidebarComponent - ); + const sidebarComponentRef = spectator.query(DotCategoryFieldSidebarComponent); expect(sidebarComponentRef).not.toBeNull(); sidebarComponentRef.closedSidebar.emit(); @@ -89,7 +115,7 @@ describe('DotEditContentCategoryFieldComponent', () => { // Due to a delay in the pipe of the subscription spectator.tick(CLOSE_SIDEBAR_CSS_DELAY_MS + 100); - expect(spectator.query(DotEditContentCategoryFieldSidebarComponent)).toBeNull(); + expect(spectator.query(DotCategoryFieldSidebarComponent)).toBeNull(); expect(selectBtn.disabled).toBe(false); })); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts index 70ab22043ef7..0417eebf6f64 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/dot-edit-content-category-field.component.ts @@ -6,6 +6,7 @@ import { DestroyRef, inject, input, + OnInit, signal, ViewChild } from '@angular/core'; @@ -19,11 +20,13 @@ import { TooltipModule } from 'primeng/tooltip'; import { delay } from 'rxjs/operators'; -import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; +import { DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models'; import { DotDynamicDirective, DotMessagePipe } from '@dotcms/ui'; -import { DotEditContentCategoryFieldSidebarComponent } from './components/dot-edit-content-category-field-sidebar/dot-edit-content-category-field-sidebar.component'; +import { DotCategoryFieldSidebarComponent } from './components/dot-category-field-sidebar/dot-category-field-sidebar.component'; import { CLOSE_SIDEBAR_CSS_DELAY_MS } from './dot-edit-content-category-field.const'; +import { CategoriesService } from './services/categories.service'; +import { CategoryFieldStore } from './store/content-category-field.store'; /** * Component for editing content category field. @@ -53,13 +56,13 @@ import { CLOSE_SIDEBAR_CSS_DELAY_MS } from './dot-edit-content-category-field.co useFactory: () => inject(ControlContainer, { skipSelf: true }) } ], - // eslint-disable-next-line @angular-eslint/no-host-metadata-property host: { - '[class.dot-category-field__container--has-categories]': 'hasCategories()', - '[class.dot-category-field__container]': '!hasCategories()' - } + '[class.dot-category-field__container--has-categories]': 'hasSelectedCategories()', + '[class.dot-category-field__container]': '!hasSelectedCategories()' + }, + providers: [CategoriesService, CategoryFieldStore] }) -export class DotEditContentCategoryFieldComponent { +export class DotEditContentCategoryFieldComponent implements OnInit { disableSelectCategoriesButton = signal(false); @ViewChild(DotDynamicDirective, { static: true }) sidebarHost!: DotDynamicDirective; @@ -71,17 +74,23 @@ export class DotEditContentCategoryFieldComponent { */ field = input.required(); - // TODO: Replace with the content of the selected categories - values = []; + /** + * Represents a DotCMS contentlet. + * + */ + contentlet = input.required(); + + readonly store = inject(CategoryFieldStore); readonly #destroyRef = inject(DestroyRef); - #componentRef: ComponentRef; + #componentRef: ComponentRef; /** - * Checks if the object has categories. - * @returns {boolean} - True if the object has categories, false otherwise. + * Determines if there are any selected categories. + * + * @returns {Boolean} - True if there are selected categories, false otherwise. */ - hasCategories(): boolean { - return this.values.length > 0; + hasSelectedCategories(): boolean { + return !!this.store.hasSelectedCategories(); } /** @@ -92,12 +101,16 @@ export class DotEditContentCategoryFieldComponent { showCategoriesSidebar(): void { this.disableSelectCategoriesButton.set(true); this.#componentRef = this.sidebarHost.viewContainerRef.createComponent( - DotEditContentCategoryFieldSidebarComponent + DotCategoryFieldSidebarComponent ); this.setSidebarListener(); } + ngOnInit(): void { + this.store.load(this.field(), this.contentlet()); + } + private setSidebarListener() { this.#componentRef.instance.closedSidebar .pipe(takeUntilDestroyed(this.#destroyRef), delay(CLOSE_SIDEBAR_CSS_DELAY_MS)) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/mocks/category-field.mocks.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/mocks/category-field.mocks.ts new file mode 100644 index 000000000000..e36216a417cf --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/mocks/category-field.mocks.ts @@ -0,0 +1,205 @@ +import { DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models'; + +import { DotCategoryFieldCategory } from '../models/dot-category-field.models'; + +export const CATEGORY_FIELD_VARIABLE_NAME = 'categorias'; + +/** + * Response Mock of Contentlet + */ +export const CATEGORY_FIELD_CONTENTLET_MOCK: DotCMSContentlet = { + modDate: '', + archived: false, + baseType: 'CONTENT', + [CATEGORY_FIELD_VARIABLE_NAME]: [ + { + '33333': 'Electrical' + }, + { + '22222': 'Doors & Windows' + } + ], + contentType: 'TEST', + creationDate: 1719237692960, + folder: 'SYSTEM_FOLDER', + hasLiveVersion: true, + hasTitleImage: false, + host: '8a7d5e23-da1e-420a-b4f0-471e7da8ea2d', + hostName: 'default', + identifier: '389e76f7c51714b8087fa9950a8f271b', + inode: '5c42dfca-bddc-4d1e-a0e1-2dfeca952c5e', + languageId: 1, + live: true, + locked: false, + modUser: 'dotcms.org.1', + modUserName: 'Admin User', + owner: 'dotcms.org.1', + ownerName: 'Admin User', + publishDate: 1719237693091, + publishUser: 'dotcms.org.1', + publishUserName: 'Admin User', + sortOrder: 0, + stInode: '61226fd915b7f025da020fc1f5856ab7', + title: '389e76f7c51714b8087fa9950a8f271b', + titleImage: 'TITLE_IMAGE_NOT_FOUND', + url: '/content.5c42dfca-bddc-4d1e-a0e1-2dfeca952c5e', + working: true +}; + +/** + * Mock of the Field Category + */ +export const CATEGORY_FIELD_MOCK: DotCMSContentTypeField = { + categories: { + categoryName: 'Categorias', + description: null, + inode: 'b3da6475e34655bed79919984bc34fc4', + key: 'categorias', + keywords: '', + sortOrder: 0 + }, + clazz: 'com.dotcms.contenttype.model.field.ImmutableCategoryField', + contentTypeId: '61226fd915b7f025da020fc1f5856ab7', + dataType: 'SYSTEM', + fieldType: 'Category', + fieldTypeLabel: 'Category', + fieldVariables: [], + fixed: false, + forceIncludeInApi: false, + iDate: 1718916209000, + id: '76f9f11132d288260e712056a4f950c5', + indexed: true, + listed: false, + modDate: 1718998303000, + name: 'Categorias', + readOnly: false, + required: false, + searchable: false, + sortOrder: 2, + unique: false, + values: 'b3da6475e34655bed79919984bc34fc4', + variable: 'categorias' +}; + +/** + * Represent a Category List of level 1 with children `childrenCount` + */ +export const CATEGORY_LEVEL_1: DotCategoryFieldCategory[] = [ + { + active: true, + categoryName: 'Cleaning Supplies', + categoryVelocityVarName: '1f208488057007cedda0e0b5d52ee3b3', + childrenCount: 1, // This make show the caret of has children + description: null, + iDate: 1719275768170, + identifier: null, + inode: '111111', + key: '1f208488057007cedda0e0b5d52ee3b3', + keywords: null, + modDate: 1718916179985, + owner: '', + sortOrder: 0, + type: 'category' + }, + { + active: true, + categoryName: 'Doors & Windows', + categoryVelocityVarName: 'cb83dc32c0a198fd0ca427b3b587f4ce', + childrenCount: 0, + description: null, + iDate: 1719410426844, + identifier: null, + inode: '22222', + key: 'cb83dc32c0a198fd0ca427b3b587f4ce', + keywords: null, + modDate: 1718916176666, + owner: '', + sortOrder: 0, + type: 'category', + checked: true + }, + { + active: true, + categoryName: 'Electrical', + categoryVelocityVarName: '0ab5e687775e4793679970e561380560', + childrenCount: 0, + description: null, + iDate: 1719410426844, + identifier: null, + inode: '33333', + key: '0ab5e687775e4793679970e561380560', + keywords: null, + modDate: 1718916175804, + owner: '', + sortOrder: 0, + type: 'category', + checked: true + } +]; + +/** + * Represent a Category List of level 2 + */ +export const CATEGORY_LEVEL_2: DotCategoryFieldCategory[] = [ + { + active: true, + categoryName: 'Concrete & Cement', + categoryVelocityVarName: 'd2fb8e67c390e3b84cd613fa15aad5d4', + childrenCount: 0, + description: null, + iDate: 1719275768170, + identifier: null, + inode: '44444', + key: 'd2fb8e67c390e3b84cd613fa15aad5d4', + keywords: null, + modDate: 1718916180738, + owner: '', + sortOrder: 0, + type: 'category' + }, + { + active: true, + categoryName: 'Flooring', + categoryVelocityVarName: '3a3effac9f26593810c8687e692817a6', + childrenCount: 0, + description: null, + iDate: 1719410426844, + identifier: null, + inode: '55555', + key: '3a3effac9f26593810c8687e692817a6', + keywords: null, + modDate: 1718916176408, + owner: '', + sortOrder: 0, + type: 'category' + }, + { + active: true, + categoryName: 'Garage Organization', + categoryVelocityVarName: '977ba2c4e2af65e303c748ec39f0f1ca', + childrenCount: 0, + description: null, + iDate: 1719410426844, + identifier: null, + inode: '66666', + key: '977ba2c4e2af65e303c748ec39f0f1ca', + keywords: null, + modDate: 1718916179380, + owner: '', + sortOrder: 0, + type: 'category' + } +]; + +/** + * Represent a Category List handling 2 levels + */ +export const CATEGORY_LIST_MOCK: DotCategoryFieldCategory[][] = [ + [...CATEGORY_LEVEL_1], + [...CATEGORY_LEVEL_2] +]; + +/** + * Represent the selected categories + */ +export const SELECTED_LIST_MOCK = [CATEGORY_LEVEL_1[1].inode, CATEGORY_LEVEL_1[2].inode]; diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/models/dot-category-field.models.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/models/dot-category-field.models.ts new file mode 100644 index 000000000000..3be15a96123a --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/models/dot-category-field.models.ts @@ -0,0 +1,25 @@ +import { DotCategory } from '@dotcms/dotcms-models'; + +/** + * Object representing a key-value pair. + * @interface + */ +export interface DotKeyValueObj { + key: string; + value: string; +} + +/** + * Represents an clicked item in a DotCategoryField. + */ +export type DotCategoryFieldItem = { index: number; item: DotCategory }; + +/** + * Represents a category for a Dot field with a checkbox. + * + * @interface + * @extends DotCategory + */ +export interface DotCategoryFieldCategory extends DotCategory { + checked?: boolean; +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.spec.ts new file mode 100644 index 000000000000..8060bcf90fe4 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.spec.ts @@ -0,0 +1,20 @@ +import { HttpMethod } from '@ngneat/spectator'; +import { createHttpFactory, SpectatorHttp } from '@ngneat/spectator/jest'; + +import { API_URL, CategoriesService, ITEMS_PER_PAGE } from './categories.service'; + +describe('CategoriesService', () => { + let spectator: SpectatorHttp; + const createHttp = createHttpFactory(CategoriesService); + + beforeEach(() => (spectator = createHttp())); + + it('can getChildren with inode', () => { + const inode = 'inode-identifier'; + spectator.service.getChildren(inode).subscribe(); + spectator.expectOne( + `${API_URL}/children?per_page=${ITEMS_PER_PAGE}&direction=ASC&inode=${inode}&showChildrenCount=true`, + HttpMethod.GET + ); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.ts new file mode 100644 index 000000000000..477d9bf4cdaa --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/services/categories.service.ts @@ -0,0 +1,44 @@ +import { Observable } from 'rxjs'; + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; + +import { pluck } from 'rxjs/operators'; + +import { DotCMSResponse } from '@dotcms/dotcms-js'; + +import { DotCategoryFieldCategory } from '../models/dot-category-field.models'; + +export const API_URL = '/api/v1/categories'; + +export const ITEMS_PER_PAGE = 7000; + +/** + * CategoriesService class. + * + * This service is responsible for retrieving the children of a given inode. + */ +@Injectable() +export class CategoriesService { + readonly #http = inject(HttpClient); + + /** + * Retrieves the children of a given inode. + * + * @param {string} inode - The inode of the parent node. + * @returns {Observable} - An Observable that emits the children of the given inode as an array of DotCategory objects. + */ + getChildren(inode: string): Observable { + const params = new HttpParams() + .set('per_page', ITEMS_PER_PAGE) + .set('direction', 'ASC') + .set('inode', inode) + .set('showChildrenCount', 'true'); + + return this.#http + .get>(`${API_URL}/children`, { + params + }) + .pipe(pluck('entity')); + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.spec.ts new file mode 100644 index 000000000000..00ad86a3945d --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.spec.ts @@ -0,0 +1,124 @@ +import { + createServiceFactory, + mockProvider, + SpectatorService, + SpyObject +} from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; + +import { ComponentStatus } from '@dotcms/dotcms-models'; + +import { CategoryFieldStore } from './content-category-field.store'; + +import { + CATEGORY_FIELD_CONTENTLET_MOCK, + CATEGORY_FIELD_MOCK, + CATEGORY_LEVEL_1, + CATEGORY_LEVEL_2, + SELECTED_LIST_MOCK +} from '../mocks/category-field.mocks'; +import { DotKeyValueObj } from '../models/dot-category-field.models'; +import { CategoriesService } from '../services/categories.service'; + +const EMPTY_ARRAY = []; +const EMPTY_STRING = ''; + +describe('CategoryFieldStore', () => { + let spectator: SpectatorService>; + let store: InstanceType; + let categoriesService: SpyObject; + const createService = createServiceFactory({ + service: CategoryFieldStore, + providers: [ + mockProvider(CategoriesService, { + getChildren: jest.fn().mockReturnValue(of([])) + }) + ] + }); + + beforeEach(() => { + spectator = createService(); + store = spectator.service; + categoriesService = spectator.inject(CategoriesService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should initialize with default state', () => { + expect(store.rootCategoryInode()).toEqual(EMPTY_STRING); + expect(store.categories()).toEqual(EMPTY_ARRAY); + expect(store.categoriesValue()).toEqual(EMPTY_ARRAY); + expect(store.parentPath()).toEqual(EMPTY_ARRAY); + expect(store.state()).toEqual(ComponentStatus.IDLE); + // computed + expect(store.selectedCategories()).toEqual(EMPTY_ARRAY); + expect(store.categoryList()).toEqual(EMPTY_ARRAY); + }); + + describe('withMethods', () => { + it('should set the correct rootCategoryInode and categoriesValue', () => { + const expectedCategoryValues: DotKeyValueObj[] = [ + { + key: '33333', + value: 'Electrical' + }, + { + key: '22222', + value: 'Doors & Windows' + } + ]; + + store.load(CATEGORY_FIELD_MOCK, CATEGORY_FIELD_CONTENTLET_MOCK); + + expect(store.rootCategoryInode()).toEqual(CATEGORY_FIELD_MOCK.values); + expect(store.categoriesValue()).toEqual(expectedCategoryValues); + }); + + describe('getCategories', () => { + beforeEach(() => { + store.load(CATEGORY_FIELD_MOCK, CATEGORY_FIELD_CONTENTLET_MOCK); + }); + + it('should fetch the categories with the rootCategoryInode', () => { + const rootCategoryInode = CATEGORY_FIELD_MOCK.values; + + const getChildrenSpy = jest.spyOn(categoriesService, 'getChildren'); + + store.getCategories(); + expect(getChildrenSpy).toHaveBeenCalled(); + expect(getChildrenSpy).toHaveBeenCalledWith(rootCategoryInode); + }); + + it('should fetch the categories with the inode sent', () => { + const getChildrenSpy = jest + .spyOn(categoriesService, 'getChildren') + .mockReturnValue(of(CATEGORY_LEVEL_2)); + + store.getCategories({ index: 0, item: CATEGORY_LEVEL_1[0] }); + + expect(getChildrenSpy).toHaveBeenCalledWith(CATEGORY_LEVEL_1[0].inode); + }); + + it('should fetch the initial categories and the get by the clicked category with children', () => { + categoriesService.getChildren.mockReturnValue(of(CATEGORY_LEVEL_1)); + store.getCategories(); + categoriesService.getChildren.mockReturnValue(of(CATEGORY_LEVEL_2)); + store.getCategories({ index: 0, item: CATEGORY_LEVEL_1[0] }); + + expect(store.categories().length).toBe(2); + }); + }); + }); + + describe('withComputed', () => { + it('should show item after load the values', () => { + const expectedSelectedValues = SELECTED_LIST_MOCK; + store.load(CATEGORY_FIELD_MOCK, CATEGORY_FIELD_CONTENTLET_MOCK); + expect(store.selectedCategories().sort()).toEqual(expectedSelectedValues.sort()); + + expect(store.categoryList()).toEqual(EMPTY_ARRAY); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.ts new file mode 100644 index 000000000000..ef87e77df810 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.ts @@ -0,0 +1,151 @@ +import { tapResponse } from '@ngrx/component-store'; +import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { pipe } from 'rxjs'; + +import { computed, inject } from '@angular/core'; + +import { filter, switchMap, tap } from 'rxjs/operators'; + +import { ComponentStatus, DotCMSContentlet, DotCMSContentTypeField } from '@dotcms/dotcms-models'; + +import { + DotCategoryFieldCategory, + DotCategoryFieldItem, + DotKeyValueObj +} from '../models/dot-category-field.models'; +import { CategoriesService } from '../services/categories.service'; +import { + addMetadata, + checkIfClickedIsLastItem, + clearCategoriesAfterIndex, + clearParentPathAfterIndex, + getSelectedCategories +} from '../utils/category-field.utils'; + +export type CategoryFieldState = { + rootCategoryInode: string; + categories: DotCategoryFieldCategory[][]; + categoriesValue: DotKeyValueObj[]; + parentPath: string[]; + state: ComponentStatus; +}; + +export const initialState: CategoryFieldState = { + rootCategoryInode: '', + categories: [], + categoriesValue: [], + parentPath: [], + state: ComponentStatus.IDLE +}; + +/** + * A Signal store responsible for managing category fields. It keeps track of the state of + * different categories, provides access to selected categories, and offers methods for + * loading, retrieving, and cleaning up categories. + * + * @typedef {Object} CategoryFieldStore + * + * @property {Array} selectedCategories - The keys of the currently selected items from the contentlet. + * @property {Array} categoryList - The list of categories ready for rendering, where each category has additional properties. + * @property {function} load - Sets a given iNode as the parent and loads selected categories into the store. + * @property {function} getCategories - Fetches categories of a given iNode category parent. + * @property {function} clean - Clears all categories from the store. + */ +export const CategoryFieldStore = signalStore( + withState(initialState), + withComputed(({ categories, categoriesValue, parentPath }) => ({ + /** + * Current selected items (key) from the contentlet + */ + selectedCategories: computed(() => categoriesValue().map((item) => item.key)), + /** + * Categories for render with added properties + */ + categoryList: computed(() => + categories().map((column) => addMetadata(column, parentPath())) + ), + + hasSelectedCategories: computed(() => { + return !!categoriesValue().map((item) => item.key).length; + }) + })), + withMethods((store, categoryService = inject(CategoriesService)) => ({ + /** + * Sets a given iNode as the main parent and loads selected categories into the store. + */ + load({ variable, values }: DotCMSContentTypeField, contentlet: DotCMSContentlet): void { + const categoriesValue = getSelectedCategories(variable, contentlet); + patchState(store, { rootCategoryInode: values, categoriesValue }); + }, + + /** + * Fetches categories from a given iNode category parent. + * This method accepts either void to get the parent, or an index and item returned after clicking an item with children. + */ + getCategories: rxMethod( + pipe( + tap((event) => { + const index = event ? event.index : 0; + const currentCategories = store.categories(); + + if (event) { + if (!checkIfClickedIsLastItem(index, currentCategories)) { + patchState(store, { + categories: [ + ...clearCategoriesAfterIndex(currentCategories, index) + ], + parentPath: [ + ...clearParentPathAfterIndex(store.parentPath(), index) + ] + }); + } + } + }), + // Only pass if you click a item with children + filter( + (event) => + !event || + (event.item.childrenCount > 0 && + !store.parentPath().includes(event.item.inode)) + ), + tap(() => patchState(store, { state: ComponentStatus.LOADING })), + switchMap((event) => { + const rootCategoryInode: string = event + ? event.item.inode + : store.rootCategoryInode(); + + return categoryService.getChildren(rootCategoryInode).pipe( + tapResponse({ + next: (newCategories) => { + if (event) { + patchState(store, { + categories: [...store.categories(), newCategories], + state: ComponentStatus.LOADED, + parentPath: [...store.parentPath(), event.item.inode] + }); + } else { + patchState(store, { + categories: [...store.categories(), newCategories], + state: ComponentStatus.LOADED + }); + } + }, + error: () => { + // TODO: Add Error Handler + patchState(store, { state: ComponentStatus.IDLE }); + } + }) + ); + }) + ) + ), + + /** + * Clears all categories from the store, effectively resetting state related to categories and their parent paths. + */ + clean() { + patchState(store, { categories: [], parentPath: [] }); + } + })) +); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.spec.ts new file mode 100644 index 000000000000..fb3e6e328e08 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.spec.ts @@ -0,0 +1,200 @@ +import { deepCopy } from '@angular-devkit/core'; + +import { + addMetadata, + clearCategoriesAfterIndex, + clearParentPathAfterIndex, + getSelectedCategories +} from './category-field.utils'; + +import { + CATEGORY_FIELD_CONTENTLET_MOCK, + CATEGORY_FIELD_VARIABLE_NAME, + CATEGORY_LEVEL_1 +} from '../mocks/category-field.mocks'; +import { DotCategoryFieldCategory, DotKeyValueObj } from '../models/dot-category-field.models'; + +describe('CategoryFieldUtils', () => { + describe('getSelectedCategories', () => { + it('should return an empty array if contentlet is null', () => { + const result = getSelectedCategories(CATEGORY_FIELD_VARIABLE_NAME, null); + expect(result).toEqual([]); + }); + it('should return an empty array if variable is null', () => { + const result = getSelectedCategories(null, CATEGORY_FIELD_CONTENTLET_MOCK); + expect(result).toEqual([]); + }); + it("should return an empty array if variable is ''", () => { + const result = getSelectedCategories('', CATEGORY_FIELD_CONTENTLET_MOCK); + expect(result).toEqual([]); + }); + + it('should return parsed the values', () => { + const expected: DotKeyValueObj[] = [ + { key: '33333', value: 'Electrical' }, + { key: '22222', value: 'Doors & Windows' } + ]; + const result = getSelectedCategories( + CATEGORY_FIELD_VARIABLE_NAME, + CATEGORY_FIELD_CONTENTLET_MOCK + ); + + expect(result).toEqual(expected); + }); + }); + + describe('addMetadata', () => { + it('should `checked: true` when category has children and exist in the parentPath', () => { + const PARENT_PATH_MOCK = [CATEGORY_LEVEL_1[0].inode]; + const expected = CATEGORY_LEVEL_1.map((item, index) => + index === 0 ? { ...item, checked: true } : { ...item, checked: false } + ); + + const result = addMetadata(CATEGORY_LEVEL_1, PARENT_PATH_MOCK); + expect(result).toEqual(expected); + }); + it('should `checked: false` when category has children and do not exist in the parentPath', () => { + const PATH_MOCK = []; + const expected = CATEGORY_LEVEL_1.map((item) => { + return { ...item, checked: false }; + }); + + const result = addMetadata(CATEGORY_LEVEL_1, PATH_MOCK); + expect(result).toEqual(expected); + }); + + it('should `checked: false` when category do not has children and do not exist in the parentPath', () => { + const PATH_MOCK = []; + const expected = CATEGORY_LEVEL_1.map((item) => { + return { ...item, checked: false }; + }); + + const result = addMetadata(CATEGORY_LEVEL_1, PATH_MOCK); + expect(result).toEqual(expected); + }); + }); + + describe('deepCopy', () => { + it('should create a deep copy of a two-dimensional array of DotCategoryFieldCategory', () => { + const array: DotCategoryFieldCategory[][] = [ + CATEGORY_LEVEL_1, + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'New Category' }] + ]; + const copy = deepCopy(array); + + // The copy should be equal to the original + expect(copy).toEqual(array); + + // Modifying an object in the copy should not affect the original + copy[0][0].categoryName = 'Modified Category'; + expect(array[0][0].categoryName).toBe('Cleaning Supplies'); + }); + + it('should create a deep copy of an empty array', () => { + const array: DotCategoryFieldCategory[][] = []; + const copy = deepCopy(array); + + // The copy should be equal to the original + expect(copy).toEqual(array); + }); + + it('should handle mixed content arrays correctly', () => { + const array: DotCategoryFieldCategory[][] = [ + CATEGORY_LEVEL_1, + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'New Category' }] + ]; + const copy = deepCopy(array); + + // The copy should be equal to the original + expect(copy).toEqual(array); + + // Modifying an object in the copy should not affect the original + copy[1][0].categoryName = 'Another Modified Category'; + expect(array[1][0].categoryName).toBe('New Category'); + }); + }); + + describe('clearCategoriesAfterIndex', () => { + it('should remove all items after the specified index + 1', () => { + const array: DotCategoryFieldCategory[][] = [ + CATEGORY_LEVEL_1, + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'New Category' }], + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'Another Category' }] + ]; + const index = 1; + const result = clearCategoriesAfterIndex(array, index); + + // The resulting array should only contain elements up to the specified index + expect(result.length).toBe(index + 1); + expect(result).toEqual([ + CATEGORY_LEVEL_1, + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'New Category' }] + ]); + }); + + it('should handle an empty array', () => { + const array: DotCategoryFieldCategory[][] = []; + const index = 0; + const result = clearCategoriesAfterIndex(array, index); + + // The resulting array should still be empty + expect(result.length).toBe(0); + expect(result).toEqual([]); + }); + + it('should handle index greater than array length', () => { + const array: DotCategoryFieldCategory[][] = [ + CATEGORY_LEVEL_1, + [{ ...CATEGORY_LEVEL_1[0], categoryName: 'New Category' }] + ]; + const index = 5; + const result = clearCategoriesAfterIndex(array, index); + + // The resulting array should remain unchanged + expect(result.length).toBe(array.length); + expect(result).toEqual(array); + }); + }); + + describe('clearParentPathAfterIndex', () => { + it('should remove all items after the specified index', () => { + const parentPath = ['item1', 'item2', 'item3', 'item4']; + const index = 2; + const result = clearParentPathAfterIndex(parentPath, index); + + // The resulting array should only contain elements up to the specified index + expect(result.length).toBe(index); + expect(result).toEqual(['item1', 'item2']); + }); + + it('should handle an empty array', () => { + const parentPath: string[] = []; + const index = 0; + const result = clearParentPathAfterIndex(parentPath, index); + + // The resulting array should still be empty + expect(result.length).toBe(0); + expect(result).toEqual([]); + }); + + it('should handle index greater than array length', () => { + const parentPath = ['item1', 'item2']; + const index = 5; + const result = clearParentPathAfterIndex(parentPath, index); + + // The resulting array should remain unchanged + expect(result.length).toBe(parentPath.length); + expect(result).toEqual(parentPath); + }); + + it('should handle index equal to 0', () => { + const parentPath = ['item1', 'item2', 'item3']; + const index = 0; + const result = clearParentPathAfterIndex(parentPath, index); + + // The resulting array should be empty + expect(result.length).toBe(0); + expect(result).toEqual([]); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.ts new file mode 100644 index 000000000000..3859e6c3e8b1 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/utils/category-field.utils.ts @@ -0,0 +1,90 @@ +import { DotCMSContentlet } from '@dotcms/dotcms-models'; + +import { DotCategoryFieldCategory, DotKeyValueObj } from '../models/dot-category-field.models'; + +/** + * Retrieves selected categories from a contentlet. + * + * @param {string} variableName - The name of the variable containing the selected categories. + * @param {DotCMSContentlet} contentlet - The contentlet from which to retrieve the selected categories. + * @returns {DotKeyValueObj[]} - An array of objects representing the selected categories. + */ +export const getSelectedCategories = ( + variableName: string, + contentlet: DotCMSContentlet +): DotKeyValueObj[] => { + if (!contentlet || !variableName) { + return []; + } + + const selectedCategories = contentlet[variableName] || []; + + return selectedCategories.map((obj: DotKeyValueObj) => { + const key = Object.keys(obj)[0]; + + return { key, value: obj[key] }; + }); +}; + +/** + * Add calculated properties to the categories + * @param categories + * @param parentPath + */ +export const addMetadata = ( + categories: DotCategoryFieldCategory[], + parentPath: string[] +): DotCategoryFieldCategory[] => { + return categories.map((category) => { + return { + ...category, + checked: parentPath.includes(category.inode) && category.childrenCount > 0 + }; + }); +}; + +/** + * Deep copy of the matrix + * @param array + */ +const deepCopy = (array: T[][]): T[][] => { + return array.map((items) => + items.map((item) => (typeof item === 'object' ? { ...item } : item)) + ); +}; + +/** + * Remove all the items over of the selected index first level + * @param array + * @param index + */ +export const clearCategoriesAfterIndex = ( + array: DotCategoryFieldCategory[][], + index: number +): DotCategoryFieldCategory[][] => { + const newArray = deepCopy(array); + newArray.splice(index + 1); + + return newArray; +}; + +/** + * Remove all the items over of the selected index of parentPath + * @param parentPath + * @param index + */ +export const clearParentPathAfterIndex = (parentPath: string[], index: number): string[] => { + return parentPath.slice(0, index); +}; + +/** + * Check if the index clicked is the last column + * @param index + * @param categories + */ +export const checkIfClickedIsLastItem = ( + index: number, + categories: DotCategoryFieldCategory[][] +) => { + return index + 1 === categories.length; +}; diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html index 8caf13c093c4..6c799391f569 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-contentlet-tools/ema-contentlet-tools.component.html @@ -1,4 +1,4 @@ -@if(contentletArea.payload.container) { +@if (contentletArea.payload.container) { - }
@@ -22,7 +21,7 @@ data-testId="edit-vtl-button" styleClass="p-button-rounded p-button-raised" icon="pi pi-code" /> - } @if(contentletArea.payload.container) { + } @if (contentletArea.payload.container) { `, + ` `, { hostProps: { + actions: ['add', 'delete', 'edit'], width: 10, items: CONTAINERS_DATA_MOCK, containerMap: CONTAINER_MAP_MOCK, @@ -80,7 +81,7 @@ describe('TemplateBuilderBoxComponent', () => { }); it('should render with medium variant and update the class', () => { - spectator.setInput('width', 3); + spectator.setHostInput('width', 3); spectator.detectComponentChanges(); expect(spectator.query(byTestId('template-builder-box')).classList).toContain( 'template-builder-box--medium' @@ -88,7 +89,7 @@ describe('TemplateBuilderBoxComponent', () => { }); it('should render with small variant and update the class', () => { - spectator.setInput('width', 1); + spectator.setHostInput('width', 1); spectator.detectComponentChanges(); expect(spectator.query(byTestId('template-builder-box-small')).classList).toContain( 'template-builder-box--small' @@ -96,7 +97,7 @@ describe('TemplateBuilderBoxComponent', () => { }); it('should render the first ng-template for large and medium variants', () => { - spectator.setInput('width', 10); + spectator.setHostInput('width', 10); spectator.detectComponentChanges(); const firstTemplate = spectator.query(byTestId('template-builder-box')); const secondTemplate = spectator.query(byTestId('template-builder-box-small')); @@ -105,8 +106,8 @@ describe('TemplateBuilderBoxComponent', () => { }); it('should only show the specified actions on actions input', () => { - spectator.setInput('actions', ['add', 'delete']); // Here we hide the edit button - spectator.detectComponentChanges(); + spectator.setHostInput('actions', ['add', 'delete']); // Here we hide the edit button + spectator.detectChanges(); const paletteButton = spectator.query(byTestId('box-style-class-button')); @@ -114,7 +115,7 @@ describe('TemplateBuilderBoxComponent', () => { }); it('should show all buttons for small variant', () => { - spectator.setInput('width', 1); + spectator.setHostInput('width', 1); spectator.detectComponentChanges(); const addButton = spectator.query(byTestId('btn-plus-small')); @@ -249,7 +250,7 @@ describe('TemplateBuilderBoxComponent', () => { describe('Dialog', () => { it('should open dialog when click on edit button', () => { - spectator.setInput('width', 1); + spectator.setHostInput('width', 1); spectator.detectComponentChanges(); const templateBuilderSmallBox = spectator.query(byTestId('template-builder-box-small')); @@ -276,7 +277,8 @@ describe('TemplateBuilderBoxComponent', () => { it('should not open dialog when the size is large', () => { // Make sure the current tampalte-builder-box component is the small one - spectator.setInput('width', 5); + spectator.setHostInput('width', 5); + spectator.detectComponentChanges(); const plusButton = spectator.query(byTestId('btn-plus')); diff --git a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts index e9246237130d..ca55d14f4a72 100644 --- a/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-ai-image-prompt/components/ai-image-prompt-form/ai-image-prompt-form.component.spec.ts @@ -7,9 +7,8 @@ import { ButtonModule } from 'primeng/button'; import { DotMessageService } from '@dotcms/data-access'; import { DotAIImageOrientation, DotGeneratedAIImage, PromptType } from '@dotcms/dotcms-models'; +import { DotClipboardUtil, DotCopyButtonComponent, DotMessagePipe } from '@dotcms/ui'; -import { DotCopyButtonComponent } from './../../../../components/dot-copy-button/dot-copy-button.component'; -import { DotMessagePipe } from './../../../../dot-message/dot-message.pipe'; import { AiImagePromptFormComponent } from './ai-image-prompt-form.component'; describe('DotAiImagePromptFormComponent', () => { @@ -22,8 +21,13 @@ describe('DotAiImagePromptFormComponent', () => { }; const createComponent = createComponentFactory({ component: AiImagePromptFormComponent, - imports: [HttpClientTestingModule, ButtonModule, ReactiveFormsModule], - providers: [DotMessageService], + imports: [ + HttpClientTestingModule, + ButtonModule, + ReactiveFormsModule, + DotCopyButtonComponent + ], + providers: [DotMessageService, DotClipboardUtil], mocks: [DotMessagePipe] }); @@ -54,21 +58,6 @@ describe('DotAiImagePromptFormComponent', () => { expect(spectator.component.form.get('text').validator).toBeNull(); }); - it('should update form when changes come', () => { - const newGeneratedValue = { - request: formValue, - response: { revised_prompt: 'New Prompt' } - } as DotGeneratedAIImage; - - spectator.setInput('value', newGeneratedValue); - spectator.setInput('isLoading', false); - - expect(spectator.component.form.value).toEqual(newGeneratedValue.request); - expect(spectator.component.aiProcessedPrompt).toBe( - newGeneratedValue.response.revised_prompt - ); - }); - it('should disable form controls when isLoading is true', () => { spectator.setInput('isLoading', true); expect(spectator.query('form').getAttribute('disabled')).toBeDefined(); @@ -122,22 +111,4 @@ describe('DotAiImagePromptFormComponent', () => { expect(spectator.query(byTestId('prompt-label')).classList).not.toContain(REQUIRED_CLASS); }); - - it('should copy to clipboard the ai rewritten text', () => { - const newGeneratedValue = { - request: formValue, - response: { revised_prompt: 'New Prompt' } - } as DotGeneratedAIImage; - - spectator.setInput('value', newGeneratedValue); - spectator.setInput('isLoading', false); - - const icon = spectator.query(byTestId('copy-to-clipboard')); - - const btnCopy = spectator.query(DotCopyButtonComponent); - const spyCopy = spyOn(btnCopy, 'copyUrlToClipboard'); - spectator.click(icon); - - expect(spyCopy).toHaveBeenCalled(); - }); }); diff --git a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts index 5f290d3c9af7..1bdb1611e57e 100644 --- a/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts +++ b/core-web/libs/ui/src/lib/components/dot-workflow-actions/dot-workflow-actions.component.spec.ts @@ -6,12 +6,9 @@ import { ToolbarModule } from 'primeng/toolbar'; import { DotMessageService } from '@dotcms/data-access'; import { DotCMSWorkflowAction } from '@dotcms/dotcms-models'; +import { DotClipboardUtil, DotMessagePipe, DotWorkflowActionsComponent } from '@dotcms/ui'; import { MockDotMessageService, mockWorkflowsActions } from '@dotcms/utils-testing'; -import { DotWorkflowActionsComponent } from './dot-workflow-actions.component'; - -import { DotMessagePipe } from '../../dot-message/dot-message.pipe'; - const WORKFLOW_ACTIONS_SEPARATOR_MOCK: DotCMSWorkflowAction = { assignable: true, commentable: true, @@ -75,7 +72,8 @@ describe('DotWorkflowActionsComponent', () => { { provide: DotMessageService, useValue: messageServiceMock - } + }, + DotClipboardUtil ], detectChanges: false }); @@ -89,7 +87,7 @@ describe('DotWorkflowActionsComponent', () => { size: 'normal' } }); - spectator.detectComponentChanges(); + spectator.detectChanges(); }); describe('without actions', () => { @@ -125,6 +123,7 @@ describe('DotWorkflowActionsComponent', () => { describe('group action', () => { it('should render an extra split button for each `SEPARATOR` Action', () => { const splitButtons = spectator.queryAll(SplitButton); + spectator.detectComponentChanges(); expect(splitButtons.length).toBe(2); }); diff --git a/core-web/package.json b/core-web/package.json index dedcbc13628a..eb166fe99541 100644 --- a/core-web/package.json +++ b/core-web/package.json @@ -149,7 +149,7 @@ "@compodoc/compodoc": "^1.1.24", "@materia-ui/ngx-monaco-editor": "^6.0.0", "@mdx-js/react": "^2.1.2", - "@ngneat/spectator": "^15.0.1", + "@ngneat/spectator": "^19.0.0", "@nrwl/cli": "15.7.2", "@nrwl/devkit": "18.0.4", "@nrwl/linter": "18.0.4", diff --git a/core-web/yarn.lock b/core-web/yarn.lock index d8119b8b6e93..0424ca45850e 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -3467,15 +3467,15 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz#e65a1c6539a671f97bb86d5183d6e3a1733c29c7" integrity sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg== -"@ngneat/spectator@^15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@ngneat/spectator/-/spectator-15.0.1.tgz#b5699d419db54fa163b8eb0902abbe4d199bceb2" - integrity sha512-rcIWYco6KngfLHBTShhU0fTgjMD4b9Gfx8tEbnML+URApTjyXqVaNu9YLiA8PVxUTRO1EOTsF9gOCIbIJA8lTA== +"@ngneat/spectator@^19.0.0": + version "19.0.0" + resolved "https://registry.npmjs.org/@ngneat/spectator/-/spectator-19.0.0.tgz#e93e7ca49a00adf0cb433e8ba44533db9f7b6db4" + integrity sha512-u3G0rpepKuwK9alYDxyTvQ5P8XK6MXiToL36O3Lt1Q3seUT6hkpbkAAGD/pG8r+cY+Wt5zM7P8cF+1m7vWUGUA== dependencies: "@testing-library/dom" "^8.11.0" - jquery "3.6.0" + jquery "3.6.4" replace-in-file "6.2.0" - tslib "^2.1.0" + tslib "^2.6.2" "@ngrx/component-store@17.0.1": version "17.0.1" @@ -11046,7 +11046,7 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== @@ -14155,7 +14155,7 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== @@ -15434,10 +15434,10 @@ jju@~1.4.0: resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== -jquery@3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" - integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== +jquery@3.6.4: + version "3.6.4" + resolved "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz#ba065c188142100be4833699852bf7c24dc0252f" + integrity sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -16276,11 +16276,6 @@ lodash-es@^4.17.21: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha512-bSYo8Pc/f0qAkr8fPJydpJjtrHiSynYfYBjtANIgXv5xEf1WlTC63dIDlgu0s9dmTvzRu1+JJTxcIAHe+sH0FQ== - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -16289,33 +16284,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - integrity sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ== - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha512-S8dUjWr7SUT/X6TBIQ/OYoCHo1Stu1ZRy6uMUSKqzFnZp5G5RyQizSm6kvxD2Ewyy6AVfMg4AToeZzKfF99T5w== - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha512-ev5SP+iFpZOugyab/DEUQxUeZP5qyciVTlgQ1f4Vlw7VUcCD8fVnyIqVUEIaoFH9zjAqdgi69KiofzvVmda/ZQ== - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA== -lodash._getnative@*, lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - integrity sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA== - lodash._root@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" @@ -16391,11 +16364,6 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - integrity sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw== - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -19909,7 +19877,8 @@ prepend-http@^1.0.1: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg== -"prettier-fallback@npm:prettier@^3": +"prettier-fallback@npm:prettier@^3", prettier@^3.3.1: + name prettier-fallback version "3.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== @@ -19919,11 +19888,6 @@ prettier@^2.8.0: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" - integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== - pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -22136,7 +22100,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22154,15 +22118,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -22259,7 +22214,7 @@ stringify-package@^1.0.0, stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22287,13 +22242,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -24309,7 +24257,7 @@ worker-farm@^1.6.0, worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -24344,15 +24292,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index cb9d2c466a54..99d96e322153 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5740,5 +5740,5 @@ edit.content.layout.no.content.to.show = No content to show. content.type.form.banner.message= Enable Edit Content Beta for a fresh editing experience (roll back anytime). edit.content.category-field.show-categories-dialog=Select -edit.content.category-field.cancel=Cancel -edit.content.category-field.apply=Apply +edit.content.category-field.sidebar.header.select-categories=Select categories +edit.content.category-field.sidebar.button.clear-all=Clear all