Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core-web/libs/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ export * from './lib/push-publish/push-publish.service';
export * from './lib/dot-page-contenttype/dot-page-contenttype.service';
export * from './lib/dot-favorite-contenttype/dot-favorite-contenttype.service';
export * from './lib/dot-content-drive/dot-content-drive.service';
export * from './lib/dot-browsing/dot-browsing.service';
235 changes: 235 additions & 0 deletions core-web/libs/data-access/src/lib/dot-browsing/dot-browsing.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { Observable, forkJoin } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';

import { filter, map } from 'rxjs/operators';

import { DotFolder, TreeNodeItem, DotCMSAPIResponse, CustomTreeNode } from '@dotcms/dotcms-models';

import { DotSiteService } from '../dot-site/dot-site.service';

/**
* Provide util methods to get Tags available in the system.
* @export
* @class DotBrowsingService
*/
@Injectable({
providedIn: 'root'
})
export class DotBrowsingService {
readonly #http = inject(HttpClient);
readonly #siteService = inject(DotSiteService);

/**
* Retrieves and transforms site data into TreeNode format for the site/folder field.
* Optionally filters out the System Host based on the isRequired parameter.
*
* @param {Object} data - The parameters for fetching sites
* @param {string} data.filter - Filter string to search sites
* @param {number} [data.perPage] - Number of items per page
* @param {number} [data.page] - Page number to fetch
* @param {boolean} data.isRequired - If true, excludes System Host from results
* @returns {Observable<TreeNodeItem[]>} Observable that emits an array of TreeNodeItems
*/
getSitesTreePath(data: {
filter: string;
perPage?: number;
page?: number;
}): Observable<TreeNodeItem[]> {
const { filter, perPage, page } = data;

return this.#siteService.getSites(filter, perPage, page).pipe(
map((sites) => {
return sites.map((site) => ({
key: site.identifier,
label: site.hostname,
data: {
id: site.identifier,
hostname: site.hostname,
path: '',
type: 'site'
},
expandedIcon: 'pi pi-folder-open',
collapsedIcon: 'pi pi-folder',
leaf: false
}));
})
);
}

/**
*
*
* @param {string} path
* @return {*} {Observable<DotFolder[]>}
* @memberof DotEditContentService
*/
getFolders(path: string): Observable<DotFolder[]> {
return this.#http
.post<DotCMSAPIResponse<DotFolder[]>>('/api/v1/folder/byPath', { path })
.pipe(map((response) => response.entity));
}

/**
* Retrieves folders and transforms them into a tree node structure.
* The first folder in the response is considered the parent folder.
*
* @param {string} path - The path to fetch folders from
* @returns {Observable<{ parent: DotFolder; folders: TreeNodeItem[] }>} Observable that emits an object containing the parent folder and child folders as TreeNodeItems
*/
getFoldersTreeNode(path: string): Observable<{ parent: DotFolder; folders: TreeNodeItem[] }> {
return this.getFolders(`//${path}`).pipe(
filter((folders) => folders.length > 0),
map((folders) => {
const parent = folders.shift() as DotFolder;

return {
parent,
folders: folders.map((folder) => ({
key: folder.id,
label: `${folder.hostName}${folder.path}`,
data: {
id: folder.id,
hostname: folder.hostName,
path: folder.path,
type: 'folder'
},
expandedIcon: 'pi pi-folder-open',
collapsedIcon: 'pi pi-folder',
leaf: false
}))
};
})
);
}

/**
* Builds a hierarchical tree structure based on the provided path.
* Splits the path into segments and creates a nested tree structure
* by making multiple API calls for each path segment.
*
* @param {string} path - The full path to build the tree from (e.g., 'hostname/folder1/folder2')
* @returns {Observable<CustomTreeNode>} Observable that emits a CustomTreeNode containing the complete tree structure and the target node
*/
buildTreeByPaths(path: string): Observable<CustomTreeNode> {
const paths = this.#createPaths(path);

const requests = paths.reverse().map((path) => {
const split = path.split('/');
const [hostName] = split;
const subPath = split.slice(1).join('/');

const fullPath = `${hostName}/${subPath}`;

return this.getFoldersTreeNode(fullPath);
});

return forkJoin(requests).pipe(
map((response) => {
const [mainNode] = response;

return response.reduce(
(rta, node, index, array) => {
const next = array[index + 1];
if (next) {
const folder = next.folders.find((item) => item.key === node.parent.id);
if (folder) {
folder.children = node.folders;
if (mainNode.parent.id === folder.key) {
rta.node = folder;
}
}
}

rta.tree = { path: node.parent.path, folders: node.folders };

return rta;
},
{ tree: null, node: null } as CustomTreeNode
);
})
);
}

/**
* Retrieves the current site and transforms it into a TreeNodeItem format.
* Useful for initializing the site/folder field with the current context.
*
* @returns {Observable<TreeNodeItem>} Observable that emits the current site as a TreeNodeItem
*/
getCurrentSiteAsTreeNodeItem(): Observable<TreeNodeItem> {
return this.#siteService.getCurrentSite().pipe(
map((site) => ({
key: site.identifier,
label: site.hostname,
data: {
id: site.identifier,
hostname: site.hostname,
path: '',
type: 'site'
},
expandedIcon: 'pi pi-folder-open',
collapsedIcon: 'pi pi-folder',
leaf: false
}))
);
}

/**
* Get content by folder
*
* @param {{ folderId: string; mimeTypes?: string[] }} { folderId, mimeTypes }
* @return {*}
* @memberof DotEditContentService
*/
getContentByFolder({ folderId, mimeTypes }: { folderId: string; mimeTypes?: string[] }) {
const params = {
hostFolderId: folderId,
showLinks: false,
showDotAssets: true,
showPages: false,
showFiles: true,
showFolders: false,
showWorking: true,
showArchived: false,
sortByDesc: true,
mimeTypes: mimeTypes || []
};

return this.#siteService.getContentByFolder(params);
}

