Merge branch 'main' into v17/dev

This commit is contained in:
Niels Lyngsø
2025-10-08 17:23:48 +02:00
18 changed files with 376 additions and 4 deletions

View File

@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1 = 'Example.Dictionary.Action1';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2 = 'Example.Dictionary.Action2';

View File

@@ -0,0 +1,9 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
export class ExampleAction1EntityAction extends UmbEntityActionBase<never> {
override async execute() {
alert('Example action 1 executed');
}
}
export { ExampleAction1EntityAction as default };

View File

@@ -0,0 +1,9 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
export class ExampleAction2EntityAction extends UmbEntityActionBase<never> {
override async execute() {
alert('Example action 2 executed');
}
}
export { ExampleAction2EntityAction as default };

View File

@@ -0,0 +1,47 @@
import {
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1,
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2,
} from '../constants.js';
import { UMB_DICTIONARY_ENTITY_TYPE } from '@umbraco-cms/backoffice/dictionary';
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/user-permission';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'entityAction',
kind: 'default',
alias: 'Example.EntityAction.Action1',
name: 'Action 1 Entity Action',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
api: () => import('./action-1.js'),
weight: 10000,
meta: {
label: 'Action 1',
icon: 'icon-car',
},
conditions: [
{
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1],
},
],
},
{
type: 'entityAction',
kind: 'default',
alias: 'Example.EntityAction.Action2',
name: 'Action 2 Entity Action',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
api: () => import('./action-2.js'),
weight: 9000,
meta: {
label: 'Action 2',
icon: 'icon-bus',
},
conditions: [
{
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2],
},
],
},
];

View File

@@ -0,0 +1,30 @@
import {
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1,
EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2,
} from '../constants.js';
import { UMB_DICTIONARY_ENTITY_TYPE } from '@umbraco-cms/backoffice/dictionary';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'entityUserPermission',
alias: 'Example.EntityUserPermission.Entity.Action1',
name: 'Action 1 for Entity User Permission',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
meta: {
verbs: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_1],
label: 'Action 1',
description: 'Description for action 1',
},
},
{
type: 'entityUserPermission',
alias: 'Example.EntityUserPermission.Entity.Action2',
name: 'Action 2 for Entity User Permission',
forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE],
meta: {
verbs: [EXAMPLE_USER_PERMISSION_DICTIONARY_ACTION_2],
label: 'Action 2',
description: 'Description for action 2',
},
},
];

View File

@@ -0,0 +1,9 @@
import { manifests as entityUserPermissionManifests } from './entity-user-permission/manifests.js';
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as localizationManifests } from './localization/manifests.js';
export const manifests: Array<UmbExtensionManifest> = [
...entityUserPermissionManifests,
...entityActionManifests,
...localizationManifests,
];

View File

@@ -0,0 +1,6 @@
export default {
user: {
// eslint-disable-next-line @typescript-eslint/naming-convention
permissionsEntityGroup_dictionary: 'Dictionary',
},
};

View File

@@ -0,0 +1,12 @@
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'localization',
alias: 'Example.UserPermission.Localization.En',
name: 'en-US User Permission Localization Example',
js: () => import('./en.js'),
weight: 1,
meta: {
culture: 'en',
},
},
];

View File

