Skip to content

Commit

Permalink
chore(edit-content): implement endpoint to fetch sites (#30562)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
nicobytes authored Nov 8, 2024
1 parent 824106f commit 9269649
Show file tree
Hide file tree
Showing 14 changed files with 356 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]);
});
Expand All @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
})
Expand All @@ -41,16 +43,17 @@ export class DotSiteService {
* @return {*} {Observable<Site[]>}
* @memberof DotSiteService
*/
getSites(filter = '*', perPage?: number): Observable<Site[]> {
getSites(filter = '*', perPage?: number, page?: number): Observable<Site[]> {
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}`
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
<p-tree [value]="$folders()" class="w-full h-full" styleClass="flex h-full" />
@let loading = $loading();

@if (!loading) {
<p-tree
[value]="$folders()"
[loading]="loading"
loadingMode="icon"
class="w-full h-full"
styleClass="flex h-full"
loadingMode="icon"
(onNodeExpand)="onNodeExpand.emit($event)">
<ng-template let-node pTemplate="default">
<span>{{ node.label | truncatePath | slice: 0 : 18 }}</span>
</ng-template>
</p-tree>
} @else {
<div class="flex w-full h-full flex-column p-2">
@for (col of $fakeColumns(); track $index) {
<p-skeleton styleClass="mb-3" [width]="col" />
}
</div>
}
Original file line number Diff line number Diff line change
@@ -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<TreeNode[]>}
* @alias folders
*/
$folders = input.required<TreeNode[]>({ alias: 'folders' });
/**
* Represents a loading state for the component.
* A boolean observable that indicates the loading state.
*
* @type {boolean}
* @alias loading
*/
$loading = input.required<boolean>({ 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<string[]>(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<TreeNodeExpandEvent>();

/**
* 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}%`;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<div class="file-selector flex flex-column gap-3">
<div class="file-selector__main grid grid-nogutter flex-grow-1 overflow-hidden">
<div class="col-3 file-selector__sidebar">
<dot-sidebar [folders]="store.folders().data" [loading]="store.foldersIsLoading()" />
<div class="col-3 file-selector__sidebar h-full">
<dot-sidebar
[folders]="store.folders().data"
[loading]="store.foldersIsLoading()"
(onNodeExpand)="store.loadChildren($event)" />
</div>
<div class="col">
<div class="col h-full">
<dot-dataview [data]="store.content().data" [loading]="store.contentIsLoading()" />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
::ng-deep {
.p-tree {
border: 0px;
padding-right: 0px;
padding-top: 0px;
padding-bottom: 0px;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof SelectExisingFileStore>;
let service: SpyObject<DotEditContentService>;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [SelectExisingFileStore, mockProvider(DotEditContentService)]
});

store = TestBed.inject(SelectExisingFileStore);
service = TestBed.inject(DotEditContentService) as SpyObject<DotEditContentService>;
});

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);
}));
});
});
Loading

0 comments on commit 9269649

Please sign in to comment.