From 92696491e0ddc979aec919a0e71b05fa169b044c Mon Sep 17 00:00:00 2001 From: Nicolas Molina Monroy Date: Fri, 8 Nov 2024 10:31:57 -0400 Subject: [PATCH] chore(edit-content): implement endpoint to fetch sites (#30562) ### Parent Issue #30215 ### Proposed Changes This pull request includes multiple changes to the `dot-site` service and the `dot-select-existing-file` component, primarily focusing on adding pagination support and improving the file selection UI and its state management. ### DotSiteService Enhancements: * Added a `DEFAULT_PAGE` constant and included a `page` parameter in the `getSites` method to support pagination. (`core-web/libs/data-access/src/lib/dot-site/dot-site.service.ts`) [[1]](diffhunk://#diff-c4872eacb42281fa7aa6fd3d5c084e08fd41e79eb2778581421e7344f9aa4e12R20-R21) [[2]](diffhunk://#diff-c4872eacb42281fa7aa6fd3d5c084e08fd41e79eb2778581421e7344f9aa4e12L44-R56) * Updated test cases to reflect the new `page` parameter in the URLs. (`core-web/libs/data-access/src/lib/dot-site/dot-site.service.spec.ts`) [[1]](diffhunk://#diff-ad4a37aa4e92da7343f4da8185ea2031a67319500d820946e0d24fa515e33241L30-R30) [[2]](diffhunk://#diff-ad4a37aa4e92da7343f4da8185ea2031a67319500d820946e0d24fa515e33241L44-R44) ### DotSelectExistingFile Component Enhancements: * Improved the `dot-sidebar` component to handle loading states and display skeleton loaders while data is being fetched. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.html`, `core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.ts`) [[1]](diffhunk://#diff-c3ff3590364bc12ded7173d7374838dfab13f67598f80daa198dfd96c935d459L1-R22) [[2]](diffhunk://#diff-b6e7f5a1ad7c221178b3ddf3f3dfc06021ba87b4e18ef9d7f6c6c9a6eab016d3L1-R82) * Added event handling for node expansion in the `dot-sidebar` component to dynamically load child nodes. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.html`) * Introduced a new test suite for the `SelectExisingFileStore` to cover folder loading and child node expansion logic. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.test.ts`) * Updated the `SelectExisingFileStore` to manage the state of folder nodes, including loading states and expanded nodes. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.ts`) [[1]](diffhunk://#diff-4e8a1b332dae5fb8ead291b76bf6099437aaf4aa6fd0a759ffe71c3074ffe7f0R2-R22) [[2]](diffhunk://#diff-4e8a1b332dae5fb8ead291b76bf6099437aaf4aa6fd0a759ffe71c3074ffe7f0L20-R35) [[3]](diffhunk://#diff-4e8a1b332dae5fb8ead291b76bf6099437aaf4aa6fd0a759ffe71c3074ffe7f0L36-R51) [[4]](diffhunk://#diff-4e8a1b332dae5fb8ead291b76bf6099437aaf4aa6fd0a759ffe71c3074ffe7f0R70-R71) [[5]](diffhunk://#diff-4e8a1b332dae5fb8ead291b76bf6099437aaf4aa6fd0a759ffe71c3074ffe7f0L75-R153) ### Miscellaneous: * Minor CSS adjustments to the `dot-select-existing-file` component to improve layout and styling. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.scss`) * Refactored the `file-field.store.spec.ts` to use `TestBed` for dependency injection and added missing imports. (`core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts`) [[1]](diffhunk://#diff-57a0231d8f4135b48e8dd7e31f8e216157f1fd49a145d9cc0c7d7bf912a00de4L2-R4) [[2]](diffhunk://#diff-57a0231d8f4135b48e8dd7e31f8e216157f1fd49a145d9cc0c7d7bf912a00de4L19-R22) ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) ### Screenshots https://github.com/user-attachments/assets/56370a81-b204-456e-85c2-7d7dc9f26a57 --- .../src/lib/dot-site/dot-site.service.spec.ts | 4 +- .../src/lib/dot-site/dot-site.service.ts | 9 +- .../dot-sidebar/dot-sidebar.component.html | 23 +++- .../dot-sidebar/dot-sidebar.component.ts | 66 +++++++++- .../dot-select-existing-file.component.html | 9 +- .../dot-select-existing-file.component.scss | 3 + .../dot-select-existing-file.component.ts | 27 +++- .../store/select-existing-file.store.test.ts | 95 ++++++++++++++ .../store/select-existing-file.store.ts | 121 ++++++++++++------ .../store/file-field.store.spec.ts | 80 ++++++------ .../store/file-field.store.ts | 8 ++ .../store/host-folder-field.store.ts | 2 +- .../lib/services/dot-edit-content.service.ts | 12 +- core-web/tsconfig.base.json | 1 + 14 files changed, 356 insertions(+), 104 deletions(-) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.test.ts diff --git a/core-web/libs/data-access/src/lib/dot-site/dot-site.service.spec.ts b/core-web/libs/data-access/src/lib/dot-site/dot-site.service.spec.ts index 0f9dbfca8933..f3475dc884e7 100644 --- a/core-web/libs/data-access/src/lib/dot-site/dot-site.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-site/dot-site.service.spec.ts @@ -27,7 +27,7 @@ describe('DotSiteService', () => { doneFn(); }); - const url = `${BASE_SITE_URL}?filter=*&per_page=10&archived=false&live=true&system=true`; + const url = `${BASE_SITE_URL}?filter=*&per_page=10&page=1&archived=false&live=true&system=true`; const req = spectator.expectOne(url, HttpMethod.GET); spectator.flushAll([req], [{ entity: mockSites }]); }); @@ -41,7 +41,7 @@ describe('DotSiteService', () => { service.searchParam = searchParams; - const url = `${BASE_SITE_URL}?filter=demo&per_page=15&archived=true&live=false&system=true`; + const url = `${BASE_SITE_URL}?filter=demo&per_page=15&page=1&archived=true&live=false&system=true`; service.getSites('demo', 15).subscribe(() => doneFn()); diff --git a/core-web/libs/data-access/src/lib/dot-site/dot-site.service.ts b/core-web/libs/data-access/src/lib/dot-site/dot-site.service.ts index cbac35433d8e..287299670349 100644 --- a/core-web/libs/data-access/src/lib/dot-site/dot-site.service.ts +++ b/core-web/libs/data-access/src/lib/dot-site/dot-site.service.ts @@ -17,6 +17,8 @@ export const BASE_SITE_URL = '/api/v1/site'; export const DEFAULT_PER_PAGE = 10; +export const DEFAULT_PAGE = 1; + @Injectable({ providedIn: 'root' }) @@ -41,16 +43,17 @@ export class DotSiteService { * @return {*} {Observable} * @memberof DotSiteService */ - getSites(filter = '*', perPage?: number): Observable { + getSites(filter = '*', perPage?: number, page?: number): Observable { return this.#http - .get<{ entity: Site[] }>(this.getSiteURL(filter, perPage)) + .get<{ entity: Site[] }>(this.getSiteURL(filter, perPage, page)) .pipe(pluck('entity')); } - private getSiteURL(filter: string, perPage?: number): string { + private getSiteURL(filter: string, perPage?: number, page?: number): string { const searchParams = new URLSearchParams({ filter, per_page: `${perPage || DEFAULT_PER_PAGE}`, + page: `${page || DEFAULT_PAGE}`, archived: `${this.#defaultParams.archived}`, live: `${this.#defaultParams.live}`, system: `${this.#defaultParams.system}` diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.html index 690560328112..cd96d04248bc 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.html @@ -1 +1,22 @@ - +@let loading = $loading(); + +@if (!loading) { + + + {{ node.label | truncatePath | slice: 0 : 18 }} + + +} @else { +
+ @for (col of $fakeColumns(); track $index) { + + } +
+} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.ts index 95baf9ce298e..17ccc9e90a01 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/components/dot-sidebar/dot-sidebar.component.ts @@ -1,29 +1,83 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { faker } from '@faker-js/faker'; + +import { SlicePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, + input, + output, + signal +} from '@angular/core'; import { TreeNode } from 'primeng/api'; -import { TreeModule } from 'primeng/tree'; +import { SkeletonModule } from 'primeng/skeleton'; +import { TreeModule, TreeNodeExpandEvent } from 'primeng/tree'; + +import { TruncatePathPipe } from '@dotcms/edit-content/pipes/truncate-path.pipe'; @Component({ selector: 'dot-sidebar', standalone: true, - imports: [TreeModule], + imports: [TreeModule, SlicePipe, TruncatePathPipe, SkeletonModule], templateUrl: './dot-sidebar.component.html', styleUrls: ['./dot-sidebar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class DotSideBarComponent { /** - * An observable that emits an array of TreeNode objects representing folders. + * A readonly private field that holds an instance of ChangeDetectorRef. + * This is used to detect and respond to changes in the component's data-bound properties. + */ + readonly #cd = inject(ChangeDetectorRef); + /** + * An observable that emits an array of TreeNode objects representing the folders. * * @type {Observable} * @alias folders */ $folders = input.required({ alias: 'folders' }); /** - * Represents a loading state for the component. + * A boolean observable that indicates the loading state. * * @type {boolean} - * @alias loading */ $loading = input.required({ alias: 'loading' }); + + /** + * Signal that generates an array of strings representing percentages. + * Each percentage is a random value between 75% and 100%. + * The array contains 50 elements. + * + * @returns {string[]} An array of 50 percentage strings. + */ + $fakeColumns = signal(Array.from({ length: 50 }).map((_) => this.getPercentage())); + + /** + * Event emitter for when a tree node is expanded. + * + * This event is triggered when a user expands a node in the tree structure. + * It emits an event of type `TreeNodeExpandEvent`. + */ + onNodeExpand = output(); + + /** + * Triggers change detection manually. + * This method is used to ensure that the view is updated when the model changes. + * It calls the `detectChanges` method on the ChangeDetectorRef instance. + */ + detectChanges() { + this.#cd.detectChanges(); + } + /** + * Generates a random percentage string between 75% and 100%. + * + * @returns {string} A string representing a percentage between 75% and 100%. + */ + getPercentage(): string { + const number = faker.number.int({ max: 100, min: 75 }); + + return `${number}%`; + } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.html index 68c1387397dc..bc7e68dda668 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.html @@ -1,9 +1,12 @@
-
- +
+
-
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.scss index 674ba19f4eed..9e493b3997b2 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.scss +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.scss @@ -7,6 +7,9 @@ ::ng-deep { .p-tree { border: 0px; + padding-right: 0px; + padding-top: 0px; + padding-bottom: 0px; } } } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts index b10f9dfa2f3b..050d6f019a89 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/dot-select-existing-file.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + OnInit, + viewChild +} from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -37,6 +44,24 @@ export class DotSelectExistingFileComponent implements OnInit { */ readonly #dialogRef = inject(DynamicDialogRef); + /** + * Reference to the DotSideBarComponent instance. + * This is used to interact with the sidebar component within the template. + * + * @type {DotSideBarComponent} + */ + $sideBarRef = viewChild.required(DotSideBarComponent); + + constructor() { + effect(() => { + const folders = this.store.folders(); + + if (folders.nodeExpaned) { + this.$sideBarRef().detectChanges(); + } + }); + } + ngOnInit() { this.store.loadContent(); this.store.loadFolders(); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.test.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.test.ts new file mode 100644 index 000000000000..9c9ba7bbdc32 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.test.ts @@ -0,0 +1,95 @@ +import { createFakeEvent } from '@ngneat/spectator'; +import { SpyObject, mockProvider } from '@ngneat/spectator/jest'; +import { of, throwError } from 'rxjs'; + +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { ComponentStatus } from '@dotcms/dotcms-models'; + +import { SelectExisingFileStore } from './select-existing-file.store'; + +import { DotEditContentService } from '../../../../../services/dot-edit-content.service'; +import { TREE_SELECT_MOCK, TREE_SELECT_SITES_MOCK } from '../../../../../utils/mocks'; + +describe('SelectExisingFileStore', () => { + let store: InstanceType; + let service: SpyObject; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SelectExisingFileStore, mockProvider(DotEditContentService)] + }); + + store = TestBed.inject(SelectExisingFileStore); + service = TestBed.inject(DotEditContentService) as SpyObject; + }); + + it('should be created', () => { + expect(store).toBeTruthy(); + }); + + describe('Method: loadFolders', () => { + it('should set folders status to LOADING and then to LOADED with data', fakeAsync(() => { + service.getSitesTreePath.mockReturnValue(of(TREE_SELECT_SITES_MOCK)); + + store.loadFolders(); + + tick(50); + + expect(store.folders().status).toBe(ComponentStatus.LOADED); + expect(store.folders().data).toEqual(TREE_SELECT_SITES_MOCK); + })); + + it('should set folders status to ERROR on service error', fakeAsync(() => { + service.getSitesTreePath.mockReturnValue(throwError('error')); + + store.loadFolders(); + + tick(50); + + expect(store.folders().status).toBe(ComponentStatus.ERROR); + expect(store.folders().data).toEqual([]); + })); + }); + + describe('Method: loadChildren', () => { + it('should load children for a node', fakeAsync(() => { + const mockChildren = [...TREE_SELECT_SITES_MOCK]; + + service.getFoldersTreeNode.mockReturnValue(of(mockChildren)); + + const node = { ...TREE_SELECT_MOCK[0] }; + + const mockItem = { + originalEvent: createFakeEvent('click'), + node + }; + store.loadChildren(mockItem); + + tick(50); + + expect(node.children).toEqual(mockChildren); + expect(node.loading).toBe(false); + expect(node.leaf).toBe(true); + expect(node.icon).toBe('pi pi-folder-open'); + expect(store.folders().nodeExpaned).toBe(node); + })); + + it('should handle error when loading children', fakeAsync(() => { + service.getFoldersTreeNode.mockReturnValue(throwError('error')); + + const node = { ...TREE_SELECT_MOCK[0], children: [] }; + + const mockItem = { + originalEvent: createFakeEvent('click'), + node + }; + store.loadChildren(mockItem); + + tick(50); + + expect(node.children).toEqual([]); + expect(node.loading).toBe(false); + })); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.ts index ea6e91dbc56d..e2c2524e31e8 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-select-existing-file/store/select-existing-file.store.ts @@ -1,12 +1,25 @@ import { faker } from '@faker-js/faker'; +import { tapResponse } from '@ngrx/component-store'; import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { pipe } from 'rxjs'; -import { computed } from '@angular/core'; +import { computed, inject } from '@angular/core'; import { TreeNode } from 'primeng/api'; +import { exhaustMap, switchMap, tap } from 'rxjs/operators'; + import { ComponentStatus, DotCMSContentlet } from '@dotcms/dotcms-models'; +import { + TreeNodeItem, + TreeNodeSelectItem +} from '../../../../../models/dot-edit-content-host-folder-field.interface'; +import { DotEditContentService } from '../../../../../services/dot-edit-content.service'; + +export const PEER_PAGE_LIMIT = 1000; + export interface Content { id: string; image: string; @@ -17,8 +30,9 @@ export interface Content { export interface SelectExisingFileState { folders: { - data: TreeNode[]; + data: TreeNodeItem[]; status: ComponentStatus; + nodeExpaned: TreeNodeSelectItem['node'] | null; }; content: { data: Content[]; @@ -33,7 +47,8 @@ export interface SelectExisingFileState { const initialState: SelectExisingFileState = { folders: { data: [], - status: ComponentStatus.INIT + status: ComponentStatus.INIT, + nodeExpaned: null }, content: { data: [], @@ -52,6 +67,8 @@ export const SelectExisingFileStore = signalStore( contentIsLoading: computed(() => state.content().status === ComponentStatus.LOADING) })), withMethods((store) => { + const dotEditContentService = inject(DotEditContentService); + return { loadContent: () => { const mockContent = faker.helpers.multiple( @@ -72,44 +89,68 @@ export const SelectExisingFileStore = signalStore( } }); }, - loadFolders: () => { - const mockFolders = [ - { - label: 'demo.dotcms.com', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'demo.dotcms.com', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder', - children: [ - { - label: 'documents' - } - ] - }, - { - label: 'demo.dotcms.com', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder' - } - ] - }, - { - label: 'nico.dotcms.com', - expandedIcon: 'pi pi-folder-open', - collapsedIcon: 'pi pi-folder' - } - ]; + loadFolders: rxMethod( + pipe( + tap(() => + patchState(store, { + folders: { ...store.folders(), status: ComponentStatus.LOADING } + }) + ), + switchMap(() => { + return dotEditContentService + .getSitesTreePath({ perPage: PEER_PAGE_LIMIT, filter: '*' }) + .pipe( + tapResponse({ + next: (data) => + patchState(store, { + folders: { + data, + status: ComponentStatus.LOADED, + nodeExpaned: null + } + }), + error: () => + patchState(store, { + folders: { + data: [], + status: ComponentStatus.ERROR, + nodeExpaned: null + } + }) + }) + ); + }) + ) + ), + loadChildren: rxMethod( + pipe( + exhaustMap((event: TreeNodeSelectItem) => { + const { node } = event; + const { hostname, path } = node.data; - patchState(store, { - folders: { - data: mockFolders, - status: ComponentStatus.LOADED - } - }); - } + node.loading = true; + + return dotEditContentService.getFoldersTreeNode(hostname, path).pipe( + tapResponse({ + next: (children) => { + node.loading = false; + node.leaf = true; + node.icon = 'pi pi-folder-open'; + node.children = [...children]; + + const folders = store.folders(); + patchState(store, { + folders: { ...folders, nodeExpaned: node } + }); + }, + error: () => { + node.loading = false; + } + }) + ); + }) + ) + ) }; }) ); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts index 9745f4626ca1..e52df720c598 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts @@ -1,8 +1,7 @@ import { SpyObject, mockProvider } from '@ngneat/spectator/jest'; -import { patchState } from '@ngrx/signals'; import { of, throwError } from 'rxjs'; -import { TestBed } from '@angular/core/testing'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { FileFieldStore } from './file-field.store'; @@ -16,11 +15,11 @@ describe('FileFieldStore', () => { let service: SpyObject; beforeEach(() => { - store = TestBed.overrideProvider( - DotFileFieldUploadService, - mockProvider(DotFileFieldUploadService) - ).runInInjectionContext(() => new FileFieldStore()); + TestBed.configureTestingModule({ + providers: [FileFieldStore, mockProvider(DotFileFieldUploadService)] + }); + store = TestBed.inject(FileFieldStore); service = TestBed.inject(DotFileFieldUploadService) as SpyObject; }); @@ -98,59 +97,54 @@ describe('FileFieldStore', () => { }); describe('Method: removeFile', () => { - it('should set the state properly when removeFile is called', () => { - patchState(store, { - uploadedFile: { - source: 'contentlet', - file: NEW_FILE_MOCK.entity - }, - value: 'some value', - fileStatus: 'preview', - uiMessage: getUiMessage('SERVER_ERROR') - }); + it('should set the state properly when removeFile is called', fakeAsync(() => { + const mockContentlet = NEW_FILE_MOCK.entity; + service.uploadFile.mockReturnValue(of({ source: 'contentlet', file: mockContentlet })); + + const file = new File([''], 'filename', { type: 'text/plain' }); + Object.defineProperty(file, 'size', { value: 5000 }); + + store.handleUploadFile(file); + + tick(50); + store.removeFile(); expect(store.value()).toBe(''); expect(store.fileStatus()).toBe('init'); expect(store.uiMessage()).toBe(getUiMessage('DEFAULT')); expect(store.uploadedFile()).toBeNull(); - }); + })); }); describe('Method: setDropZoneState', () => { it('should set dropZoneActive to true', () => { - patchState(store, { - dropZoneActive: false - }); store.setDropZoneState(true); expect(store.dropZoneActive()).toBe(true); }); it('should set dropZoneActive to false', () => { - patchState(store, { - dropZoneActive: true - }); store.setDropZoneState(false); expect(store.dropZoneActive()).toBe(false); }); }); describe('Method: handleUploadFile', () => { - it('should does not call uploadService with maxFileSize exceeded', () => { - patchState(store, { - maxFileSize: 10000 - }); + it('should does not call uploadService with maxFileSize exceeded', fakeAsync(() => { + store.setMaxSizeFile(10000); + + tick(50); const file = new File([''], 'filename', { type: 'text/plain' }); Object.defineProperty(file, 'size', { value: 20000 }); store.handleUploadFile(file); expect(service.uploadFile).not.toHaveBeenCalled(); - }); + })); - it('should set state properly with maxFileSize exceeded', () => { - patchState(store, { - maxFileSize: 10000 - }); + it('should set state properly with maxFileSize exceeded', fakeAsync(() => { + store.setMaxSizeFile(10000); + + tick(50); const file = new File([''], 'filename', { type: 'text/plain' }); Object.defineProperty(file, 'size', { value: 20000 }); @@ -162,15 +156,15 @@ describe('FileFieldStore', () => { ...getUiMessage('MAX_FILE_SIZE_EXCEEDED'), args: ['10000'] }); - }); + })); - it('should call uploadService with maxFileSize not exceeded', () => { + it('should call uploadService with maxFileSize not exceeded', fakeAsync(() => { const mockContentlet = NEW_FILE_MOCK.entity; service.uploadFile.mockReturnValue(of({ source: 'contentlet', file: mockContentlet })); - patchState(store, { - maxFileSize: 10000 - }); + store.setMaxSizeFile(10000); + + tick(50); const file = new File([''], 'filename', { type: 'text/plain' }); Object.defineProperty(file, 'size', { value: 5000 }); @@ -182,15 +176,15 @@ describe('FileFieldStore', () => { maxSize: '10000', uploadType: 'dotasset' }); - }); + })); - it('should set state properly with maxFileSize not exceeded', () => { + it('should set state properly with maxFileSize not exceeded', fakeAsync(() => { const mockContentlet = NEW_FILE_MOCK.entity; service.uploadFile.mockReturnValue(of({ source: 'contentlet', file: mockContentlet })); - patchState(store, { - maxFileSize: 10000 - }); + store.setMaxSizeFile(10000); + + tick(50); const file = new File([''], 'filename', { type: 'text/plain' }); Object.defineProperty(file, 'size', { value: 5000 }); @@ -202,7 +196,7 @@ describe('FileFieldStore', () => { source: 'contentlet', file: mockContentlet }); - }); + })); it('should set state properly with an error calling uploadFile', () => { service.uploadFile.mockReturnValue(throwError('error')); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts index 5ec332eecd08..c5995e7f5c1a 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts @@ -91,6 +91,14 @@ export const FileFieldStore = signalStore( ...actions }); }, + /** + * setAcceptedFiles is used to set accepted files + */ + setMaxSizeFile: (maxFileSize: number) => { + patchState(store, { + maxFileSize + }); + }, /** * setUIMessage is used to set uiMessage * @param uiMessage diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/store/host-folder-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/store/host-folder-field.store.ts index 0685318af172..c4c4d20a58ca 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/store/host-folder-field.store.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-host-folder-field/store/host-folder-field.store.ts @@ -73,7 +73,7 @@ export const HostFolderFiledStore = signalStore( tap(() => patchState(store, { status: 'LOADING' })), switchMap(({ path, isRequired }) => { return dotEditContentService - .getSitesTreePath({ perPage: PEER_PAGE_LIMIT, filter: '*' }) + .getSitesTreePath({ perPage: PEER_PAGE_LIMIT, filter: '*', page: 1 }) .pipe( map((sites) => { if (isRequired) { diff --git a/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts b/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts index 73d2030d1795..10f245b9cb4c 100644 --- a/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts +++ b/core-web/libs/edit-content/src/lib/services/dot-edit-content.service.ts @@ -75,10 +75,14 @@ export class DotEditContentService { * @return {*} {Observable} * @memberof DotEditContentService */ - getSitesTreePath(data: { filter: string; perPage: number }): Observable { - const { filter, perPage } = data; - - return this.#siteService.getSites(filter, perPage).pipe( + getSitesTreePath(data: { + filter: string; + perPage?: number; + page?: number; + }): Observable { + const { filter, perPage, page } = data; + + return this.#siteService.getSites(filter, perPage, page).pipe( map((sites) => { return sites.map((site) => ({ key: site.hostname, diff --git a/core-web/tsconfig.base.json b/core-web/tsconfig.base.json index afe7e6788348..5b5c62b2db5d 100644 --- a/core-web/tsconfig.base.json +++ b/core-web/tsconfig.base.json @@ -35,6 +35,7 @@ "@dotcms/dotcms-webcomponents": ["libs/dotcms-webcomponents/src/index.ts"], "@dotcms/dotcms-webcomponents/loader": ["dist/libs/dotcms-webcomponents/loader"], "@dotcms/edit-content": ["libs/edit-content/src/index.ts"], + "@dotcms/edit-content/*": ["libs/edit-content/src/lib/*"], "@dotcms/experiments": ["libs/sdk/experiments/src/index.ts"], "@dotcms/portlets/dot-analytics-search/portlet": [ "libs/portlets/dot-analytics-search/portlet/src/index.ts"