diff --git a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts index 5c06539f36..04dc42b3bb 100644 --- a/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts +++ b/src/Umbraco.Web.UI.Client/libs/observable-api/array-state.ts @@ -12,11 +12,37 @@ import { pushToUniqueArray } from './push-to-unique-array.function'; * The ArrayState provides methods to append data when the data is an Object. */ export class ArrayState extends DeepState { - private _getUnique?: (entry: T) => unknown; + #getUnique?: (entry: T) => unknown; + #sortMethod?: (a: T, b: T) => number; constructor(initialData: T[], getUniqueMethod?: (entry: T) => unknown) { super(initialData); - this._getUnique = getUniqueMethod; + this.#getUnique = getUniqueMethod; + } + + /** + * @method sortBy + * @param {(a: T, b: T) => number} sortMethod - A method to be used for sorting everytime data is set. + * @description - A sort method to this Subject. + * @example Example add sort method + * const data = [ + * { key: 1, value: 'foo'}, + * { key: 2, value: 'bar'} + * ]; + * const myState = new ArrayState(data, (x) => x.key); + * myState.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + */ + sortBy(sortMethod?: (a: T, b: T) => number) { + this.#sortMethod = sortMethod; + return this; + } + + next(value: T[]) { + if (this.#sortMethod) { + super.next(value.sort(this.#sortMethod)); + } else { + super.next(value); + } } /** @@ -34,12 +60,12 @@ export class ArrayState extends DeepState { */ remove(uniques: unknown[]) { let next = this.getValue(); - if (this._getUnique) { + if (this.#getUnique) { uniques.forEach((unique) => { next = next.filter((x) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return this._getUnique(x) !== unique; + return this.#getUnique(x) !== unique; }); }); @@ -63,11 +89,11 @@ export class ArrayState extends DeepState { */ removeOne(unique: unknown) { let next = this.getValue(); - if (this._getUnique) { + if (this.#getUnique) { next = next.filter((x) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return this._getUnique(x) !== unique; + return this.#getUnique(x) !== unique; }); this.next(next); @@ -116,8 +142,8 @@ export class ArrayState extends DeepState { */ appendOne(entry: T) { const next = [...this.getValue()]; - if (this._getUnique) { - pushToUniqueArray(next, entry, this._getUnique); + if (this.#getUnique) { + pushToUniqueArray(next, entry, this.#getUnique); } else { next.push(entry); } @@ -142,10 +168,10 @@ export class ArrayState extends DeepState { * ]); */ append(entries: T[]) { - if (this._getUnique) { + if (this.#getUnique) { const next = [...this.getValue()]; entries.forEach((entry) => { - pushToUniqueArray(next, entry, this._getUnique!); + pushToUniqueArray(next, entry, this.#getUnique!); }); this.next(next); } else { @@ -169,16 +195,10 @@ export class ArrayState extends DeepState { * myState.updateOne(2, {value: 'updated-bar'}); */ updateOne(unique: unknown, entry: Partial) { - if (!this._getUnique) { + if (!this.#getUnique) { throw new Error("Can't partial update an ArrayState without a getUnique method provided when constructed."); } - this.next( - partialUpdateFrozenArray( - this.getValue(), - entry, - (x) => unique === (this._getUnique as Exclude)(x) - ) - ); + this.next(partialUpdateFrozenArray(this.getValue(), entry, (x) => unique === this.#getUnique!(x))); return this; } } diff --git a/src/Umbraco.Web.UI.Client/libs/utils/generate-guid.ts b/src/Umbraco.Web.UI.Client/libs/utils/generate-guid.ts new file mode 100644 index 0000000000..b0b7f6b981 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/utils/generate-guid.ts @@ -0,0 +1,4 @@ +import { v4 as uuid } from 'uuid'; +export function generateGuid() { + return uuid(); +} diff --git a/src/Umbraco.Web.UI.Client/libs/utils/index.ts b/src/Umbraco.Web.UI.Client/libs/utils/index.ts index b7439f0c3a..d16b3e3261 100644 --- a/src/Umbraco.Web.UI.Client/libs/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/utils/index.ts @@ -1,3 +1,4 @@ export * from './utils'; export * from './umbraco-path'; export * from './udi-service'; +export * from './generate-guid'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts index ebb3b26d47..21d8c3e526 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace-editor.element.ts @@ -2,7 +2,7 @@ import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbWorkspaceDocumentTypeContext } from './document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; @@ -32,7 +32,9 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { } #alias { - padding: 0 var(--uui-size-space-3); + height: calc(100% - 2px); + --uui-input-border-width: 0; + --uui-button-height: calc(100% -2px); } #icon { @@ -48,7 +50,7 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { name: 'umb:document-dashed-line', }; - #workspaceContext?: UmbWorkspaceDocumentTypeContext; + #workspaceContext?: UmbDocumentTypeWorkspaceContext; //@state() //private _documentType?: DocumentTypeResponseModel; @@ -64,7 +66,7 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { super(); this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance as UmbWorkspaceDocumentTypeContext; + this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext; this.#observeDocumentType(); }); @@ -76,10 +78,12 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { #observeDocumentType() { if (!this.#workspaceContext) return; //this.observe(this.#workspaceContext.data, (data) => (this._documentType = data)); + this.observe(this.#workspaceContext.name, (name) => (this._name = name)); + this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias)); } // TODO. find a way where we don't have to do this for all workspaces. - private _handleInput(event: UUIInputEvent) { + private _handleNameInput(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; @@ -89,6 +93,18 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { } } + // TODO. find a way where we don't have to do this for all workspaces. + private _handleAliasInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.setAlias(target.value); + } + } + event.stopPropagation(); + } + private async _handleIconClick() { const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL); @@ -106,8 +122,9 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { - -
${this._alias}
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts index 7d6ddd92d3..18df653f8a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.context.ts @@ -1,18 +1,36 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbDocumentTypeRepository } from '../repository/document-type.repository'; -import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-property-structure-manager.class'; +import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; type EntityType = DocumentTypeResponseModel; -export class UmbWorkspaceDocumentTypeContext +export class UmbDocumentTypeWorkspaceContext extends UmbWorkspaceContext implements UmbEntityWorkspaceContextInterface { // Draft is located in structure manager + + // General for content types: readonly data; readonly name; + readonly alias; + readonly description; + readonly icon; + + // TODO: Consider if each of these should go the view it self, but only if its used in that one view, otherwise make then go here. + readonly allowedAsRoot; + readonly variesByCulture; + readonly variesBySegment; + readonly isElement; + readonly allowedContentTypes; + readonly compositions; + + // Document type specific: + readonly allowedTemplateKeys; + readonly defaultTemplateKey; + readonly cleanup; readonly structure; @@ -20,12 +38,28 @@ export class UmbWorkspaceDocumentTypeContext super(host, new UmbDocumentTypeRepository(host)); this.structure = new UmbWorkspacePropertyStructureManager(this.host, this.repository); + + // General for content types: this.data = this.structure.rootDocumentType; this.name = this.structure.rootDocumentTypeObservablePart((data) => data?.name); + this.alias = this.structure.rootDocumentTypeObservablePart((data) => data?.alias); + this.description = this.structure.rootDocumentTypeObservablePart((data) => data?.description); + this.icon = this.structure.rootDocumentTypeObservablePart((data) => data?.icon); + this.allowedAsRoot = this.structure.rootDocumentTypeObservablePart((data) => data?.allowedAsRoot); + this.variesByCulture = this.structure.rootDocumentTypeObservablePart((data) => data?.variesByCulture); + this.variesBySegment = this.structure.rootDocumentTypeObservablePart((data) => data?.variesBySegment); + this.isElement = this.structure.rootDocumentTypeObservablePart((data) => data?.isElement); + this.allowedContentTypes = this.structure.rootDocumentTypeObservablePart((data) => data?.allowedContentTypes); + this.compositions = this.structure.rootDocumentTypeObservablePart((data) => data?.compositions); + + // Document type specific: + this.allowedTemplateKeys = this.structure.rootDocumentTypeObservablePart((data) => data?.allowedTemplateKeys); + this.defaultTemplateKey = this.structure.rootDocumentTypeObservablePart((data) => data?.defaultTemplateKey); + this.cleanup = this.structure.rootDocumentTypeObservablePart((data) => data?.defaultTemplateKey); } public setPropertyValue(alias: string, value: unknown) { - throw new Error('setPropertyValue is not implemented for UmbWorkspaceDocumentTypeContext'); + throw new Error('setPropertyValue is not implemented for UmbDocumentTypeWorkspaceContext'); } getData() { @@ -43,6 +77,9 @@ export class UmbWorkspaceDocumentTypeContext setName(name: string) { this.structure.updateRootDocumentType({ name }); } + setAlias(alias: string) { + this.structure.updateRootDocumentType({ alias }); + } // TODO => manage setting icon color setIcon(icon: string) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts index 28748e430c..54086efee8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/document-type-workspace.element.ts @@ -1,7 +1,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbWorkspaceDocumentTypeContext } from './document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context'; import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element'; import { IRoutingInfo } from '@umbraco-cms/internal/router'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; @@ -10,7 +10,7 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; export class UmbDocumentTypeWorkspaceElement extends UmbLitElement { static styles = [UUITextStyles]; - #workspaceContext = new UmbWorkspaceDocumentTypeContext(this); + #workspaceContext = new UmbDocumentTypeWorkspaceContext(this); #element = new UmbDocumentTypeWorkspaceEditorElement(); @state() diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts index 3baf28c927..37e4baa850 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/manifests.ts @@ -20,7 +20,7 @@ const workspaceViews: Array = [ type: 'workspaceView', alias: 'Umb.WorkspaceView.DocumentType.Design', name: 'Document Type Workspace Design View', - loader: () => import('./views/design/document-type-workspace-view-design.element'), + loader: () => import('./views/design/document-type-workspace-view-edit.element'), weight: 1000, meta: { label: 'Design', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-design.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-design.element.ts index 9266050fbe..5f7c657b58 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-design.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-design.element.ts @@ -2,9 +2,8 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import '../../../../../shared/property-creator/property-creator.element.ts'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @@ -78,7 +77,7 @@ export class UmbDocumentTypeWorkspaceViewDesignElement extends UmbLitElement { `, ]; - private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + private _workspaceContext?: UmbDocumentTypeWorkspaceContext; @state() private _tabs: any[] = []; @@ -88,7 +87,7 @@ export class UmbDocumentTypeWorkspaceViewDesignElement extends UmbLitElement { // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbWorkspaceDocumentTypeContext; + this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts new file mode 100644 index 0000000000..99e55283a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts @@ -0,0 +1,130 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { UmbWorkspacePropertyStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-property-structure-helper.class'; +import { PropertyContainerTypes } from '../../../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UMB_MODAL_CONTEXT_TOKEN, UMB_PROPERTY_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal'; +import './document-type-workspace-view-edit-property.element'; + +@customElement('umb-document-type-workspace-view-edit-properties') +export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitElement { + private _containerKey: string | undefined; + + public get containerKey(): string | undefined { + return this._containerKey; + } + public set containerKey(value: string | undefined) { + if (value === this._containerKey) return; + const oldValue = this._containerKey; + this._containerKey = value; + this.requestUpdate('containerKey', oldValue); + } + + @property({ type: String, attribute: 'container-name', reflect: false }) + public get containerName(): string | undefined { + return this._propertyStructureHelper.getContainerName(); + } + public set containerName(value: string | undefined) { + this._propertyStructureHelper.setContainerName(value); + } + + @property({ type: String, attribute: 'container-type', reflect: false }) + public get containerType(): PropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); + } + public set containerType(value: PropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); + } + + _propertyStructureHelper = new UmbWorkspacePropertyStructureHelper(this); + + @state() + _propertyStructure: Array = []; + + #modalContext?: typeof UMB_MODAL_CONTEXT_TOKEN.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => (this.#modalContext = instance)); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; + }); + } + + async #onAddProperty() { + const property = await this._propertyStructureHelper.addProperty(this._containerKey); + if (!property) return; + + // Take key and parse to modal: + console.log('property key:', property.key!); + + const modalHandler = this.#modalContext?.open(UMB_PROPERTY_SETTINGS_MODAL); + + modalHandler?.onSubmit().then((result) => { + console.log(result); + }); + } + + render() { + return html`${repeat( + this._propertyStructure, + (property) => property.alias, + (property) => + html` { + this._propertyStructureHelper.partialUpdateProperty(property.key, event.detail); + }}>` + )} Add property `; + } + + static styles = [ + UUITextStyles, + css` + .property:first-of-type { + padding-top: 0; + } + .property { + border-bottom: 1px solid var(--uui-color-divider); + } + .property:last-child { + border-bottom: 0; + } + + .property { + display: grid; + grid-template-columns: 200px auto; + column-gap: var(--uui-size-layout-2); + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-layout-1) 0; + container-type: inline-size; + } + + .property > div { + grid-column: span 2; + } + + @container (width > 600px) { + .property:not([orientation='vertical']) > div { + grid-column: span 1; + } + } + + #add { + width: 100%; + } + `, + ]; +} + +export default UmbDocumentTypeWorkspaceViewEditPropertiesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-type-workspace-view-edit-properties': UmbDocumentTypeWorkspaceViewEditPropertiesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts new file mode 100644 index 0000000000..45fad85fcb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts @@ -0,0 +1,106 @@ +import { css, html, LitElement } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property } from 'lit/decorators.js'; +import { PropertyTypeResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; + +/** + * @element document-type-workspace-view-edit-property + * @description - Element for displaying a property in an workspace. + * @slot editor - Slot for rendering the Property Editor + */ +@customElement('document-type-workspace-view-edit-property') +export class UmbDocumentTypeWorkspacePropertyElement extends LitElement { + /** + * Property, the data object for the property. + * @type {string} + * @attr + * @default '' + */ + @property({ type: Object }) + public property?: PropertyTypeResponseModelBaseModel; + + _firePartialUpdate(propertyName: string, value: string | number | boolean | null | undefined) { + const partialObject = {} as any; + partialObject[propertyName] = value; + + this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); + } + + render() { + // TODO: Only show alias on label if user has access to DocumentType within settings: + return this.property + ? html` + +
+ ` + : ''; + } + + static styles = [ + UUITextStyles, + css` + :host { + display: grid; + grid-template-columns: 200px auto; + column-gap: var(--uui-size-layout-2); + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-layout-1) 0; + container-type: inline-size; + } + + :host > div { + grid-column: span 2; + } + + @container (width > 600px) { + :host(:not([orientation='vertical'])) > div { + grid-column: span 1; + } + } + + :host(:last-of-type) { + border-bottom: none; + } + + :host-context(umb-variantable-property:first-of-type) { + padding-top: 0; + } + + p { + margin-bottom: 0; + } + + #header { + position: sticky; + top: var(--uui-size-space-4); + height: min-content; + z-index: 2; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'document-type-workspace-view-edit-property': UmbDocumentTypeWorkspacePropertyElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts new file mode 100644 index 0000000000..19de474446 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit-tab.element.ts @@ -0,0 +1,115 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { UmbWorkspaceContainerStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-container-structure-helper.class'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { PropertyTypeContainerResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; +import './document-type-workspace-view-edit-properties.element'; + +@customElement('umb-document-type-workspace-view-edit-tab') +export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + uui-box { + margin: var(--uui-size-layout-1); + } + + #add { + width: 100%; + } + `, + ]; + + private _ownerTabKey?: string | undefined; + + @property({ type: String }) + public get ownerTabKey(): string | undefined { + return this._ownerTabKey; + } + public set ownerTabKey(value: string | undefined) { + if (value === this._ownerTabKey) return; + const oldValue = this._ownerTabKey; + this._ownerTabKey = value; + this.requestUpdate('ownerTabKey', oldValue); + } + + private _tabName?: string | undefined; + + @property({ type: String }) + public get tabName(): string | undefined { + return this._groupStructureHelper.getName(); + } + public set tabName(value: string | undefined) { + if (value === this._tabName) return; + const oldValue = this._tabName; + this._tabName = value; + this._groupStructureHelper.setName(value); + this.requestUpdate('tabName', oldValue); + } + + @property({ type: Boolean }) + public get noTabName(): boolean { + return this._groupStructureHelper.getIsRoot(); + } + public set noTabName(value: boolean) { + this._groupStructureHelper.setIsRoot(value); + } + + _groupStructureHelper = new UmbWorkspaceContainerStructureHelper(this); + + @state() + _groups: Array = []; + + @state() + _hasProperties = false; + + constructor() { + super(); + + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; + }); + } + + #onAddGroup = () => { + // Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it? + this._groupStructureHelper.addGroup(this._ownerTabKey); + }; + + render() { + return html` + ${this._hasProperties + ? html` + + + + ` + : ''} + ${repeat( + this._groups, + (group) => group.name, + (group) => html` + + ` + )} + Add Group + `; + } +} + +export default UmbDocumentTypeWorkspaceViewEditTabElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-type-workspace-view-edit-tab': UmbDocumentTypeWorkspaceViewEditTabElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts new file mode 100644 index 0000000000..3fb75e1c6f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts @@ -0,0 +1,207 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; +import { UmbWorkspaceContainerStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-container-structure-helper.class'; +import type { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent, IRoute } from '@umbraco-cms/internal/router'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { PropertyTypeContainerResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; + +@customElement('umb-document-type-workspace-view-edit') +export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + --uui-tab-background: var(--uui-color-surface); + } + + /* TODO: This should be replaced with a general workspace bar — naming is hard */ + #workspace-tab-bar { + padding: 0 var(--uui-size-layout-1); + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--uui-color-surface); + flex-wrap: nowrap; + } + `, + ]; + + //private _hasRootProperties = false; + private _hasRootGroups = false; + + @state() + private _routes: IRoute[] = []; + + @state() + _tabs: Array = []; + + @state() + private _routerPath?: string; + + @state() + private _activePath = ''; + + private _workspaceContext?: UmbDocumentTypeWorkspaceContext; + + private _tabsStructureHelper = new UmbWorkspaceContainerStructureHelper(this); + + constructor() { + super(); + + this._tabsStructureHelper.setIsRoot(true); + this._tabsStructureHelper.setContainerChildType('Tab'); + this.observe(this._tabsStructureHelper.containers, (tabs) => { + this._tabs = tabs; + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext as UmbDocumentTypeWorkspaceContext; + this._observeRootGroups(); + }); + } + + private _observeRootGroups() { + if (!this._workspaceContext) return; + + this.observe( + this._workspaceContext.structure.hasRootContainers('Group'), + (hasRootGroups) => { + this._hasRootGroups = hasRootGroups; + this._createRoutes(); + }, + '_observeGroups' + ); + } + + private _createRoutes() { + const routes: any[] = []; + + if (this._tabs.length > 0) { + this._tabs?.forEach((tab) => { + const tabName = tab.name; + routes.push({ + path: `tab/${encodeURI(tabName || '').toString()}`, + component: () => import('./document-type-workspace-view-edit-tab.element'), + setup: (component: Promise) => { + (component as any).tabName = tabName; + (component as any).ownerTabKey = tab.key; + }, + }); + }); + } + + if (this._hasRootGroups) { + routes.push({ + path: '', + component: () => import('./document-type-workspace-view-edit-tab.element'), + setup: (component: Promise) => { + (component as any).noTabName = true; + }, + }); + } + + if (routes.length !== 0) { + routes.push({ + path: '**', + redirectTo: routes[0]?.path, + }); + } + + this._routes = routes; + } + + #remove(tabKey: string | undefined) { + if (!tabKey) return; + this._workspaceContext?.structure.removeContainer(null, tabKey); + } + async #addTab() { + this._workspaceContext?.structure.createContainer(null, null, 'Tab'); + } + + renderTabsNavigation() { + return html` + ${this._hasRootGroups + ? html` + Content + ` + : ''} + ${repeat( + this._tabs, + (tab) => tab.key, + (tab) => { + // TODO: make better url folder name: + const path = this._routerPath + '/tab/' + encodeURI(tab.name || ''); + return html` + ${path === this._activePath + ? html` + + this.#remove(tab.key)} + compact> + + + ` + : tab.name} + `; + } + )} + + + Add tab + + `; + } + + renderActions() { + return html`
+ + + Compositions + + + + Recorder + +
`; + } + + render() { + return html` +
${this._routerPath ? this.renderTabsNavigation() : ''}${this.renderActions()}
+ + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.absoluteActiveViewPath || ''; + }}> + + `; + } +} + +export default UmbDocumentTypeWorkspaceViewEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-type-workspace-view-edit': UmbDocumentTypeWorkspaceViewEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts index 5e755d67d7..803fcf76a0 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/details/document-type-workspace-view-details.element.ts @@ -1,7 +1,7 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @@ -27,14 +27,14 @@ export class UmbDocumentTypeWorkspaceViewDetailsElement extends UmbLitElement { `, ]; - private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + private _workspaceContext?: UmbDocumentTypeWorkspaceContext; constructor() { super(); // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbWorkspaceDocumentTypeContext; + this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts index f7d3f88e5f..94ee23568c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts @@ -1,7 +1,7 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, state } from 'lit/decorators.js'; -import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @@ -27,14 +27,14 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement `, ]; - private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + private _workspaceContext?: UmbDocumentTypeWorkspaceContext; constructor() { super(); // TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbWorkspaceDocumentTypeContext; + this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts index 2cdbfc6392..32f96d6687 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/workspace/views/templates/document-type-workspace-view-templates.element.ts @@ -1,7 +1,7 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement } from 'lit/decorators.js'; -import { UmbWorkspaceDocumentTypeContext } from '../../document-type-workspace.context'; +import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @@ -35,12 +35,12 @@ export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement `, ]; - private _workspaceContext?: UmbWorkspaceDocumentTypeContext; + private _workspaceContext?: UmbDocumentTypeWorkspaceContext; constructor() { super(); this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => { - this._workspaceContext = documentTypeContext as UmbWorkspaceDocumentTypeContext; + this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext; this._observeDocumentType(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index 246b1866a6..81629020e2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -3,7 +3,7 @@ import { UmbDocumentRepository } from '../repository/document.repository'; import { UmbDocumentTypeRepository } from '../../document-types/repository/document-type.repository'; import { UmbWorkspaceVariableEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-variable-entity-context.interface'; import { UmbVariantId } from '../../../shared/variants/variant-id.class'; -import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-property-structure-manager.class'; +import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; import { UmbWorkspaceSplitViewManager } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class'; import type { CreateDocumentRequestModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api'; import { partialUpdateFrozenArray, ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-properties.element.ts index a64174b210..2cc4063213 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-properties.element.ts @@ -2,13 +2,10 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { UmbDocumentWorkspaceContext } from '../../document-workspace.context'; +import { UmbWorkspacePropertyStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-property-structure-helper.class'; +import { PropertyContainerTypes } from '../../../../../shared/components/workspace/workspace-context/workspace-structure-manager.class'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { - DocumentTypePropertyTypeResponseModel, - PropertyTypeContainerResponseModelBaseModel, -} from '@umbraco-cms/backoffice/backend-api'; -import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; +import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; @customElement('umb-document-workspace-view-edit-properties') export class UmbDocumentWorkspaceViewEditPropertiesElement extends UmbLitElement { @@ -24,93 +21,35 @@ export class UmbDocumentWorkspaceViewEditPropertiesElement extends UmbLitElement `, ]; - private _containerName?: string; - @property({ type: String, attribute: 'container-name', reflect: false }) public get containerName(): string | undefined { - return this._containerName; + return this._propertyStructureHelper.getContainerName(); } public set containerName(value: string | undefined) { - if (this._containerName === value) return; - this._containerName = value; - this._observeGroupContainers(); + this._propertyStructureHelper.setContainerName(value); } - private _containerType?: 'Group' | 'Tab'; - @property({ type: String, attribute: 'container-type', reflect: false }) - public get containerType(): 'Group' | 'Tab' | undefined { - return this._containerType; + public get containerType(): PropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); } - public set containerType(value: 'Group' | 'Tab' | undefined) { - if (this._containerType === value) return; - this._containerType = value; - this._observeGroupContainers(); + public set containerType(value: PropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); } - @state() - _groupContainers: Array = []; + _propertyStructureHelper = new UmbWorkspacePropertyStructureHelper(this); @state() _propertyStructure: Array = []; - private _workspaceContext?: UmbDocumentWorkspaceContext; - constructor() { super(); - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (workspaceContext) => { - this._workspaceContext = workspaceContext as UmbDocumentWorkspaceContext; - this._observeGroupContainers(); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; }); } - private _observeGroupContainers() { - if (!this._workspaceContext || !this._containerName || !this._containerType) return; - - // TODO: Should be no need to update this observable if its already there. - this.observe( - this._workspaceContext!.structure.containersByNameAndType(this._containerName, this._containerType), - (groupContainers) => { - this._groupContainers = groupContainers || []; - groupContainers.forEach((group) => { - if (group.key) { - // Gather property aliases of this group, by group key. - this._observePropertyStructureOfGroup(group); - } - }); - }, - '_observeGroupContainers' - ); - } - - private _observePropertyStructureOfGroup(group: PropertyTypeContainerResponseModelBaseModel) { - if (!this._workspaceContext || !group.key) return; - - // TODO: Should be no need to update this observable if its already there. - this.observe( - this._workspaceContext.structure.propertyStructuresOf(group.key), - (properties) => { - // If this need to be able to remove properties, we need to clean out the ones of this group.key before inserting them: - this._propertyStructure = this._propertyStructure.filter((x) => x.containerKey !== group.key); - - properties?.forEach((property) => { - if (!this._propertyStructure.find((x) => x.alias === property.alias)) { - this._propertyStructure.push(property); - } - }); - - if (this._propertyStructure.length > 0) { - // TODO: Missing sort order? - //this._propertyStructure.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - } - }, - '_observePropertyStructureOfGroup' + group.key - ); - - // cache observable - } - render() { return repeat( this._propertyStructure, diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-tab.element.ts index 53bfdf0ae6..6febfe89a9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/views/edit/document-workspace-view-edit-tab.element.ts @@ -2,25 +2,19 @@ import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import { UmbDocumentWorkspaceContext } from '../../document-workspace.context'; +import { UmbWorkspaceContainerStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-container-structure-helper.class'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { PropertyTypeContainerResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; import './document-workspace-view-edit-properties.element'; -import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-document-workspace-view-edit-tab') export class UmbDocumentWorkspaceViewEditTabElement extends UmbLitElement { static styles = [ UUITextStyles, css` - :host { - display: block; + uui-box { margin: var(--uui-size-layout-1); } - - uui-box + uui-box { - margin-top: var(--uui-size-layout-1); - } `, ]; @@ -28,127 +22,46 @@ export class UmbDocumentWorkspaceViewEditTabElement extends UmbLitElement { @property({ type: String }) public get tabName(): string | undefined { - return this._tabName; + return this._groupStructureHelper.getName(); } public set tabName(value: string | undefined) { + if (value === this._tabName) return; const oldValue = this._tabName; - if (oldValue === value) return; this._tabName = value; - this._observeTabContainers(); + this._groupStructureHelper.setName(value); this.requestUpdate('tabName', oldValue); } - private _noTabName = false; - @property({ type: Boolean }) public get noTabName(): boolean { - return this._noTabName; + return this._groupStructureHelper.getIsRoot(); } public set noTabName(value: boolean) { - const oldValue = this._noTabName; - if (oldValue === value) return; - this._noTabName = value; - if (this._noTabName) { - this._tabName = undefined; - } - this._observeTabContainers(); - this.requestUpdate('noTabName', oldValue); + this._groupStructureHelper.setIsRoot(value); } - @state() - _tabContainers: PropertyTypeContainerResponseModelBaseModel[] = []; - - @state() - _hasTabProperties = false; + _groupStructureHelper = new UmbWorkspaceContainerStructureHelper(this); @state() _groups: Array = []; - private _workspaceContext?: UmbDocumentWorkspaceContext; + @state() + _hasProperties = false; constructor() { super(); - this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (workspaceContext) => { - this._workspaceContext = workspaceContext as UmbDocumentWorkspaceContext; - this._observeTabContainers(); + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; }); } - private _observeHasTabProperties() { - if (!this._workspaceContext) return; - - this._tabContainers.forEach((container) => { - this.observe( - this._workspaceContext!.structure.hasPropertyStructuresOf(container.key!), - (hasTabProperties) => { - this._hasTabProperties = hasTabProperties; - }, - '_observeHasTabProperties_' + container.key - ); - }); - } - - private _observeTabContainers() { - if (!this._workspaceContext) return; - - if (this._tabName) { - this._groups = []; - this.observe( - this._workspaceContext.structure.containersByNameAndType(this._tabName, 'Tab'), - (tabContainers) => { - this._tabContainers = tabContainers || []; - if (this._tabContainers.length > 0) { - this._observeHasTabProperties(); - this._observeGroups(); - } - }, - '_observeTabContainers' - ); - } else if (this._noTabName) { - this._groups = []; - this._observeRootGroups(); - } - } - - private _observeGroups() { - if (!this._workspaceContext || !this._tabName) return; - - this._tabContainers.forEach((container) => { - this.observe( - this._workspaceContext!.structure.containersOfParentKey(container.key, 'Group'), - this._insertGroupContainers, - '_observeGroupsOf_' + container.key - ); - }); - } - - private _observeRootGroups() { - if (!this._workspaceContext || !this._noTabName) return; - - // This is where we potentially could observe root properties as well. - this.observe( - this._workspaceContext!.structure.rootContainers('Group'), - this._insertGroupContainers, - '_observeRootGroups' - ); - } - - private _insertGroupContainers = (groupContainers: PropertyTypeContainerResponseModelBaseModel[]) => { - groupContainers.forEach((group) => { - if (group.name) { - if (!this._groups.find((x) => x.name === group.name)) { - this._groups.push(group); - this._groups.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); - } - } - }); - }; - render() { - // TODO: only show tab properties if there was any. We might do this with an event? to tell us when the properties is empty. return html` - ${this._hasTabProperties + ${this._hasProperties ? html` { + this._tabs = tabs; + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (workspaceContext) => { this._workspaceContext = workspaceContext as UmbDocumentWorkspaceContext; - this._observeTabs(); + this._observeRootGroups(); }); } - private _observeTabs() { + private _observeRootGroups() { if (!this._workspaceContext) return; - this.observe( - this._workspaceContext.structure.rootContainers('Tab'), - (tabs) => { - tabs.forEach((tab) => { - // Only add each tab name once, as our containers merge on name: - if (!this._tabs.find((x) => x.name === tab.name || '')) { - this._tabs.push(tab); - } - }); - this._createRoutes(); - }, - '_observeTabs' - ); - - /* - Impleent this, when it becomes an option to have properties directly in the root of the document. - this.observe( - this._workspaceContext.rootPropertyStructures(), - (rootPropertyStructure) => { - this._hasRootProperties = rootPropertyStructure.length > 0; - this._createRoutes(); - }, - '_observeTabs' - ); - */ - this.observe( this._workspaceContext.structure.hasRootContainers('Group'), (hasRootGroups) => { this._hasRootGroups = hasRootGroups; this._createRoutes(); }, - '_observeTabs' + '_observeGroups' ); } @@ -139,6 +125,7 @@ export class UmbDocumentWorkspaceViewEditElement extends UmbLitElement { this._tabs, (tab) => tab.name, (tab) => { + // TODO: make better url folder name: const path = this._routerPath + '/tab/' + encodeURI(tab.name || ''); return html`${tab.name}([], (x) => x.key); + readonly containers = this.#containers.asObservable(); + + #hasProperties = new BooleanState(false); + readonly hasProperties = this.#hasProperties.asObservable(); + + constructor(host: UmbControllerHostElement) { + this.#host = host; + + this.#containers.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + + new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context as UmbDocumentWorkspaceContext; + this._observeOwnerContainers(); + }); + } + + public setType(value?: PropertyContainerTypes) { + if (this._ownerType === value) return; + this._ownerType = value; + this._observeOwnerContainers(); + } + public getType() { + return this._ownerType; + } + + public setContainerChildType(value?: PropertyContainerTypes) { + if (this._childType === value) return; + this._childType = value; + this._observeOwnerContainers(); + } + public getContainerChildType() { + return this._childType; + } + + public setName(value?: string) { + if (this._ownerName === value) return; + this._ownerName = value; + this._observeOwnerContainers(); + } + public getName() { + return this._ownerName; + } + + public setIsRoot(value: boolean) { + if (this._isRoot === value) return; + this._isRoot = value; + this._observeOwnerContainers(); + } + public getIsRoot() { + return this._isRoot; + } + + private _observeOwnerContainers() { + if (!this.#workspaceContext) return; + + if (this._isRoot) { + this.#containers.next([]); + // We cannot have root properties currently, therefor we set it to false: + this.#hasProperties.next(false); + this._observeRootContainers(); + } else if (this._ownerName && this._ownerType) { + new UmbObserverController( + this.#host, + this.#workspaceContext.structure.containersByNameAndType(this._ownerName, this._ownerType), + (ownerContainers) => { + this.#containers.next([]); + this._ownerContainers = ownerContainers || []; + if (this._ownerContainers.length > 0) { + this._observeOwnerProperties(); + this._observeChildContainers(); + } + }, + '_observeOwnerContainers' + ); + } + } + + private _observeOwnerProperties() { + if (!this.#workspaceContext) return; + + this._ownerContainers.forEach((container) => { + new UmbObserverController( + this.#host, + this.#workspaceContext!.structure.hasPropertyStructuresOf(container.key!), + (hasProperties) => { + this.#hasProperties.next(hasProperties); + }, + '_observeOwnerHasProperties_' + container.key + ); + }); + } + + private _observeChildContainers() { + if (!this.#workspaceContext || !this._ownerName || !this._childType) return; + + this._ownerContainers.forEach((container) => { + new UmbObserverController( + this.#host, + this.#workspaceContext!.structure.containersOfParentKey(container.key, this._childType!), + this._insertGroupContainers, + '_observeGroupsOf_' + container.key + ); + }); + } + + private _observeRootContainers() { + if (!this.#workspaceContext || !this._isRoot) return; + + new UmbObserverController( + this.#host, + this.#workspaceContext!.structure.rootContainers(this._childType!), + (rootContainers) => { + this.#containers.next([]); + this._insertGroupContainers(rootContainers); + }, + '_observeRootContainers' + ); + } + + private _insertGroupContainers = (groupContainers: PropertyTypeContainerResponseModelBaseModel[]) => { + groupContainers.forEach((group) => { + if (group.name !== null && group.name !== undefined) { + if (!this.#containers.getValue().find((x) => x.name === group.name)) { + this.#containers.appendOne(group); + } + } + }); + }; + + /** Manipulate methods: */ + + async addGroup(ownerKey?: string, sortOrder?: number) { + if (!this.#workspaceContext) return; + + await this.#workspaceContext.structure.createContainer(null, ownerKey, this._childType, sortOrder); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts new file mode 100644 index 0000000000..651e18e549 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-helper.class.ts @@ -0,0 +1,114 @@ +import { UmbDocumentWorkspaceContext } from '../../../../documents/documents/workspace/document-workspace.context'; +import { PropertyContainerTypes } from './workspace-structure-manager.class'; +import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import { UmbContextConsumerController, UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api'; +import { ArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbWorkspacePropertyStructureHelper { + #host: UmbControllerHostElement; + + #workspaceContext?: UmbDocumentWorkspaceContext; + + private _containerType?: PropertyContainerTypes; + private _isRoot?: boolean; + private _containerName?: string; + + #propertyStructure = new ArrayState([], (x) => x.key); + readonly propertyStructure = this.#propertyStructure.asObservable(); + + constructor(host: UmbControllerHostElement) { + this.#host = host; + new UmbContextConsumerController(host, UMB_ENTITY_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context as UmbDocumentWorkspaceContext; + this._observeGroupContainers(); + }); + } + + public setContainerType(value?: PropertyContainerTypes) { + if (this._containerType === value) return; + this._containerType = value; + this._observeGroupContainers(); + } + public getContainerType() { + return this._containerType; + } + + public setContainerName(value?: string) { + if (this._containerName === value) return; + this._containerName = value; + this._observeGroupContainers(); + } + public getContainerName() { + return this._containerName; + } + + public setIsRoot(value: boolean) { + if (this._isRoot === value) return; + this._isRoot = value; + this._observeGroupContainers(); + } + public getIsRoot() { + return this._isRoot; + } + + private _observeGroupContainers() { + if (!this.#workspaceContext || !this._containerType) return; + + if (this._isRoot === true) { + this._observePropertyStructureOf(null); + } else if (this._containerName !== undefined) { + new UmbObserverController( + this.#host, + this.#workspaceContext!.structure.containersByNameAndType(this._containerName, this._containerType), + (groupContainers) => { + groupContainers.forEach((group) => this._observePropertyStructureOf(group.key)); + }, + '_observeGroupContainers' + ); + } + } + + private _observePropertyStructureOf(groupKey?: string | null) { + if (!this.#workspaceContext || groupKey === undefined) return; + + new UmbObserverController( + this.#host, + this.#workspaceContext.structure.propertyStructuresOf(groupKey), + (properties) => { + // If this need to be able to remove properties, we need to clean out the ones of this group.key before inserting them: + const _propertyStructure = this.#propertyStructure.getValue().filter((x) => x.containerKey !== groupKey); + + properties?.forEach((property) => { + if (!_propertyStructure.find((x) => x.alias === property.alias)) { + _propertyStructure.push(property); + } + }); + + if (_propertyStructure.length > 0) { + // TODO: End-point: Missing sort order? + //_propertyStructure = _propertyStructure.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); + } + + // Fire update to subscribers: + this.#propertyStructure.next(_propertyStructure); + }, + '_observePropertyStructureOfGroup' + groupKey + ); + } + + /** Manipulate methods: */ + + async addProperty(ownerKey?: string, sortOrder?: number) { + if (!this.#workspaceContext) return; + + return await this.#workspaceContext.structure.createProperty(null, ownerKey, sortOrder); + } + + // Takes optional arguments as this is easier for the implementation in the view: + async partialUpdateProperty(propertyKey?: string, partialUpdate?: Partial) { + if (!this.#workspaceContext || !propertyKey || !partialUpdate) return; + + return await this.#workspaceContext.structure.updateProperty(null, propertyKey, partialUpdate); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts similarity index 58% rename from src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts rename to src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts index df36f1db8c..9b7cb37126 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-property-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-structure-manager.class.ts @@ -1,12 +1,19 @@ import { UmbDocumentTypeRepository } from '../../../../documents/document-types/repository/document-type.repository'; +import { generateGuid } from '@umbraco-cms/backoffice/utils'; import { DocumentTypeResponseModel, DocumentTypePropertyTypeResponseModel, PropertyTypeContainerResponseModelBaseModel, ContentTypeResponseModelBaseDocumentTypePropertyTypeResponseModelDocumentTypePropertyTypeContainerResponseModel, + PropertyTypeResponseModelBaseModel, } from '@umbraco-cms/backoffice/backend-api'; import { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller'; -import { ArrayState, UmbObserverController, MappingFunction } from '@umbraco-cms/backoffice/observable-api'; +import { + ArrayState, + UmbObserverController, + MappingFunction, + partialUpdateFrozenArray, +} from '@umbraco-cms/backoffice/observable-api'; export type PropertyContainerTypes = 'Group' | 'Tab'; @@ -16,18 +23,36 @@ type T = DocumentTypeResponseModel; // TODO: make general interface for NodeTypeRepository, to replace UmbDocumentTypeRepository: export class UmbWorkspacePropertyStructureManager { #host: UmbControllerHostElement; + #init!: Promise; #documentTypeRepository: R; #rootDocumentTypeKey?: string; #documentTypeObservers = new Array(); #documentTypes = new ArrayState([], (x) => x.key); + readonly documentTypes = this.#documentTypes.asObservable(); + private readonly _documentTypeContainers = this.#documentTypes.getObservablePart((x) => + x.flatMap((x) => x.containers ?? []) + ); #containers = new ArrayState([], (x) => x.key); constructor(host: UmbControllerHostElement, typeRepository: R) { this.#host = host; this.#documentTypeRepository = typeRepository; + + new UmbObserverController(host, this.documentTypes, (documentTypes) => { + documentTypes.forEach((documentType) => { + // We could cache by docType Key? + // TODO: how do we ensure a container goes away? + + //this._initDocumentTypeContainers(documentType); + this._loadDocumentTypeCompositions(documentType); + }); + }); + new UmbObserverController(host, this._documentTypeContainers, (documentTypeContainers) => { + this.#containers.next(documentTypeContainers); + }); } /** @@ -39,7 +64,10 @@ export class UmbWorkspacePropertyStructureManager x.key === key)) return; + await this._loadType(key); + } + private async _loadType(key?: string) { if (!key) return {}; @@ -77,9 +112,11 @@ export class UmbWorkspacePropertyStructureManager { if (docType) { + // TODO: Handle if there was changes made to the specific document type in this context. + /* + possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones). + */ this.#documentTypes.appendOne(docType); - this._initDocumentTypeContainers(docType); - this._loadDocumentTypeCompositions(docType); } }) ); @@ -87,15 +124,17 @@ export class UmbWorkspacePropertyStructureManager { - this._loadType(composition.key); + this._ensureType(composition.key); }); } + /* private async _initDocumentTypeContainers(documentType: T) { documentType.containers?.forEach((container) => { - this.#containers.appendOne(container); + this.#containers.appendOne({ ...container, _ownerDocumentTypeKey: documentType.key }); }); } + */ /** Public methods for consuming structure: */ @@ -106,7 +145,77 @@ export class UmbWorkspacePropertyStructureManager y.key === this.#rootDocumentTypeKey); } updateRootDocumentType(entry: T) { - return this.#documentTypes.updateOne(this.#rootDocumentTypeKey, entry); + this.#documentTypes.updateOne(this.#rootDocumentTypeKey, entry); + } + + // We could move the actions to another class? + + async createContainer( + documentTypeKey: string | null, + parentKey: string | null = null, + type: PropertyContainerTypes = 'Group', + sortOrder?: number + ) { + await this.#init; + documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + + const container: PropertyTypeContainerResponseModelBaseModel = { + key: generateGuid(), + parentKey: parentKey, + name: 'New', + type: type, + sortOrder: sortOrder ?? 0, + }; + + const containers = [...(this.#documentTypes.getValue().find((x) => x.key === documentTypeKey)?.containers ?? [])]; + containers.push(container); + + this.#documentTypes.updateOne(documentTypeKey, { containers }); + + return container; + } + + async removeContainer(documentTypeKey: string | null, containerKey: string | null = null) { + await this.#init; + documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + + const frozenContainers = this.#documentTypes.getValue().find((x) => x.key === documentTypeKey)?.containers ?? []; + const containers = frozenContainers.filter((x) => x.key !== containerKey); + + this.#documentTypes.updateOne(documentTypeKey, { containers }); + } + + async createProperty(documentTypeKey: string | null, containerKey: string | null = null, sortOrder?: number) { + await this.#init; + documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + + const property: PropertyTypeResponseModelBaseModel = { + key: generateGuid(), + containerKey: containerKey, + //sortOrder: sortOrder ?? 0, + }; + + const properties = [...(this.#documentTypes.getValue().find((x) => x.key === documentTypeKey)?.properties ?? [])]; + properties.push(property); + + this.#documentTypes.updateOne(documentTypeKey, { properties }); + + return property; + } + + async updateProperty( + documentTypeKey: string | null, + propertyKey: string, + partialUpdate: Partial + ) { + await this.#init; + documentTypeKey = documentTypeKey ?? this.#rootDocumentTypeKey!; + + const frozenProperties = this.#documentTypes.getValue().find((x) => x.key === documentTypeKey)?.properties ?? []; + + const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.key === propertyKey!); + + this.#documentTypes.updateOne(documentTypeKey, { properties }); } /* @@ -180,6 +289,7 @@ export class UmbWorkspacePropertyStructureManager { return data.filter((x) => x.name === name && x.type === containerType); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts index 96cedd6512..0363bb948b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element.ts @@ -11,49 +11,6 @@ import { customElement, property } from 'lit/decorators.js'; */ @customElement('umb-workspace-property-layout') export class UmbWorkspacePropertyLayoutElement extends LitElement { - static styles = [ - UUITextStyles, - css` - :host { - display: grid; - grid-template-columns: 200px auto; - column-gap: var(--uui-size-layout-2); - border-bottom: 1px solid var(--uui-color-divider); - padding: var(--uui-size-layout-1) 0; - container-type: inline-size; - } - - :host > div { - grid-column: span 2; - } - - @container (width > 600px) { - :host(:not([orientation='vertical'])) > div { - grid-column: span 1; - } - } - - :host(:last-of-type) { - border-bottom: none; - } - - :host-context(umb-variantable-property:first-of-type) { - padding-top: 0; - } - - p { - margin-bottom: 0; - } - - #header { - position: sticky; - top: var(--uui-size-space-4); - height: min-content; - z-index: 2; - } - `, - ]; - /** * Alias. The technical name of the property. * @type {string} @@ -107,6 +64,49 @@ export class UmbWorkspacePropertyLayoutElement extends LitElement { `; } + + static styles = [ + UUITextStyles, + css` + :host { + display: grid; + grid-template-columns: 200px auto; + column-gap: var(--uui-size-layout-2); + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-layout-1) 0; + container-type: inline-size; + } + + :host > div { + grid-column: span 2; + } + + @container (width > 600px) { + :host(:not([orientation='vertical'])) > div { + grid-column: span 1; + } + } + + :host(:last-of-type) { + border-bottom: none; + } + + :host-context(umb-variantable-property:first-of-type) { + padding-top: 0; + } + + p { + margin-bottom: 0; + } + + #header { + position: sticky; + top: var(--uui-size-space-4); + height: min-content; + z-index: 2; + } + `, + ]; } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts index ec6d416519..1df0a4c0ea 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/document-type.data.ts @@ -1030,23 +1030,33 @@ export const treeData: Array = [ }, { $type: 'DocumentTypeTreeItemViewModel', - name: 'Document Type 1', + name: 'Page Document Type', type: 'document-type', hasChildren: false, - key: 'd81c7957-153c-4b5a-aa6f-b434a4964624', + key: '29643452-cff9-47f2-98cd-7de4b6807681', isContainer: false, parentKey: null, - icon: '', + icon: 'umb:document', }, { $type: 'DocumentTypeTreeItemViewModel', - name: 'Document Type 2', + name: 'Page Document Type Compositional', type: 'document-type', hasChildren: false, - key: 'a99e4018-3ffc-486b-aa76-eecea9593d17', + key: '5035d7d9-0a63-415c-9e75-ee2cf931db92', isContainer: false, parentKey: null, - icon: '', + icon: 'umb:document', + }, + { + $type: 'DocumentTypeTreeItemViewModel', + name: 'Page Document Type Inherited', + type: 'document-type', + hasChildren: false, + key: '8f68ba66-6fb2-4778-83b8-6ab4ca3a7c5d', + isContainer: false, + parentKey: null, + icon: 'umb:document', }, { $type: 'DocumentTypeTreeItemViewModel',