Document permission inheritance in UI (#18935)
* check the full path for permissions * fix race condition * wip update permission when variants change * Populate ancestor keys on document tree response items. * Populate ancestor keys on document collection response items. * Update OpenApi.json * generate server models * update types * map data * add ancestor context * set ancestors in context * use ancestor context in tree * clean up * provide ancestor context from a collection item * provide ancestor context from structure context * Use array of objects rather than Ids for the ancestor collection. * Update OpenApi.json. * add ancestor data to mocks * set ancestors ids in mocks * omit ancestors for recycle bin item * use correct models for document blueprint mock data * remove constructor * mock documents for testing * add user group permission test data * wip document user permission condition tests * generate new server models * update data efter server models update * clean up * Update entity-actions-table-column-view.element.ts * longer time for not found to appear * use arg * observe alias * set new the right place * remove const --------- Co-authored-by: Andy Butland <abutland73@gmail.com> Co-authored-by: Niels Lyngsø <nsl@umbraco.dk> Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
This commit is contained in:
@@ -1,30 +1,25 @@
|
||||
import type { UmbMockDocumentModel } from '../document/document.data.js';
|
||||
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import {
|
||||
DocumentVariantStateModel,
|
||||
type DocumentBlueprintItemResponseModel,
|
||||
type DocumentBlueprintResponseModel,
|
||||
type DocumentBlueprintTreeItemResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UmbMockDocumentBlueprintModel extends UmbMockDocumentModel {}
|
||||
export type UmbMockDocumentBlueprintModel = DocumentBlueprintResponseModel &
|
||||
DocumentBlueprintItemResponseModel &
|
||||
DocumentBlueprintTreeItemResponseModel;
|
||||
|
||||
export const data: Array<UmbMockDocumentBlueprintModel> = [
|
||||
{
|
||||
ancestors: [],
|
||||
urls: [
|
||||
{
|
||||
culture: 'en-US',
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
template: null,
|
||||
id: 'the-simplest-document-id',
|
||||
createDate: '2023-02-06T15:32:05.350038',
|
||||
parent: null,
|
||||
documentType: {
|
||||
id: 'the-simplest-document-type-id',
|
||||
icon: 'icon-document',
|
||||
},
|
||||
hasChildren: false,
|
||||
noAccess: false,
|
||||
isProtected: false,
|
||||
isTrashed: false,
|
||||
isFolder: false,
|
||||
name: 'The Simplest Document Blueprint',
|
||||
variants: [
|
||||
{
|
||||
state: DocumentVariantStateModel.DRAFT,
|
||||
|
||||
@@ -8,10 +8,10 @@ import type { UmbMockDocumentBlueprintModel } from './document-blueprint.data.js
|
||||
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import type {
|
||||
CreateDocumentRequestModel,
|
||||
DocumentItemResponseModel,
|
||||
DocumentResponseModel,
|
||||
DocumentTreeItemResponseModel,
|
||||
CreateDocumentBlueprintRequestModel,
|
||||
DocumentBlueprintItemResponseModel,
|
||||
DocumentBlueprintResponseModel,
|
||||
DocumentBlueprintTreeItemResponseModel,
|
||||
DocumentValueResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
@@ -23,41 +23,34 @@ export class UmbDocumentBlueprintMockDB extends UmbEntityMockDbBase<UmbMockDocum
|
||||
createMockDocumentBlueprintMapper,
|
||||
detailResponseMapper,
|
||||
);
|
||||
|
||||
constructor(data: Array<UmbMockDocumentBlueprintModel>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
const treeItemMapper = (model: UmbMockDocumentBlueprintModel): Omit<DocumentTreeItemResponseModel, 'type'> => {
|
||||
const treeItemMapper = (model: UmbMockDocumentBlueprintModel): DocumentBlueprintTreeItemResponseModel => {
|
||||
const documentType = umbDocumentTypeMockDb.read(model.documentType.id);
|
||||
if (!documentType) throw new Error(`Document type with id ${model.documentType.id} not found`);
|
||||
|
||||
return {
|
||||
ancestors: model.ancestors,
|
||||
documentType: {
|
||||
icon: documentType.icon,
|
||||
id: documentType.id,
|
||||
},
|
||||
hasChildren: model.hasChildren,
|
||||
id: model.id,
|
||||
isProtected: model.isProtected,
|
||||
isTrashed: model.isTrashed,
|
||||
noAccess: model.noAccess,
|
||||
isFolder: model.isFolder,
|
||||
name: model.name,
|
||||
parent: model.parent,
|
||||
variants: model.variants,
|
||||
createDate: model.createDate,
|
||||
};
|
||||
};
|
||||
|
||||
const createMockDocumentBlueprintMapper = (request: CreateDocumentRequestModel): UmbMockDocumentBlueprintModel => {
|
||||
const createMockDocumentBlueprintMapper = (
|
||||
request: CreateDocumentBlueprintRequestModel,
|
||||
): UmbMockDocumentBlueprintModel => {
|
||||
const documentType = umbDocumentTypeMockDb.read(request.documentType.id);
|
||||
if (!documentType) throw new Error(`Document type with id ${request.documentType.id} not found`);
|
||||
|
||||
const now = new Date().toString();
|
||||
|
||||
return {
|
||||
ancestors: [],
|
||||
documentType: {
|
||||
id: documentType.id,
|
||||
icon: documentType.icon,
|
||||
@@ -65,10 +58,8 @@ const createMockDocumentBlueprintMapper = (request: CreateDocumentRequestModel):
|
||||
},
|
||||
hasChildren: false,
|
||||
id: request.id ? request.id : UmbId.new(),
|
||||
createDate: now,
|
||||
isProtected: false,
|
||||
isTrashed: false,
|
||||
noAccess: false,
|
||||
isFolder: false,
|
||||
name: request.variants[0].name,
|
||||
parent: request.parent,
|
||||
values: request.values as DocumentValueResponseModel[],
|
||||
variants: request.variants.map((variantRequest) => {
|
||||
@@ -82,35 +73,27 @@ const createMockDocumentBlueprintMapper = (request: CreateDocumentRequestModel):
|
||||
publishDate: null,
|
||||
};
|
||||
}),
|
||||
urls: [],
|
||||
};
|
||||
};
|
||||
|
||||
const detailResponseMapper = (model: UmbMockDocumentBlueprintModel): DocumentResponseModel => {
|
||||
const detailResponseMapper = (model: UmbMockDocumentBlueprintModel): DocumentBlueprintResponseModel => {
|
||||
return {
|
||||
documentType: model.documentType,
|
||||
id: model.id,
|
||||
isTrashed: model.isTrashed,
|
||||
template: model.template,
|
||||
urls: model.urls,
|
||||
values: model.values,
|
||||
variants: model.variants,
|
||||
};
|
||||
};
|
||||
|
||||
const itemMapper = (model: UmbMockDocumentBlueprintModel): DocumentItemResponseModel => {
|
||||
const itemMapper = (model: UmbMockDocumentBlueprintModel): DocumentBlueprintItemResponseModel => {
|
||||
return {
|
||||
documentType: {
|
||||
collection: model.documentType.collection,
|
||||
icon: model.documentType.icon,
|
||||
id: model.documentType.id,
|
||||
},
|
||||
hasChildren: model.hasChildren,
|
||||
id: model.id,
|
||||
isProtected: model.isProtected,
|
||||
isTrashed: model.isTrashed,
|
||||
parent: model.parent,
|
||||
variants: model.variants,
|
||||
name: model.name,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { UmbMockDocumentModel } from '../document.data.js';
|
||||
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
const permissionsTestDocument = {
|
||||
ancestors: [],
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
template: null,
|
||||
id: 'permissions-document-id',
|
||||
createDate: '2023-02-06T15:32:05.350038',
|
||||
parent: null,
|
||||
documentType: {
|
||||
id: 'the-simplest-document-type-id',
|
||||
icon: 'icon-document',
|
||||
},
|
||||
hasChildren: false,
|
||||
noAccess: false,
|
||||
isProtected: false,
|
||||
isTrashed: false,
|
||||
values: [],
|
||||
variants: [
|
||||
{
|
||||
state: DocumentVariantStateModel.PUBLISHED,
|
||||
publishDate: '2023-02-06T15:32:24.957009',
|
||||
culture: null,
|
||||
segment: null,
|
||||
name: 'Permissions',
|
||||
createDate: '2023-02-06T15:32:05.350038',
|
||||
updateDate: '2023-02-06T15:32:24.957009',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const data: Array<UmbMockDocumentModel> = [
|
||||
permissionsTestDocument,
|
||||
{
|
||||
...permissionsTestDocument,
|
||||
ancestors: [{ id: 'permissions-document-id' }],
|
||||
hasChildren: false,
|
||||
id: 'permissions-1-document-id',
|
||||
parent: { id: 'permissions-document-id' },
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/permission-1',
|
||||
},
|
||||
],
|
||||
variants: permissionsTestDocument.variants.map((variant) => ({
|
||||
...variant,
|
||||
name: 'Permissions 1',
|
||||
})),
|
||||
},
|
||||
{
|
||||
...permissionsTestDocument,
|
||||
ancestors: [{ id: 'permissions-document-id' }],
|
||||
hasChildren: true,
|
||||
id: 'permissions-2-document-id',
|
||||
parent: { id: 'permissions-document-id' },
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/permissions-2',
|
||||
},
|
||||
],
|
||||
variants: permissionsTestDocument.variants.map((variant) => ({
|
||||
...variant,
|
||||
name: 'Permissions 2',
|
||||
})),
|
||||
},
|
||||
{
|
||||
...permissionsTestDocument,
|
||||
ancestors: [{ id: 'permissions-document-id' }, { id: 'permissions-2-document-id' }],
|
||||
hasChildren: true,
|
||||
id: 'permission-2-1-document-id',
|
||||
parent: { id: 'permissions-2-document-id' },
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/permissions-1/permissions-2-1',
|
||||
},
|
||||
],
|
||||
variants: permissionsTestDocument.variants.map((variant) => ({
|
||||
...variant,
|
||||
name: 'Permissions 2.1',
|
||||
})),
|
||||
},
|
||||
{
|
||||
...permissionsTestDocument,
|
||||
ancestors: [{ id: 'permissions-document-id' }, { id: 'permissions-2-document-id' }],
|
||||
hasChildren: false,
|
||||
id: 'permissions-2-2-document-id',
|
||||
parent: { id: 'permissions-2-document-id' },
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/permissions-1/permissions-2-2',
|
||||
},
|
||||
],
|
||||
variants: permissionsTestDocument.variants.map((variant) => ({
|
||||
...variant,
|
||||
name: 'Permissions 2.2',
|
||||
})),
|
||||
},
|
||||
{
|
||||
...permissionsTestDocument,
|
||||
ancestors: [
|
||||
{ id: 'permissions-document-id' },
|
||||
{ id: 'permissions-2-document-id' },
|
||||
{ id: 'permissions-2-2-document-id' },
|
||||
],
|
||||
hasChildren: false,
|
||||
id: 'permission-2-2-1-document-id',
|
||||
parent: { id: 'permissions-2-2-document-id' },
|
||||
urls: [
|
||||
{
|
||||
culture: null,
|
||||
url: '/permissions-1/permissions-2-2/permissions-2-2-1',
|
||||
},
|
||||
],
|
||||
variants: permissionsTestDocument.variants.map((variant) => ({
|
||||
...variant,
|
||||
name: 'Permissions 2.2.1',
|
||||
})),
|
||||
},
|
||||
];
|
||||
@@ -1,3 +1,4 @@
|
||||
import { data as permissionsTestData } from './data/permissions-test.data.js';
|
||||
import type {
|
||||
DocumentItemResponseModel,
|
||||
DocumentResponseModel,
|
||||
@@ -1240,4 +1241,5 @@ export const data: Array<UmbMockDocumentModel> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...permissionsTestData,
|
||||
];
|
||||
|
||||
@@ -78,10 +78,24 @@ const createMockDocumentMapper = (request: CreateDocumentRequestModel): UmbMockD
|
||||
const documentType = umbDocumentTypeMockDb.read(request.documentType.id);
|
||||
if (!documentType) throw new Error(`Document type with id ${request.documentType.id} not found`);
|
||||
|
||||
const isRoot = request.parent === null || request.parent === undefined;
|
||||
let ancestors: Array<{ id: string }> = [];
|
||||
|
||||
if (!isRoot) {
|
||||
const parentId = request.parent!.id;
|
||||
|
||||
const parentAncestors = umbDocumentMockDb.tree.getAncestorsOf({ descendantId: parentId }).map((ancestor) => {
|
||||
return {
|
||||
id: ancestor.id,
|
||||
};
|
||||
});
|
||||
ancestors = [...parentAncestors, { id: parentId }];
|
||||
}
|
||||
|
||||
const now = new Date().toString();
|
||||
|
||||
return {
|
||||
ancestors: [],
|
||||
ancestors,
|
||||
documentType: {
|
||||
id: documentType.id,
|
||||
icon: documentType.icon,
|
||||
|
||||
@@ -26,7 +26,29 @@ export const data: Array<UmbMockUserGroupModel> = [
|
||||
'Umb.Document.PublicAccess',
|
||||
'Umb.Document.Rollback',
|
||||
],
|
||||
permissions: [],
|
||||
permissions: [
|
||||
{
|
||||
$type: 'DocumentPermissionPresentationModel',
|
||||
document: {
|
||||
id: 'permissions-document-id',
|
||||
},
|
||||
verbs: ['Umb.Document.Read'],
|
||||
},
|
||||
{
|
||||
$type: 'DocumentPermissionPresentationModel',
|
||||
document: {
|
||||
id: 'permissions-2-document-id',
|
||||
},
|
||||
verbs: ['Umb.Document.Create', 'Umb.Document.Read'],
|
||||
},
|
||||
{
|
||||
$type: 'DocumentPermissionPresentationModel',
|
||||
document: {
|
||||
id: 'permissions-2-2-document-id',
|
||||
},
|
||||
verbs: ['Umb.Document.Delete', 'Umb.Document.Read'],
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
UMB_CONTENT_SECTION_ALIAS,
|
||||
'Umb.Section.Media',
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
const elementName = 'umb-entity-actions-table-column-view';
|
||||
@customElement(elementName)
|
||||
@customElement('umb-entity-actions-table-column-view')
|
||||
export class UmbEntityActionsTableColumnViewElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
value?: UmbEntityModel;
|
||||
|
||||
@state()
|
||||
_isOpen = false;
|
||||
|
||||
override render() {
|
||||
if (!this.value) return nothing;
|
||||
|
||||
@@ -23,6 +19,6 @@ export class UmbEntityActionsTableColumnViewElement extends UmbLitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[elementName]: UmbEntityActionsTableColumnViewElement;
|
||||
'umb-entity-actions-table-column-view': UmbEntityActionsTableColumnViewElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './contexts/ancestors/constants.js';
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { UmbAncestorsEntityContext } from './ancestors.entity-context.js';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export const UMB_ANCESTORS_ENTITY_CONTEXT = new UmbContextToken<UmbAncestorsEntityContext>('UmbAncestorsEntityContext');
|
||||
@@ -0,0 +1,38 @@
|
||||
import { UMB_ANCESTORS_ENTITY_CONTEXT } from './ancestors.entity-context-token.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
/**
|
||||
* A entity context for the ancestors
|
||||
* @class UmbAncestorsEntityContext
|
||||
* @augments {UmbContextBase<UmbAncestorsEntityContext>}
|
||||
* @implements {UmbAncestorsEntityContext}
|
||||
*/
|
||||
export class UmbAncestorsEntityContext extends UmbContextBase<UmbAncestorsEntityContext> {
|
||||
#ancestors = new UmbArrayState<UmbEntityModel>([], (x) => x.unique);
|
||||
ancestors = this.#ancestors.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UMB_ANCESTORS_ENTITY_CONTEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ancestors state
|
||||
* @returns {Array<UmbEntityModel>} - The ancestors state
|
||||
* @memberof UmbAncestorsEntityContext
|
||||
*/
|
||||
getAncestors(): Array<UmbEntityModel> {
|
||||
return this.#ancestors.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ancestors state
|
||||
* @param {Array<UmbEntityModel>} ancestors - The ancestors state
|
||||
* @memberof UmbAncestorsEntityContext
|
||||
*/
|
||||
setAncestors(ancestors: Array<UmbEntityModel>) {
|
||||
this.#ancestors.setValue(ancestors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { UMB_ANCESTORS_ENTITY_CONTEXT } from './ancestors.entity-context-token.js';
|
||||
@@ -0,0 +1 @@
|
||||
export { UmbAncestorsEntityContext } from './ancestors.entity-context.js';
|
||||
@@ -1,3 +1,5 @@
|
||||
export { UMB_ENTITY_CONTEXT } from './entity.context-token.js';
|
||||
export { UmbEntityContext } from './entity.context.js';
|
||||
export * from './constants.js';
|
||||
export * from './contexts/ancestors/index.js';
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbAncestorsEntityContext } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs {
|
||||
treeRepositoryAlias: string;
|
||||
@@ -21,6 +22,8 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
#parent = new UmbObjectState<UmbVariantStructureItemModel | undefined>(undefined);
|
||||
public readonly parent = this.#parent.asObservable();
|
||||
|
||||
#ancestorContext = new UmbAncestorsEntityContext(this);
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs) {
|
||||
// TODO: set up context token
|
||||
super(host, 'UmbMenuStructureWorkspaceContext');
|
||||
@@ -85,6 +88,15 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
};
|
||||
});
|
||||
|
||||
const ancestorEntities = data.map((treeItem) => {
|
||||
return {
|
||||
unique: treeItem.unique,
|
||||
entityType: treeItem.entityType,
|
||||
};
|
||||
});
|
||||
|
||||
this.#ancestorContext.setAncestors(ancestorEntities);
|
||||
|
||||
structureItems.push(...ancestorItems);
|
||||
|
||||
const parent = structureItems[structureItems.length - 2];
|
||||
|
||||
@@ -33,7 +33,7 @@ export class UmbRouteNotFoundElement extends UmbLitElement {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
animation: fadeIn 6s 0.2s forwards;
|
||||
animation: fadeIn 5s 5s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
||||
@@ -171,6 +171,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
|
||||
return (await this._getDataPromise) as GetDataType;
|
||||
}
|
||||
this.resetState();
|
||||
this.setIsNew(false);
|
||||
this.#entityContext.setUnique(unique);
|
||||
this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` });
|
||||
await this.#init;
|
||||
@@ -182,7 +183,6 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
|
||||
if (data) {
|
||||
this._data.setPersisted(data);
|
||||
this._data.setCurrent(data);
|
||||
this.setIsNew(false);
|
||||
|
||||
this.observe(
|
||||
response.asObservable(),
|
||||
@@ -246,8 +246,8 @@ export abstract class UmbEntityDetailWorkspaceContextBase<
|
||||
data = { ...data, ...this.modalContext.data.preset };
|
||||
}
|
||||
|
||||
this.#entityContext.setUnique(data.unique);
|
||||
this.setIsNew(true);
|
||||
this.#entityContext.setUnique(data.unique);
|
||||
this._data.setPersisted(data);
|
||||
this._data.setCurrent(data);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ export class UmbDocumentCollectionServerDataSource implements UmbCollectionDataS
|
||||
const variant = item.variants[0];
|
||||
|
||||
const model: UmbDocumentCollectionItemModel = {
|
||||
ancestors: item.ancestors.map((ancestor) => {
|
||||
return {
|
||||
unique: ancestor.id,
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
};
|
||||
}),
|
||||
unique: item.id,
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
contentTypeAlias: item.documentType.alias,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { UmbDocumentEntityType } from '../entity.js';
|
||||
import type { UmbDocumentItemVariantModel } from '../item/repository/types.js';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
|
||||
|
||||
export interface UmbDocumentCollectionFilterModel extends UmbCollectionFilterModel {
|
||||
@@ -12,6 +13,7 @@ export interface UmbDocumentCollectionFilterModel extends UmbCollectionFilterMod
|
||||
}
|
||||
|
||||
export interface UmbDocumentCollectionItemModel {
|
||||
ancestors: Array<UmbEntityModel>;
|
||||
unique: string;
|
||||
entityType: UmbDocumentEntityType;
|
||||
creator?: string | null;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { UmbDocumentCollectionItemModel } from '../../../types.js';
|
||||
import { UmbAncestorsEntityContext } from '@umbraco-cms/backoffice/entity';
|
||||
import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-document-entity-actions-table-column-view')
|
||||
export class UmbDocumentEntityActionsTableColumnViewElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
public get value(): UmbDocumentCollectionItemModel | undefined {
|
||||
return this._value;
|
||||
}
|
||||
public set value(value: UmbDocumentCollectionItemModel | undefined) {
|
||||
this._value = value;
|
||||
this.#ancestorContext.setAncestors(this._value?.ancestors ?? []);
|
||||
}
|
||||
|
||||
private _value?: UmbDocumentCollectionItemModel | undefined;
|
||||
|
||||
#ancestorContext = new UmbAncestorsEntityContext(this);
|
||||
|
||||
override render() {
|
||||
if (!this._value) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-entity-actions-table-column-view
|
||||
.value=${{ unique: this._value.unique, entityType: this._value.entityType }}>
|
||||
</umb-entity-actions-table-column-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
['umb-document-entity-actions-table-column-view']: UmbDocumentEntityActionsTableColumnViewElement;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
UmbTableSelectedEvent,
|
||||
} from '@umbraco-cms/backoffice/components';
|
||||
|
||||
import './column-layouts/document-entity-actions-table-column-view.element.js';
|
||||
import './column-layouts/document-table-column-name.element.js';
|
||||
import './column-layouts/document-table-column-state.element.js';
|
||||
|
||||
@@ -143,11 +144,8 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
if (column.alias === 'entityActions') {
|
||||
return {
|
||||
columnAlias: 'entityActions',
|
||||
value: html`<umb-entity-actions-table-column-view
|
||||
.value=${{
|
||||
entityType: item.entityType,
|
||||
unique: item.unique,
|
||||
}}></umb-entity-actions-table-column-view>`,
|
||||
value: html`<umb-document-entity-actions-table-column-view
|
||||
.value=${item}></umb-document-entity-actions-table-column-view>`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export class UmbDocumentSaveAndPublishWorkspaceAction extends UmbWorkspaceAction
|
||||
will first be triggered when the condition is changed to permitted */
|
||||
this.disable();
|
||||
|
||||
const condition = new UmbDocumentUserPermissionCondition(host, {
|
||||
new UmbDocumentUserPermissionCondition(host, {
|
||||
host,
|
||||
config: {
|
||||
alias: 'Umb.Condition.UserPermission.Document',
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE, UMB_USER_PERMISSION_DOCUMENT_PUBLISH],
|
||||
},
|
||||
onChange: () => {
|
||||
if (condition.permitted) {
|
||||
onChange: (permitted) => {
|
||||
if (permitted) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { UmbDocumentTreeItemModel } from '../../tree/index.js';
|
||||
import type { UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UmbDocumentRecycleBinTreeItemModel extends UmbDocumentTreeItemModel {}
|
||||
export interface UmbDocumentRecycleBinTreeItemModel extends Omit<UmbDocumentTreeItemModel, 'ancestors'> {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UmbDocumentRecycleBinTreeRootModel extends UmbTreeRootModel {}
|
||||
|
||||
@@ -66,6 +66,12 @@ const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
|
||||
|
||||
const mapper = (item: DocumentTreeItemResponseModel): UmbDocumentTreeItemModel => {
|
||||
return {
|
||||
ancestors: item.ancestors.map((ancestor) => {
|
||||
return {
|
||||
unique: ancestor.id,
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
};
|
||||
}),
|
||||
unique: item.id,
|
||||
parent: {
|
||||
unique: item.parent ? item.parent.id : null,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UmbDocumentItemDataResolver } from '../../item/index.js';
|
||||
import { UmbDefaultTreeItemContext } from '@umbraco-cms/backoffice/tree';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin';
|
||||
import { UmbAncestorsEntityContext } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
export class UmbDocumentTreeItemContext extends UmbDefaultTreeItemContext<
|
||||
UmbDocumentTreeItemModel,
|
||||
@@ -10,12 +11,14 @@ export class UmbDocumentTreeItemContext extends UmbDefaultTreeItemContext<
|
||||
> {
|
||||
// TODO: Provide this together with the EntityContext, ideally this takes part via a extension-type [NL]
|
||||
#isTrashedContext = new UmbIsTrashedEntityContext(this);
|
||||
#ancestorsContext = new UmbAncestorsEntityContext(this);
|
||||
#item = new UmbDocumentItemDataResolver(this);
|
||||
|
||||
readonly name = this.#item.name;
|
||||
readonly icon = this.#item.icon;
|
||||
readonly isDraft = this.#item.isDraft;
|
||||
|
||||
readonly ancestors = this._treeItem.asObservablePart((item) => item?.ancestors ?? []);
|
||||
readonly isTrashed = this._treeItem.asObservablePart((item) => item?.isTrashed ?? false);
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
@@ -24,6 +27,10 @@ export class UmbDocumentTreeItemContext extends UmbDefaultTreeItemContext<
|
||||
this.observe(this.isTrashed, (isTrashed) => {
|
||||
this.#isTrashedContext.setIsTrashed(isTrashed);
|
||||
});
|
||||
|
||||
this.observe(this.ancestors, (ancestors) => {
|
||||
this.#ancestorsContext.setAncestors(ancestors);
|
||||
});
|
||||
}
|
||||
|
||||
public override setTreeItem(treeItem: UmbDocumentTreeItemModel | undefined) {
|
||||
|
||||
@@ -7,8 +7,10 @@ import type {
|
||||
} from '@umbraco-cms/backoffice/tree';
|
||||
import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
export interface UmbDocumentTreeItemModel extends UmbTreeItemModel {
|
||||
ancestors: Array<UmbEntityModel>;
|
||||
entityType: UmbDocumentEntityType;
|
||||
noAccess: boolean;
|
||||
isTrashed: boolean;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbCurrentUserContext, UmbCurrentUserStore } from '@umbraco-cms/backoffice/current-user';
|
||||
import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbDocumentUserPermissionCondition } from './document-user-permission.condition';
|
||||
import { UmbAncestorsEntityContext, UmbEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import {
|
||||
UMB_DOCUMENT_ENTITY_TYPE,
|
||||
UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS,
|
||||
UMB_USER_PERMISSION_DOCUMENT_READ,
|
||||
} from '@umbraco-cms/backoffice/document';
|
||||
|
||||
@customElement('test-controller-host')
|
||||
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {
|
||||
currentUserContext = new UmbCurrentUserContext(this);
|
||||
entityContext = new UmbEntityContext(this);
|
||||
ancestorsContext = new UmbAncestorsEntityContext(this);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new UmbNotificationContext(this);
|
||||
new UmbCurrentUserStore(this);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.currentUserContext.load();
|
||||
}
|
||||
|
||||
setEntity(entity: UmbEntityModel) {
|
||||
this.entityContext.setUnique(entity.unique);
|
||||
this.entityContext.setEntityType(entity.entityType);
|
||||
}
|
||||
|
||||
setEntityAncestors(ancestors: Array<UmbEntityModel>) {
|
||||
this.ancestorsContext.setAncestors(ancestors);
|
||||
}
|
||||
}
|
||||
|
||||
describe('UmbDocumentUserPermissionCondition', () => {
|
||||
let hostElement: UmbTestControllerHostElement;
|
||||
let condition: UmbDocumentUserPermissionCondition;
|
||||
|
||||
beforeEach(async () => {
|
||||
hostElement = new UmbTestControllerHostElement();
|
||||
document.body.appendChild(hostElement);
|
||||
await hostElement.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('Specific permissions', () => {
|
||||
it('should return true if a user has permissions', (done) => {
|
||||
// Sets the current entity data
|
||||
hostElement.setEntity({
|
||||
unique: 'permissions-document-id',
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
});
|
||||
|
||||
// This entity does not have any ancestors.
|
||||
hostElement.setEntityAncestors([]);
|
||||
|
||||
// We expect to find the read permission on the current entity
|
||||
condition = new UmbDocumentUserPermissionCondition(hostElement, {
|
||||
host: hostElement,
|
||||
config: {
|
||||
alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS,
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ],
|
||||
},
|
||||
onChange: (permitted) => {
|
||||
expect(permitted).to.be.true;
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inherited permissions', () => {
|
||||
it('should inherit permissions from closest ancestor with specific permissions set', (done) => {
|
||||
// Sets the current entity data
|
||||
hostElement.setEntity({
|
||||
unique: 'permissions-document-1-id',
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
});
|
||||
|
||||
// Sets the ancestors of the current entity. These are the ancestors that will be checked for permissions.
|
||||
hostElement.setEntityAncestors([{ unique: 'permissions-document-id', entityType: UMB_DOCUMENT_ENTITY_TYPE }]);
|
||||
|
||||
// We expect to find the read permission on the ancestor
|
||||
condition = new UmbDocumentUserPermissionCondition(hostElement, {
|
||||
host: hostElement,
|
||||
config: {
|
||||
alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS,
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ],
|
||||
},
|
||||
onChange: (permitted) => {
|
||||
expect(permitted).to.be.true;
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fallback Permissions', () => {
|
||||
it('should use the fallback permissions if no specific permissions are set for the entity or ancestors', (done) => {
|
||||
// Sets the current entity to a document without permissions
|
||||
hostElement.setEntity({
|
||||
unique: 'no-permissions-document-id',
|
||||
entityType: UMB_DOCUMENT_ENTITY_TYPE,
|
||||
});
|
||||
|
||||
// Sets the ancestors of the current entity. These are the ancestors that will be checked for permissions.
|
||||
// This ancestor does not have any permissions either.
|
||||
hostElement.setEntityAncestors([
|
||||
{ unique: 'no-permissions-parent-document-id', entityType: UMB_DOCUMENT_ENTITY_TYPE },
|
||||
]);
|
||||
|
||||
// We expect to find the read permission in the fallback permissions
|
||||
condition = new UmbDocumentUserPermissionCondition(hostElement, {
|
||||
host: hostElement,
|
||||
config: {
|
||||
alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS,
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ],
|
||||
},
|
||||
onChange: (permitted) => {
|
||||
expect(permitted).to.be.true;
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isDocumentUserPermission } from '../utils.js';
|
||||
import type { UmbDocumentUserPermissionConditionConfig } from './types.js';
|
||||
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
|
||||
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_ANCESTORS_ENTITY_CONTEXT, UMB_ENTITY_CONTEXT, type UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
|
||||
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
@@ -20,6 +20,7 @@ export class UmbDocumentUserPermissionCondition extends UmbControllerBase implem
|
||||
#documentPermissions: Array<DocumentPermissionPresentationModel> = [];
|
||||
#fallbackPermissions: string[] = [];
|
||||
#onChange: UmbOnChangeCallbackType;
|
||||
#ancestors: Array<UmbEntityUnique> = [];
|
||||
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
@@ -54,6 +55,17 @@ export class UmbDocumentUserPermissionCondition extends UmbControllerBase implem
|
||||
'umbUserPermissionEntityContextObserver',
|
||||
);
|
||||
});
|
||||
|
||||
this.consumeContext(UMB_ANCESTORS_ENTITY_CONTEXT, (instance) => {
|
||||
this.observe(
|
||||
instance?.ancestors,
|
||||
(ancestors) => {
|
||||
this.#ancestors = ancestors.map((item) => item.unique);
|
||||
this.#checkPermissions();
|
||||
},
|
||||
'observeAncestors',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#checkPermissions() {
|
||||
@@ -68,21 +80,29 @@ export class UmbDocumentUserPermissionCondition extends UmbControllerBase implem
|
||||
return;
|
||||
}
|
||||
|
||||
/* If there are document permission we check if there are permissions for the current document
|
||||
If there aren't we use the fallback permissions */
|
||||
// If there are document permissions, we need to check the full path to see if any permissions are defined for the current document
|
||||
// If we find multiple permissions in the same path, we will apply the closest one
|
||||
if (hasDocumentPermissions) {
|
||||
const permissionsForCurrentDocument = this.#documentPermissions.find(
|
||||
(permission) => permission.document.id === this.#unique,
|
||||
);
|
||||
// Path including the current document and all ancestors
|
||||
const path = [...this.#ancestors, this.#unique].filter((unique) => unique !== null);
|
||||
// Reverse the path to find the closest document permission quickly
|
||||
const reversedPath = [...path].reverse();
|
||||
const documentPermissionsMap = new Map(this.#documentPermissions.map((p) => [p.document.id, p]));
|
||||
|
||||
// Find the closest document permission in the path
|
||||
const closestDocumentPermission = reversedPath.find((id) => documentPermissionsMap.has(id));
|
||||
|
||||
// Retrieve the corresponding permission data
|
||||
const match = closestDocumentPermission ? documentPermissionsMap.get(closestDocumentPermission) : undefined;
|
||||
|
||||
// no permissions for the current document - use the fallback permissions
|
||||
if (!permissionsForCurrentDocument) {
|
||||
if (!match) {
|
||||
this.#check(this.#fallbackPermissions);
|
||||
return;
|
||||
}
|
||||
|
||||
// we found permissions for the current document - check them
|
||||
this.#check(permissionsForCurrentDocument.verbs);
|
||||
// we found permissions - check them
|
||||
this.#check(match.verbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,13 @@ export class UmbDocumentWorkspaceContext
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_CREATE],
|
||||
},
|
||||
onChange: (permitted: boolean) => {
|
||||
if (permitted === this.#userCanCreate) return;
|
||||
this.#userCanCreate = permitted;
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_CREATE,
|
||||
this.#userCanCreate,
|
||||
'You do not have permission to create documents.',
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -110,11 +116,31 @@ export class UmbDocumentWorkspaceContext
|
||||
allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE],
|
||||
},
|
||||
onChange: (permitted: boolean) => {
|
||||
if (permitted === this.#userCanUpdate) return;
|
||||
this.#userCanUpdate = permitted;
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_UPDATE,
|
||||
this.#userCanUpdate,
|
||||
'You do not have permission to update documents.',
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
this.observe(this.variants, () => {
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_CREATE,
|
||||
this.#userCanCreate,
|
||||
'You do not have permission to create documents.',
|
||||
);
|
||||
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_UPDATE,
|
||||
this.#userCanUpdate,
|
||||
'You do not have permission to update documents.',
|
||||
);
|
||||
});
|
||||
|
||||
this.routes.setRoutes([
|
||||
{
|
||||
path: UMB_CREATE_FROM_BLUEPRINT_DOCUMENT_WORKSPACE_PATH_PATTERN.toString(),
|
||||
@@ -147,13 +173,6 @@ export class UmbDocumentWorkspaceContext
|
||||
const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique;
|
||||
const documentTypeUnique = info.match.params.documentTypeUnique;
|
||||
await this.create({ entityType: parentEntityType, unique: parentUnique }, documentTypeUnique);
|
||||
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_CREATE,
|
||||
this.#userCanCreate,
|
||||
'You do not have permission to create documents.',
|
||||
);
|
||||
|
||||
new UmbWorkspaceIsNewRedirectController(
|
||||
this,
|
||||
this,
|
||||
@@ -168,11 +187,6 @@ export class UmbDocumentWorkspaceContext
|
||||
this.removeUmbControllerByAlias(UmbWorkspaceIsNewRedirectControllerAlias);
|
||||
const unique = info.match.params.unique;
|
||||
await this.load(unique);
|
||||
this.#setReadOnlyStateForUserPermission(
|
||||
UMB_USER_PERMISSION_DOCUMENT_UPDATE,
|
||||
this.#userCanUpdate,
|
||||
'You do not have permission to update documents.',
|
||||
);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user