From 0c7d70c26e6e8e989b1c42d02a07e2914862d185 Mon Sep 17 00:00:00 2001 From: Arcadio Quintero Date: Wed, 26 Jun 2024 13:03:22 -0400 Subject: [PATCH] feat(edit-content) fix comments, added new test #28831 --- .../dot-categories.service.spec.ts | 6 + .../dot-categories-list.component.spec.ts | 16 +- core-web/libs/edit-content/.eslintrc.json | 11 +- .../dot-edit-content-field.component.html | 144 +++---- ...ategory-field-category-list.component.html | 5 +- ...ategory-field-category-list.component.scss | 2 +- ...gory-field-category-list.component.spec.ts | 25 +- ...-category-field-category-list.component.ts | 17 +- ...t-category-field-sidebar.component.spec.ts | 4 +- ...t-edit-content-category-field.component.ts | 13 +- .../mocks/category-field.mocks.ts | 162 +++++--- .../models/dot-category-field.models.ts | 2 +- .../services/categories.service.ts | 2 +- .../content-category-field.store.spec.ts | 124 ++++++ .../store/content-category-field.store.ts | 20 +- .../utils/category-field.utils.spec.ts | 360 +++++++++--------- 16 files changed, 552 insertions(+), 361 deletions(-) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/store/content-category-field.store.spec.ts 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 bb6baf68be60..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 @@ -15,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/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 67100e4acddf..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 @@ -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/libs/edit-content/.eslintrc.json b/core-web/libs/edit-content/.eslintrc.json index ef666e409965..66d6ee4d484c 100644 --- a/core-web/libs/edit-content/.eslintrc.json +++ b/core-web/libs/edit-content/.eslintrc.json @@ -4,7 +4,12 @@ "overrides": [ { "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], "rules": { + "@angular-eslint/no-host-metadata-property": 0, "@angular-eslint/directive-selector": [ "error", { @@ -21,11 +26,7 @@ "style": "kebab-case" } ] - }, - "extends": [ - "plugin:@nx/angular", - "plugin:@angular-eslint/template/process-inline-templates" - ] + } }, { "files": ["*.html"], diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html index 9e0265812f12..f5d356765301 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html @@ -6,94 +6,58 @@ {{ field.name }} -@switch (field.fieldType) { - @case (fieldTypes.SELECT) { - - } - @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.CUSTOM_FIELD) { - - } - @case (fieldTypes.BLOCK_EDITOR) { - - } - @case (fieldTypes.KEY_VALUE) { - - } - @case (fieldTypes.WYSIWYG) { - - } - @case (fieldTypes.HOST_FOLDER) { - - } - @case (fieldTypes.CATEGORY) { - - } -} -@if (field.hint) { - {{ field.hint }} +@switch (field.fieldType) { @case (fieldTypes.SELECT) { + +} @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.CUSTOM_FIELD) { + +} @case (fieldTypes.BLOCK_EDITOR) { + +} @case (fieldTypes.KEY_VALUE) { + +} @case (fieldTypes.WYSIWYG) { + +} @case (fieldTypes.HOST_FOLDER) { + +} @case (fieldTypes.CATEGORY) { + +} } @if (field.hint) { +{{ field.hint }} } 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 index a743a3891327..f0812ca8f83c 100644 --- 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 @@ -2,7 +2,7 @@
- @for (column of categories(); let index = $index; track categories()[index]) { + @for (column of categories(); let index = $index; track index) {
@for (item of column; track item.inode) { @@ -32,8 +32,9 @@ } + @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 index c179aadbdf21..2e20396ccc5a 100644 --- 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 @@ -45,7 +45,7 @@ } .category-list__item { - gap: 1rem; + gap: $spacing-3; padding: 0 $spacing-1 0 $spacing-1; transition: background-color $basic-speed; height: 40px; 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 index acb448411853..e3b11fb4ec6c 100644 --- 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 @@ -8,7 +8,7 @@ import { MINIMUM_CATEGORY_COLUMNS } from './dot-category-field-category-list.component'; -import { CATEGORIES_MOCK, SELECTED_MOCK } from '../../mocks/category-field.mocks'; +import { CATEGORY_LIST_MOCK, SELECTED_LIST_MOCK } from '../../mocks/category-field.mocks'; describe('DotCategoryFieldCategoryListComponent', () => { let spectator: Spectator; @@ -21,8 +21,8 @@ describe('DotCategoryFieldCategoryListComponent', () => { beforeEach(() => { spectator = createComponent({ props: { - categories: CATEGORIES_MOCK, - selected: SELECTED_MOCK + categories: CATEGORY_LIST_MOCK, + selected: SELECTED_LIST_MOCK } }); @@ -34,24 +34,26 @@ describe('DotCategoryFieldCategoryListComponent', () => { }); it('should render correct number of category columns', () => { - expect(spectator.queryAll(byTestId('category-column')).length).toBe(CATEGORIES_MOCK.length); + 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( - CATEGORIES_MOCK.flat().length + CATEGORY_LIST_MOCK.flat().length ); }); it('should render correct number of category item labels', () => { expect(spectator.queryAll(byTestId('category-item-label')).length).toBe( - CATEGORIES_MOCK.flat().length + 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 - CATEGORIES_MOCK.length + MINIMUM_CATEGORY_COLUMNS - CATEGORY_LIST_MOCK.length ); }); @@ -66,23 +68,24 @@ describe('DotCategoryFieldCategoryListComponent', () => { expect(emitSpy).toHaveBeenCalledWith({ index: 0, - item: CATEGORIES_MOCK[0][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[0].className).toContain('category-list__item--selected'); + 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(CATEGORIES_MOCK[0]); + const testCategories = Array(minColumns).fill(CATEGORY_LIST_MOCK[0]); spectator = createComponent({ props: { categories: testCategories, - selected: SELECTED_MOCK + selected: SELECTED_LIST_MOCK } }); 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 index 35fbc2769ef2..b9509c738018 100644 --- 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 @@ -10,6 +10,7 @@ import { EventEmitter, inject, input, + OnDestroy, Output, QueryList, ViewChildren @@ -41,12 +42,11 @@ const MINIMUM_CATEGORY_WITHOUT_SCROLLING = 3; templateUrl: './dot-category-field-category-list.component.html', styleUrl: './dot-category-field-category-list.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - // eslint-disable-next-line @angular-eslint/no-host-metadata-property host: { class: 'category-list__wrapper' } }) -export class DotCategoryFieldCategoryListComponent implements AfterViewInit { +export class DotCategoryFieldCategoryListComponent implements AfterViewInit, OnDestroy { @ViewChildren('categoryColumn') categoryColumns: QueryList; /** @@ -82,11 +82,9 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit { readonly #destroyRef = inject(DestroyRef); - constructor() { - effect(() => { - this.itemsSelected = this.selected(); - }); - } + #effectRef = effect(() => { + this.itemsSelected = this.selected(); + }); ngAfterViewInit() { // Handle the horizontal scroll to make visible the last column @@ -95,6 +93,10 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit { }); } + ngOnDestroy(): void { + this.#effectRef.destroy(); + } + private scrollHandler() { try { const columnsArray = this.categoryColumns.toArray(); @@ -114,6 +116,7 @@ export class DotCategoryFieldCategoryListComponent implements AfterViewInit { inline: 'end' }); } else { + // scroll to the first column columnsArray[0].nativeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', 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 index 5dbc8b0f63b5..529a39f7315c 100644 --- 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 @@ -8,7 +8,7 @@ import { DotMessageService } from '@dotcms/data-access'; import { DotCategoryFieldSidebarComponent } from './dot-category-field-sidebar.component'; -import { CATEGORIES_MOCK } from '../../mocks/category-field.mocks'; +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'; @@ -25,7 +25,7 @@ describe('DotEditContentCategoryFieldSidebarComponent', () => { spectator = createComponent({ providers: [ mockProvider(CategoriesService, { - getChildren: jest.fn().mockReturnValue(of(CATEGORIES_MOCK)) + getChildren: jest.fn().mockReturnValue(of(CATEGORY_LIST_MOCK)) }) ] }); 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 f3806f042c8a..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 @@ -3,7 +3,6 @@ import { ChangeDetectionStrategy, Component, ComponentRef, - computed, DestroyRef, inject, input, @@ -57,7 +56,6 @@ import { CategoryFieldStore } from './store/content-category-field.store'; useFactory: () => inject(ControlContainer, { skipSelf: true }) } ], - // eslint-disable-next-line @angular-eslint/no-host-metadata-property host: { '[class.dot-category-field__container--has-categories]': 'hasSelectedCategories()', '[class.dot-category-field__container]': '!hasSelectedCategories()' @@ -83,18 +81,17 @@ export class DotEditContentCategoryFieldComponent implements OnInit { contentlet = input.required(); readonly store = inject(CategoryFieldStore); + readonly #destroyRef = inject(DestroyRef); + #componentRef: ComponentRef; /** * Determines if there are any selected categories. * * @returns {Boolean} - True if there are selected categories, false otherwise. */ - hasSelectedCategories = computed(() => { - return !!this.store.selectedCategories().length; - }); - - readonly #destroyRef = inject(DestroyRef); - #componentRef: ComponentRef; + hasSelectedCategories(): boolean { + return !!this.store.hasSelectedCategories(); + } /** * Open the "DotEditContentCategoryFieldDialogComponent" dialog to show categories. 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 index 40662ea1ede2..e36216a417cf 100644 --- 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 @@ -4,16 +4,19 @@ 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]: [ { - '11111': 'Cleaning Supplies' + '33333': 'Electrical' }, { - '22222': 'Concrete & Cement' + '22222': 'Doors & Windows' } ], contentType: 'TEST', @@ -43,6 +46,9 @@ export const CATEGORY_FIELD_CONTENTLET_MOCK: DotCMSContentlet = { working: true }; +/** + * Mock of the Field Category + */ export const CATEGORY_FIELD_MOCK: DotCMSContentTypeField = { categories: { categoryName: 'Categorias', @@ -75,65 +81,125 @@ export const CATEGORY_FIELD_MOCK: DotCMSContentTypeField = { variable: 'categorias' }; -export const CATEGORIES_WITH_CHILD_MOCK: DotCategoryFieldCategory[] = [ +/** + * Represent a Category List of level 1 with children `childrenCount` + */ +export const CATEGORY_LEVEL_1: DotCategoryFieldCategory[] = [ { active: true, - categoryName: 'Adhesives & Sealants', + categoryName: 'Cleaning Supplies', categoryVelocityVarName: '1f208488057007cedda0e0b5d52ee3b3', - childrenCount: 10, + childrenCount: 1, // This make show the caret of has children description: null, - iDate: 1719242808952, + iDate: 1719275768170, identifier: null, - inode: '11111', + 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: false + checked: true } ]; -export const CATEGORIES_MOCK: 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: '8259cf9bb5b895196fc2b4127e929f02', - key: '1f208488057007cedda0e0b5d52ee3b3', - keywords: null, - modDate: 1718916179985, - owner: '', - sortOrder: 0, - type: 'category', - checked: true - } - ], - [ - { - active: true, - categoryName: 'Concrete & Cement', - categoryVelocityVarName: 'd2fb8e67c390e3b84cd613fa15aad5d4', - childrenCount: 0, - description: null, - iDate: 1719275768170, - identifier: null, - inode: '22222', - key: 'd2fb8e67c390e3b84cd613fa15aad5d4', - keywords: null, - modDate: 1718916180738, - owner: '', - sortOrder: 0, - type: 'category', - checked: false - } - ] +/** + * 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] ]; -export const SELECTED_MOCK = [CATEGORIES_MOCK[0][0].inode]; +/** + * 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 index 2337ef16ee62..3be15a96123a 100644 --- 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 @@ -21,5 +21,5 @@ export type DotCategoryFieldItem = { index: number; item: DotCategory }; * @extends DotCategory */ export interface DotCategoryFieldCategory extends DotCategory { - checked: boolean; + checked?: boolean; } 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 index 9d007aa9c4d5..477d9bf4cdaa 100644 --- 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 @@ -20,7 +20,7 @@ export const ITEMS_PER_PAGE = 7000; */ @Injectable() export class CategoriesService { - readonly #http: HttpClient = inject(HttpClient); + readonly #http = inject(HttpClient); /** * Retrieves the children of a given inode. 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 index b3eaa832af22..ef87e77df810 100644 --- 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 @@ -24,15 +24,15 @@ import { } from '../utils/category-field.utils'; export type CategoryFieldState = { - categoryInode: string; + rootCategoryInode: string; categories: DotCategoryFieldCategory[][]; categoriesValue: DotKeyValueObj[]; parentPath: string[]; state: ComponentStatus; }; -const initialState: CategoryFieldState = { - categoryInode: '', +export const initialState: CategoryFieldState = { + rootCategoryInode: '', categories: [], categoriesValue: [], parentPath: [], @@ -64,7 +64,11 @@ export const CategoryFieldStore = signalStore( */ categoryList: computed(() => categories().map((column) => addMetadata(column, parentPath())) - ) + ), + + hasSelectedCategories: computed(() => { + return !!categoriesValue().map((item) => item.key).length; + }) })), withMethods((store, categoryService = inject(CategoriesService)) => ({ /** @@ -72,7 +76,7 @@ export const CategoryFieldStore = signalStore( */ load({ variable, values }: DotCMSContentTypeField, contentlet: DotCMSContentlet): void { const categoriesValue = getSelectedCategories(variable, contentlet); - patchState(store, { categoryInode: values, categoriesValue }); + patchState(store, { rootCategoryInode: values, categoriesValue }); }, /** @@ -107,9 +111,11 @@ export const CategoryFieldStore = signalStore( ), tap(() => patchState(store, { state: ComponentStatus.LOADING })), switchMap((event) => { - const categoryInode: string = event ? event.item.inode : store.categoryInode(); + const rootCategoryInode: string = event + ? event.item.inode + : store.rootCategoryInode(); - return categoryService.getChildren(categoryInode).pipe( + return categoryService.getChildren(rootCategoryInode).pipe( tapResponse({ next: (newCategories) => { if (event) { 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 index b8961f01c493..fb3e6e328e08 100644 --- 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 @@ -8,185 +8,193 @@ import { } from './category-field.utils'; import { - CATEGORIES_WITH_CHILD_MOCK, CATEGORY_FIELD_CONTENTLET_MOCK, - CATEGORY_FIELD_VARIABLE_NAME + CATEGORY_FIELD_VARIABLE_NAME, + CATEGORY_LEVEL_1 } from '../mocks/category-field.mocks'; import { DotCategoryFieldCategory, DotKeyValueObj } from '../models/dot-category-field.models'; -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: '11111', value: 'Cleaning Supplies' }, - { key: '22222', value: 'Concrete & Cement' } - ]; - 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 = [CATEGORIES_WITH_CHILD_MOCK[0].inode]; - const expected = [{ ...CATEGORIES_WITH_CHILD_MOCK[0], checked: true }]; - - const result = addMetadata(CATEGORIES_WITH_CHILD_MOCK, 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 = [{ ...CATEGORIES_WITH_CHILD_MOCK[0], checked: false }]; - - const result = addMetadata(CATEGORIES_WITH_CHILD_MOCK, 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 = [{ ...CATEGORIES_WITH_CHILD_MOCK[0], checked: false }]; - - const result = addMetadata(CATEGORIES_WITH_CHILD_MOCK, PATH_MOCK); - expect(result).toEqual(expected); - }); -}); - -describe('deepCopy', () => { - it('should create a deep copy of a two-dimensional array of DotCategoryFieldCategory', () => { - const array: DotCategoryFieldCategory[][] = [ - CATEGORIES_WITH_CHILD_MOCK, - [{ ...CATEGORIES_WITH_CHILD_MOCK[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('Adhesives & Sealants'); - }); - - 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[][] = [ - CATEGORIES_WITH_CHILD_MOCK, - [{ ...CATEGORIES_WITH_CHILD_MOCK[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[][] = [ - CATEGORIES_WITH_CHILD_MOCK, - [{ ...CATEGORIES_WITH_CHILD_MOCK[0], categoryName: 'New Category' }], - [{ ...CATEGORIES_WITH_CHILD_MOCK[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([ - CATEGORIES_WITH_CHILD_MOCK, - [{ ...CATEGORIES_WITH_CHILD_MOCK[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[][] = [ - CATEGORIES_WITH_CHILD_MOCK, - [{ ...CATEGORIES_WITH_CHILD_MOCK[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([]); +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([]); + }); }); });