diff --git a/src/Umbraco.Web.UI.Client/examples/block-custom-view/README.md b/src/Umbraco.Web.UI.Client/examples/block-custom-view/README.md new file mode 100644 index 0000000000..e0894b30b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/block-custom-view/README.md @@ -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 diff --git a/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts b/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts new file mode 100644 index 0000000000..abd194cf81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/block-custom-view/block-custom-view.ts @@ -0,0 +1,42 @@ +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'; + +@customElement('example-block-custom-view') +export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement { + // + @property({ attribute: false }) + content?: UmbBlockDataType; + + override render() { + return html` +
+
My Custom View
+

Headline: ${this.content.headline}

+
+ `; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts b/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts new file mode 100644 index 0000000000..e50355a08b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/block-custom-view/index.ts @@ -0,0 +1,12 @@ +import type { ManifestBlockEditorCustomView } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'blockEditorCustomView', + alias: 'Umb.blockEditorCustomView.TestView', + name: 'Block Editor Custom View Test', + element: () => import('./block-custom-view.js'), + forContentTypeAlias: 'headlineUmbracoDemoBlock', + forBlockEditor: 'block-grid', + }, +]; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dafc1e3c8f..bfe157ce3a 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -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" }, diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3ed5ffa50b..766ac0c7bb 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -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", diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 7e0e77d5cd..f5419e0bea 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -1562,6 +1562,7 @@ export default { ascending: 'ascending', descending: 'descending', template: 'Template', + systemFields: 'System fields', }, grid: { media: 'Image', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts index 875b2333d2..10dd58fa03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts @@ -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 @@ -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() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts index 3f102682ed..380ea33f83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts @@ -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}>`, )} - ` + ` : ''; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts index 7585986cb5..74b2b8bfcb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts @@ -112,7 +112,6 @@ export class UmbBlockTypeWorkspaceContext + label=${this.localize.term('general_choose')}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/composition-picker/composition-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/composition-picker/composition-picker-modal.element.ts index eb9739e8c0..24da1d9032 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/composition-picker/composition-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/modals/composition-picker/composition-picker-modal.element.ts @@ -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<
${!this._references.length - ? html`` + ? html` + + ` : nothing}
@@ -141,7 +144,8 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< } #renderHasReference() { - return html` + return html` + This Content Type is used in a composition, and therefore cannot be composed itself.

