Skip to content

Add third-party permissions section to user group workspace #19777

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,7 @@ export default {
permissionsDefault: 'Default permissions',
permissionsGranular: 'Granular permissions',
permissionsGranularHelp: 'Set permissions for specific nodes',
permissionsExtensions: 'Third-party permissions',
granularRightsLabel: 'Documents',
granularRightsDescription: 'Assign permissions to specific documents',
permissionsEntityGroup_document: 'Document',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbUserGroupPermissionsListBaseElement } from './user-group-permission-list-base.element.js';

@customElement('umb-user-group-entity-user-permission-list')
export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement {
@state()
private _fallBackPermissions?: Array<string>;

export class UmbUserGroupEntityUserPermissionListElement extends UmbUserGroupPermissionsListBaseElement {
@state()
private _groups: Array<{ entityType: string; headline: string }> = [];

#userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE;

constructor() {
super();

this.#observeEntityUserPermissions();

this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => {
this.#userGroupWorkspaceContext = instance;
this.observe(
this.#userGroupWorkspaceContext?.fallbackPermissions,
(fallbackPermissions) => {
this._fallBackPermissions = fallbackPermissions;
},
'umbUserGroupEntityUserPermissionsObserver',
);
});
}

#observeEntityUserPermissions() {
Expand All @@ -50,14 +31,6 @@ export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement {
);
}

#onPermissionChange(event: UmbSelectionChangeEvent) {
event.stopPropagation();
const target = event.target as any;
const verbs = target.allowedVerbs;
if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined');
this.#userGroupWorkspaceContext?.setFallbackPermissions(verbs);
}

override render() {
return html` ${this._groups.map((group) => this.#renderPermissionsForEntityType(group))}`;
}
Expand All @@ -68,11 +41,9 @@ export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement {
<umb-input-entity-user-permission
.entityType=${group.entityType}
.allowedVerbs=${this._fallBackPermissions || []}
@change=${this.#onPermissionChange}></umb-input-entity-user-permission>
@change=${this.onPermissionChange}></umb-input-entity-user-permission>
`;
}

static override styles = [UmbTextStyles];
}

export default UmbUserGroupEntityUserPermissionListElement;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { customElement, html, nothing, state } from '@umbraco-cms/backoffice/external/lit';
import type {
ManifestExtensionPermissions,
ManifestExtensionUserPermission,
} from '@umbraco-cms/backoffice/user-permission';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import { UmbUserGroupPermissionsListBaseElement } from './user-group-permission-list-base.element.js';

@customElement('umb-user-group-extension-permission-list')
export class UmbUserGroupExtensionPermissionsListElement extends UmbUserGroupPermissionsListBaseElement {
@state()
private _manifests?: ManifestExtensionUserPermission[];

@state()
private _extensionElements = new Map<string, HTMLElement>();

@state()
private _extensions?: ManifestExtensionPermissions[];

constructor() {
super();

this.#observeExtensionPermissions();
this.#observeEntityUserPermissions();
}

#observeExtensionPermissions() {
this.observe(
umbExtensionsRegistry.byType('extensionPermissions'),
async (manifests) => {
this._extensions = manifests;

// Pre-create extension elements for each manifest
for (const manifest of manifests) {
if (!manifest.element && !manifest.js) continue;
const element = await createExtensionElement(manifest);

if (element) {
this._extensionElements.set(manifest.meta.extensionAlias, element);
}
}
this.requestUpdate('_extensionElements');
},
'umbExtensionPermissionsObserver',
);
}

#observeEntityUserPermissions() {
this.observe(
umbExtensionsRegistry.byType('extensionUserPermission'),
(manifests) => (this._manifests = manifests),
'umbExtensionUserPermissionsObserver',
);
}

#groupPermissionsForExtension(manifests: ManifestExtensionUserPermission[]) {
const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))];

return entityTypes
.map((entityType) => ({
entityType,
headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`),
}))
.sort((a, b) => a.headline.localeCompare(b.headline));
}

