Skip to content
12 changes: 11 additions & 1 deletion core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@ export interface DotContentDriveFolder {
owner: string | null;
parent: string;
path: string;
permissions: number[];
permissions: PermissionType[];
showOnMenu: boolean;
sortOrder: number;
title: string;
type: 'folder';
}

export const PERMISSIONS_TYPE = {
READ: 'READ',
EDIT: 'EDIT',
PUBLISH: 'PUBLISH',
EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
CAN_ADD_CHILDREN: 'CAN_ADD_CHILDREN'
} as const;

export type PermissionType = (typeof PERMISSIONS_TYPE)[keyof typeof PERMISSIONS_TYPE];

// This will extend the DotCMSContentlet with more properties,
// but for now we will just use the DotCMSContentlet until we have folders on the request response
export type DotContentDriveItem = DotCMSContentlet | DotContentDriveFolder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ActivatedRoute, Router } from '@angular/router';

import { MenuItemCommandEvent, MessageService } from 'primeng/api';
import { ContextMenu } from 'primeng/contextmenu';
import { DialogService } from 'primeng/dynamicdialog';

import {
DotContentDriveService,
Expand All @@ -26,8 +27,10 @@ import {
import {
DotCMSBaseTypesContentTypes,
DotContentDriveFolder,
DotContentDriveItem
DotContentDriveItem,
PERMISSIONS_TYPE
} from '@dotcms/dotcms-models';
import { DotPermissionsIframeDialogComponent } from '@dotcms/ui';
import { createFakeContentlet, mockWorkflowsActionsWithMove } from '@dotcms/utils-testing';

import { DotFolderListViewContextMenuComponent } from './dot-folder-list-context-menu.component';
Expand Down Expand Up @@ -62,7 +65,7 @@ describe('DotFolderListViewContextMenuComponent', () => {

const createComponent = createComponentFactory({
component: DotFolderListViewContextMenuComponent,
componentProviders: [DotContentDriveStore],
componentProviders: [DotContentDriveStore, DialogService],
providers: [
mockProvider(DotContentDriveService, {
search: jest
Expand Down Expand Up @@ -298,7 +301,7 @@ describe('DotFolderListViewContextMenuComponent', () => {
owner: 'admin',
Comment thread
zJaaal marked this conversation as resolved.
parent: '/',
path: '/documents/',
permissions: [],
permissions: [PERMISSIONS_TYPE.EDIT],
showOnMenu: true,
sortOrder: 0,
title: 'Test Folder',
Expand Down Expand Up @@ -384,12 +387,106 @@ describe('DotFolderListViewContextMenuComponent', () => {
expect(component.$items()).toHaveLength(1);
});

it('should build empty menu when folder has no permissions', async () => {
const folderNoPermissions: DotContentDriveFolder = {
...mockFolder,
permissions: []
};
await component.getMenuItems({
triggeredEvent: mockEvent,
contentlet: folderNoPermissions,
showAddToBundle: false
});

expect(component.$items()).toHaveLength(0);
});

it('should not show context menu when folder has no applicable permissions', async () => {
const mockContextMenu = {
show: jest.fn(),
visible: jest.fn().mockReturnValue(false)
} as unknown as ContextMenu;

jest.spyOn(component, 'contextMenu').mockReturnValue(mockContextMenu);

const folderNoPermissions: DotContentDriveFolder = {
...mockFolder,
permissions: []
};

await component.getMenuItems({
triggeredEvent: mockEvent,
contentlet: folderNoPermissions,
showAddToBundle: false
});

expect(mockContextMenu.show).not.toHaveBeenCalled();
});

it('should use identifier as memoization key for folders, not inode', async () => {
await component.getMenuItems(mockFolderContextMenuData);

expect(component.$memoizedMenuItems()[mockFolder.identifier]).toBeDefined();
expect(component.$memoizedMenuItems()[mockFolder.inode]).toBeUndefined();
});

describe('permissions dialog', () => {
const folderWithEditPermissions: DotContentDriveFolder = {
...mockFolder,
permissions: [PERMISSIONS_TYPE.EDIT, PERMISSIONS_TYPE.EDIT_PERMISSIONS]
};

const folderContextMenuWithEditPermissions: DotContentDriveContextMenu = {
triggeredEvent: mockEvent,
contentlet: folderWithEditPermissions,
showAddToBundle: false
};

let dialogService: SpyObject<DialogService>;

beforeEach(() => {
dialogService = spectator.inject(DialogService, true);
jest.spyOn(dialogService, 'open').mockReturnValue(null as never);
component.$memoizedMenuItems.set({});
});

it('should show Edit-Permissions item when folder has EDIT_PERMISSIONS permission', async () => {
await component.getMenuItems(folderContextMenuWithEditPermissions);

expect(
component.$items().find((item) => item.label === 'Edit-Permissions')
).toBeDefined();
});

it('should not show Edit-Permissions item when folder lacks EDIT_PERMISSIONS permission', async () => {
await component.getMenuItems(mockFolderContextMenuData);

expect(
component.$items().find((item) => item.label === 'Edit-Permissions')
).toBeUndefined();
});

it('should open DotPermissionsIframeDialogComponent with correct config when triggered', async () => {
await component.getMenuItems(folderContextMenuWithEditPermissions);

component
.$items()
.find((item) => item.label === 'Edit-Permissions')
?.command?.({} as unknown as MenuItemCommandEvent);

expect(dialogService.open).toHaveBeenCalledWith(
DotPermissionsIframeDialogComponent,
expect.objectContaining({
width: 'min(92vw, 75rem)',
closable: true,
closeOnEscape: true,
data: {
url: `/html/portlet/ext/folders/permissions.jsp?folderIdentifier=${folderWithEditPermissions.identifier}&popup=true`
}
})
);
});
});
});

describe('lock/unlock functionality', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {

import { MenuItem, MessageService } from 'primeng/api';
import { ContextMenu, ContextMenuModule } from 'primeng/contextmenu';
import { DialogService } from 'primeng/dynamicdialog';

import { take } from 'rxjs/operators';

Expand All @@ -29,8 +30,10 @@ import {
DotCMSWorkflowAction,
DotContentletCanLock,
DotProcessedWorkflowPayload,
DotWorkflowPayload
DotWorkflowPayload,
PERMISSIONS_TYPE
} from '@dotcms/dotcms-models';
import { DotPermissionsIframeDialogComponent, DotPermissionsIframeDialogData } from '@dotcms/ui';

import {
DIALOG_TYPE,
Expand All @@ -46,7 +49,7 @@ import { isFolder } from '../../utils/functions';
selector: 'dot-folder-list-context-menu',
templateUrl: './dot-folder-list-context-menu.component.html',
imports: [ContextMenuModule],
providers: [DotContentletService],
providers: [DotContentletService, DialogService],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'relative' }
})
Expand All @@ -62,6 +65,7 @@ export class DotFolderListViewContextMenuComponent {
#messageService = inject(MessageService);
#dotWizardService = inject(DotWizardService);
#dotContentletService = inject(DotContentletService);
#dialogService = inject(DialogService);

/** The menu items for the context menu. */
$items = signal<MenuItem[]>([]);
Expand All @@ -82,7 +86,7 @@ export class DotFolderListViewContextMenuComponent {
readonly rightClickEffect = effect(() => {
const contextMenuData = this.$contextMenuData();

if (contextMenuData && !this.contextMenu()?.visible()) {
if (contextMenuData) {
this.getMenuItems(contextMenuData);
}
});
Expand Down Expand Up @@ -137,8 +141,10 @@ export class DotFolderListViewContextMenuComponent {
}

if (isFolder(contentlet)) {
const folderMenuItems = [
{
const folderMenuItems = [];

if (contentlet.permissions.includes(PERMISSIONS_TYPE.EDIT)) {
folderMenuItems.push({
label: this.#dotMessageService.get('content-drive.context-menu.edit-folder'),
command: () => {
this.#store.setDialog({
Expand All @@ -149,8 +155,20 @@ export class DotFolderListViewContextMenuComponent {
payload: contentlet
});
}
}
];
});
}

if (contentlet.permissions.includes(PERMISSIONS_TYPE.EDIT_PERMISSIONS)) {
folderMenuItems.push({
label: this.#dotMessageService.get('Edit-Permissions'),
command: () => this.#openPermissionsDialog(contentlet.identifier)
});
}

if (!folderMenuItems.length) {
return;
}

this.$items.set(folderMenuItems);
this.$memoizedMenuItems.set({
...this.$memoizedMenuItems(),
Expand Down Expand Up @@ -212,11 +230,16 @@ export class DotFolderListViewContextMenuComponent {
}
});

if (!actionsMenu.length) {
return;
}

this.$items.set(actionsMenu);
this.$memoizedMenuItems.set({
...this.$memoizedMenuItems(),
[key]: this.$items()
});

this.contextMenu()?.show(triggeredEvent);
}

Expand Down Expand Up @@ -357,4 +380,30 @@ export class DotFolderListViewContextMenuComponent {
);
}
}

#openPermissionsDialog(identifier: string): void {
this.#dialogService.open(DotPermissionsIframeDialogComponent, {
header: this.#dotMessageService.get('Edit-Permissions'),
width: 'min(92vw, 75rem)',
contentStyle: { overflow: 'hidden' },
data: {
url: this.#buildPermissionsUrl(identifier)
} satisfies DotPermissionsIframeDialogData,
modal: true,
appendTo: 'body',
closable: true,
closeOnEscape: true,
draggable: false,
resizable: false,
position: 'center'
});
}

#buildPermissionsUrl(identifier: string): string {
const params = new URLSearchParams({
folderIdentifier: identifier,
popup: 'true'
});
return `/html/portlet/ext/folders/permissions.jsp?${params.toString()}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
[tableStyle]="{ width: '100%', maxHeight: '100%' }"
[paginator]="true"
[showFirstLastIcon]="false"
[showPageLinks]="true"
[showPageLinks]="false"
[showCurrentPageReport]="true"
[totalRecords]="$totalItems()"
[rows]="MIN_ROWS_PER_PAGE"
[rowsPerPageOptions]="rowsPerPageOptions"
Expand All @@ -24,6 +25,7 @@
[sortOrder]="-1"
data-testId="table"
scrollHeight="flex"
[currentPageReportTemplate]="`${('Page' | dm)} {currentPage}`"
[pt]="$ptConfig()">
<ng-template #header>
<tr data-testId="header-row">
Expand Down
19 changes: 19 additions & 0 deletions dotCMS/src/main/webapp/html/portlet/ext/folders/permissions.jsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<%@ page import="com.dotmarketing.portlets.folders.model.Folder" %>
<%@ page import="com.dotmarketing.business.APILocator" %>
<%@ page import="com.dotmarketing.util.UtilMethods" %>
<%@ include file="/html/common/init.jsp" %>
<%@ include file="/html/common/top_inc.jsp" %>
<%@ include file="/html/common/messages_inc.jsp" %>

<%
final String folderIdentifier = request.getParameter("folderIdentifier");
if (UtilMethods.isSet(folderIdentifier)) {
final Folder folder = APILocator.getFolderAPI().find(folderIdentifier, user, false);
if (folder != null && UtilMethods.isSet(folder.getInode())) {
request.setAttribute(com.dotmarketing.util.WebKeys.PERMISSIONABLE_EDIT, folder);
%>
<%@ include file="/html/portlet/ext/common/edit_permissions_tab_inc.jsp" %>
<%
}
}
%>
2 changes: 0 additions & 2 deletions examples/nextjs/src/components/DestinationListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ export default function DestinationListing({ destinations }) {
return <div>No destinations found</div>;
}

console.log(destinations[0]);

return (
<div className="container mx-auto my-12">
<h2 className="text-4xl font-bold mb-6 text-gray-800 text-center">
Expand Down
2 changes: 0 additions & 2 deletions examples/nextjs/src/components/RecommendedCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ const RecommendedCard = ({ contentlet }) => {
day: 'numeric'
};

console.log(image);

return (
<div className="flex gap-7 min-h-16 relative">
<EditButton contentlet={contentlet} />
Expand Down
Loading