@@ -154,19 +158,22 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< ${repeat( this._references, (item) => item.unique, - (item) => - html` html` + + name=${this.localize.string(item.name)}> - `, + + `, )} - `; + + `; } #renderAvailableCompositions() { if (this._compatibleCompositions) { - return html` + return html` + Inherit tabs and properties from an existing Document Type. New tabs will be
added to the current Document Type or merged if a tab with an identical name exists.
@@ -184,11 +191,14 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< : nothing} ${this.#renderCompositionsItems(folder.compositions)}`, )} - `; + + `; } else { - return html` - There are no Content Types available to use as a composition - `; + return html` + + There are no Content Types available to use as a composition + + `; } } @@ -196,15 +206,16 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< return repeat( compositionsList, (compositions) => compositions.unique, - (compositions) => - html` html` + this.#onSelectionAdd(compositions.unique)} @deselected=${() => this.#onSelectionRemove(compositions.unique)} ?selected=${this._selection.find((unique) => unique === compositions.unique)}> - `, + + `, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-container-structure-helper.class.ts index 07b2268a71..32912b50e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-container-structure-helper.class.ts @@ -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; + #containerObservers: Array = []; + // State containing the all containers defined in the data: #childContainers = new UmbArrayState([], (x) => x.id); readonly containers = this.#childContainers.asObservable(); @@ -150,30 +152,31 @@ export class UmbContentTypeContainerStructureHelper { - // 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, + ), ); }); }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-property-structure-helper.class.ts index 55be7cf04d..14753917ba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-property-structure-helper.class.ts @@ -21,7 +21,7 @@ export class UmbContentTypePropertyStructureHelper; - 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([], (x) => x.id); @@ -59,12 +59,12 @@ export class UmbContentTypePropertyStructureHelper; #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 { if (container) { this._containerName = container.name ?? ''; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts index 3c811652d4..8fe26a1460 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts @@ -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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts index 109077161d..34e5c02eeb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -416,14 +416,16 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements return html`
${this._compositionRepositoryAlias - ? html` - - ${this.localize.term('contentTypeEditor_compositions')} - ` + ? html` + + + ${this.localize.term('contentTypeEditor_compositions')} + + ` : ''} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-tab.element.ts index 1fb52834a2..bb8a5a451f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-tab.element.ts @@ -63,7 +63,7 @@ export class UmbContentWorkspaceViewEditTabElement extends UmbLitElement { this._groups, (group) => group.id, (group) => - html` + html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor.element.ts index f37657e16d..87db2c05b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor.element.ts @@ -138,7 +138,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements (tab) => { const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); return html`${tab.name}${this.localize.string(tab.name)}`; }, )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/lit-element/directives/focus.lit-directive.ts b/src/Umbraco.Web.UI.Client/src/packages/core/lit-element/directives/focus.lit-directive.ts index 5d237844ea..f4e7032fd8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/lit-element/directives/focus.lit-directive.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/lit-element/directives/focus.lit-directive.ts @@ -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() {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts index f2b5e98a4e..cc755ebd24 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts @@ -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 = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/item-picker/item-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/item-picker/item-picker-modal.element.ts index 20f8747a4f..0f5fa9b803 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/item-picker/item-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/item-picker/item-picker-modal.element.ts @@ -51,7 +51,7 @@ export class UmbItemPickerModalElement extends UmbModalBaseElement +
item.value, (item) => html` this.#submit(item)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts index c94e3d9126..0a25c5c759 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts @@ -215,6 +215,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement extends UmbLitElement { return html` html` - + diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index ce3702cb23..a0a7d6a13c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -204,7 +204,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin + ${this.#renderIcon(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/index.ts index 99c179ec94..8a9f7eae16 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/index.ts @@ -1,2 +1 @@ -export { UmbDocumentTypeCompositionRepository } from './document-type-composition.repository.js'; export { UMB_DOCUMENT_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/manifests.ts index 1732701de6..b01f3b0be9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/composition/manifests.ts @@ -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 = [queryRepository]; +export const manifests: Array = [compositionRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts index c6e010236a..3f6751d76b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts @@ -133,60 +133,65 @@ export class UmbDocumentCreateOptionsModalElement extends UmbModalBaseElement< } #renderDocumentTypes() { - return html` - ${when( - this._allowedDocumentTypes.length === 0, - () => html` - - There are no allowed Document Types available for creating content here. You must enable these in - Document Types within the Settings section, by editing the - Allowed child node types under Permissions.
-
- this._rejectModal()} - href=${`/section/settings/workspace/document-type/edit/${this.data?.documentType?.unique}/view/structure`} - label=${this.localize.term('create_noDocumentTypesEditPermissions')}> - `, - () => - repeat( - this._allowedDocumentTypes, - (documentType) => documentType.unique, - (documentType) => - html` this.#onSelectDocumentType(documentType.unique)}> - - `, - ), - )} -
`; + return html` + + ${when( + this._allowedDocumentTypes.length === 0, + () => html` + + There are no allowed Document Types available for creating content here. You must enable these in + Document Types within the Settings section, by editing the + Allowed child node types under Permissions.
+
+ this._rejectModal()}> + `, + () => + repeat( + this._allowedDocumentTypes, + (documentType) => documentType.unique, + (documentType) => html` + this.#onSelectDocumentType(documentType.unique)}> + + + `, + ), + )} +
+ `; } #renderBlueprints() { - return html` - this.#onNavigate(this.#documentTypeUnique)}> - - - ${repeat( - this._availableBlueprints, - (blueprint) => blueprint.unique, - (blueprint) => - html` this.#onNavigate(this.#documentTypeUnique, blueprint.unique)}> - - `, - )} `; + return html` + + this.#onNavigate(this.#documentTypeUnique)}> + + + ${repeat( + this._availableBlueprints, + (blueprint) => blueprint.unique, + (blueprint) => + html` this.#onNavigate(this.#documentTypeUnique, blueprint.unique)}> + + `, + )} + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index 0a650e22e4..13344c29a1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -252,7 +252,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { + name=${ifDefined(this.localize.string(this._documentTypeName ?? ''))}>
diff --git a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group-box-overview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group-box-overview.element.ts index 42d8679729..3dadc4cee6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group-box-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group-box-overview.element.ts @@ -64,25 +64,25 @@ export class UmbHealthCheckGroupBoxOverviewElement extends UmbLitElement { _renderCheckResults(resultObject: any) { return html`${resultObject.success > 0 ? html` - + ${resultObject.success} ` : nothing} ${resultObject.warning > 0 ? html` - + ${resultObject.warning} ` : nothing} ${resultObject.error > 0 ? html` - + ${resultObject.error} ` : nothing} ${resultObject.info > 0 ? html` - + ${resultObject.info} ` : nothing} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts index 787edc26c6..084d41a1a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts @@ -129,7 +129,7 @@ export class UmbDashboardHealthCheckGroupElement extends UmbLitElement { case StatusResultTypeModel.SUCCESS: return html``; case StatusResultTypeModel.WARNING: - return html``; + return html``; case StatusResultTypeModel.ERROR: return html``; case StatusResultTypeModel.INFO: diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/constants.ts new file mode 100644 index 0000000000..43f9eb4861 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/constants.ts @@ -0,0 +1 @@ +export const UMB_LANGUAGE_WORKSPACE_ALIAS = 'Umb.Workspace.Language'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts index 87d2816bd5..934b5700d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.context.ts @@ -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([ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts index 6abbf9bf69..ce98f96aaa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts @@ -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: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index 5c2173cdd9..9b7c64f927 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -174,7 +174,7 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin + ${this.#renderIcon(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts index 39c2b4ea8b..4fdc741116 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/index.ts new file mode 100644 index 0000000000..e9c3158173 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/index.ts @@ -0,0 +1 @@ +export { UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/manifests.ts new file mode 100644 index 0000000000..16ad5b903c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/manifests.ts @@ -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 = [compositionRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.repository.ts new file mode 100644 index 0000000000..a73570e069 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.repository.ts @@ -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 }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.server.data-source.ts new file mode 100644 index 0000000000..81e1e7d28b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/composition/media-type-composition.server.data-source.ts @@ -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 | 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 | undefined = response.data?.map((composition) => { + return { + unique: composition.id, + name: composition.name, + icon: composition.icon, + folderPath: composition.folderPath, + isCompatible: composition.isCompatible, + }; + }); + + return { data, error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/index.ts index e84c58d982..e3bd416604 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/manifests.ts index 37dcb889ef..ce74b2570d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/manifests.ts @@ -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 = [...detailManifests, ...itemManifests]; +export const manifests: Array = [...detailManifests, ...itemManifests, ...compositionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/types.ts index 828514db06..17acb2e86e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/types.ts @@ -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 {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts index a0cb73a866..01293d0d96 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts @@ -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 = [ label: '#general_design', pathname: 'design', icon: 'icon-document-dashed-line', + compositionRepositoryAlias: UMB_MEDIA_TYPE_COMPOSITION_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts index 2773d3c991..8de7a5fef3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts @@ -89,7 +89,8 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement< } #renderNotAllowed() { - return html` + return html` + There are no allowed Media Types available for creating media here. You must enable these in Media Types within the Settings section, by editing the Allowed child node types under Permissions. this._rejectModal()} href=${`/section/settings/workspace/media-type/edit/${this.data?.mediaType?.unique}/view/structure`} - label=${this.localize.term('create_noMediaTypesEditPermissions')}>
`; + label=${this.localize.term('create_noMediaTypesEditPermissions')}> + `; } #renderAllowedMediaTypes() { return repeat( this._allowedMediaTypes, (mediaType) => mediaType.unique, - (mediaType) => - html` html` + this.#onNavigate(mediaType)}> ${mediaType.icon ? html`` : nothing} - `, + + `, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts index 02d36f5921..d66e61df7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts @@ -137,7 +137,7 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin + ${when(item.icon, () => html``)} import('./member-type-composition.repository.js'), +}; + +export const manifests: Array = [compositionRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.repository.ts new file mode 100644 index 0000000000..a64e0a3ef8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.repository.ts @@ -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 }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.server.data-source.ts new file mode 100644 index 0000000000..dc38ede4a2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/composition/member-type-composition.server.data-source.ts @@ -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 | 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 | undefined = response.data?.map((composition) => { + return { + unique: composition.id, + name: composition.name, + icon: composition.icon, + folderPath: composition.folderPath, + isCompatible: composition.isCompatible, + }; + }); + + return { data, error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/index.ts index f396068800..5a07494cdb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/manifests.ts index 37dcb889ef..ce74b2570d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/manifests.ts @@ -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 = [...detailManifests, ...itemManifests]; +export const manifests: Array = [...detailManifests, ...itemManifests, ...compositionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/types.ts index 41bb9c2f08..c17ebcebe6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/types.ts @@ -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 {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts index ac2e37d3da..fbac4d07c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts @@ -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 = [ +const workspaceViews: Array = [ { type: 'workspaceView', kind: 'contentTypeDesignEditor', @@ -29,6 +30,7 @@ const workspaceViews: Array = [ label: '#general_design', pathname: 'design', icon: 'icon-member-dashed-line', + compositionRepositoryAlias: UMB_MEMBER_TYPE_COMPOSITION_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/components/template-field-dropdown-list/template-field-dropdown-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/components/template-field-dropdown-list/template-field-dropdown-list.element.ts index 5ced56e80c..ef744bf99c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/components/template-field-dropdown-list/template-field-dropdown-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-page-field-builder/components/template-field-dropdown-list/template-field-dropdown-list.element.ts @@ -152,7 +152,7 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement { - ${this.localize.term('formSettings_systemFields')} + ${this.localize.term('template_systemFields')} ${this.localize.term('content_documentType')} @@ -174,7 +174,8 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement { #renderAliasDropdown() { if (this._type !== FieldType.SYSTEM && !this._unique) return; - return html`${this._uniqueName} + return html` + ${this.localize.string(this._uniqueName ?? '')} ${repeat( @@ -184,7 +185,8 @@ export class UmbTemplateFieldDropdownListElement extends UmbLitElement { html`${field.alias}`, )} - `; + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json new file mode 100644 index 0000000000..6362dc5347 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json @@ -0,0 +1,8 @@ +{ + "name": "@umbraco-backoffice/tiny-mce", + "private": true, + "type": "module", + "scripts": { + "build": "vite build" + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts new file mode 100644 index 0000000000..d0c457d1e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts @@ -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 }), +});