#renderProperty(manifest: ManifestExtensionPermissions) {
const label = manifest.meta.labelKey ? this.localize.term(manifest.meta.labelKey) : manifest.meta.label;
const description = manifest.meta.descriptionKey
? this.localize.term(manifest.meta.descriptionKey)
: manifest.meta.description;

const permissions = this._manifests?.filter((x) => x.forExtension === manifest.meta.extensionAlias) || [];
const groupedPermissions = this.#groupPermissionsForExtension(permissions);

return html`
<umb-property-layout .label=${label || ''} .description=${description || ''}>
<div slot="editor">
${this._extensionElements.get(manifest.meta.extensionAlias)}
${groupedPermissions.map(
(group) =>
html` <h4>${group.headline}</h4>
<umb-input-extension-user-permission
.forExtension=${manifest.meta.extensionAlias}
.entityType=${group.entityType}
.allowedVerbs=${this._fallBackPermissions || []}
@change=${this.onPermissionChange}></umb-input-extension-user-permission>`,
)}
</div>
</umb-property-layout>
`;
}

override render() {
if (!this._extensions) return nothing;

return html`${this._extensions.map((extension) => this.#renderProperty(extension))}`;
}
}

export default UmbUserGroupExtensionPermissionsListElement;

declare global {
interface HTMLElementTagNameMap {
'umb-user-group-extension-permission-list': UmbUserGroupExtensionPermissionsListElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';

export abstract class UmbUserGroupPermissionsListBaseElement extends UmbLitElement {
protected userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE;

@state()
protected _fallBackPermissions?: Array<string>;

constructor() {
super();

this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => {
this.userGroupWorkspaceContext = instance;
this.observe(
this.userGroupWorkspaceContext?.fallbackPermissions,
(fallbackPermissions) => {
this._fallBackPermissions = fallbackPermissions;
},
'umbUserGroupFallbackPermissionsObserver',
);
});
}

protected renderPermissionsForEntityType(group: { entityType: string; headline: string }) {
return html`
<h4>${group.headline}</h4>
<umb-input-entity-user-permission
.entityType=${group.entityType}
.allowedVerbs=${this._fallBackPermissions || []}
@change=${this.onPermissionChange}></umb-input-entity-user-permission>
`;
}

protected onPermissionChange(event: UmbSelectionChangeEvent) {
event.stopPropagation();
const target = event.target as any;
const verbs = target.allowedVerbs;

if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined');

this.userGroupWorkspaceContext?.setFallbackPermissions(verbs);
}

static override styles = [UmbTextStyles];
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/component

import './components/user-group-entity-user-permission-list.element.js';
import './components/user-group-granular-permission-list.element.js';
import './components/user-group-extension-permission-list.element.js';

@customElement('umb-user-group-workspace-editor')
export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
Expand Down Expand Up @@ -256,6 +257,11 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
<div slot="headline"><umb-localize key="user_permissionsGranular"></umb-localize></div>
<umb-user-group-granular-permission-list></umb-user-group-granular-permission-list>
</uui-box>

<uui-box>
<div slot="headline"><umb-localize key="user_permissionsExtensions"></umb-localize></div>
<umb-user-group-extension-permission-list></umb-user-group-extension-permission-list>
</uui-box>
</umb-stack>
</div>
`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import './input-entity-user-permission/input-entity-user-permission.element.js';
import './input-user-permission-verb/input-user-permission-verb.element.js';
import './input-extension-user-permission/input-extension-user-permission.element.js';
import './input-user-permission-base.element.js';

export * from './input-entity-user-permission/input-entity-user-permission.element.js';
export * from './input-user-permission-verb/input-user-permission-verb.element.js';
export * from './input-extension-user-permission/input-extension-user-permission.element.js';
export * from './input-user-permission-base.element.js';
Original file line number Diff line number Diff line change
@@ -1,120 +1,24 @@
import type { ManifestEntityUserPermission } from '../../entity-user-permission.extension.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property, state, nothing, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { UmbUserPermissionVerbElement } from '@umbraco-cms/backoffice/user';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbInputUserPermissionBaseElement } from '../input-user-permission-base.element.js';

@customElement('umb-input-entity-user-permission')
export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(UmbLitElement) {
@property({ type: String, attribute: 'entity-type' })
public get entityType(): string {
return this._entityType;
}
public set entityType(value: string) {
if (value === this._entityType) return;
this._entityType = value;
this.#observeEntityUserPermissions();
}
private _entityType: string = '';

@property({ attribute: false })
allowedVerbs: Array<string> = [];

@state()
private _manifests: Array<ManifestEntityUserPermission> = [];

#manifestObserver?: UmbObserverController<Array<ManifestEntityUserPermission>>;

protected override getFormElement() {
return undefined;
}

#isAllowed(permissionVerbs: Array<string>) {
return permissionVerbs.every((verb) => this.allowedVerbs.includes(verb));
}
export class UmbInputEntityUserPermissionElement extends UmbInputUserPermissionBaseElement<ManifestEntityUserPermission> {

#observeEntityUserPermissions() {
this.#manifestObserver?.destroy();
observePermissions() {
this.manifestObserver?.destroy();

this.#manifestObserver = this.observe(
this.manifestObserver = this.observe(
umbExtensionsRegistry.byType('entityUserPermission'),
(userPermissionManifests) => {
this._manifests = userPermissionManifests.filter((manifest) =>
this.manifests = userPermissionManifests.filter((manifest) =>
manifest.forEntityTypes.includes(this.entityType),
);
},
'umbUserPermissionManifestsObserver',
);
}

#onChangeUserPermission(event: UmbChangeEvent, permissionVerbs: Array<string>) {
event.stopPropagation();
const target = event.target as UmbUserPermissionVerbElement;
if (target.allowed) {
this.#addUserPermission(permissionVerbs);
} else {
this.#removeUserPermission(permissionVerbs);
}
}

#addUserPermission(permissionVerbs: Array<string>) {
const verbs = [...this.allowedVerbs, ...permissionVerbs];
// ensure we only have unique verbs
this.allowedVerbs = [...new Set(verbs)];
this.dispatchEvent(new UmbChangeEvent());
}

#removeUserPermission(permissionVerbs: Array<string>) {
this.allowedVerbs = this.allowedVerbs.filter((p) => !permissionVerbs.includes(p));
this.dispatchEvent(new UmbChangeEvent());
}

override render() {
return html`${this.#renderGroupedPermissions(this._manifests)} `;
}

#renderGroupedPermissions(permissionManifests: Array<ManifestEntityUserPermission>) {
// TODO: groupBy is not known by TS yet
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const groupedPermissions = Object.groupBy(
permissionManifests,
(manifest: ManifestEntityUserPermission) => manifest.meta.group,
) as Record<string, Array<ManifestEntityUserPermission>>;
return html`
${Object.entries(groupedPermissions).map(
([group, manifests]) => html`
${group !== 'undefined'
? html` <h5><umb-localize .key=${`actionCategories_${group}`}>${group}</umb-localize></h5> `
: nothing}
<div>${manifests.map((manifest) => html` ${this.#renderPermission(manifest)} `)}</div>
`,
)}
`;
}

#renderPermission(manifest: ManifestEntityUserPermission) {
return html` <umb-input-user-permission-verb
label=${ifDefined(manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name)}
description=${ifDefined(manifest.meta.description ? this.localize.string(manifest.meta.description) : undefined)}
?allowed=${this.#isAllowed(manifest.meta.verbs)}
@change=${(event: UmbChangeEvent) =>
this.#onChangeUserPermission(event, manifest.meta.verbs)}></umb-input-user-permission-verb>`;
}

override disconnectedCallback() {
super.disconnectedCallback();
this.#manifestObserver?.destroy();
}

static override styles = css`
umb-input-user-permission-verb:not(:last-of-type) {
border-bottom: 1px solid var(--uui-color-divider);
}
`;
}

export default UmbInputEntityUserPermissionElement;
Expand Down
Loading
Loading