/**
* Converts a JSON string into a JavaScript object.
* Create all paths based in a Path
*
* @param {string} path - the path
* @return {string[]} - An arrray with all posibles pats
*
* @usageNotes
*
* ### Example
*
* ```ts
* const path = 'demo.com/level1/level2';
* const paths = createPaths(path);
* console.log(paths); // ['demo.com/', 'demo.com/level1/', 'demo.com/level1/level2/']
* ```
*/
#createPaths(path: string): string[] {
const split = path.split('/').filter((item) => item !== '');

return split.reduce((array, item, index) => {
const prev = array[index - 1];
let path = `${item}/`;
if (prev) {
path = `${prev}${path}`;
}

array.push(path);

return array;
}, [] as string[]);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { Observable } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';

import { pluck, take } from 'rxjs/operators';
import { map, pluck, switchMap } from 'rxjs/operators';

import { DotCMSContentlet, DotContentletCanLock, DotLanguage } from '@dotcms/dotcms-models';
import {
DotCMSAPIResponse,
DotCMSContentlet,
DotContentletCanLock,
DotLanguage
} from '@dotcms/dotcms-models';

import { DotUploadFileService } from '../dot-upload-file/dot-upload-file.service';

@Injectable({
providedIn: 'root'
})
export class DotContentletService {
private http = inject(HttpClient);
readonly #http = inject(HttpClient);
readonly #dotUploadFileService = inject(DotUploadFileService);

private readonly CONTENTLET_API_URL = '/api/v1/content/';

Expand All @@ -24,9 +32,11 @@ export class DotContentletService {
* @memberof DotContentletService
*/
getContentletVersions(identifier: string, language: string): Observable<DotCMSContentlet[]> {
return this.http
.get(`${this.CONTENTLET_API_URL}versions?identifier=${identifier}&groupByLang=1`)
.pipe(take(1), pluck('entity', 'versions', language));
return this.#http
.get<
DotCMSAPIResponse<DotCMSContentlet[]>
>(`${this.CONTENTLET_API_URL}versions?identifier=${identifier}&groupByLang=1`)
.pipe(pluck('entity', 'versions', language));
}

/**
Expand All @@ -36,8 +46,28 @@ export class DotContentletService {
* @returns {Observable<DotCMSContentlet>} An observable emitting the contentlet.
* @memberof DotContentletService
*/
getContentletByInode(inode: string): Observable<DotCMSContentlet> {
return this.http.get(`${this.CONTENTLET_API_URL}${inode}`).pipe(take(1), pluck('entity'));
getContentletByInode(inode: string, httpParams?: HttpParams): Observable<DotCMSContentlet> {
return this.#http
.get<
DotCMSAPIResponse<DotCMSContentlet>
>(`${this.CONTENTLET_API_URL}${inode}`, { params: httpParams })
.pipe(map((response) => response.entity));
}

/**
* Get the Contentlet by its inode and adds the content if it's a editable as text file.
*
* @param {string} inode - The inode of the contentlet.
* @returns {Observable<DotCMSContentlet>} An observable emitting the contentlet.
* @memberof DotContentletService
*/
getContentletByInodeWithContent(
inode: string,
httpParams?: HttpParams
): Observable<DotCMSContentlet> {
return this.getContentletByInode(inode, httpParams).pipe(
switchMap((contentlet) => this.#dotUploadFileService.addContent(contentlet))
);
}

/**
Expand All @@ -48,9 +78,11 @@ export class DotContentletService {
* @memberof DotContentletService
*/
getLanguages(identifier: string): Observable<DotLanguage[]> {
return this.http
.get(`${this.CONTENTLET_API_URL}${identifier}/languages`)
.pipe(take(1), pluck('entity'));
return this.#http
.get<
DotCMSAPIResponse<DotLanguage[]>
>(`${this.CONTENTLET_API_URL}${identifier}/languages`)
.pipe(map((response) => response.entity));
}

/**
Expand All @@ -61,9 +93,11 @@ export class DotContentletService {
* @memberof DotContentletService
*/
lockContent(inode: string): Observable<DotCMSContentlet> {
return this.http
.put(`${this.CONTENTLET_API_URL}_lock/${inode}`, {})
.pipe(take(1), pluck('entity'));
return this.#http
.put<
DotCMSAPIResponse<DotCMSContentlet>
>(`${this.CONTENTLET_API_URL}_lock/${inode}`, {})
.pipe(map((response) => response.entity));
}

/**
Expand All @@ -74,9 +108,11 @@ export class DotContentletService {
* @memberof DotContentletService
*/
unlockContent(inode: string): Observable<DotCMSContentlet> {
return this.http
.put(`${this.CONTENTLET_API_URL}_unlock/${inode}`, {})
.pipe(take(1), pluck('entity'));
return this.#http
.put<
DotCMSAPIResponse<DotCMSContentlet>
>(`${this.CONTENTLET_API_URL}_unlock/${inode}`, {})
.pipe(map((response) => response.entity));
}

/**
Expand All @@ -87,8 +123,10 @@ export class DotContentletService {
* @memberof DotContentletService
*/
canLock(inode: string): Observable<DotContentletCanLock> {
return this.http
.get(`${this.CONTENTLET_API_URL}_canlock/${inode}`)
.pipe(take(1), pluck('entity'));
return this.#http
.get<
DotCMSAPIResponse<DotContentletCanLock>
>(`${this.CONTENTLET_API_URL}_canlock/${inode}`)
.pipe(map((response) => response.entity));
}
}
Loading
Loading