diff --git a/.nx/cache/18.3.5-nx.darwin-arm64.node b/.nx/cache/18.3.5-nx.darwin-arm64.node
deleted file mode 100644
index 9f025e997503..000000000000
Binary files a/.nx/cache/18.3.5-nx.darwin-arm64.node and /dev/null differ
diff --git a/core-web/libs/dotcms-models/src/lib/shared-models.ts b/core-web/libs/dotcms-models/src/lib/shared-models.ts
index 1f9784a5d791..665e02e46561 100644
--- a/core-web/libs/dotcms-models/src/lib/shared-models.ts
+++ b/core-web/libs/dotcms-models/src/lib/shared-models.ts
@@ -6,13 +6,15 @@
* |-> IDLE = Finished Loading or Saving
* SAVING = Status of an action of the component loaded (delete, saving, editing)
* |-> IDLE = Finished delete, saving, editing
+ * ERROR = Error state for the component
**/
export enum ComponentStatus {
INIT = 'INIT',
LOADING = 'LOADING',
LOADED = 'LOADED',
SAVING = 'SAVING',
- IDLE = 'IDLE'
+ IDLE = 'IDLE',
+ ERROR = 'ERROR'
}
export const enum FeaturedFlags {
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.html
index e63e1d7aeafe..6bcd3de48109 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.html
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.html
@@ -1,5 +1,5 @@
- @if (!isLoading()) {
+ @if (!$isLoading()) {
+ styleClass="dotTable">
@@ -42,6 +42,16 @@
|
+
+
+
+
+
+ |
+
+
} @else {
.p-datatable-wrapper {
- border-radius: 0;
- border: none;
+:host {
+ ::ng-deep {
+ .dotTable.p-datatable > .p-datatable-wrapper {
+ border-radius: 0;
+ border: none;
+ }
+ }
+
+ &.category-field__search-list--empty ::ng-deep {
+ p-table,
+ .dotTable.p-datatable,
+ .p-datatable-wrapper,
+ table {
+ height: 100%;
+
+ tr:not(.p-highlight):hover {
+ background: $white;
+ cursor: auto;
+ }
+ }
+ }
}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts
index ea01e28d77e6..a1005bdc9ab1 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.spec.ts
@@ -1,25 +1,34 @@
-import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
+import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { DotMessageService } from '@dotcms/data-access';
+import { ComponentStatus } from '@dotcms/dotcms-models';
+import { DotEmptyContainerComponent } from '@dotcms/ui';
import { DotCategoryFieldSearchListComponent } from './dot-category-field-search-list.component';
+import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import { MockResizeObserver } from '../../../../utils/mocks';
import { CATEGORY_MOCK_TRANSFORMED } from '../../mocks/category-field.mocks';
+const mockMessageService = {
+ get: jest.fn((key: string) => `${key}`)
+};
+
describe('DotCategoryFieldSearchListComponent', () => {
let spectator: Spectator;
+
const createComponent = createComponentFactory({
component: DotCategoryFieldSearchListComponent,
- providers: [mockProvider(DotMessageService)]
+ providers: [{ provide: DotMessageService, useValue: mockMessageService }]
});
beforeEach(() => {
spectator = createComponent({
+ detectChanges: false,
props: {
categories: CATEGORY_MOCK_TRANSFORMED,
selected: CATEGORY_MOCK_TRANSFORMED,
- isLoading: false
+ status: ComponentStatus.LOADED
}
});
@@ -30,19 +39,15 @@ describe('DotCategoryFieldSearchListComponent', () => {
global.ResizeObserver = MockResizeObserver;
});
- afterEach(() => {
- jest.resetAllMocks();
- });
-
it('should show the skeleton if the component is loading', () => {
- spectator.setInput('isLoading', true);
+ spectator.setInput('status', ComponentStatus.LOADING);
spectator.detectChanges();
expect(spectator.query(byTestId('categories-skeleton'))).not.toBeNull();
expect(spectator.query(byTestId('categories-table'))).toBeNull();
});
it('should show the table if the component is not loading', () => {
- spectator.setInput('isLoading', false);
+ spectator.setInput('status', ComponentStatus.LOADED);
spectator.detectChanges();
expect(spectator.query(byTestId('categories-table'))).not.toBeNull();
expect(spectator.query(byTestId('categories-skeleton'))).toBeNull();
@@ -63,4 +68,24 @@ describe('DotCategoryFieldSearchListComponent', () => {
const rows = spectator.queryAll(byTestId('table-row'));
expect(rows.length).toBe(CATEGORY_MOCK_TRANSFORMED.length);
});
+
+ it('should render `dot-empty-container` with `empty` configuration ', () => {
+ const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES.empty;
+ spectator.setInput('status', ComponentStatus.LOADED);
+ spectator.setInput('categories', []);
+ spectator.detectChanges();
+
+ expect(spectator.query(DotEmptyContainerComponent)).not.toBeNull();
+ expect(spectator.component.$emptyOrErrorMessage()).toEqual(expectedConfig);
+ });
+
+ it('should render `dot-empty-container` with `error` configuration ', () => {
+ const expectedConfig = CATEGORY_FIELD_EMPTY_MESSAGES[ComponentStatus.ERROR];
+ spectator.setInput('status', ComponentStatus.ERROR);
+ spectator.setInput('categories', []);
+
+ spectator.detectChanges();
+ expect(spectator.query(DotEmptyContainerComponent)).not.toBeNull();
+ expect(spectator.component.$emptyOrErrorMessage()).toEqual(expectedConfig);
+ });
});
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.ts
index 90d40227f74f..ac17553d7129 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-category-field/components/dot-category-field-search-list/dot-category-field-search-list.component.ts
@@ -5,12 +5,13 @@ import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
+ computed,
effect,
ElementRef,
- EventEmitter,
+ inject,
input,
OnDestroy,
- Output,
+ output,
signal,
ViewChild
} from '@angular/core';
@@ -22,8 +23,11 @@ import { TooltipModule } from 'primeng/tooltip';
import { debounceTime } from 'rxjs/operators';
-import { DotMessagePipe } from '@dotcms/ui';
+import { DotMessageService } from '@dotcms/data-access';
+import { ComponentStatus } from '@dotcms/dotcms-models';
+import { DotEmptyContainerComponent, DotMessagePipe, PrincipalConfiguration } from '@dotcms/ui';
+import { CATEGORY_FIELD_EMPTY_MESSAGES } from '../../../../models/dot-edit-content-field.constant';
import {
DotCategoryFieldKeyValueObj,
DotTableHeaderCheckboxSelectEvent,
@@ -31,6 +35,9 @@ import {
} from '../../models/dot-category-field.models';
import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skeleton.component';
+/**
+ * Represents a search list component for category field.
+ */
@Component({
selector: 'dot-category-field-search-list',
standalone: true,
@@ -40,10 +47,14 @@ import { DotTableSkeletonComponent } from '../dot-table-skeleton/dot-table-skele
SkeletonModule,
DotTableSkeletonComponent,
DotMessagePipe,
- TooltipModule
+ TooltipModule,
+ DotEmptyContainerComponent
],
templateUrl: './dot-category-field-search-list.component.html',
styleUrl: './dot-category-field-search-list.component.scss',
+ host: {
+ '[class.category-field__search-list--empty]': '$tableIsEmpty()'
+ },
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDestroy {
@@ -52,11 +63,13 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* viewport to use in the virtual scroll
*/
@ViewChild('tableContainer', { static: false }) tableContainer!: ElementRef;
+
/**
* The scrollHeight variable represents a signal with a default value of '0px'.
* It can be used to track and manipulate the height of a scrollable element.
*/
$scrollHeight = signal('0px');
+
/**
* Represents the categories found with the filter
*/
@@ -66,35 +79,55 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* Represent the selected items in the store
*/
selected = input.required();
+
/**
- * EventEmitter for emit the selected category(ies).
+ * Represents the current state of the component.
*/
- @Output() itemChecked = new EventEmitter<
- DotCategoryFieldKeyValueObj | DotCategoryFieldKeyValueObj[]
- >();
+ status = input.required();
+
/**
- * EventEmitter that emits events to remove a selected item(s).
+ * Output for emit the selected category(ies).
*/
- @Output() removeItem = new EventEmitter();
+ $itemChecked = output({
+ alias: 'itemChecked'
+ });
+
/**
- * Represents a variable indicating if the component is in loading state.
+ * Output that emits events to remove a selected item(s).
*/
- isLoading = input.required();
+ $removeItem = output({ alias: 'removeItem' });
/**
* Model of the items selected
*/
itemsSelected: DotCategoryFieldKeyValueObj[];
+
/**
* Represents an array of temporary selected items.
*/
temporarySelectedAll: string[] = [];
+ /**
+ * Flag indicating whether the table is empty.
+ */
+ $tableIsEmpty = computed(() => !this.$isLoading() && this.categories().length === 0);
+
+ /**
+ * A computed variable that represents the loading status of a component.
+ */
+ $isLoading = computed(() => this.status() === ComponentStatus.LOADING);
+
+ /**
+ * Gets the computed value of $emptyOrErrorMessage.
+ */
+ $emptyOrErrorMessage = computed(() => this.getMessageConfig());
+
+ #messageService = inject(DotMessageService);
+
readonly #effectRef = effect(() => {
// Todo: find a better way to update this
this.itemsSelected = this.selected();
});
-
readonly #resize$ = new Subject();
readonly #resizeObserver = new ResizeObserver((entries) => this.#resize$.next(entries[0]));
@@ -111,7 +144,7 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* @return {void}
*/
onSelectItem({ data }: DotTableRowSelectEvent): void {
- this.itemChecked.emit(data);
+ this.$itemChecked.emit(data);
}
/**
@@ -121,7 +154,7 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
* @return {void}
*/
onRemoveItem({ data: { key } }: DotTableRowSelectEvent): void {
- this.removeItem.emit(key);
+ this.$removeItem.emit(key);
}
/**
@@ -134,10 +167,10 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
onHeaderCheckboxToggle({ checked }: DotTableHeaderCheckboxSelectEvent): void {
if (checked) {
const values = this.categories().map((item) => item.key);
- this.itemChecked.emit(this.categories());
+ this.$itemChecked.emit(this.categories());
this.temporarySelectedAll = [...values];
} else {
- this.removeItem.emit(this.temporarySelectedAll);
+ this.$removeItem.emit(this.temporarySelectedAll);
this.temporarySelectedAll = [];
}
}
@@ -162,4 +195,21 @@ export class DotCategoryFieldSearchListComponent implements AfterViewInit, OnDes
this.$scrollHeight.set(`${containerHeight}px`);
}
}
+
+ /**
+ * Retrieves the message configuration based on the current component status.
+ *
+ * @private
+ * @returns {PrincipalConfiguration | null} Returns the message configuration, or null if no configuration is found.
+ */
+ private getMessageConfig(): PrincipalConfiguration | null {
+ const configKey = this.status() === ComponentStatus.ERROR ? ComponentStatus.ERROR : 'empty';
+ const { title, icon, subtitle } = CATEGORY_FIELD_EMPTY_MESSAGES[configKey];
+
+ return {
+ title: this.#messageService.get(title),
+ icon: icon,
+ subtitle: this.#messageService.get(subtitle)
+ };
+ }
}
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.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
index 0cbcc04b4672..42d51421d336 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.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
@@ -44,7 +44,7 @@
@fadeAnimation
(itemChecked)="store.addSelected($event)"
(removeItem)="store.removeSelected($event)"
- [isLoading]="store.isSearchLoading()"
+ [status]="store.searchStatus()"
[categories]="store.searchCategoryList()"
[selected]="store.selected()" />
}
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
index 5ef4d25ca400..4ae13c2edb79 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.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
@@ -68,16 +68,19 @@ export class DotCategoryFieldSidebarComponent implements OnInit, OnDestroy {
* @memberof DotCategoryFieldSidebarComponent
*/
@Input() visible = false;
+
/**
* Output that emit if the sidebar is closed
*/
@Output() closedSidebar = new EventEmitter();
+
/**
* Store based on the `CategoryFieldStore`.
*
* @memberof DotCategoryFieldSidebarComponent
*/
readonly store = inject(CategoryFieldStore);
+
/**
* Computed property for retrieving all category keys.
*/
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 f22691092006..96df742a848e 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
@@ -101,7 +101,10 @@ export class DotEditContentCategoryFieldComponent implements OnInit {
effect(
() => {
const categoryValues = this.store.selectedCategoriesValues();
- this.categoryFieldControl.setValue(categoryValues);
+
+ if (this.categoryFieldControl) {
+ this.categoryFieldControl.setValue(categoryValues);
+ }
},
{
injector: this.#injector
@@ -117,7 +120,7 @@ export class DotEditContentCategoryFieldComponent implements OnInit {
this.$showCategoriesSidebar.set(true);
}
/**
- * Close the categories sidebar.
+ * Close the categories' sidebar.
*
* @memberof DotEditContentCategoryFieldComponent
*/
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 025a5b076bc3..9bd655453b5b 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
@@ -102,6 +102,13 @@ export const CategoryFieldStore = signalStore(
() => store.mode() === 'search' && store.state() === ComponentStatus.LOADING
),
+ /**
+ * Status of the Search Component
+ */
+ searchStatus: computed(() =>
+ store.mode() === 'search' ? store.state() : ComponentStatus.INIT
+ ),
+
/**
* Categories for render with added properties
*/
@@ -141,13 +148,13 @@ export const CategoryFieldStore = signalStore(
patchState(store, {
field,
selected,
- state: ComponentStatus.IDLE
+ state: ComponentStatus.LOADED
});
},
error: (error: HttpErrorResponse) => {
patchState(store, {
field,
- state: ComponentStatus.IDLE
+ state: ComponentStatus.ERROR
});
dotHttpErrorManagerService.handle(error);
@@ -285,7 +292,7 @@ export const CategoryFieldStore = signalStore(
},
error: () => {
// TODO: Add Error Handler
- patchState(store, { state: ComponentStatus.IDLE });
+ patchState(store, { state: ComponentStatus.ERROR });
}
})
);
@@ -315,8 +322,10 @@ export const CategoryFieldStore = signalStore(
});
},
error: () => {
- // TODO: Add Error Handler
- patchState(store, { state: ComponentStatus.IDLE });
+ patchState(store, {
+ state: ComponentStatus.ERROR,
+ searchCategories: []
+ });
}
})
);
diff --git a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.constant.ts b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.constant.ts
index c35a07107569..45bdb2213f31 100644
--- a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.constant.ts
+++ b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.constant.ts
@@ -1,5 +1,8 @@
import { MonacoEditorConstructionOptions } from '@materia-ui/ngx-monaco-editor';
+import { ComponentStatus } from '@dotcms/dotcms-models';
+import { PrincipalConfiguration } from '@dotcms/ui';
+
import { FIELD_TYPES } from './dot-edit-content-field.enum';
export const CALENDAR_FIELD_TYPES = [FIELD_TYPES.DATE, FIELD_TYPES.DATE_AND_TIME, FIELD_TYPES.TIME];
@@ -29,3 +32,22 @@ export const DEFAULT_MONACO_CONFIG: MonacoEditorConstructionOptions = {
language: 'text',
fontSize: 14
};
+
+/**
+ * Represent the able messages to use in the component DotEmptyContainerComponent
+ */
+export const CATEGORY_FIELD_EMPTY_MESSAGES: Record<
+ ComponentStatus.ERROR | 'empty',
+ PrincipalConfiguration
+> = {
+ empty: {
+ title: 'edit.content.category-field.search.not-found.title',
+ icon: 'pi-exclamation-circle',
+ subtitle: 'edit.content.category-field.search.not-found.legend'
+ },
+ [ComponentStatus.ERROR]: {
+ title: 'edit.content.category-field.search.error.title',
+ icon: 'pi-exclamation-triangle',
+ subtitle: 'edit.content.category-field.search.error.legend'
+ }
+};
diff --git a/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.html b/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.html
index 0c9eec942bc1..85dc57f16066 100644
--- a/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.html
+++ b/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.html
@@ -1,17 +1,14 @@
- @if (configuration?.icon) {
-
+ data-testid="message-principal">
+ @if (configuration.icon) {
+
}
-
{{ configuration?.title }}
- @if (configuration?.subtitle) {
-
- {{ configuration?.subtitle }}
+ {{ configuration.title }}
+ @if (configuration.subtitle) {
+
+ {{ configuration.subtitle }}
}
@@ -19,25 +16,26 @@
@if (!hideContactUsLink || buttonLabel) {
diff --git a/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.ts b/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.ts
index 08a8b844b589..8aa422cc4cde 100644
--- a/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.ts
+++ b/core-web/libs/ui/src/lib/components/dot-empty-container/dot-empty-container.component.ts
@@ -24,7 +24,7 @@ export interface PrincipalConfiguration {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotEmptyContainerComponent {
- //Todo: make required when Angular 16 updated
+ //Todo: change to input signal when ui migrated to jest
/**
* Principal configuration of the component
*/
diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
index 8c7471c794d0..2dc7cbd24121 100644
--- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
+++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
@@ -5754,5 +5754,10 @@ edit.content.category-field.list.show.less=Less
edit.content.category-field.search.name=Name
edit.content.category-field.search.assignee=Category Path
edit.content.category-field.search.input.placeholder=Search
+edit.content.category-field.search.not-found.title=No categories found
+edit.content.category-field.search.not-found.legend=Your search does not match any of the categories, please try again.
+edit.content.category-field.search.error.title=Something went wrong
+edit.content.category-field.search.error.legend=Oops! Something went wrong. Please try again later.
edit.content.category-field.category.root-name=Root
+