Merge branch 'main' into v14/feature/search-in-pickers

This commit is contained in:
Mads Rasmussen
2024-08-06 12:58:51 +02:00
committed by GitHub
87 changed files with 1081 additions and 481 deletions

View File

@@ -0,0 +1,7 @@
# Property Dataset Dashboard Example
This example is a work in progress example of how to write a property editor.
This example covers a few points:
- Using an existing Property Editor Schema

View File

@@ -0,0 +1,44 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, LitElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { UmbBlockDataType, UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/extension-registry';
// eslint-disable-next-line local-rules/enforce-umb-prefix-on-element-name
@customElement('example-block-custom-view')
// eslint-disable-next-line local-rules/umb-class-prefix
export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {
//
@property({ attribute: false })
content?: UmbBlockDataType;
override render() {
return html`
<div class="uui-text">
<h5 class="uui-text">My Custom View</h5>
<p>Headline: ${this.content?.headline}</p>
</div>
`;
}
static override styles = [
UmbTextStyles,
css`
:host {
display: block;
height: 100%;
box-sizing: border-box;
background-color: #dddddd;
border-radius: 9px;
padding: 12px;
}
`,
];
}
export default ExampleBlockCustomView;
declare global {
interface HTMLElementTagNameMap {
'example-block-custom-view': ExampleBlockCustomView;
}
}

View File

@@ -0,0 +1,12 @@
import type { ManifestBlockEditorCustomView } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestBlockEditorCustomView> = [
{
type: 'blockEditorCustomView',
alias: 'Umb.blockEditorCustomView.TestView',
name: 'Block Editor Custom View Test',
element: () => import('./block-custom-view.js'),
forContentTypeAlias: 'headlineUmbracoDemoBlock',
forBlockEditor: 'block-grid',
},
];

View File

@@ -0,0 +1,7 @@
# Property Dataset Dashboard Example
This example is a work in progress example of how to write a property editor.
This example covers a few points:
- Using an existing Property Editor Schema

View File

@@ -0,0 +1,31 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestPropertyEditorUi> = [
{
type: 'propertyEditorUi',
alias: 'example.propertyEditorUi.propertyEditor',
name: 'Example Property Editor UI',
element: () => import('./property-editor.js'),
meta: {
label: 'Example Editor',
propertyEditorSchemaAlias: 'Umbraco.ListView',
icon: 'icon-code',
group: 'common',
settings: {
properties: [
{
alias: 'customText',
label: 'Custom text',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox',
},
],
defaultData: [
{
alias: 'customText',
value: 'Default value',
},
],
},
},
},
];

View File

@@ -0,0 +1,20 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
@customElement('example-property-editor')
export class ExamplePropertyEditor extends UmbElementMixin(LitElement) {
override render() {
return html` <h1 class="uui-h2">Property Editor Example</h1> `;
}
static override styles = [UmbTextStyles];
}
export default ExamplePropertyEditor;
declare global {
interface HTMLElementTagNameMap {
'example-property-editor': ExamplePropertyEditor;
}
}

View File

@@ -24,6 +24,7 @@
"./src/packages/property-editors",
"./src/packages/tags",
"./src/packages/templating",
"./src/packages/tiny-mce",
"./src/packages/umbraco-news",
"./src/packages/user",
"./src/packages/webhook"
@@ -52,7 +53,7 @@
"@hey-api/openapi-ts": "^0.48.3",
"@mdx-js/react": "^3.0.1",
"@open-wc/testing": "^4.0.0",
"@playwright/test": "^1.45.2",
"@playwright/test": "^1.45.3",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
@@ -97,7 +98,7 @@
"storybook": "^7.6.17",
"tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.10",
"typedoc": "^0.26.4",
"typedoc": "^0.26.5",
"typescript": "^5.5.3",
"typescript-eslint": "^7.16.1",
"typescript-json-schema": "^0.64.0",
@@ -3386,12 +3387,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.45.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz",
"integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==",
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz",
"integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==",
"dev": true,
"dependencies": {
"playwright": "1.45.2"
"playwright": "1.45.3"
},
"bin": {
"playwright": "cli.js"
@@ -7412,6 +7413,10 @@
"resolved": "src/packages/templating",
"link": true
},
"node_modules/@umbraco-backoffice/tiny-mce": {
"resolved": "src/packages/tiny-mce",
"link": true
},
"node_modules/@umbraco-backoffice/umbraco-news": {
"resolved": "src/packages/umbraco-news",
"link": true
@@ -17498,12 +17503,12 @@
}
},
"node_modules/playwright": {
"version": "1.45.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz",
"integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==",
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz",
"integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==",
"dev": true,
"dependencies": {
"playwright-core": "1.45.2"
"playwright-core": "1.45.3"
},
"bin": {
"playwright": "cli.js"
@@ -17516,9 +17521,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.45.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz",
"integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==",
"version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz",
"integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -20084,9 +20089,9 @@
"dev": true
},
"node_modules/typedoc": {
"version": "0.26.4",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.4.tgz",
"integrity": "sha512-FlW6HpvULDKgc3rK04V+nbFyXogPV88hurarDPOjuuB5HAwuAlrCMQ5NeH7Zt68a/ikOKu6Z/0hFXAeC9xPccQ==",
"version": "0.26.5",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.5.tgz",
"integrity": "sha512-Vn9YKdjKtDZqSk+by7beZ+xzkkr8T8CYoiasqyt4TTRFy5+UHzL/mF/o4wGBjRF+rlWQHDb0t6xCpA3JNL5phg==",
"dev": true,
"dependencies": {
"lunr": "^2.3.9",
@@ -21741,6 +21746,9 @@
"src/packages/templating": {
"name": "@umbraco-backoffice/templating"
},
"src/packages/tiny-mce": {
"name": "@umbraco-backoffice/tiny-mce"
},
"src/packages/umbraco-news": {
"name": "@umbraco-backoffice/umbraco-news"
},

View File

@@ -143,6 +143,7 @@
"./src/packages/property-editors",
"./src/packages/tags",
"./src/packages/templating",
"./src/packages/tiny-mce",
"./src/packages/umbraco-news",
"./src/packages/user",
"./src/packages/webhook"
@@ -222,7 +223,7 @@
"@hey-api/openapi-ts": "^0.48.3",
"@mdx-js/react": "^3.0.1",
"@open-wc/testing": "^4.0.0",
"@playwright/test": "^1.45.2",
"@playwright/test": "^1.45.3",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
@@ -267,7 +268,7 @@
"storybook": "^7.6.17",
"tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.10",
"typedoc": "^0.26.4",
"typedoc": "^0.26.5",
"typescript": "^5.5.3",
"typescript-eslint": "^7.16.1",
"typescript-json-schema": "^0.64.0",

View File

@@ -19,8 +19,8 @@ export class UmbBackofficeContext extends UmbContextBase<UmbBackofficeContext> {
#allowedSections = new UmbBasicState<Array<UmbExtensionManifestInitializer<ManifestSection>>>([]);
public readonly allowedSections = this.#allowedSections.asObservable();
#verison = new UmbStringState(undefined);
public readonly version = this.#verison.asObservable();
#version = new UmbStringState(undefined);
public readonly version = this.#version.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_BACKOFFICE_CONTEXT);
@@ -70,7 +70,7 @@ export class UmbBackofficeContext extends UmbContextBase<UmbBackofficeContext> {
) ?? [];
const version = [major, minor, patch].join('.') + (prerelease ? `-${prerelease}` : '');
this.#verison.setValue(version);
this.#version.setValue(version);
}
public setActiveSectionAlias(alias: string) {

View File

@@ -1,35 +0,0 @@
/** Example of how a grid layout stylehseet could be done with Flex box: */
.umb-block-grid__layout-container {
position: relative;
display: flex;
flex-wrap: wrap;
gap: var(--umb-block-grid--row-gap, 0) var(--umb-block-grid--column-gap, 0);
}
.umb-block-grid__layout-item {
position: relative;
--umb-block-grid__layout-item-calc: calc(var(--umb-block-grid--item-column-span) / var(--umb-block-grid--grid-columns));
width: calc(var(--umb-block-grid__layout-item-calc) * 100% - (1 - var(--umb-block-grid__layout-item-calc)) * var(--umb-block-grid--column-gap, 0px));
}
.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) {
position: relative;
display: flex;
flex-wrap: wrap;
width: 100%;
gap: var(--umb-block-grid--areas-row-gap, 0) var(--umb-block-grid--areas-column-gap, 0);
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
--umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1));
width: calc(var(--umb-block-grid__area-calc) * 100% - (1 - var(--umb-block-grid__area-calc)) * var(--umb-block-grid--areas-column-gap, 0px));
}
.umb-block-grid__actions {
clear: both;
}

View File

@@ -1,46 +0,0 @@
.umb-block-grid__layout-container {
position: relative;
display: grid;
grid-template-columns: repeat(var(--umb-block-grid--grid-columns, 1), minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-rows: minmax(50px, min-content);
column-gap: var(--umb-block-grid--column-gap, 0);
row-gap: var(--umb-block-grid--row-gap, 0);
}
.umb-block-grid__layout-item {
position: relative;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
grid-column-end: span min(calc(var(--umb-block-grid--item-column-span, 1) * 3), var(--umb-block-grid--grid-columns));
grid-row: span var(--umb-block-grid--item-row-span, 1);
}
.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) {
position: relative;
display: grid;
grid-template-columns: repeat(var(--umb-block-grid--area-grid-columns, var(--umb-block-grid--grid-columns, 1)), minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-rows: minmax(50px, min-content);
column-gap: var(--umb-block-grid--areas-column-gap, 0);
row-gap: var(--umb-block-grid--areas-row-gap, 0);
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--area-grid-columns));
grid-row: span var(--umb-block-grid--area-row-span, 1);
}
@media (min-width:1024px) {
.umb-block-grid__layout-item {
grid-column-end: span min(var(--umb-block-grid--item-column-span, 1), var(--umb-block-grid--grid-columns));
}
.umb-block-grid__area {
grid-column-end: span min(var(--umb-block-grid--area-column-span, 1), var(--umb-block-grid--area-grid-columns));
}
}

View File

