diff --git a/core-web/libs/dotcms-webcomponents/src/elements/dot-contentlet-thumbnail/dot-contentlet-thumbnail.tsx b/core-web/libs/dotcms-webcomponents/src/elements/dot-contentlet-thumbnail/dot-contentlet-thumbnail.tsx index a0d190e272ab..e3fd97375c3f 100644 --- a/core-web/libs/dotcms-webcomponents/src/elements/dot-contentlet-thumbnail/dot-contentlet-thumbnail.tsx +++ b/core-web/libs/dotcms-webcomponents/src/elements/dot-contentlet-thumbnail/dot-contentlet-thumbnail.tsx @@ -117,9 +117,9 @@ export class DotContentletThumbnail { } private getIcon() { - return this.contentlet?.baseType !== 'FILEASSET' - ? this.contentlet?.contentTypeIcon - : this.contentlet?.__icon__; + return this.contentlet?.baseType === 'FILEASSET' + ? this.contentlet?.__icon__ + : this.contentlet?.contentTypeIcon; } private shouldShowVideoThumbnail() { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.html new file mode 100644 index 000000000000..c167ac74c31e --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.html @@ -0,0 +1,44 @@ + + + + + {{ node?.label | truncatePath }} + + + @if (item?.label) { + //{{ item?.label }} + } @else { + {{ 'dot.file.relationship.dialog.search.site.placeholder' | dm }} + } + + + +@let error = store.error(); +@if (error) { +
+ {{ error | dm }} +
+} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.scss new file mode 100644 index 000000000000..aa0189a4ed76 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.scss @@ -0,0 +1,7 @@ +:host { + ::ng-deep { + .p-treeselect { + width: 100%; + } + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.spec.ts new file mode 100644 index 000000000000..54cf64992d1f --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.spec.ts @@ -0,0 +1,198 @@ +import { createFakeEvent } from '@ngneat/spectator'; +import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; + +import { ReactiveFormsModule } from '@angular/forms'; + +import { TreeSelectModule } from 'primeng/treeselect'; + +import { DotMessageService } from '@dotcms/data-access'; +import { + TreeNodeItem, + TreeNodeSelectItem +} from '@dotcms/edit-content/models/dot-edit-content-host-folder-field.interface'; +import { TruncatePathPipe } from '@dotcms/edit-content/pipes/truncate-path.pipe'; +import { DotEditContentService } from '@dotcms/edit-content/services/dot-edit-content.service'; +import { DotMessagePipe } from '@dotcms/ui'; +import { MockDotMessageService } from '@dotcms/utils-testing'; + +import { SiteFieldComponent } from './site-field.component'; +import { SiteFieldStore } from './site-field.store'; + +describe('SiteFieldComponent', () => { + let spectator: Spectator; + let component: SiteFieldComponent; + let store: InstanceType; + + const messageServiceMock = new MockDotMessageService({ + 'dot.file.relationship.dialog.search.language.failed': 'Failed to load languages' + }); + + const mockSites: TreeNodeItem[] = [ + { + label: 'demo.dotcms.com', + data: { + id: '123', + hostname: 'demo.dotcms.com', + path: '', + type: 'site' + }, + icon: 'pi pi-globe', + leaf: false, + children: [] + } + ]; + + const mockFolders = { + parent: { + id: 'parent-id', + hostName: 'demo.dotcms.com', + path: '/parent', + addChildrenAllowed: true + }, + folders: [ + { + label: 'folder1', + data: { + id: 'folder1', + hostname: 'demo.dotcms.com', + path: 'folder1', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + ] + }; + + const createComponent = createComponentFactory({ + component: SiteFieldComponent, + imports: [ReactiveFormsModule, TreeSelectModule, TruncatePathPipe, DotMessagePipe], + componentProviders: [SiteFieldStore], + providers: [ + { provide: DotMessageService, useValue: messageServiceMock }, + mockProvider(DotEditContentService, { + getSitesTreePath: jest.fn().mockReturnValue(of(mockSites)), + getFoldersTreeNode: jest.fn().mockReturnValue(of(mockFolders)) + }) + ] + }); + + beforeEach(() => { + spectator = createComponent({ + detectChanges: false + }); + component = spectator.component; + store = spectator.inject(SiteFieldStore, true); + }); + + it('should create', () => { + spectator.detectChanges(); + + expect(component).toBeTruthy(); + }); + + describe('Initial State', () => { + it('should initialize with empty site control', () => { + spectator.detectChanges(); + + expect(component.siteControl.value).toBe(''); + }); + + it('should load sites on init', () => { + const loadSitesSpy = jest.spyOn(store, 'loadSites'); + + spectator.detectChanges(); + + expect(loadSitesSpy).toHaveBeenCalled(); + }); + }); + + describe('ControlValueAccessor Implementation', () => { + const testValue = 'test-site-id'; + + it('should write value to form control', () => { + spectator.detectChanges(); + + component.writeValue(testValue); + expect(component.siteControl.value).toBe(''); + }); + + it('should register onChange callback', () => { + const onChangeSpy = jest.fn(); + component.registerOnChange(onChangeSpy); + + const mockEvent: TreeNodeSelectItem = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Test Node', + data: { id: '123', hostname: 'test.com', path: 'test', type: 'folder' } + } + }; + + store.chooseNode(mockEvent); + spectator.detectChanges(); + + expect(onChangeSpy).toHaveBeenCalledWith(mockEvent.node.data.id); + }); + + it('should register onTouched callback', () => { + const onTouchedSpy = jest.fn(); + component.registerOnTouched(onTouchedSpy); + + // Trigger touched state + component.siteControl.markAsTouched(); + spectator.detectChanges(); + + // Note: Since we're not explicitly calling onTouched in the component, + // this test mainly verifies the registration + expect(onTouchedSpy).not.toHaveBeenCalled(); + }); + + describe('Disabled State', () => { + it('should disable the form control', () => { + component.setDisabledState(true); + expect(component.siteControl.disabled).toBe(true); + }); + + it('should enable the form control', () => { + // First disable + component.setDisabledState(true); + expect(component.siteControl.disabled).toBe(true); + + // Then enable + component.setDisabledState(false); + expect(component.siteControl.disabled).toBe(false); + }); + }); + }); + + describe('Edge Cases', () => { + it('should handle undefined value in writeValue', () => { + component.writeValue(undefined); + expect(component.siteControl.value).toBe(''); + }); + + it('should handle null value in writeValue', () => { + component.writeValue(null); + expect(component.siteControl.value).toBe(''); + }); + + it('should handle empty string in writeValue', () => { + component.writeValue(''); + expect(component.siteControl.value).toBe(''); + }); + + it('should not emit change when writeValue is called with same value', () => { + const onChangeSpy = jest.fn(); + component.registerOnChange(onChangeSpy); + + const testValue = 'test-value'; + component.writeValue(testValue); + component.writeValue(testValue); + + expect(onChangeSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.ts new file mode 100644 index 000000000000..7e240d9ad669 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.component.ts @@ -0,0 +1,153 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + OnInit, + effect, + forwardRef, + inject, + viewChild +} from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + NG_VALUE_ACCESSOR, + ReactiveFormsModule +} from '@angular/forms'; + +import { TreeSelect, TreeSelectModule } from 'primeng/treeselect'; + +import { TruncatePathPipe } from '@dotcms/edit-content/pipes/truncate-path.pipe'; +import { DotMessagePipe } from '@dotcms/ui'; + +import { SiteFieldStore } from './site-field.store'; + +/** + * Component for selecting a site from a tree structure. + * Implements ControlValueAccessor to work with Angular forms. + * Uses PrimeNG's TreeSelect component for the UI. + */ +@Component({ + selector: 'dot-site-field', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + TreeSelectModule, + TruncatePathPipe, + DotMessagePipe + ], + providers: [ + SiteFieldStore, + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SiteFieldComponent) + } + ], + styleUrls: ['./site-field.component.scss'], + templateUrl: './site-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SiteFieldComponent implements ControlValueAccessor, OnInit { + /** + * Store service that manages the site data and state. + * Handles loading sites and managing selection state. + */ + protected readonly store = inject(SiteFieldStore); + + /** + * Form control for the site selection. + * Binds to the TreeSelect component and manages the selected site value. + */ + readonly siteControl = new FormControl(''); + + /** + * View child for the TreeSelect component. + * Allows access to the TreeSelect component's tree view child. + */ + $treeSelect = viewChild(TreeSelect); + + /** + * Creates an instance of SiteFieldComponent. + * Sets up an effect to handle value changes and propagate them through the ControlValueAccessor. + */ + constructor() { + effect(() => { + const valueToSave = this.store.valueToSave(); + + if (valueToSave) { + this.onChange(valueToSave); + } + }); + + effect(() => { + this.store.nodeExpanded(); + const treeSelect = this.$treeSelect(); + if (treeSelect.treeViewChild) { + treeSelect.treeViewChild.updateSerializedValue(); + treeSelect.cd.detectChanges(); + } + }); + } + + /** + * Lifecycle hook that runs after component initialization. + * Triggers the loading of available sites through the store. + */ + ngOnInit(): void { + this.store.loadSites(); + } + + /** + * Internal callback function for handling value changes. + * Used by the ControlValueAccessor to propagate changes to the form model. + */ + private onChange = (_value: string): void => { + // noop + }; + + /** + * Internal callback function for handling touched state. + * Used by the ControlValueAccessor to mark the control as touched. + */ + private onTouched = (): void => { + // noop + }; + + /** + * Writes a new value to the form control. + * Implements ControlValueAccessor method to update the control's value programmatically. + */ + writeValue(_value: string): void { + // noop + } + + /** + * Registers the callback function for value changes. + * Implements ControlValueAccessor method to set up change notifications. + */ + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + /** + * Registers the callback function for touched state. + * Implements ControlValueAccessor method to handle touch events. + */ + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + /** + * Sets the disabled state of the form control. + * Implements ControlValueAccessor method to handle control's disabled state. + */ + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.siteControl.disable(); + } else { + this.siteControl.enable(); + } + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.spec.ts new file mode 100644 index 000000000000..16eaa5740b2c --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.spec.ts @@ -0,0 +1,253 @@ +import { createFakeEvent } from '@ngneat/spectator'; +import { mockProvider, SpyObject } from '@ngneat/spectator/jest'; +import { of, throwError } from 'rxjs'; + +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { delay } from 'rxjs/operators'; + +import { ComponentStatus } from '@dotcms/dotcms-models'; +import { + TreeNodeItem, + TreeNodeSelectItem +} from '@dotcms/edit-content/models/dot-edit-content-host-folder-field.interface'; +import { DotEditContentService } from '@dotcms/edit-content/services/dot-edit-content.service'; + +import { PEER_PAGE_LIMIT, SiteFieldStore } from './site-field.store'; + +describe('SiteFieldStore', () => { + let store: InstanceType; + let dotEditContentService: SpyObject; + + const mockSites: TreeNodeItem[] = [ + { + label: 'demo.dotcms.com', + data: { + id: '123', + hostname: 'demo.dotcms.com', + path: '', + type: 'site' + }, + icon: 'pi pi-globe', + leaf: false, + children: [] + } + ]; + + const mockFolders = { + parent: { + id: 'parent-id', + hostName: 'demo.dotcms.com', + path: '/parent', + addChildrenAllowed: true + }, + folders: [ + { + label: 'folder1', + data: { + id: 'folder1', + hostname: 'demo.dotcms.com', + path: 'folder1', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + ] + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SiteFieldStore, + mockProvider(DotEditContentService, { + getSitesTreePath: jest.fn().mockReturnValue(of(mockSites)), + getFoldersTreeNode: jest.fn().mockReturnValue(of(mockFolders)) + }) + ] + }); + + store = TestBed.inject(SiteFieldStore); + dotEditContentService = TestBed.inject( + DotEditContentService + ) as SpyObject; + }); + + describe('Initial State', () => { + it('should have initial state', () => { + expect(store.nodeSelected()).toBeNull(); + expect(store.nodeExpanded()).toBeNull(); + expect(store.tree()).toEqual([]); + expect(store.status()).toBe(ComponentStatus.INIT); + expect(store.error()).toBeNull(); + }); + }); + + describe('Computed Properties', () => { + it('should indicate loading state correctly', fakeAsync(() => { + const mockObservable = of(mockSites).pipe(delay(100)); + dotEditContentService.getSitesTreePath.mockReturnValue(mockObservable); + + store.loadSites(); + expect(store.isLoading()).toBeTruthy(); + expect(store.status()).toBe(ComponentStatus.LOADING); + + tick(100); + expect(store.isLoading()).toBeFalsy(); + expect(store.status()).toBe(ComponentStatus.LOADED); + })); + + it('should return null for valueToSave when no node is selected', () => { + expect(store.valueToSave()).toBeNull(); + }); + + it('should return correct id for valueToSave when node is selected', () => { + const mockNode: TreeNodeSelectItem = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Test Node', + data: { + id: '123', + hostname: 'test.com', + path: 'test', + type: 'folder' + }, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + }; + store.chooseNode(mockNode); + expect(store.valueToSave()).toBe('123'); + }); + }); + + describe('loadSites', () => { + it('should load sites successfully', () => { + dotEditContentService.getSitesTreePath.mockReturnValue(of(mockSites)); + + store.loadSites(); + + expect(dotEditContentService.getSitesTreePath).toHaveBeenCalledWith({ + perPage: PEER_PAGE_LIMIT, + filter: '*', + page: 1 + }); + expect(store.tree()).toEqual(mockSites); + expect(store.status()).toBe(ComponentStatus.LOADED); + expect(store.error()).toBeNull(); + }); + + it('should handle error when loading sites fails', () => { + dotEditContentService.getSitesTreePath.mockReturnValue( + throwError(() => new Error('Failed to load sites')) + ); + + store.loadSites(); + + expect(store.status()).toBe(ComponentStatus.ERROR); + expect(store.error()).toBe(''); + expect(store.tree()).toEqual([]); + }); + }); + + describe('loadChildren', () => { + it('should load children nodes successfully', () => { + dotEditContentService.getFoldersTreeNode.mockReturnValue(of(mockFolders)); + + const mockEvent = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Parent', + data: { + id: 'parent-id', + hostname: 'demo.dotcms.com', + path: 'parent', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: false, + children: [] + } + }; + + store.loadChildren(mockEvent); + + expect(dotEditContentService.getFoldersTreeNode).toHaveBeenCalledWith( + 'demo.dotcms.com/parent' + ); + expect(store.nodeExpanded()).toEqual({ + ...mockEvent.node, + leaf: true, + icon: 'pi pi-folder-open', + children: mockFolders.folders + }); + }); + + it('should handle error when loading children fails', () => { + dotEditContentService.getFoldersTreeNode.mockReturnValue( + throwError(() => new Error('Failed to load folders')) + ); + + const mockEvent = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Parent', + data: { + id: 'parent-id', + hostname: 'demo.dotcms.com', + path: 'parent', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: false, + children: [] + } + }; + + store.loadChildren(mockEvent); + + expect(store.nodeExpanded()).toBeNull(); + }); + }); + + describe('chooseNode', () => { + it('should update selected node', () => { + const mockEvent = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Selected Node', + data: { + id: '123', + hostname: 'demo.dotcms.com', + path: 'selected', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + }; + + store.chooseNode(mockEvent); + expect(store.nodeSelected()).toEqual(mockEvent.node); + }); + + it('should not update selected node when data is missing', () => { + const mockEvent = { + originalEvent: createFakeEvent('click'), + node: { + label: 'Invalid Node', + data: null, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + }; + + store.chooseNode(mockEvent); + expect(store.nodeSelected()).toBeNull(); + }); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.ts new file mode 100644 index 000000000000..35a3faca85d5 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/components/site-field/site-field.store.ts @@ -0,0 +1,139 @@ +import { tapResponse } from '@ngrx/operators'; +import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { pipe } from 'rxjs'; + +import { computed, inject } from '@angular/core'; + +import { tap, exhaustMap, switchMap } from 'rxjs/operators'; + +import { ComponentStatus } from '@dotcms/dotcms-models'; +import { + TreeNodeItem, + TreeNodeSelectItem +} from '@dotcms/edit-content/models/dot-edit-content-host-folder-field.interface'; +import { DotEditContentService } from '@dotcms/edit-content/services/dot-edit-content.service'; + +/** Maximum number of items to fetch per page */ +export const PEER_PAGE_LIMIT = 7000; + +/** + * Represents the state structure for the Site Field component + * @interface SiteFieldState + */ +export type SiteFieldState = { + nodeSelected: TreeNodeItem | null; + nodeExpanded: TreeNodeSelectItem['node'] | null; + tree: TreeNodeItem[]; + status: ComponentStatus; + error: string | null; +}; + +/** + * Initial state for the Site Field store + */ +export const initialState: SiteFieldState = { + nodeSelected: null, + nodeExpanded: null, + tree: [], + status: ComponentStatus.INIT, + error: null +}; + +/** + * Signal store for managing site field state and operations + * Provides functionality for loading and managing site tree data + */ +export const SiteFieldStore = signalStore( + withState(initialState), + withComputed(({ status, nodeSelected }) => ({ + /** Indicates if the store is in a loading state */ + isLoading: computed(() => status() === ComponentStatus.LOADING), + /** Computed value to be saved, derived from the selected node */ + valueToSave: computed(() => { + const node = nodeSelected(); + + if (node?.data?.id) { + return node.data.id; + } + + return null; + }) + })), + withMethods((store) => { + const dotEditContentService = inject(DotEditContentService); + + return { + /** + * Loads the sites tree structure + * @description Fetches the initial tree structure of sites with pagination + * @method loadSites + */ + loadSites: rxMethod( + pipe( + tap(() => patchState(store, { status: ComponentStatus.LOADING })), + switchMap(() => { + return dotEditContentService + .getSitesTreePath({ + perPage: PEER_PAGE_LIMIT, + filter: '*', + page: 1 + }) + .pipe( + tapResponse({ + next: (sites) => + patchState(store, { + tree: sites, + status: ComponentStatus.LOADED + }), + error: () => + patchState(store, { + status: ComponentStatus.ERROR, + error: '' + }) + }) + ); + }) + ) + ), + /** + * Loads children nodes for a selected tree node + * @method loadChildren + * @param {TreeNodeSelectItem} event - The selected tree node item + */ + loadChildren: rxMethod( + pipe( + exhaustMap((event: TreeNodeSelectItem) => { + const { node } = event; + const { hostname, path } = node.data; + + const fullPath = `${hostname}/${path}`; + + return dotEditContentService.getFoldersTreeNode(fullPath).pipe( + tap(({ folders }) => { + node.leaf = true; + node.icon = 'pi pi-folder-open'; + node.children = [...folders]; + patchState(store, { nodeExpanded: node }); + }) + ); + }) + ) + ), + /** + * Updates the store with the selected node + * @method chooseNode + * @param {TreeNodeSelectItem} event - The selected tree node item + */ + chooseNode: (event: TreeNodeSelectItem) => { + const { node: nodeSelected } = event; + const data = nodeSelected.data; + if (!data) { + return; + } + + patchState(store, { nodeSelected }); + } + }; + }) +); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.html index 360669e92758..38469cc8065b 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.html @@ -20,6 +20,7 @@
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.ts index a97cde6c3943..14caeebdc66a 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.compoment.ts @@ -11,6 +11,7 @@ import { SearchParams } from '@dotcms/edit-content/fields/dot-edit-content-relat import { DotMessagePipe } from '@dotcms/ui'; import { LanguageFieldComponent } from './components/language-field/language-field.component'; +import { SiteFieldComponent } from './components/site-field/site-field.component'; /** * A standalone component that provides search functionality with language and site filtering. @@ -32,7 +33,8 @@ import { LanguageFieldComponent } from './components/language-field/language-fie DotMessagePipe, DropdownModule, ReactiveFormsModule, - LanguageFieldComponent + LanguageFieldComponent, + SiteFieldComponent ], templateUrl: './search.compoment.html' }) diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.component.spec.ts index 07b358a2649f..0e8f2c149b9b 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-relationship-field/components/dot-select-existing-content/components/search/search.component.spec.ts @@ -1,5 +1,6 @@ -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; import { ReactiveFormsModule } from '@angular/forms'; @@ -9,8 +10,12 @@ import { InputGroupModule } from 'primeng/inputgroup'; import { InputTextModule } from 'primeng/inputtext'; import { OverlayPanelModule } from 'primeng/overlaypanel'; +import { DotLanguagesService, DotMessageService } from '@dotcms/data-access'; import { SearchParams } from '@dotcms/edit-content/fields/dot-edit-content-relationship-field/models/search.model'; +import { TreeNodeItem } from '@dotcms/edit-content/models/dot-edit-content-host-folder-field.interface'; +import { DotEditContentService } from '@dotcms/edit-content/services/dot-edit-content.service'; import { DotMessagePipe } from '@dotcms/ui'; +import { MockDotMessageService, mockLocales } from '@dotcms/utils-testing'; import { LanguageFieldComponent } from './components/language-field/language-field.component'; import { SearchComponent } from './search.compoment'; @@ -19,6 +24,48 @@ describe('SearchComponent', () => { let spectator: Spectator; let component: SearchComponent; + const messageServiceMock = new MockDotMessageService({ + 'dot.file.relationship.dialog.search.language.failed': 'Failed to load languages' + }); + + const mockSites: TreeNodeItem[] = [ + { + label: 'demo.dotcms.com', + data: { + id: '123', + hostname: 'demo.dotcms.com', + path: '', + type: 'site' + }, + icon: 'pi pi-globe', + leaf: false, + children: [] + } + ]; + + const mockFolders = { + parent: { + id: 'parent-id', + hostName: 'demo.dotcms.com', + path: '/parent', + addChildrenAllowed: true + }, + folders: [ + { + label: 'folder1', + data: { + id: 'folder1', + hostname: 'demo.dotcms.com', + path: 'folder1', + type: 'folder' as const + }, + icon: 'pi pi-folder', + leaf: true, + children: [] + } + ] + }; + const createComponent = createComponentFactory({ component: SearchComponent, imports: [ @@ -31,7 +78,17 @@ describe('SearchComponent', () => { ], declarations: [MockComponent(LanguageFieldComponent)], mocks: [DotMessagePipe], - detectChanges: true + detectChanges: true, + providers: [ + { provide: DotMessageService, useValue: messageServiceMock }, + mockProvider(DotEditContentService, { + getSitesTreePath: jest.fn().mockReturnValue(of(mockSites)), + getFoldersTreeNode: jest.fn().mockReturnValue(of(mockFolders)) + }), + mockProvider(DotLanguagesService, { + get: jest.fn().mockReturnValue(of(mockLocales)) + }) + ] }); beforeEach(() => { @@ -155,7 +212,7 @@ describe('SearchComponent', () => { component.form.patchValue({ query: 'test search', languageId: 1, - siteId: 'site1' + siteId: 'site123' }); const openFiltersButton = spectator.query( @@ -169,7 +226,7 @@ describe('SearchComponent', () => { expect(searchSpy).toHaveBeenCalledWith({ query: 'test search', languageId: 1, - siteId: 'site1' + siteId: 'site123' }); }); @@ -177,7 +234,7 @@ describe('SearchComponent', () => { component.form.patchValue({ query: 'test query', languageId: 1, - siteId: 'site1' + siteId: 'site123' }); const openFiltersButton = spectator.query( diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 7109db6f36da..d97929fa9387 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -1236,6 +1236,8 @@ dot.file.relationship.dialog.search.btn.clear=Clear All dot.file.relationship.dialog.search.btn.search=Search dot.file.relationship.dialog.search.language.label=Language dot.file.relationship.dialog.search.language.placeholder=Select Language +dot.file.relationship.dialog.search.site.label=Site or Folder +dot.file.relationship.dialog.search.site.placeholder=Select Site or Folder dot.file.relationship.dialog.search.language.failed=Failed to load languages dot.file.relationship.dialog.content.id.required=ContentId is required dot.file.relationship.dialog.content.request.failed=Failed to load content