@@ -198,8 +198,8 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
} else {
return html`
<div id="header">
<b>${this.property.name}</b>
<i>${this.property.alias}</i>
<p><b>${this.property.name}</b></p>
<p><i>${this.property.alias}</i></p>
<p>${this.property.description}</p>
</div>
<div id="editor">
@@ -451,6 +451,12 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
border-radius: var(--uui-border-radius);
}
:host([_inherited]) {
#header {
padding: 0 var(--uui-size-3, 9px);
}
}
p {
margin-bottom: 0;
}
@@ -470,6 +476,11 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
--uui-button-background-color: var(--uui-color-background);
--uui-button-background-color-hover: var(--uui-color-background);
}
#editor:not(uui-button) {
background-color: var(--uui-color-background);
border-radius: var(--uui-button-border-radius, var(--uui-border-radius, 3px));
min-height: 143px;
}
#editor uui-action-bar {
--uui-button-background-color: var(--uui-color-surface);
--uui-button-background-color-hover: var(--uui-color-surface);

View File

@@ -116,7 +116,7 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
#getHref(structureItem: any) {
if (structureItem.isFolder) return undefined;
const workspaceBasePath = `section/${this.#sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit`;
return `${workspaceBasePath}/${structureItem.unique}/${this._workspaceActiveVariantId?.culture}`;
return `${workspaceBasePath}/${structureItem.unique}/${this._workspaceActiveVariantId?.toCultureString()}`;
}
override render() {

View File

@@ -0,0 +1 @@
export * from './fallback-permission-condition/constants.js';

View File

@@ -0,0 +1 @@
export const UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS = 'Umb.Condition.UserPermission.Fallback';

View File

@@ -0,0 +1,142 @@
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 { UmbFallbackUserPermissionCondition } from './fallback-user-permission.condition';
import { UMB_USER_PERMISSION_DOCUMENT_READ } from '@umbraco-cms/backoffice/document';
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from './constants';
@customElement('test-controller-host')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {
currentUserContext = new UmbCurrentUserContext(this);
constructor() {
super();
new UmbNotificationContext(this);
new UmbCurrentUserStore(this);
}
async init() {
await this.currentUserContext.load();
}
}
describe('UmbFallbackUserPermissionCondition', () => {
let hostElement: UmbTestControllerHostElement;
let condition: UmbFallbackUserPermissionCondition;
beforeEach(async () => {
hostElement = new UmbTestControllerHostElement();
document.body.appendChild(hostElement);
await hostElement.init();
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('Fallback Permissions', () => {
it('should permit the condition when allOf is satisfied', (done) => {
let callbackCount = 0;
// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ],
},
onChange: () => {
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
}
},
});
});
it('should forbid the condition when allOf is not satisfied', (done) => {
let callbackCount = 0;
// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
allOf: [UMB_USER_PERMISSION_DOCUMENT_READ, 'non-existing-permission'],
},
onChange: () => {
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}
},
});
// The onChange callback is not called when the condition is false, so we need to wait and check manually
setTimeout(() => {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}, 200);
});
it('should permit the condition when oneOf is satisfied', (done) => {
let callbackCount = 0;
// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
oneOf: [UMB_USER_PERMISSION_DOCUMENT_READ, 'non-existing-permission'],
},
onChange: () => {
/* The onChange callback is not called when the condition is false, so this should never be called
But in case it is, we want to fail the test */
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.true;
condition.hostDisconnected();
done();
}
},
});
});
it('should forbid the condition when oneOf is not satisfied', (done) => {
let callbackCount = 0;
// We expect to find the read permission in the fallback permissions
condition = new UmbFallbackUserPermissionCondition(hostElement, {
host: hostElement,
config: {
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
oneOf: ['non-existing-permission', 'another-non-existing-permission'],
},
onChange: () => {
/* The onChange callback is not called when the condition is false, so this should never be called
But in case it is, we want to fail the test */
callbackCount++;
if (callbackCount === 1) {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}
},
});
// The onChange callback is not called when the condition is false, so we need to wait and check manually
setTimeout(() => {
expect(condition.permitted).to.be.false;
condition.hostDisconnected();
done();
}, 200);
});
});
});

View File

@@ -0,0 +1,55 @@
import type { UmbFallbackUserPermissionConditionConfig } from './types.js';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
export class UmbFallbackUserPermissionCondition
extends UmbConditionBase<UmbFallbackUserPermissionConditionConfig>
implements UmbExtensionCondition
{
constructor(
host: UmbControllerHost,
args: UmbConditionControllerArguments<UmbFallbackUserPermissionConditionConfig>,
) {
super(host, args);
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
this.observe(
context?.currentUser,
(currentUser) => {
const fallbackPermissions = currentUser?.fallbackPermissions || [];
this.#check(fallbackPermissions);
},
'umbUserFallbackPermissionConditionObserver',
);
});
}
#check(verbs: Array<string>) {
/* we default to true se we don't require both allOf and oneOf to be defined
but they can be combined for more complex scenarios */
let allOfPermitted = true;
let oneOfPermitted = true;
// check if all of the verbs are present
if (this.config.allOf?.length) {
allOfPermitted = this.config.allOf.every((verb) => verbs.includes(verb));
}
// check if at least one of the verbs is present
if (this.config.oneOf?.length) {
oneOfPermitted = this.config.oneOf.some((verb) => verbs.includes(verb));
}
// if neither allOf or oneOf is defined we default to false
if (!allOfPermitted && !oneOfPermitted) {
allOfPermitted = false;
oneOfPermitted = false;
}
this.permitted = allOfPermitted && oneOfPermitted;
}
}
export { UmbFallbackUserPermissionCondition as api };

View File

@@ -0,0 +1,10 @@
import { UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS } from './constants.js';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'condition',
name: 'User Fallback Permission Condition',
alias: UMB_FALLBACK_USER_PERMISSION_CONDITION_ALIAS,
api: () => import('./fallback-user-permission.condition.js'),
},
];

View File

@@ -0,0 +1,24 @@
import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api';
export type UmbFallbackUserPermissionConditionConfig =
UmbConditionConfigBase<'Umb.Condition.UserPermission.Fallback'> & {
/**
* The user must have all of the permissions in this array for the condition to be met.
* @example
* ["Umb.PermissionOne", "Umb.PermissionTwo"]
*/
allOf?: Array<string>;
/**
* The user must have at least one of the permissions in this array for the condition to be met.
* @example
* ["Umb.PermissionOne", "Umb.PermissionTwo"]
*/
oneOf?: Array<string>;
};
declare global {
interface UmbExtensionConditionConfigMap {
UmbFallbackUserPermissionConditionConfig: UmbFallbackUserPermissionConditionConfig;
}
}

View File

@@ -1,4 +1,5 @@
export * from './components/index.js';
export * from './constants.js';
export * from './modals/index.js';
export type * from './types.js';

View File

@@ -1,3 +1,4 @@
import { manifests as userPermissionModalManifests } from './modals/manifests.js';
import { manifests as fallbackConditionPermission } from './fallback-permission-condition/manifests.js';
export const manifests: Array<UmbExtensionManifest> = [...userPermissionModalManifests];
export const manifests: Array<UmbExtensionManifest> = [...userPermissionModalManifests, ...fallbackConditionPermission];