@@ -695,6 +695,7 @@ export default {
hasReferencesDeleteConsequence:
'Deleting <strong>%0%</strong> will delete the properties and their data from the following items',
acceptDeleteConsequence: 'I understand this action will delete the properties and data based on this Data Type',
noConfiguration: 'There is no configuration for this property editor.',
},
errorHandling: {
errorButDataWasSaved:
@@ -1561,6 +1562,7 @@ export default {
ascending: 'ascending',
descending: 'descending',
template: 'Template',
systemFields: 'System fields',
},
grid: {
media: 'Image',
@@ -1734,11 +1736,11 @@ export default {
enabled: 'Enabled',
events: 'Events',
event: 'Event',
url: 'Url',
url: 'URL',
types: 'Types',
webhookKey: 'Webhook key',
retryCount: 'Retry count',
urlDescription: 'The url to call when the webhook is triggered.',
urlDescription: 'The URL to call when the webhook is triggered.',
eventDescription: 'The events for which the webhook should be triggered.',
contentTypeDescription: 'Only trigger the webhook for a specific content type.',
enabledDescription: 'Is the webhook enabled?',
@@ -1746,6 +1748,7 @@ export default {
contentType: 'Content Type',
headers: 'Headers',
selectEventFirst: 'Please select an event first.',
selectEvents: 'Select events',
},
languages: {
addLanguage: 'Add language',

View File

@@ -14,6 +14,7 @@ import { UmbArrayState, UmbObjectState, appendToFrozenArray } from '@umbraco-cms
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { ManifestWorkspace, PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry';
import { UmbId } from '@umbraco-cms/backoffice/id';
export class UmbBlockGridAreaTypeWorkspaceContext
extends UmbSubmittableWorkspaceContextBase<UmbBlockGridTypeAreaType>
@@ -43,12 +44,20 @@ export class UmbBlockGridAreaTypeWorkspaceContext
{
path: 'edit/:id',
component: () => import('./block-grid-area-type-workspace-editor.element.js'),
setup: (_component, info) => {
setup: (component, info) => {
const id = info.match.params.id;
(_component as any).workspaceAlias = manifest.alias;
(component as any).workspaceAlias = manifest.alias;
this.load(id);
},
},
{
path: 'create',
component: () => import('./block-grid-area-type-workspace-editor.element.js'),
setup: (component) => {
(component as any).workspaceAlias = manifest.alias;
this.create();
},
},
]);
}
@@ -78,17 +87,24 @@ export class UmbBlockGridAreaTypeWorkspaceContext
}
async create() {
throw new Error('Method not implemented.');
/*
//Only set groupKey property if it exists
const data: UmbBlockGridTypeAreaType = {
this.resetState();
let data: UmbBlockGridTypeAreaType = {
key: UmbId.new(),
alias: '',
columnSpan: 12,
rowSpan: 1,
minAllowed: 0,
maxAllowed: undefined,
specifiedAllowance: [],
};
// If we have a modal context, we blend in the modal preset data: [NL]
if (this.modalContext) {
data = { ...data, ...this.modalContext.data.preset };
}
this.setIsNew(true);
this.#data.setValue(data);
return { data };
*/
}
getData() {

View File

@@ -7,6 +7,11 @@ import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-ed
import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/data-type';
import type { UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type';
import type { UUIComboboxElement, UUIComboboxEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
import {
UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
type UmbDocumentTypeItemModel,
} from '@umbraco-cms/backoffice/document-type';
@customElement('umb-property-editor-ui-block-grid-area-type-permission')
export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
@@ -24,20 +29,41 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
@state()
private _value: Array<UmbBlockGridTypeAreaTypePermission> = [];
_blockTypes: Array<UmbBlockTypeWithGroupKey> = [];
@state()
private _blockTypes: Array<UmbBlockTypeWithGroupKey> = [];
private _blockTypesWithElementName: Array<{ type: UmbBlockTypeWithGroupKey; name: string }> = [];
@state()
private _blockGroups: Array<UmbBlockGridTypeGroupType> = [];
#itemsManager = new UmbRepositoryItemsManager<UmbDocumentTypeItemModel>(
this,
UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS,
(x) => x.unique,
);
constructor() {
super();
this.observe(this.#itemsManager.items, (items) => {
this._blockTypesWithElementName = items
.map((item) => {
const blockType = this._blockTypes.find((block) => block.contentElementTypeKey === item.unique);
if (blockType) {
return { type: blockType, name: item.name };
}
return undefined;
})
.filter((x) => x !== undefined) as Array<{ type: UmbBlockTypeWithGroupKey; name: string }>;
});
this.consumeContext(UMB_DATA_TYPE_WORKSPACE_CONTEXT, async (context) => {
this.observe(
await context.propertyValueByAlias<Array<UmbBlockTypeWithGroupKey>>('blocks'),
(blockTypes) => {
this._blockTypes = blockTypes ?? [];
this.#itemsManager.setUniques(blockTypes.map((block) => block.contentElementTypeKey));
},
'observeBlockType',
);
@@ -103,7 +129,7 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
this._value,
(permission) => permission,
(permission, index) => {
const showCategoryHeader = this._blockGroups.length && this._blockTypes.length;
const showCategoryHeader = this._blockGroups.length > 0 && this._blockTypesWithElementName.length > 0;
return html`<div class="permission-setting">
<uui-combobox
@@ -169,13 +195,13 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
#renderBlockTypes(area: UmbBlockGridTypeAreaTypePermission) {
return repeat(
this._blockTypes,
(block) => block.contentElementTypeKey,
this._blockTypesWithElementName,
(block) => block.type.contentElementTypeKey,
(block) =>
html`<uui-combobox-list-option
.value=${block.contentElementTypeKey}
?selected=${area.elementTypeKey === block.contentElementTypeKey}>
${block.label}
.value=${block.type.contentElementTypeKey}
?selected=${area.elementTypeKey === block.type.contentElementTypeKey}>
${block.name}
</uui-combobox-list-option>`,
);
}

View File

@@ -6,11 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import {
UmbPropertyValueChangeEvent,
type UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
import { incrementString } from '@umbraco-cms/backoffice/utils';
@@ -61,7 +57,17 @@ export class UmbPropertyEditorUIBlockGridAreasConfigElement
new UmbModalRouteRegistrationController(this, UMB_BLOCK_GRID_AREA_TYPE_WORKSPACE_MODAL)
.addAdditionalPath('block-grid-area-type')
.onSetup(() => {
return { data: { entityType: 'block-grid-area-type', preset: {} }, modal: { size: 'large' } };
if (!this._areaGridColumns) return false;
const halfGridColumns = this._areaGridColumns * 0.5;
const columnSpan = halfGridColumns === Math.round(halfGridColumns) ? halfGridColumns : this._areaGridColumns;
return {
data: {
entityType: 'block-grid-area-type',
preset: { columnSpan, alias: this.#generateUniqueAreaAlias('area') },
},
modal: { size: 'large' },
};
})
.observeRouteBuilder((routeBuilder) => {
this._workspacePath = routeBuilder({});
@@ -104,29 +110,6 @@ export class UmbPropertyEditorUIBlockGridAreasConfigElement
return alias;
}
#addNewArea() {
if (!this._areaGridColumns) return;
const halfGridColumns = this._areaGridColumns * 0.5;
const columnSpan = halfGridColumns === Math.round(halfGridColumns) ? halfGridColumns : this._areaGridColumns;
this._value = [
...this._value,
{
key: UmbId.new(),
alias: this.#generateUniqueAreaAlias('area'),
columnSpan: columnSpan,
rowSpan: 1,
minAllowed: 0,
maxAllowed: undefined,
specifiedAllowance: [],
},
];
this.requestUpdate('_value');
this.dispatchEvent(new UmbPropertyValueChangeEvent());
//TODO: open area edit workspace
}
override render() {
return this._areaGridColumns
? html`${this._styleElement}
@@ -144,7 +127,11 @@ export class UmbPropertyEditorUIBlockGridAreasConfigElement
.key=${area.key}></umb-block-area-config-entry>`,
)}
</div>
<uui-button id="add-button" look="placeholder" label=${'Add area'} @click=${this.#addNewArea}></uui-button>`
<uui-button
id="add-button"
look="placeholder"
label=${'Add area'}
href=${this._workspacePath + 'create'}></uui-button>`
: '';
}
}

View File

@@ -79,7 +79,9 @@ export class UmbPropertyEditorUIBlockGridLayoutStylesheetElement
.min=${this._limitMin}
.max=${this._limitMax}></umb-input-static-file>
<br />
<a href="/umbraco/backoffice/assets/css/umbraco-blockgridlayout.css">Link to default layout stylesheet</a>
<a href="/umbraco/backoffice/css/umbraco-blockgridlayout.css" target="_blank"
>Link to default layout stylesheet</a
>
`;
}

View File

@@ -112,7 +112,6 @@ export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeWith
this.setIsNew(true);
this.#data.setValue(data);
return { data };
}
getData() {

View File

@@ -148,7 +148,7 @@ export class UmbInputEntityElement extends UmbFormControlMixin<string | undefine
id="btn-add"
look="placeholder"
@click=${this.#openPicker}
label="${this.localize.term('general_choose')}"></uui-button>
label=${this.localize.term('general_choose')}></uui-button>
`;
}

View File

@@ -90,17 +90,18 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
await this.#init;
if (!this.#compositionRepository) return;
const isElement = this.data?.isElement;
const currentPropertyAliases = this.data?.currentPropertyAliases;
// Notice isElement is not available on all types that can be composed.
const isElement = this.data?.isElement ?? undefined;
const currentPropertyAliases = this.data?.currentPropertyAliases ?? [];
const { data } = await this.#compositionRepository.availableCompositions({
unique: this.#unique,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: isElement is not available on all types that can be composed.
isElement: isElement ?? false,
isElement: isElement,
currentCompositeUniques: this._selection,
currentPropertyAliases: currentPropertyAliases ?? [],
currentPropertyAliases: currentPropertyAliases,
});
if (!data) return;
@@ -129,11 +130,13 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
${!this._references.length
? html`<uui-button
label=${this.localize.term('general_submit')}
look="primary"
color="positive"
@click=${this._submitModal}></uui-button>`
? html`
<uui-button
label=${this.localize.term('general_submit')}
look="primary"
color="positive"
@click=${this._submitModal}></uui-button>
`
: nothing}
</div>
</umb-body-layout>
@@ -141,7 +144,8 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
}
#renderHasReference() {
return html` <umb-localize key="contentTypeEditor_compositionInUse">
return html`
<umb-localize key="contentTypeEditor_compositionInUse">
This Content Type is used in a composition, and therefore cannot be composed itself.
</umb-localize>
<h4>
@@ -154,19 +158,22 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
${repeat(
this._references,
(item) => item.unique,
(item) =>
html`<uui-ref-node-document-type
(item) => html`
<uui-ref-node-document-type
href=${'/section/settings/workspace/document-type/edit/' + item.unique}
name=${item.name}>
name=${this.localize.string(item.name)}>
<umb-icon slot="icon" name=${item.icon}></umb-icon>
</uui-ref-node-document-type>`,
</uui-ref-node-document-type>
`,
)}
</div>`;
</div>
`;
}
#renderAvailableCompositions() {
if (this._compatibleCompositions) {
return html`<umb-localize key="contentTypeEditor_compositionsDescription">
return html`
<umb-localize key="contentTypeEditor_compositionsDescription">
Inherit tabs and properties from an existing Document Type. New tabs will be<br />added to the current
Document Type or merged if a tab with an identical name exists.<br />
</umb-localize>
@@ -184,11 +191,14 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
: nothing}
${this.#renderCompositionsItems(folder.compositions)}`,
)}
</div>`;
</div>
`;
} else {
return html`<umb-localize key="contentTypeEditor_noAvailableCompositions">
There are no Content Types available to use as a composition
</umb-localize>`;
return html`
<umb-localize key="contentTypeEditor_noAvailableCompositions">
There are no Content Types available to use as a composition
</umb-localize>
`;
}
}
@@ -196,15 +206,16 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
return repeat(
compositionsList,
(compositions) => compositions.unique,
(compositions) =>
html`<uui-menu-item
label=${compositions.name}
(compositions) => html`
<uui-menu-item
label=${this.localize.string(compositions.name)}
selectable
@selected=${() => this.#onSelectionAdd(compositions.unique)}
@deselected=${() => this.#onSelectionRemove(compositions.unique)}
?selected=${this._selection.find((unique) => unique === compositions.unique)}>
<umb-icon name=${compositions.icon} slot="icon"></umb-icon>
</uui-menu-item>`,
</uui-menu-item>
`,
);
}

View File

@@ -1,7 +1,7 @@
import type { UmbContentTypeModel, UmbPropertyContainerTypes, UmbPropertyTypeContainerModel } from '../types.js';
import type { UmbContentTypeStructureManager } from './content-type-structure-manager.class.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
/**
@@ -17,6 +17,8 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
#structure?: UmbContentTypeStructureManager<T>;
#containerObservers: Array<UmbController> = [];
// State containing the all containers defined in the data:
#childContainers = new UmbArrayState<UmbPropertyTypeContainerModel>([], (x) => x.id);
readonly containers = this.#childContainers.asObservable();
@@ -150,30 +152,31 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
this.#parentType,
),
(containers) => {
// We want to remove hasProperties of groups that does not exist anymore.:
// this.#removeHasPropertiesOfGroup()
this.#hasProperties.setValue([]);
this.#childContainers.setValue([]);
this.#containerObservers.forEach((x) => x.destroy());
this.#containerObservers = [];
containers.forEach((container) => {
this.#observeHasPropertiesOf(container.id);
this.observe(
this.#structure!.containersOfParentId(container.id, this.#childType!),
(containers) => {
// get the direct owner containers of this container id:
this.#ownerChildContainers =
this.#structure!.getOwnerContainers(this.#childType!, this.#containerId!) ?? [];
// TODO: Maybe check for dif before setting it? Cause currently we are setting it every time one of the containers change. [NL]
this.#containerObservers.push(
this.observe(
this.#structure!.containersOfParentId(container.id, this.#childType!),
(containers) => {
// get the direct owner containers of this container id: [NL]
this.#ownerChildContainers =
this.#structure!.getOwnerContainers(this.#childType!, this.#containerId!) ?? [];
// Remove existing containers that are not the parent of the new containers:
this.#childContainers.filter(
(x) => x.parent?.id !== container.id || containers.some((y) => y.id === x.id),
);
// Remove existing containers that are not the parent of the new containers: [NL]
this.#childContainers.filter(
(x) => x.parent?.id !== container.id || containers.some((y) => y.id === x.id),
);
this.#childContainers.append(containers);
},
'_observeGroupsOf_' + container.id,
this.#childContainers.append(containers);
},
'_observeGroupsOf_' + container.id,
),
);
});
},

View File

@@ -21,7 +21,7 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
#structure?: UmbContentTypeStructureManager<T>;
private _containerId?: string | null;
#containerId?: string | null;
// State which holds all the properties of the current container, this is a composition of all properties from the containers that matches our target [NL]
#propertyStructure = new UmbArrayState<UmbPropertyTypeModel>([], (x) => x.id);
@@ -59,12 +59,12 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
}
public setContainerId(value?: string | null) {
if (this._containerId === value) return;
this._containerId = value;
if (this.#containerId === value) return;
this.#containerId = value;
this.#observeContainers();
}
public getContainerId() {
return this._containerId;
return this.#containerId;
}
private _containerName?: string;
@@ -74,9 +74,9 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
#containers?: Array<UmbPropertyTypeContainerModel>;
#observeContainers() {
if (!this.#structure || this._containerId === undefined) return;
if (!this.#structure || this.#containerId === undefined) return;
if (this._containerId === null) {
if (this.#containerId === null) {
this.observe(
this.#structure.propertyStructuresOf(null),
(properties) => {
@@ -87,7 +87,7 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
this.removeUmbControllerByAlias('_observeContainers');
} else {
this.observe(
this.#structure.containerById(this._containerId),
this.#structure.containerById(this.#containerId),
(container) => {
if (container) {
this._containerName = container.name ?? '';

View File

@@ -42,10 +42,13 @@ export class UmbContentTypeStructureManager<
readonly ownerContentType = this.#contentTypes.asObservablePart((x) =>
x.find((y) => y.unique === this.#ownerContentTypeUnique),
);
private readonly _contentTypeContainers = this.#contentTypes.asObservablePart((x) =>
x.flatMap((x) => x.containers ?? []),
readonly ownerContentTypeCompositions = this.#contentTypes.asObservablePart(
(x) => x.find((y) => y.unique === this.#ownerContentTypeUnique)?.compositions,
);
readonly #contentTypeContainers = this.#contentTypes.asObservablePart(() => {
return this.#contentTypes.getValue().flatMap((x) => x.containers ?? []);
});
readonly contentTypeUniques = this.#contentTypes.asObservablePart((x) => x.map((y) => y.unique));
readonly contentTypeAliases = this.#contentTypes.asObservablePart((x) => x.map((y) => y.alias));
@@ -61,12 +64,12 @@ export class UmbContentTypeStructureManager<
super(host);
this.#repository = typeRepository;
this.observe(this.contentTypes, (contentTypes) => {
contentTypes.forEach((contentType) => {
this._loadContentTypeCompositions(contentType);
});
// Observe owner content type compositions, as we only allow one level of compositions at this moment. [NL]
// But, we could support more, we would just need to flatMap all compositions and make sure the entries are unique and then base the observation on that. [NL]
this.observe(this.ownerContentTypeCompositions, (ownerContentTypeCompositions) => {
this._loadContentTypeCompositions(ownerContentTypeCompositions);
});
this.observe(this._contentTypeContainers, (contentTypeContainers) => {
this.observe(this.#contentTypeContainers, (contentTypeContainers) => {
this.#containers.setValue(contentTypeContainers);
});
}
@@ -136,8 +139,24 @@ export class UmbContentTypeStructureManager<
this._observeContentType(data);
}
private async _loadContentTypeCompositions(contentType: T) {
contentType.compositions?.forEach((composition) => {
private async _loadContentTypeCompositions(ownerContentTypeCompositions: T['compositions'] | undefined) {
if (!ownerContentTypeCompositions) {
// Owner content type was undefined, so we can not load compositions. But at this point we neither offload existing compositions, this is most likely not a case that needs to be handled.
return;
}
const ownerUnique = this.getOwnerContentTypeUnique();
// Remove content types that does not exist as compositions anymore:
this.#contentTypes.getValue().forEach((x) => {
if (
x.unique !== ownerUnique &&
!ownerContentTypeCompositions.find((comp) => comp.contentType.unique === x.unique)
) {
this.#contentTypeObservers.find((y) => y.controllerAlias === 'observeContentType_' + x.unique)?.destroy();
this.#contentTypes.removeOne(x.unique);
}
});
ownerContentTypeCompositions.forEach((composition) => {
this._ensureType(composition.contentType.unique);
});
}
@@ -164,23 +183,19 @@ export class UmbContentTypeStructureManager<
// Notice we do not store the content type in the store here, cause it will happen shortly after when the observations gets its first initial callback. [NL]
// Load inherited and composed types:
//this._loadContentTypeCompositions(data);// Should not be necessary as this will be done when appended to the contentTypes state. [NL]
const ctrl = this.observe(
// Then lets start observation of the content type:
await this.#repository.byUnique(data.unique),
(docType) => {
if (docType) {
// TODO: Handle if there was changes made to the owner document type in this context. [NL]
/*
possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones). [NL]
*/
this.#contentTypes.appendOne(docType);
} else {
// Remove the content type from the store, if it does not exist anymore.
this.#contentTypes.removeOne(data.unique);
}
// TODO: Do we need to handle the undefined case? [NL]
},
'observeContentType_' + data.unique,
// Controller Alias is used to stop observation when no longer needed. [NL]
);
this.#contentTypeObservers.push(ctrl);

View File

@@ -416,14 +416,16 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
return html`
<div id="actions">
${this._compositionRepositoryAlias
? html` <uui-button
look="outline"
label=${this.localize.term('contentTypeEditor_compositions')}
compact
@click=${this.#openCompositionModal}>
<uui-icon name="icon-merge"></uui-icon>
${this.localize.term('contentTypeEditor_compositions')}
</uui-button>`
? html`
<uui-button
look="outline"
label=${this.localize.term('contentTypeEditor_compositions')}
compact
@click=${this.#openCompositionModal}>
<uui-icon name="icon-merge"></uui-icon>
${this.localize.term('contentTypeEditor_compositions')}
</uui-button>
`
: ''}
<uui-button look="outline" label=${sortButtonText} compact @click=${this.#toggleSortMode}>
<uui-icon name="icon-navigation"></uui-icon>

View File

@@ -63,7 +63,7 @@ export class UmbContentWorkspaceViewEditTabElement extends UmbLitElement {
this._groups,
(group) => group.id,
(group) =>
html`<uui-box .headline=${group.name ?? ''}>
html`<uui-box .headline=${this.localize.string(group.name) ?? ''}>
<umb-content-workspace-view-edit-properties
class="properties"
.containerId=${group.id}></umb-content-workspace-view-edit-properties>

View File

@@ -138,7 +138,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements
(tab) => {
const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || '');
return html`<uui-tab label=${tab.name ?? 'Unnamed'} .active=${path === this._activePath} href=${path}
>${tab.name}</uui-tab
>${this.localize.string(tab.name)}</uui-tab
>`;
},
)}

View File

@@ -18,3 +18,5 @@ export class UmbCultureRepository extends UmbControllerBase implements UmbApi {
override destroy() {}
}
export { UmbCultureRepository as api };

View File

@@ -1,4 +1,3 @@
import { UmbCultureRepository } from './culture.repository.js';
import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_CULTURE_REPOSITORY_ALIAS = 'Umb.Repository.Culture';
@@ -7,7 +6,7 @@ const repository: ManifestRepository = {
type: 'repository',
alias: UMB_CULTURE_REPOSITORY_ALIAS,
name: 'Cultures Repository',
api: UmbCultureRepository,
api: () => import('./culture.repository.js'),
};
export const manifests: Array<ManifestTypes> = [repository];

View File

@@ -1,10 +1,12 @@
import type { ManifestTypes } from '../models/index.js';
import { manifest as menuAliasConditionManifest } from './menu-alias.condition.js';
import { manifest as multipleAppLanguagesConditionManifest } from './multiple-app-languages.condition.js';
import { manifest as sectionAliasConditionManifest } from './section-alias.condition.js';
import { manifest as switchConditionManifest } from './switch.condition.js';
export const manifests: Array<ManifestTypes> = [
menuAliasConditionManifest,
multipleAppLanguagesConditionManifest,
sectionAliasConditionManifest,
switchConditionManifest,
];

View File

@@ -17,9 +17,13 @@ export class UmbMenuAliasCondition extends UmbConditionBase<MenuAliasConditionCo
super(host, args);
this.consumeContext(UMB_MENU_CONTEXT, (context) => {
this.observe(context.alias, (MenuAlias) => {
this.permitted = MenuAlias === this.config.match;
});
this.observe(
context.alias,
(MenuAlias) => {
this.permitted = MenuAlias === this.config.match;
},
'observeAlias',
);
});
}
}

View File

@@ -0,0 +1,37 @@
import { UmbConditionBase } from './condition-base.controller.js';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type {
ManifestCondition,
UmbConditionConfigBase,
UmbConditionControllerArguments,
UmbExtensionCondition,
} from '@umbraco-cms/backoffice/extension-api';
export type UmbMultipleAppLanguageConditionConfig = UmbConditionConfigBase;
export class UmbMultipleAppLanguageCondition
extends UmbConditionBase<UmbMultipleAppLanguageConditionConfig>
implements UmbExtensionCondition
{
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<UmbMultipleAppLanguageConditionConfig>) {
super(host, args);
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (context) => {
this.observe(
context.moreThanOneLanguage,
(moreThanOneLanguage) => {
this.permitted = moreThanOneLanguage;
},
'observeLanguages',
);
});
}
}
export const manifest: ManifestCondition = {
type: 'condition',
name: 'Multiple App Languages Condition',
alias: 'Umb.Condition.MultipleAppLanguages',
api: UmbMultipleAppLanguageCondition,
};

View File

@@ -24,9 +24,13 @@ export class UmbSectionAliasCondition
if (permissionCheck !== undefined) {
this.consumeContext(UMB_SECTION_CONTEXT, (context) => {
this.observe(context.alias, (sectionAlias) => {
this.permitted = sectionAlias ? permissionCheck!(sectionAlias) : false;
});
this.observe(
context.alias,
(sectionAlias) => {
this.permitted = sectionAlias ? permissionCheck!(sectionAlias) : false;
},
'observeAlias',
);
});
}
}

View File

@@ -3,6 +3,7 @@ import type { CollectionBulkActionPermissionConditionConfig } from '../../collec
import type { UmbSectionUserPermissionConditionConfig } from '../../section/conditions/index.js';
import type { SectionAliasConditionConfig } from './section-alias.condition.js';
import type { SwitchConditionConfig } from './switch.condition.js';
import type { UmbMultipleAppLanguageConditionConfig } from './multiple-app-languages.condition.js';
import type {
WorkspaceAliasConditionConfig,
WorkspaceEntityTypeConditionConfig,
@@ -29,8 +30,9 @@ export type ConditionTypes =
| CollectionBulkActionPermissionConditionConfig
| SectionAliasConditionConfig
| SwitchConditionConfig
| UmbConditionConfigBase
| UmbDocumentUserPermissionConditionConfig
| UmbMultipleAppLanguageConditionConfig
| UmbSectionUserPermissionConditionConfig
| WorkspaceAliasConditionConfig
| WorkspaceEntityTypeConditionConfig
| UmbConditionConfigBase;
| WorkspaceEntityTypeConditionConfig;

View File

@@ -1,30 +1,76 @@
import { AsyncDirective, directive, nothing, type ElementPart } from '@umbraco-cms/backoffice/external/lit';
/**
*
* test if a element has focus
* this also returns true if the focused element is a child of the target.
* @param current
* @param target
* @returns bool
*/
function hasFocus(current: any, target: HTMLElement): boolean {
if (current === target) {
return true;
}
if (current.shadowRoot) {
const node = current.shadowRoot.activeElement;
if (node) {
return hasFocus(node, target);
}
}
return false;
}
/**
* The `focus` directive sets focus on the given element once its connected to the DOM.
*/
class UmbFocusDirective extends AsyncDirective {
private _el?: HTMLElement;
static #next?: HTMLElement;
#el?: HTMLElement;
#timeout?: number;
override render() {
return nothing;
}
override update(part: ElementPart) {
if (this._el !== part.element) {
// This does feel wrong that we need to wait one render. [NL]
// Because even if our elements focus method is implemented so it can be called initially, my research shows that calling the focus method at this point is too early, thought the element is connected to the DOM and the focus method is available. [NL]
// This smells a bit like the DOMPart of which the directive is in is not connected to the main DOM yet, and therefor cant receive focus. [NL]
// Which is why we need to await one render: [NL]
requestAnimationFrame(() => {
(this._el = part.element as HTMLElement).focus();
});
if (this.#el !== part.element) {
UmbFocusDirective.#next = this.#el = part.element as HTMLElement;
this.#setFocus();
}
return nothing;
}
/**
* This method tries to set focus, if it did not succeed, it will try again.
* It always tests against the latest element, because the directive can be used multiple times in the same render.
* This is NOT needed because the elements focus method isn't ready to be called, but due to something with rendering of the DOM.
* But I'm not completely sure at this movement why the browser does not accept the focus call.
* But I have tested that everything is in place for it to be good, so something else must have an effect,
* setting the focus somewhere else, maybe a re-appending of some sort?
* cause Lit does not re-render the element but also notice reconnect callback on the directive is not triggered either. [NL]
*/
#setFocus = () => {
// Make sure we clear the timeout, so we don't get multiple timeouts running.
if (this.#timeout) {
clearTimeout(this.#timeout);
this.#timeout = undefined;
}
// If this is the next element to focus, then try to focus it.
if (this.#el && this.#el === UmbFocusDirective.#next) {
this.#el.focus();
if (hasFocus(document.activeElement, this.#el) === false) {
this.#timeout = setTimeout(this.#setFocus, 100) as unknown as number;
} else {
UmbFocusDirective.#next = undefined;
}
}
};
override disconnected() {
this._el = undefined;
if (this.#el === UmbFocusDirective.#next) {
UmbFocusDirective.#next = undefined;
}
this.#el = undefined;
}
//override reconnected() {}

View File

@@ -1,8 +1,9 @@
import type {
UmbLocalizationSetBase,
UmbLocalizationDictionary,
UmbLocalizationFlatDictionary,
} from '@umbraco-cms/backoffice/localization-api';
import {
type UmbLocalizationSetBase,
type UmbLocalizationDictionary,
type UmbLocalizationFlatDictionary,
UMB_DEFAULT_LOCALIZATION_CULTURE
} from "@umbraco-cms/backoffice/localization-api";
import { umbLocalizationManager } from '@umbraco-cms/backoffice/localization-api';
import type { ManifestLocalization, UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -21,7 +22,7 @@ function addOrUpdateDictionary(
}
export class UmbLocalizationRegistry {
#currentLanguage = new UmbStringState(document.documentElement.lang ?? 'en-us');
#currentLanguage = new UmbStringState(document.documentElement.lang !== '' ? document.documentElement.lang : UMB_DEFAULT_LOCALIZATION_CULTURE);
readonly currentLanguage = this.#currentLanguage.asObservable();
#loadedExtAliases: Array<string> = [];

View File

@@ -1,4 +1,3 @@
import { UmbOEmbedRepository } from './oembed.repository.js';
import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_OEMBED_REPOSITORY_ALIAS = 'Umb.Repository.OEmbed';
@@ -7,7 +6,7 @@ const repository: ManifestRepository = {
type: 'repository',
alias: UMB_OEMBED_REPOSITORY_ALIAS,
name: 'OEmbed Repository',
api: UmbOEmbedRepository,
api: () => import('./oembed.repository.js'),
};
export const manifests: Array<ManifestTypes> = [repository];

View File

@@ -18,3 +18,5 @@ export class UmbOEmbedRepository extends UmbControllerBase implements UmbApi {
return { error };
}
}
export { UmbOEmbedRepository as api };

View File

@@ -51,7 +51,7 @@ export class UmbItemPickerModalElement extends UmbModalBaseElement<UmbItemPicker
if (!this.data) return nothing;
const items = this._filtered;
return html`
<umb-body-layout headline=${this.data.headline}>
<umb-body-layout headline=${this.localize.string(this.data.headline)}>
<div id="main">
<uui-input
type="search"
@@ -72,7 +72,7 @@ export class UmbItemPickerModalElement extends UmbModalBaseElement<UmbItemPicker
(item) => item.value,
(item) => html`
<umb-ref-item
name=${item.label}
name=${this.localize.string(item.label)}
detail=${ifDefined(item.description)}
icon=${ifDefined(item.icon)}
@click=${() => this.#submit(item)}>

View File

@@ -212,7 +212,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement extends UmbLitElement {
return html`
<uui-input
id="name-input"
label="Document name (TODO: Localize)"
label=${this.localize.term('placeholders_entername')}
.value=${this._name ?? ''}
@input=${this.#handleInput}
${umbFocus()}

View File

@@ -12,7 +12,7 @@ import { UmbDataPathPropertyValueFilter } from '@umbraco-cms/backoffice/validati
*/
@customElement('umb-property-editor-config')
export class UmbPropertyEditorConfigElement extends UmbLitElement {
// TODO: Make this element generic, so its not bound to DATA-TYPEs. This will require moving some functionality of Data-Type-Context to this. and this might need to self provide a variant Context for its inner property editor UIs.
// TODO: Make this element generic, so its not bound to DATA-TYPEs. This will require moving some functionality of Data-Type-Context to this. and this might need to self provide a variant Context for its inner property editor UIs. [NL]
#workspaceContext?: typeof UMB_DATA_TYPE_WORKSPACE_CONTEXT.TYPE;
@state()
@@ -53,7 +53,9 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement {
property-editor-ui-alias=${property.propertyEditorUiAlias}
.config=${property.config}></umb-property>`,
)
: html`<div>No configuration</div>`;
: html`<umb-localize key="editdatatype_noConfiguration"
>There is no configuration for this property editor.</umb-localize
>`;
}
static override styles = [UmbTextStyles];

View File

@@ -30,6 +30,24 @@ import {
} from '@umbraco-cms/backoffice/entity-action';
type EntityType = UmbDataTypeDetailModel;
/**
* @class UmbDataTypeWorkspaceContext
* @description - Context for handling data type workspace
* There is two overall code flows to be aware about:
*
* propertyEditorUiAlias is observed
* loads propertyEditorUi manifest
* then the propertyEditorSchemaAlias is set to what the UI is configured for.
*
* propertyEditorSchemaAlias is observed
* loads the propertyEditorSchema manifest
* if no UI is defined then the propertyEditorSchema manifest default ui is set for the propertyEditorUiAlias.
*
* This supports two cases:
* - when editing an existing data type that only has a schema alias set, then it gets the UI set.
* - a new property editor ui is picked for a data-type, uses the data-type configuration to set the schema, if such is configured for the Property Editor UI. (The user picks the UI via the UI, the schema comes from the UI that the user picked, we store both on the data-type)
*/
export class UmbDataTypeWorkspaceContext
extends UmbSubmittableWorkspaceContextBase<EntityType>
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext
@@ -72,8 +90,6 @@ export class UmbDataTypeWorkspaceContext
#settingsDefaultData?: Array<PropertyEditorSettingsDefaultData>;
#propertyEditorUISettingsSchemaAlias?: string;
#propertyEditorUiIcon = new UmbStringState<string | null>(null);
readonly propertyEditorUiIcon = this.#propertyEditorUiIcon.asObservable();
@@ -82,6 +98,8 @@ export class UmbDataTypeWorkspaceContext
constructor(host: UmbControllerHost) {
super(host, 'Umb.Workspace.DataType');
this.#observePropertyEditorSchemaAlias();
this.#observePropertyEditorUIAlias();
this.routes.setRoutes([
@@ -121,7 +139,7 @@ export class UmbDataTypeWorkspaceContext
this.#propertyEditorUISettingsDefaultData = [];
this.#settingsDefaultData = undefined;
this._mergeConfigProperties();
this.#mergeConfigProperties();
}
// Hold the last set property editor ui alias, so we know when it changes, so we can reset values. [NL]
@@ -131,30 +149,13 @@ export class UmbDataTypeWorkspaceContext
this.observe(
this.propertyEditorUiAlias,
async (propertyEditorUiAlias) => {
const previousPropertyEditorUIAlias = this.#lastPropertyEditorUIAlias;
this.#lastPropertyEditorUIAlias = propertyEditorUiAlias;
this.#propertyEditorUISettingsProperties = [];
this.#propertyEditorUISettingsDefaultData = [];
// we only want to react on the change if the alias is set or null. When it is undefined something is still loading
if (propertyEditorUiAlias === undefined) return;
// if the property editor ui alias is not set, we use the default alias from the schema
if (propertyEditorUiAlias === null) {
await this.#observePropertyEditorSchemaAlias();
if (this.#propertyEditorSchemaConfigDefaultUIAlias !== null) {
this.setPropertyEditorUiAlias(this.#propertyEditorSchemaConfigDefaultUIAlias);
}
} else {
await this.#setPropertyEditorUIConfig(propertyEditorUiAlias);
this.setPropertyEditorSchemaAlias(this.#propertyEditorUISettingsSchemaAlias!);
await this.#observePropertyEditorSchemaAlias();
}
if (
this.getIsNew() ||
(previousPropertyEditorUIAlias && previousPropertyEditorUIAlias !== propertyEditorUiAlias)
) {
this.#transferConfigDefaultData();
}
this._mergeConfigProperties();
this.#observePropertyEditorUIManifest(propertyEditorUiAlias);
},
'editorUiAlias',
);
@@ -164,13 +165,19 @@ export class UmbDataTypeWorkspaceContext
return this.observe(
this.propertyEditorSchemaAlias,
(propertyEditorSchemaAlias) => {
this.#setPropertyEditorSchemaConfig(propertyEditorSchemaAlias);
this.#propertyEditorSchemaSettingsProperties = [];
this.#propertyEditorSchemaSettingsDefaultData = [];
this.#observePropertyEditorSchemaManifest(propertyEditorSchemaAlias);
},
'schemaAlias',
).asPromise();
);
}
#setPropertyEditorSchemaConfig(propertyEditorSchemaAlias?: string) {
#observePropertyEditorSchemaManifest(propertyEditorSchemaAlias?: string) {
if (!propertyEditorSchemaAlias) {
this.removeUmbControllerByAlias('schema');
return;
}
this.observe(
propertyEditorSchemaAlias
? umbExtensionsRegistry.byTypeAndAlias('propertyEditorSchema', propertyEditorSchemaAlias)
@@ -183,36 +190,56 @@ export class UmbDataTypeWorkspaceContext
}));
this.#propertyEditorSchemaSettingsDefaultData = manifest?.meta.settings?.defaultData || [];
this.#propertyEditorSchemaConfigDefaultUIAlias = manifest?.meta.defaultPropertyEditorUiAlias || null;
if (this.#propertyEditorSchemaConfigDefaultUIAlias && this.getPropertyEditorUiAlias() === null) {
// Fallback to the default property editor ui for this property editor schema.
this.setPropertyEditorUiAlias(this.#propertyEditorSchemaConfigDefaultUIAlias);
}
this.#mergeConfigProperties();
},
'schema',
);
}
#setPropertyEditorUIConfig(propertyEditorUIAlias: string) {
return this.observe(
#observePropertyEditorUIManifest(propertyEditorUIAlias: string | null) {
if (!propertyEditorUIAlias) {
this.removeUmbControllerByAlias('editorUi');
return;
}
this.observe(
umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', propertyEditorUIAlias),
(manifest) => {
this.#propertyEditorUiIcon.setValue(manifest?.meta.icon || null);
this.#propertyEditorUiName.setValue(manifest?.name || null);
this.#propertyEditorUISettingsSchemaAlias = manifest?.meta.propertyEditorSchemaAlias;
// Maps properties to have a weight, so they can be sorted, notice UI properties have a +1000 weight compared to schema properties.
this.#propertyEditorUISettingsProperties = (manifest?.meta.settings?.properties ?? []).map((x, i) => ({
...x,
weight: x.weight ?? 1000 + i,
}));
this.#propertyEditorUISettingsDefaultData = manifest?.meta.settings?.defaultData || [];
this.setPropertyEditorSchemaAlias(manifest?.meta.propertyEditorSchemaAlias);
this.#mergeConfigProperties();
},
'editorUi',
).asPromise();
);
}
private _mergeConfigProperties() {
#mergeConfigProperties() {
if (this.#propertyEditorSchemaSettingsProperties && this.#propertyEditorUISettingsProperties) {
// Reset the value to this array, and then afterwards append:
this.#properties.setValue(this.#propertyEditorSchemaSettingsProperties);
// Append the UI settings properties to the schema properties, so they can override the schema properties:
this.#properties.append(this.#propertyEditorUISettingsProperties);
// If new or if the alias was changed then set default values. This 'complexity' to prevent setting default data when initialized [NL]
const previousPropertyEditorUIAlias = this.#lastPropertyEditorUIAlias;
this.#lastPropertyEditorUIAlias = this.getPropertyEditorUiAlias();
if (
this.getIsNew() ||
(previousPropertyEditorUIAlias && previousPropertyEditorUIAlias !== this.#lastPropertyEditorUIAlias)
) {
this.#transferConfigDefaultData();
}
}
}
@@ -301,9 +328,15 @@ export class UmbDataTypeWorkspaceContext
this.#currentData.update({ name });
}
getPropertyEditorSchemaAlias() {
return this.#currentData.getValue()?.editorAlias;
}
setPropertyEditorSchemaAlias(alias?: string) {
this.#currentData.update({ editorAlias: alias });
}
getPropertyEditorUiAlias() {
return this.#currentData.getValue()?.editorUiAlias;
}
setPropertyEditorUiAlias(alias?: string) {
this.#currentData.update({ editorUiAlias: alias });
}

View File

@@ -87,7 +87,9 @@ export class UmbDataTypeWorkspaceViewInfoReferenceElement extends UmbLitElement
(item) => html`
<uui-table-row>
<uui-table-cell>
<uui-ref-node-document-type href=${this.#getEditPath(item)} name=${item.name ?? item.unique}>
<uui-ref-node-document-type
href=${this.#getEditPath(item)}
name=${this.localize.string(item.name ?? item.unique)}>
<umb-icon slot="icon" name=${item.icon ?? 'icon-document'}></umb-icon>
</uui-ref-node-document-type>
</uui-table-cell>

View File

@@ -204,7 +204,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
if (!item.unique) return;
const href = `${this._editPath}edit/${item.unique}`;
return html`
<uui-ref-node-document-type name=${item.name} id=${item.unique}>
<uui-ref-node-document-type name=${this.localize.string(item.name)} id=${item.unique}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button href=${href} label=${this.localize.term('general_open')}></uui-button>

View File

@@ -45,7 +45,7 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement
.min=${this.min}
.max=${this.max}
.value=${this.value}
?elementTypesOnly=${this.onlyElementTypes}
.elementTypesOnly=${this.onlyElementTypes ?? false}
?showOpenButton=${this.showOpenButton}
@change=${this.#onChange}>
</umb-input-document-type>

View File

@@ -1,2 +1 @@
export { UmbDocumentTypeCompositionRepository } from './document-type-composition.repository.js';
export { UMB_DOCUMENT_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -2,11 +2,11 @@ import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/
export const UMB_DOCUMENT_TYPE_COMPOSITION_REPOSITORY_ALIAS = 'Umb.Repository.DocumentType.Composition';
const queryRepository: ManifestRepository = {
const compositionRepository: ManifestRepository = {
type: 'repository',
alias: UMB_DOCUMENT_TYPE_COMPOSITION_REPOSITORY_ALIAS,
name: 'Document Type Composition Repository',
api: () => import('./document-type-composition.repository.js'),
};
export const manifests: Array<ManifestTypes> = [queryRepository];
export const manifests: Array<ManifestTypes> = [compositionRepository];

View File

@@ -133,60 +133,65 @@ export class UmbDocumentCreateOptionsModalElement extends UmbModalBaseElement<
}
#renderDocumentTypes() {
return html`<uui-box .headline=${this._headline}>
${when(
this._allowedDocumentTypes.length === 0,
() => html`
<umb-localize key="create_noDocumentTypes">
There are no allowed Document Types available for creating content here. You must enable these in
<strong>Document Types</strong> within the <strong>Settings</strong> section, by editing the
<strong>Allowed child node types</strong> under <strong>Permissions</strong>.<br />
</umb-localize>
<uui-button
id="edit-permissions"
look="secondary"
@click=${() => this._rejectModal()}
href=${`/section/settings/workspace/document-type/edit/${this.data?.documentType?.unique}/view/structure`}
label=${this.localize.term('create_noDocumentTypesEditPermissions')}></uui-button>
`,
() =>
repeat(
this._allowedDocumentTypes,
(documentType) => documentType.unique,
(documentType) =>
html` <uui-ref-node-document-type
data-id=${ifDefined(documentType.unique)}
.name=${documentType.name}
.alias=${documentType.description ?? ''}
select-only
selectable
@selected=${() => this.#onSelectDocumentType(documentType.unique)}>
<umb-icon slot="icon" name=${documentType.icon || 'icon-circle-dotted'}></umb-icon>
</uui-ref-node-document-type>`,
),
)}
</uui-box>`;
return html`
<uui-box .headline=${this._headline}>
${when(
this._allowedDocumentTypes.length === 0,
() => html`
<umb-localize key="create_noDocumentTypes">
There are no allowed Document Types available for creating content here. You must enable these in
<strong>Document Types</strong> within the <strong>Settings</strong> section, by editing the
<strong>Allowed child node types</strong> under <strong>Permissions</strong>.<br />
</umb-localize>
<uui-button
id="edit-permissions"
look="secondary"
href=${`/section/settings/workspace/document-type/edit/${this.data?.documentType?.unique}/view/structure`}
label=${this.localize.term('create_noDocumentTypesEditPermissions')}
@click=${() => this._rejectModal()}></uui-button>
`,
() =>
repeat(
this._allowedDocumentTypes,
(documentType) => documentType.unique,
(documentType) => html`
<uui-ref-node-document-type
data-id=${ifDefined(documentType.unique)}
.name=${this.localize.string(documentType.name)}
.alias=${this.localize.string(documentType.description ?? '')}
select-only
selectable
@selected=${() => this.#onSelectDocumentType(documentType.unique)}>
<umb-icon slot="icon" name=${documentType.icon || 'icon-circle-dotted'}></umb-icon>
</uui-ref-node-document-type>
`,
),
)}
</uui-box>
`;
}
#renderBlueprints() {
return html`<uui-box headline=${this.localize.term('blueprints_selectBlueprint')}>
<uui-menu-item
id="blank"
label=${this.localize.term('blueprints_blankBlueprint')}
@click=${() => this.#onNavigate(this.#documentTypeUnique)}>
<umb-icon slot="icon" name=${this.#documentTypeIcon}></umb-icon>
</uui-menu-item>
${repeat(
this._availableBlueprints,
(blueprint) => blueprint.unique,
(blueprint) =>
html`<uui-menu-item
label=${blueprint.name}
@click=${() => this.#onNavigate(this.#documentTypeUnique, blueprint.unique)}>
<umb-icon slot="icon" name="icon-blueprint"></umb-icon>
</uui-menu-item>`,
)}</uui-box
> `;
return html`
<uui-box headline=${this.localize.term('blueprints_selectBlueprint')}>
<uui-menu-item
id="blank"
label=${this.localize.term('blueprints_blankBlueprint')}
@click=${() => this.#onNavigate(this.#documentTypeUnique)}>
<umb-icon slot="icon" name=${this.#documentTypeIcon}></umb-icon>
</uui-menu-item>
${repeat(
this._availableBlueprints,
(blueprint) => blueprint.unique,
(blueprint) =>
html`<uui-menu-item
label=${blueprint.name}
@click=${() => this.#onNavigate(this.#documentTypeUnique, blueprint.unique)}>
<umb-icon slot="icon" name="icon-blueprint"></umb-icon>
</uui-menu-item>`,
)}
</uui-box>
`;
}
static override styles = [

View File

@@ -252,7 +252,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement {
<uui-ref-node-document-type
standalone
href=${editDocumentTypePath + 'edit/' + this._documentTypeUnique}
name=${ifDefined(this._documentTypeName)}>
name=${ifDefined(this.localize.string(this._documentTypeName ?? ''))}>
<umb-icon slot="icon" name=${ifDefined(this._documentTypeIcon)}></umb-icon>
</uui-ref-node-document-type>
</div>

View File

@@ -64,25 +64,25 @@ export class UmbHealthCheckGroupBoxOverviewElement extends UmbLitElement {
_renderCheckResults(resultObject: any) {
return html`${resultObject.success > 0
? html`<uui-tag look="secondary" color="positive">
<uui-icon name="icon-check"></uui-icon>
<uui-icon name="check"></uui-icon>
${resultObject.success}
</uui-tag> `
: nothing}
${resultObject.warning > 0
? html`<uui-tag look="secondary" color="warning">
<uui-icon name="icon-alert"></uui-icon>
<uui-icon name="alert"></uui-icon>
${resultObject.warning}
</uui-tag>`
: nothing}
${resultObject.error > 0
? html`<uui-tag look="secondary" color="danger">
<uui-icon name="icon-wrong"></uui-icon>
<uui-icon name="remove"></uui-icon>
${resultObject.error}
</uui-tag>`
: nothing}
${resultObject.info > 0
? html`<uui-tag look="secondary">
<uui-icon name="icon-info"></uui-icon>
<uui-icon name="info"></uui-icon>
${resultObject.info}
</uui-tag>`
: nothing} `;

View File

@@ -129,7 +129,7 @@ export class UmbDashboardHealthCheckGroupElement extends UmbLitElement {
case StatusResultTypeModel.SUCCESS:
return html`<uui-icon style="color: var(--uui-color-positive);" name="check"></uui-icon>`;
case StatusResultTypeModel.WARNING:
return html`<uui-icon style="color: var(--uui-color-warning);" name="alert"></uui-icon>`;
return html`<uui-icon style="color: var(--uui-color-warning-standalone);" name="alert"></uui-icon>`;
case StatusResultTypeModel.ERROR:
return html`<uui-icon style="color: var(--uui-color-danger);" name="remove"></uui-icon>`;
case StatusResultTypeModel.INFO:

View File

@@ -12,6 +12,9 @@ const entityActions: Array<ManifestSectionSidebarApp> = [
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Content',
},
{
alias: 'Umb.Condition.MultipleAppLanguages',
},
],
},
];

View File

@@ -12,6 +12,7 @@ import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
export class UmbAppLanguageContext extends UmbContextBase<UmbAppLanguageContext> implements UmbApi {
#languageCollectionRepository: UmbLanguageCollectionRepository;
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1);
#appLanguage = new UmbObjectState<UmbLanguageDetailModel | undefined>(undefined);
appLanguage = this.#appLanguage.asObservable();

View File

@@ -0,0 +1 @@
export const UMB_LANGUAGE_WORKSPACE_ALIAS = 'Umb.Workspace.Language';

View File

@@ -1,6 +1,7 @@
import { UmbLanguageDetailRepository } from '../../repository/index.js';
import type { UmbLanguageDetailModel } from '../../types.js';
import { UmbLanguageWorkspaceEditorElement } from './language-workspace-editor.element.js';
import { UMB_LANGUAGE_WORKSPACE_ALIAS } from './constants.js';
import {
type UmbSubmittableWorkspaceContext,
UmbSubmittableWorkspaceContextBase,
@@ -27,7 +28,7 @@ export class UmbLanguageWorkspaceContext
readonly validationErrors = this.#validationErrors.asObservable();
constructor(host: UmbControllerHost) {
super(host, 'Umb.Workspace.Language');
super(host, UMB_LANGUAGE_WORKSPACE_ALIAS);
this.routes.setRoutes([
{

View File

@@ -1,3 +1,4 @@
import { UMB_LANGUAGE_WORKSPACE_ALIAS } from './constants.js';
import { UmbSubmitWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import type {
ManifestWorkspaces,
@@ -9,7 +10,7 @@ import type {
const workspace: ManifestWorkspaces = {
type: 'workspace',
kind: 'routable',
alias: 'Umb.Workspace.Language',
alias: UMB_LANGUAGE_WORKSPACE_ALIAS,
name: 'Language Workspace',
api: () => import('./language-workspace.context.js'),
meta: {

View File

@@ -174,7 +174,7 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin<string | undef
if (!item.unique) return;
const href = `${this._editPath}edit/${item.unique}`;
return html`
<uui-ref-node-document-type name=${item.name} id=${item.unique}>
<uui-ref-node-document-type name=${this.localize.string(item.name)} id=${item.unique}>
${this.#renderIcon(item)}
<uui-action-bar slot="actions">
<uui-button href=${href} label=${this.localize.term('general_open')}></uui-button>

View File

@@ -1,12 +1,11 @@
import './components/index.js';
export * from './components/index.js';
export * from './workspace/index.js';
export * from './entity.js';
export * from './repository/index.js';
export * from './tree/types.js';
export * from './utils.ts/index.js';
export * from './types.js';
export * from './entity.js';
export * from './utils.ts/index.js';
export * from './workspace/index.js';
export { UMB_MEDIA_TYPE_PICKER_MODAL } from './tree/index.js';

View File

@@ -0,0 +1 @@
export { UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,12 @@
import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Composition';
const compositionRepository: ManifestRepository = {
type: 'repository',
alias: UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS,
name: 'Media Type Composition Repository',
api: () => import('./media-type-composition.repository.js'),
};
export const manifests: Array<ManifestTypes> = [compositionRepository];

View File

@@ -0,0 +1,36 @@
import { UmbMediaTypeCompositionServerDataSource } from './media-type-composition.server.data-source.js';
import type { UmbContentTypeCompositionRepository } from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type {
UmbMediaTypeAvailableCompositionRequestModel,
UmbMediaTypeCompositionCompatibleModel,
UmbMediaTypeCompositionReferenceModel,
} from '@umbraco-cms/backoffice/media-type';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbMediaTypeCompositionRepository
extends UmbRepositoryBase
implements
UmbContentTypeCompositionRepository<
UmbMediaTypeCompositionReferenceModel,
UmbMediaTypeCompositionCompatibleModel,
UmbMediaTypeAvailableCompositionRequestModel
>
{
#compositionSource: UmbMediaTypeCompositionServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#compositionSource = new UmbMediaTypeCompositionServerDataSource(this);
}
async getReferences(unique: string) {
return this.#compositionSource.getReferences(unique);
}
async availableCompositions(args: UmbMediaTypeAvailableCompositionRequestModel) {
return this.#compositionSource.availableCompositions(args);
}
}
export { UmbMediaTypeCompositionRepository as api };

View File

@@ -0,0 +1,86 @@
import type {
UmbMediaTypeCompositionCompatibleModel,
UmbMediaTypeCompositionReferenceModel,
UmbMediaTypeAvailableCompositionRequestModel,
} from '../../types.js';
import { type MediaTypeCompositionRequestModel, MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbContentTypeCompositionDataSource } from '@umbraco-cms/backoffice/content-type';
/**
* A data source for the Media Type Composition that fetches data from the server
* @export
* @class UmbMediaTypeCompositionServerDataSource
*/
export class UmbMediaTypeCompositionServerDataSource
implements
UmbContentTypeCompositionDataSource<
UmbMediaTypeCompositionReferenceModel,
UmbMediaTypeCompositionCompatibleModel,
UmbMediaTypeAvailableCompositionRequestModel
>
{
#host: UmbControllerHost;
/**
* Creates an instance of UmbMediaTypeCompositionServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMediaTypeCompositionServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the compatible compositions for a Media type from the server
* @param {string} unique
* @return {*}
* @memberof UmbMediaTypeCompositionServerDataSource
*/
async getReferences(unique: string) {
const response = await tryExecuteAndNotify(
this.#host,
MediaTypeService.getMediaTypeByIdCompositionReferences({ id: unique }),
);
const error = response.error;
const data: Array<UmbMediaTypeCompositionReferenceModel> | undefined = response.data?.map((reference) => {
return {
unique: reference.id,
icon: reference.icon,
name: reference.name,
};
});
return { data, error };
}
/**
* Updates the compositions for a media type on the server
* @param {MediaTypeCompositionRequestModel} requestBody
* @return {*}
* @memberof UmbMediaTypeCompositionServerDataSource
*/
async availableCompositions(args: UmbMediaTypeAvailableCompositionRequestModel) {
const requestBody: MediaTypeCompositionRequestModel = {
id: args.unique,
currentCompositeIds: args.currentCompositeUniques,
currentPropertyAliases: args.currentPropertyAliases,
};
const response = await tryExecuteAndNotify(
this.#host,
MediaTypeService.postMediaTypeAvailableCompositions({ requestBody }),
);
const error = response.error;
const data: Array<UmbMediaTypeCompositionCompatibleModel> | undefined = response.data?.map((composition) => {
return {
unique: composition.id,
name: composition.name,
icon: composition.icon,
folderPath: composition.folderPath,
isCompatible: composition.isCompatible,
};
});
return { data, error };
}
}

View File

@@ -1,3 +1,4 @@
export * from './item/index.js';
export * from './composition/index.js';
export * from './detail/index.js';
export * from './item/index.js';
export * from './structure/index.js';

View File

@@ -1,5 +1,6 @@
import { manifests as detailManifests } from './detail/manifests.js';
import { manifests as itemManifests } from './item/manifests.js';
import { manifests as compositionManifests } from './composition/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [...detailManifests, ...itemManifests];
export const manifests: Array<ManifestTypes> = [...detailManifests, ...itemManifests, ...compositionManifests];

View File

@@ -1,6 +1,17 @@
import type { UmbMediaTypeEntityType } from './entity.js';
import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type';
import type {
UmbContentTypeAvailableCompositionRequestModel,
UmbContentTypeCompositionCompatibleModel,
UmbContentTypeCompositionReferenceModel,
UmbContentTypeModel,
} from '@umbraco-cms/backoffice/content-type';
export interface UmbMediaTypeDetailModel extends UmbContentTypeModel {
entityType: UmbMediaTypeEntityType;
}
export interface UmbMediaTypeAvailableCompositionRequestModel extends UmbContentTypeAvailableCompositionRequestModel {}
export interface UmbMediaTypeCompositionCompatibleModel extends UmbContentTypeCompositionCompatibleModel {}
export interface UmbMediaTypeCompositionReferenceModel extends UmbContentTypeCompositionReferenceModel {}

View File

@@ -1,3 +1,4 @@
import { UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS } from '../repository/index.js';
import { UMB_MEDIA_TYPE_WORKSPACE_ALIAS } from './constants.js';
import type {
ManifestWorkspaces,
@@ -29,6 +30,7 @@ const workspaceViews: Array<ManifestWorkspaceViews> = [
label: '#general_design',
pathname: 'design',
icon: 'icon-document-dashed-line',
compositionRepositoryAlias: UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS,
},
conditions: [
{

View File

@@ -89,7 +89,8 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement<
}
#renderNotAllowed() {
return html`<umb-localize key="create_noMediaTypes">
return html`
<umb-localize key="create_noMediaTypes">
There are no allowed Media Types available for creating media here. You must enable these in
<strong>Media Types</strong> within the <strong>Settings</strong> section, by editing the
<strong>Allowed child node types</strong> under <strong>Permissions</strong>. </umb-localize
@@ -99,23 +100,25 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement<
look="secondary"
@click=${() => this._rejectModal()}
href=${`/section/settings/workspace/media-type/edit/${this.data?.mediaType?.unique}/view/structure`}
label=${this.localize.term('create_noMediaTypesEditPermissions')}></uui-button>`;
label=${this.localize.term('create_noMediaTypesEditPermissions')}></uui-button>
`;
}
#renderAllowedMediaTypes() {
return repeat(
this._allowedMediaTypes,
(mediaType) => mediaType.unique,
(mediaType) =>
html`<uui-ref-node-document-type
(mediaType) => html`
<uui-ref-node-document-type
data-id=${ifDefined(mediaType.unique)}
.name=${mediaType.name}
.alias=${mediaType.description ?? ''}
.name=${this.localize.string(mediaType.name)}
.alias=${this.localize.string(mediaType.description ?? '')}
select-only
selectable
@selected=${() => this.#onNavigate(mediaType)}>
${mediaType.icon ? html`<umb-icon slot="icon" name=${mediaType.icon}></umb-icon>` : nothing}
</uui-ref-node-document-type>`,
</uui-ref-node-document-type>
`,
);
}

View File

@@ -137,7 +137,7 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin<string | unde
#renderItem(item: UmbUniqueItemModel) {
if (!item.unique) return;
return html`
<uui-ref-node-document-type name=${item.name}>
<uui-ref-node-document-type name=${this.localize.string(item.name)}>
${when(item.icon, () => html`<umb-icon slot="icon" name=${item.icon!}></umb-icon>`)}
<uui-action-bar slot="actions">
<uui-button

View File

@@ -1,10 +1,9 @@
import './components/index.js';
export * from './workspace/index.js';
export * from './components/index.js';
export * from './repository/index.js';
export * from './entity.js';
export * from './tree/index.js';
export * from './modal/member-type-picker-modal.token.js';
export type { UmbMemberTypeDetailModel } from './types.js';
export * from './repository/index.js';
export * from './tree/index.js';
export * from './types.js';
export * from './workspace/index.js';

View File

@@ -0,0 +1 @@
export { UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,12 @@
import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS = 'Umb.Repository.MemberType.Composition';
const compositionRepository: ManifestRepository = {
type: 'repository',
alias: UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS,
name: 'Member Type Composition Repository',
api: () => import('./member-type-composition.repository.js'),
};
export const manifests: Array<ManifestTypes> = [compositionRepository];

View File

@@ -0,0 +1,36 @@
import { UmbMemberTypeCompositionServerDataSource } from './member-type-composition.server.data-source.js';
import type { UmbContentTypeCompositionRepository } from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type {
UmbMemberTypeAvailableCompositionRequestModel,
UmbMemberTypeCompositionCompatibleModel,
UmbMemberTypeCompositionReferenceModel,
} from '@umbraco-cms/backoffice/member-type';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbMemberTypeCompositionRepository
extends UmbRepositoryBase
implements
UmbContentTypeCompositionRepository<
UmbMemberTypeCompositionReferenceModel,
UmbMemberTypeCompositionCompatibleModel,
UmbMemberTypeAvailableCompositionRequestModel
>
{
#compositionSource: UmbMemberTypeCompositionServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#compositionSource = new UmbMemberTypeCompositionServerDataSource(this);
}
async getReferences(unique: string) {
return this.#compositionSource.getReferences(unique);
}
async availableCompositions(args: UmbMemberTypeAvailableCompositionRequestModel) {
return this.#compositionSource.availableCompositions(args);
}
}
export { UmbMemberTypeCompositionRepository as api };

View File

@@ -0,0 +1,89 @@
import type {
UmbMemberTypeCompositionCompatibleModel,
UmbMemberTypeCompositionReferenceModel,
UmbMemberTypeAvailableCompositionRequestModel,
} from '../../types.js';
import {
type MemberTypeCompositionRequestModel,
MemberTypeService,
} from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbContentTypeCompositionDataSource } from '@umbraco-cms/backoffice/content-type';
/**
* A data source for the Member Type Composition that fetches data from the server
* @export
* @class UmbMemberTypeCompositionServerDataSource
*/
export class UmbMemberTypeCompositionServerDataSource
implements
UmbContentTypeCompositionDataSource<
UmbMemberTypeCompositionReferenceModel,
UmbMemberTypeCompositionCompatibleModel,
UmbMemberTypeAvailableCompositionRequestModel
>
{
#host: UmbControllerHost;
/**
* Creates an instance of UmbMemberTypeCompositionServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMemberTypeCompositionServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the compatible compositions for a document type from the server
* @param {string} unique
* @return {*}
* @memberof UmbMemberTypeCompositionServerDataSource
*/
async getReferences(unique: string) {
const response = await tryExecuteAndNotify(
this.#host,
MemberTypeService.getMemberTypeByIdCompositionReferences({ id: unique }),
);
const error = response.error;
const data: Array<UmbMemberTypeCompositionReferenceModel> | undefined = response.data?.map((reference) => {
return {
unique: reference.id,
icon: reference.icon,
name: reference.name,
};
});
return { data, error };
}
/**
* Updates the compositions for a document type on the server
* @param {MemberTypeCompositionRequestModel} requestBody
* @return {*}
* @memberof UmbMemberTypeCompositionServerDataSource
*/
async availableCompositions(args: UmbMemberTypeAvailableCompositionRequestModel) {
const requestBody: MemberTypeCompositionRequestModel = {
id: args.unique,
currentCompositeIds: args.currentCompositeUniques,
currentPropertyAliases: args.currentPropertyAliases,
};
const response = await tryExecuteAndNotify(
this.#host,
MemberTypeService.postMemberTypeAvailableCompositions({ requestBody }),
);
const error = response.error;
const data: Array<UmbMemberTypeCompositionCompatibleModel> | undefined = response.data?.map((composition) => {
return {
unique: composition.id,
name: composition.name,
icon: composition.icon,
folderPath: composition.folderPath,
isCompatible: composition.isCompatible,
};
});
return { data, error };
}
}

View File

@@ -1,2 +1,3 @@
export { UmbMemberTypeDetailRepository, UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS } from './detail/index.js';
export { UmbMemberTypeItemRepository, UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS } from './item/index.js';
export { UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './composition/index.js';

View File

@@ -1,5 +1,6 @@
import { manifests as detailManifests } from './detail/manifests.js';
import { manifests as itemManifests } from './item/manifests.js';
import { manifests as compositionManifests } from './composition/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [...detailManifests, ...itemManifests];
export const manifests: Array<ManifestTypes> = [...detailManifests, ...itemManifests, ...compositionManifests];

View File

@@ -1,6 +1,17 @@
import type { UmbMemberTypeEntityType } from './entity.js';
import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type';
import type {
UmbContentTypeAvailableCompositionRequestModel,
UmbContentTypeCompositionCompatibleModel,
UmbContentTypeCompositionReferenceModel,
UmbContentTypeModel,
} from '@umbraco-cms/backoffice/content-type';
export interface UmbMemberTypeDetailModel extends UmbContentTypeModel {
entityType: UmbMemberTypeEntityType;
}
export interface UmbMemberTypeAvailableCompositionRequestModel extends UmbContentTypeAvailableCompositionRequestModel {}
export interface UmbMemberTypeCompositionCompatibleModel extends UmbContentTypeCompositionCompatibleModel {}
export interface UmbMemberTypeCompositionReferenceModel extends UmbContentTypeCompositionReferenceModel {}

View File

@@ -1,7 +1,8 @@
import { UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS } from '../repository/index.js';
import type {
ManifestWorkspaces,
ManifestWorkspaceActions,
ManifestWorkspaceView,
ManifestWorkspaceViews,
ManifestTypes,
} from '@umbraco-cms/backoffice/extension-registry';
import { UmbSubmitWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
@@ -19,7 +20,7 @@ const workspace: ManifestWorkspaces = {
},
};
const workspaceViews: Array<ManifestWorkspaceView> = [
const workspaceViews: Array<ManifestWorkspaceViews> = [
{
type: 'workspaceView',
kind: 'contentTypeDesignEditor',
@@ -29,6 +30,7 @@ const workspaceViews: Array<ManifestWorkspaceView> = [
label: '#general_design',
pathname: 'design',
icon: 'icon-member-dashed-line',
compositionRepositoryAlias: UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS,
},
conditions: [
{

View File

@@ -152,7 +152,7 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement {
<uui-combobox id="preview">
<uui-combobox-list @change=${this.#onChange}>
<uui-combobox-list-option value="system">
<strong>${this.localize.term('formSettings_systemFields')}</strong>
<strong>${this.localize.term('template_systemFields')}</strong>
</uui-combobox-list-option>
<uui-combobox-list-option value="document-type" display-value=${this.localize.term('content_documentType')}>
<strong> ${this.localize.term('content_documentType')} </strong>
@@ -174,7 +174,8 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement {
#renderAliasDropdown() {
if (this._type !== FieldType.SYSTEM && !this._unique) return;
return html`<strong>${this._uniqueName}</strong>
return html`
<strong>${this.localize.string(this._uniqueName ?? '')}</strong>
<uui-combobox id="value" value=${ifDefined(this.value?.alias)}>
<uui-combobox-list @change=${this.#onChangeValue}>
${repeat(
@@ -184,7 +185,8 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement {
html`<uui-combobox-list-option value=${ifDefined(field.alias)}>${field.alias}</uui-combobox-list-option>`,
)}
</uui-combobox-list>
</uui-combobox>`;
</uui-combobox>
`;
}
static override styles = [

View File

@@ -0,0 +1,8 @@
{
"name": "@umbraco-backoffice/tiny-mce",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
}
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite';
import { rmSync } from 'fs';
import { getDefaultConfig } from '../../vite-config-base';
const dist = '../../../dist-cms/packages/tiny-mce';
// delete the unbundled dist folder
rmSync(dist, { recursive: true, force: true });
export default defineConfig({
...getDefaultConfig({ dist }),
});

View File

@@ -1,4 +1,4 @@
import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-webhook-table-boolean-column-layout')
@@ -7,7 +7,7 @@ export class UmbWebhookTableBooleanColumnLayoutElement extends UmbLitElement {
value = false;
override render() {
return this.value ? html`<uui-icon name="icon-check"></uui-icon>` : nothing;
return html`<uui-icon name="${this.value ? 'check' : 'remove'}"></uui-icon>`;
}
}

View File

@@ -32,7 +32,7 @@ export class UmbWebhookTableContentTypeColumnLayoutElement extends UmbLitElement
if (this.value?.contentTypeName && this.#repository) {
const { data } = await this.#repository.requestItems(this.value.contentTypes);
this._contentTypes = data?.map((item) => item.name).join(', ') ?? '';
this._contentTypes = data?.map((item) => this.localize.string(item.name)).join(', ') ?? '';
}
}

View File

@@ -1,10 +1,9 @@
import type { UmbWebhookDetailModel } from '../../../types.js';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './column-layouts/boolean/webhook-table-boolean-column-layout.element.js';
import './column-layouts/name/webhook-table-name-column-layout.element.js';
@@ -21,25 +20,25 @@ export class UmbWebhookTableCollectionViewElement extends UmbLitElement {
@state()
private _tableColumns: Array<UmbTableColumn> = [
{
name: 'Name',
name: this.localize.term('general_name'),
alias: 'name',
elementName: 'umb-webhook-table-name-column-layout',
},
{
name: 'Enabled',
name: this.localize.term('webhooks_enabled'),
alias: 'enabled',
elementName: 'umb-webhook-table-boolean-column-layout',
},
{
name: 'URL',
name: this.localize.term('webhooks_url'),
alias: 'url',
},
{
name: 'Events',
name: this.localize.term('webhooks_events'),
alias: 'events',
},
{
name: 'Types',
name: this.localize.term('webhooks_types'),
alias: 'types',
elementName: 'umb-webhook-table-content-type-column-layout',
},
@@ -112,7 +111,6 @@ export class UmbWebhookTableCollectionViewElement extends UmbLitElement {
}
static override styles = [
UmbTextStyles,
css`
:host {
display: flex;

View File

@@ -47,23 +47,29 @@ export class UmbInputWebhookEventsElement extends UmbLitElement {
if (!this.events.length) return nothing;
return html`
${repeat(
this.events,
(item) => item.alias,
(item) => html`
<span>${item.eventName}</span>
<uui-button
label=${this.localize.term('general_remove')}
@click=${() => this.#removeEvent(item.alias)}></uui-button>
`,
)}
<uui-ref-list>
${repeat(
this.events,
(item) => item.alias,
(item) => html`
<uui-ref-node name=${item.eventName} @open=${this.#openModal}>
<umb-icon slot="icon" name="icon-globe"></umb-icon>
<uui-action-bar slot="actions">
<uui-button
label=${this.localize.term('general_remove')}
@click=${() => this.#removeEvent(item.alias)}></uui-button>
</uui-action-bar>
</uui-ref-node>
`,
)}
</uui-ref-list>
`;
}
override render() {
return html`${this.#renderEvents()}
<uui-button
id="choose"
id="btn-add"
look="placeholder"
label=${this.localize.term('general_choose')}
@click=${this.#openModal}></uui-button>`;
@@ -72,15 +78,8 @@ export class UmbInputWebhookEventsElement extends UmbLitElement {
static override styles = [
UmbTextStyles,
css`
:host {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--uui-size-space-2) var(--uui-size-space-2);
align-items: center;
}
#choose {
grid-column: -1 / 1;
#btn-add {
display: block;
}
`,
];

View File

@@ -14,7 +14,7 @@ export class UmbInputWebhookHeadersElement extends UmbLitElement {
private _headers: Array<{ name: string; value: string }> = [];
@state()
private _headerNames: string[] = ['Accept', 'Content-Type', 'User-Agent', 'Content-Length'];
private _headerNames: string[] = ['Accept', 'Content-Length', 'Content-Type', 'User-Agent'];
get #filterHeaderNames() {
return this._headerNames.filter((name) => !this._headers.find((header) => header.name === name));
@@ -78,7 +78,7 @@ export class UmbInputWebhookHeadersElement extends UmbLitElement {
.value=${header.value}
@input=${(e: InputEvent) => this.#onInput(e, 'value', index)}
list="valueList" />
<uui-button @click=${() => this.#removeHeader(index)} label="Remove"></uui-button>
<uui-button @click=${() => this.#removeHeader(index)} label=${this.localize.term('general_remove')}></uui-button>
`;
}
@@ -105,8 +105,8 @@ export class UmbInputWebhookHeadersElement extends UmbLitElement {
if (!this._headers.length) return nothing;
return html`
<span class="grid-top">KEY</span>
<span class="grid-top">VALUE</span>
<span class="grid-top"><umb-localize key="general_name">Name</umb-localize></span>
<span class="grid-top"><umb-localize key="general_value">Value</umb-localize></span>
<span class="grid-top"></span>
${repeat(
this._headers,

View File

@@ -1,9 +1,7 @@
import { UmbWebhookEventRepository } from '../../repository/event/webhook-event.repository.js';
import type { UmbWebhookEventModel } from '../../types.js';
import type { UmbWebhookPickerModalData, UmbWebhookPickerModalValue } from './webhook-events-modal.token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { customElement, html, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
@@ -67,32 +65,36 @@ export class UmbWebhookEventsModalElement extends UmbModalBaseElement<
}
override render() {
return html`<umb-body-layout headline="Select events">
<uui-box>
${repeat(
this._events,
(item) => item.alias,
(item) => html`
<uui-menu-item
label=${item.eventName}
?disabled=${this.#getItemDisabled(item)}
selectable
@selected=${() => this.#selectionManager.select(item.alias)}
@deselected=${() => this.#selectionManager.deselect(item.alias)}
?selected=${this.value.events.includes(item)}></uui-menu-item>
<uui-icon slot="icon" name="icon-globe"></uui-icon>
</uui-menu-item>
`,
)}
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this.#close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this.#submit}></uui-button>
</div>
</umb-body-layout> `;
return html`
<umb-body-layout headline=${this.localize.term('webhooks_selectEvents')}>
<uui-box>
${repeat(
this._events,
(item) => item.alias,
(item) => html`
<uui-menu-item
label=${item.eventName}
?disabled=${this.#getItemDisabled(item)}
selectable
@selected=${() => this.#selectionManager.select(item.alias)}
@deselected=${() => this.#selectionManager.deselect(item.alias)}
?selected=${this.value.events.includes(item)}>
<uui-icon slot="icon" name="icon-globe"></uui-icon>
</uui-menu-item>
`,
)}
</uui-box>
<div slot="actions">
<uui-button label=${this.localize.term('general_cancel')} @click=${this.#close}></uui-button>
<uui-button
label=${this.localize.term('general_submit')}
look="primary"
color="positive"
@click=${this.#submit}></uui-button>
</div>
</umb-body-layout>
`;
}
static override styles = [UmbTextStyles, css``];
}
export default UmbWebhookEventsModalElement;

View File

@@ -5,9 +5,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-webhook-root-workspace')
export class UmbWebhookRootWorkspaceElement extends UmbLitElement {
override render() {
return html` <umb-body-layout main-no-padding headline="Webhooks">
<umb-collection alias=${UMB_WEBHOOK_COLLECTION_ALIAS}></umb-collection>;
</umb-body-layout>`;
return html`
<umb-body-layout main-no-padding headline=${this.localize.term('treeHeaders_webhooks')}>
<umb-collection alias=${UMB_WEBHOOK_COLLECTION_ALIAS}></umb-collection>;
</umb-body-layout>
`;
}
}

View File

@@ -1,17 +1,16 @@
import type { UmbInputWebhookHeadersElement } from '../../../components/input-webhook-headers.element.js';
import { UMB_WEBHOOK_WORKSPACE_CONTEXT } from '../webhook-workspace.context-token.js';
import type { UmbInputWebhookHeadersElement } from '../../../components/input-webhook-headers.element.js';
import type { UmbInputWebhookEventsElement } from '../../../components/input-webhook-events.element.js';
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import '@umbraco-cms/backoffice/culture';
import type { UmbWebhookDetailModel } from '@umbraco-cms/backoffice/webhook';
import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbInputDocumentTypeElement } from '@umbraco-cms/backoffice/document-type';
import type { UmbWebhookDetailModel } from '@umbraco-cms/backoffice/webhook';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UUIBooleanInputEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import '@umbraco-cms/backoffice/culture';
import '../../../components/input-webhook-headers.element.js';
import '../../../components/input-webhook-events.element.js';
@@ -43,9 +42,9 @@ export class UmbWebhookDetailsWorkspaceViewElement extends UmbLitElement impleme
});
}
#onEventsChange(event: UmbChangeEvent) {
const events = (event.target as UmbInputWebhookEventsElement).events;
if (events[0].eventType !== this.contentType) {
#onEventsChange(event: UmbChangeEvent & { target: UmbInputWebhookEventsElement }) {
const events = event.target.events ?? [];
if (events.length && events[0].eventType !== this.contentType) {
this.#webhookWorkspaceContext?.setTypes([]);
}
this.#webhookWorkspaceContext?.setEvents(events);
@@ -75,7 +74,9 @@ export class UmbWebhookDetailsWorkspaceViewElement extends UmbLitElement impleme
if (this.contentType !== 'Content' && this.contentType !== 'Media') return nothing;
return html`
<umb-property-layout label="Content Type" description="Only trigger the webhook for a specific content type.">
<umb-property-layout
label=${this.localize.term('webhooks_contentType')}
description=${this.localize.term('webhooks_contentTypeDescription')}>
${this.#renderContentTypePickerEditor()}
</umb-property-layout>
`;
@@ -84,17 +85,20 @@ export class UmbWebhookDetailsWorkspaceViewElement extends UmbLitElement impleme
#renderContentTypePickerEditor() {
switch (this.contentType) {
case 'Content':
return html`<umb-input-document-type
@change=${this.#onTypesChange}
.selection=${this._webhook?.contentTypes ?? []}
slot="editor"
?elementTypesOnly=${true}></umb-input-document-type>`;
return html`
<umb-input-document-type
slot="editor"
@change=${this.#onTypesChange}
.selection=${this._webhook?.contentTypes ?? []}
.documentTypesOnly=${true}></umb-input-document-type>
`;
case 'Media':
return html`<umb-input-media-type
@change=${this.#onTypesChange}
.selection=${this._webhook?.contentTypes ?? []}
slot="editor"
?elementTypesOnly=${true}></umb-input-media-type>`;
return html`
<umb-input-media-type
slot="editor"
@change=${this.#onTypesChange}
.selection=${this._webhook?.contentTypes ?? []}></umb-input-media-type>
`;
default:
return nothing;
}
@@ -105,20 +109,28 @@ export class UmbWebhookDetailsWorkspaceViewElement extends UmbLitElement impleme
return html`
<uui-box>
<umb-property-layout label="Url" description="The url to call when the webhook is triggered.">
<umb-property-layout
label=${this.localize.term('webhooks_url')}
description=${this.localize.term('webhooks_urlDescription')}>
<uui-input @input=${this.#onUrlChange} .value=${this._webhook.url} slot="editor"></uui-input>
</umb-property-layout>
<umb-property-layout label="Events" description="The events for which the webhook should be triggered.">
<umb-property-layout
label=${this.localize.term('webhooks_events')}
description=${this.localize.term('webhooks_eventDescription')}>
<umb-input-webhook-events
@change=${this.#onEventsChange}
.events=${this._webhook.events ?? []}
slot="editor"></umb-input-webhook-events>
</umb-property-layout>
${this.#renderContentTypePicker()}
<umb-property-layout label="Enabled" description="Is the webhook enabled?">
<uui-toggle slot="editor" .checked=${this._webhook.enabled} @input=${this.#onEnabledChange}></uui-toggle>
<umb-property-layout
label=${this.localize.term('webhooks_enabled')}
description=${this.localize.term('webhooks_enabledDescription')}>
<uui-toggle slot="editor" .checked=${this._webhook.enabled} @change=${this.#onEnabledChange}></uui-toggle>
</umb-property-layout>
<umb-property-layout label="Headers" description="Custom headers to include in the webhook request.">
<umb-property-layout
label=${this.localize.term('webhooks_headers')}
description=${this.localize.term('webhooks_headersDescription')}>
<umb-input-webhook-headers
@change=${this.#onHeadersChange}
.headers=${this._webhook.headers}

View File

@@ -4,9 +4,11 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-webhook-workspace-editor')
export class UmbWebhookWorkspaceEditorElement extends UmbLitElement {
override render() {
return html`<umb-workspace-editor
alias="Umb.Workspace.Webhook"
back-path="section/settings/workspace/webhook-root"></umb-workspace-editor>`;
return html`
<umb-workspace-editor
alias="Umb.Workspace.Webhook"
back-path="section/settings/workspace/webhook-root"></umb-workspace-editor>
`;
}
static override styles = [UmbTextStyles];