diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-property-structure-helper.class.ts index f084ca17a5..b7f19d1d10 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-property-structure-helper.class.ts @@ -29,6 +29,10 @@ export class UmbContentTypePropertyStructureHelper { this.#propertyStructure.sortBy((a, b) => ((a as any).sortOrder ?? 0) - ((b as any).sortOrder ?? 0)); } + public getOwnerDocumentTypes() { + return this.#structure?.documentTypes; + } + public setStructureManager(structure: UmbContentTypePropertyStructureManager) { this.#structure = structure; this.#initResolver?.(undefined); @@ -75,7 +79,7 @@ export class UmbContentTypePropertyStructureHelper { (groupContainers) => { groupContainers.forEach((group) => this._observePropertyStructureOf(group.id)); }, - '_observeGroupContainers' + '_observeGroupContainers', ); } } @@ -99,13 +103,20 @@ export class UmbContentTypePropertyStructureHelper { // Fire update to subscribers: this.#propertyStructure.next(_propertyStructure); }, - '_observePropertyStructureOfGroup' + groupId + '_observePropertyStructureOfGroup' + groupId, ); } // TODO: consider moving this to another class, to separate 'viewer' from 'manipulator': /** Manipulate methods: */ + async createPropertyScaffold(ownerId?: string, sortOrder?: number) { + await this.#init; + if (!this.#structure) return; + + return await this.#structure.createPropertyScaffold(ownerId, sortOrder); + } + async addProperty(ownerId?: string, sortOrder?: number) { await this.#init; if (!this.#structure) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts index 815f59cd12..44165935af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts @@ -33,7 +33,7 @@ export class UmbContentTypePropertyStructureManager([], (x) => x.id); readonly documentTypes = this.#documentTypes.asObservable(); private readonly _documentTypeContainers = this.#documentTypes.asObservablePart((x) => - x.flatMap((x) => x.containers ?? []) + x.flatMap((x) => x.containers ?? []), ); #containers: UmbArrayState = @@ -137,7 +137,7 @@ export class UmbContentTypePropertyStructureManager + partialUpdate: Partial, ) { await this.#init; documentTypeId = documentTypeId ?? this.#ownerDocumentTypeId!; @@ -237,10 +237,7 @@ export class UmbContentTypePropertyStructureManager x.id === documentTypeId)?.properties ?? [])]; properties.push(property); @@ -287,7 +293,7 @@ export class UmbContentTypePropertyStructureManager x.id === documentTypeId)?.properties ?? []; - const properties = filterFrozenArray(frozenProperties, (x) => x.id === propertyId); + const properties = filterFrozenArray(frozenProperties, (x) => x.id !== propertyId); this.#documentTypes.updateOne(documentTypeId, { properties }); } @@ -295,7 +301,7 @@ export class UmbContentTypePropertyStructureManager + partialUpdate: Partial, ) { await this.#init; documentTypeId = documentTypeId ?? this.#ownerDocumentTypeId!; @@ -387,7 +393,7 @@ export class UmbContentTypePropertyStructureManager { return data.filter((x) => x.parentId === parentId && x.type === containerType); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts index 50aa020935..8354432926 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts @@ -153,7 +153,6 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement< this._customValidationOptions.forEach((option) => { option.selected = option.value === regEx; }); - console.log(this._customValidationOptions); this._returnData.validation!.regEx = regEx ?? null; this.requestUpdate('_returnData'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/modal-route-registration.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/modal-route-registration.ts index 3ec3939463..8063774c3a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/modal-route-registration.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/modal-route-registration.ts @@ -14,7 +14,7 @@ export class UmbModalRouteRegistration | string; #modalConfig?: UmbModalConfig; - #onSetupCallback?: (routingInfo: Params) => UmbModalTokenData | false; + #onSetupCallback?: (routingInfo: Params) => Promise | UmbModalTokenData | false; #onSubmitCallback?: (data: UmbModalTokenResult) => void; #onRejectCallback?: () => void; @@ -87,7 +87,7 @@ export class UmbModalRouteRegistration UmbModalTokenData | false) { + public onSetup(callback: (routingInfo: Params) => Promise | UmbModalTokenData | false) { this.#onSetupCallback = callback; return this; } @@ -109,11 +109,11 @@ export class UmbModalRouteRegistration { let hostElement: UmbTestSorterControllerElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts index 379a4d5c4c..0d0befa8e2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-properties.element.ts @@ -1,13 +1,17 @@ import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context.js'; +import './document-type-workspace-view-edit-property.element.js'; import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; import { UmbContentTypePropertyStructureHelper, PropertyContainerTypes } from '@umbraco-cms/backoffice/content-type'; import { UmbSorterController, UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { DocumentTypePropertyTypeResponseModel, PropertyTypeModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UMB_MODAL_MANAGER_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal'; -import './document-type-workspace-view-edit-property.element.js'; +import { + DocumentTypePropertyTypeResponseModel, + DocumentTypeResponseModel, + PropertyTypeModelBaseModel, +} from '@umbraco-cms/backoffice/backend-api'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; const SORTER_CONFIG: UmbSorterConfig = { compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeResponseModel) => { return element.getAttribute('data-umb-property-id') === model.id; @@ -77,26 +81,57 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle @state() _propertyStructure: Array = []; + @state() + _ownerDocumentTypes?: DocumentTypeResponseModel[]; + + @state() + protected _modalRouteNewProperty?: string; + constructor() { super(); this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { this._propertyStructureHelper.setStructureManager( - (workspaceContext as UmbDocumentTypeWorkspaceContext).structure + (workspaceContext as UmbDocumentTypeWorkspaceContext).structure, ); }); this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { this._propertyStructure = propertyStructure; this.#propertySorter.setModel(this._propertyStructure); }); + + // Note: Route for adding a new property + new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL) + .addAdditionalPath('new-property') + .onSetup(async () => { + return (await this._propertyStructureHelper.createPropertyScaffold(this._containerId)) ?? false; + }) + .onSubmit((result) => { + this.#addProperty(result); + }) + .observeRouteBuilder((routeBuilder) => { + this._modalRouteNewProperty = routeBuilder(null); + }); } - async #onAddProperty() { - const property = await this._propertyStructureHelper.addProperty(this._containerId); - if (!property) return; + connectedCallback(): void { + super.connectedCallback(); + const doctypes = this._propertyStructureHelper.getOwnerDocumentTypes(); + if (!doctypes) return; + this.observe( + doctypes, + (documents) => { + this._ownerDocumentTypes = documents; + }, + 'observeOwnerDocumentTypes', + ); + } - // TODO: Figure out how we from this location can get into the route modal, via URL. - // The modal is registered by the document-type-workspace-view-edit-property element, therefor a bit hard to get the URL from here. + async #addProperty(propertyData: PropertyTypeModelBaseModel) { + const propertyPlaceholder = await this._propertyStructureHelper.addProperty(this._containerId); + if (!propertyPlaceholder) return; + + this._propertyStructureHelper.partialUpdateProperty(propertyPlaceholder.id, propertyData); } render() { @@ -104,21 +139,35 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle ${repeat( this._propertyStructure, (property) => property.id ?? '' + property.containerId ?? '' + (property as any).sortOrder ?? '', - (property) => - html` { + // Note: This piece might be moved into the property component + const inheritedFromDocument = this._ownerDocumentTypes?.find( + (types) => types.containers?.find((containers) => containers.id === property.containerId), + ); + + return html` { this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail); - }}>` + }} + @property-delete=${() => { + this._propertyStructureHelper.removeProperty(property.id!); + }}> + `; + }, )} - - Add property + + Add property `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts index a209aced8f..fd396111ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit-property.element.ts @@ -1,9 +1,17 @@ import { UUIInputElement, UUIInputEvent, UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; -import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; import { PropertyTypeModelBaseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { + UMB_CONFIRM_MODAL, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, + UMB_PROPERTY_SETTINGS_MODAL, + UMB_WORKSPACE_MODAL, + UmbConfirmModalData, + UmbModalRouteRegistrationController, +} from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { generateAlias } from '@umbraco-cms/backoffice/utils'; +import { UmbDataTypeRepository } from '@umbraco-cms/backoffice/data-type'; /** * @element document-type-workspace-view-edit-property @@ -27,6 +35,7 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { const oldValue = this._property; this._property = value; this.#modalRegistration.setUniquePathValue('propertyId', value?.id?.toString()); + this.setDataType(this._property?.dataTypeId); this.requestUpdate('property', oldValue); } @@ -40,11 +49,36 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { @property({ type: Boolean }) public inherited?: boolean; + #dataTypeRepository = new UmbDataTypeRepository(this); + #modalRegistration; + private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT_TOKEN.TYPE; @state() protected _modalRoute?: string; + @state() + protected _editDocumentTypePath?: string; + + @property() + public get modalRoute() { + return this._modalRoute; + } + + @property({ type: String, attribute: 'owner-document-type-id' }) + public ownerDocumentTypeId?: string; + + @property({ type: String, attribute: 'owner-document-type-name' }) + public ownerDocumentTypeName?: string; + + @state() + private _dataTypeName?: string; + + async setDataType(dataTypeId: string | undefined) { + if (!dataTypeId) return; + this.#dataTypeRepository.requestById(dataTypeId).then((x) => (this._dataTypeName = x?.data?.name)); + } + constructor() { super(); @@ -59,6 +93,19 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { .observeRouteBuilder((routeBuilder) => { this._modalRoute = routeBuilder(null); }); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('document-type') + .onSetup(() => { + return { entityType: 'document-type', preset: {} }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editDocumentTypePath = routeBuilder({}); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (context) => { + this._modalManagerContext = context; + }); } _partialUpdate(partialObject: PropertyTypeModelBaseModel) { @@ -72,19 +119,6 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); } - renderInheritedProperty() { - return this.property - ? html` - -
- ` - : ''; - } - @state() private _aliasLocked = true; @@ -92,6 +126,36 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { this._aliasLocked = !this._aliasLocked; } + #requestRemove(e: Event) { + e.preventDefault(); + e.stopImmediatePropagation(); + if (!this.property || !this.property.id) return; + + const Message: UmbConfirmModalData = { + headline: `${this.localize.term('actions_delete')} property`, + content: html` + Are you sure you want to delete the property ${this.property.name || this.property.id} + + `, + confirmLabel: this.localize.term('actions_delete'), + color: 'danger', + }; + + const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, Message); + + modalHandler + ?.onSubmit() + .then(() => { + this.dispatchEvent(new CustomEvent('property-delete')); + }) + .catch(() => { + // We do not need to react to cancel, so we will leave an empty method to prevent Uncaught Promise Rejection error. + return; + }); + } + #onNameChange(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; @@ -123,22 +187,7 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { label="label" .value=${this.property.name} @input=${this.#onNameChange}> - - { - if (e.target) this._singleValueUpdate('alias', (e.target as HTMLInputElement).value); - }}> - -
''} id="alias-lock" slot="prepend"> - -
-
+ ${this.renderPropertyAlias()}

- - + + ${this.renderPropertyTags()} + + + + + ` : ''; } + renderInheritedProperty() { + return this.property + ? html` + +
+ ${this.renderPropertyTags()} + + + + ${this.localize.term('contentTypeEditor_inheritedFrom')} + + ${this.ownerDocumentTypeName ?? '??'} + + + +
+ ` + : ''; + } + + renderPropertyAlias() { + return this.property + ? html` { + if (e.target) this._singleValueUpdate('alias', (e.target as HTMLInputElement).value); + }}> + + +
''} id="alias-lock" slot="prepend"> + +
+
` + : ''; + } + + renderPropertyTags() { + return this.property + ? html`
+ ${this.property.dataTypeId ? html`${this._dataTypeName}` : nothing} + ${this.property.variesByCulture + ? html` + ${this.localize.term('contentTypeEditor_cultureVariantLabel')} + ` + : nothing} + ${this.property.appearance?.labelOnTop == true + ? html` + ${this.localize.term('contentTypeEditor_displaySettingsLabelOnTop')} + ` + : nothing} +
` + : nothing; + } + render() { // TODO: Only show alias on label if user has access to DocumentType within settings: return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty(); @@ -201,7 +321,8 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { :host(.--umb-sorter-placeholder) { height: 2px; } - :host(.--umb-sorter-placeholder) > div { + :host(.--umb-sorter-placeholder) > div, + :host(.--umb-sorter-placeholder) > uui-button { display: none; } :host(.--umb-sorter-placeholder)::after { @@ -225,6 +346,7 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { } #editor { + position: relative; background-color: var(--uui-color-background); } #alias-input, @@ -261,6 +383,40 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement { --uui-textarea-border-color: transparent; font-weight: 0.5rem; /* TODO: Cant change font size of UUI textarea yet */ } + + .types > div uui-icon, + .inherited uui-icon { + vertical-align: sub; + } + + .inherited { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + } + + .types { + position: absolute; + top: var(--uui-size-space-2); + left: var(--uui-size-space-2); + display: flex; + gap: var(--uui-size-space-2); + } + + #editor uui-action-bar { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + display: none; + } + #editor:hover uui-action-bar, + #editor:focus uui-action-bar { + display: block; + } + + a { + color: inherit; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/shared/router/route.context.ts b/src/Umbraco.Web.UI.Client/src/shared/router/route.context.ts index 95057f0bab..9c06390502 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/router/route.context.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/router/route.context.ts @@ -49,9 +49,9 @@ export class UmbRouteContext extends UmbBaseController { __modalKey: modalRegistration.key, path: '/' + modalRegistration.generateModalPath(), component: EmptyDiv, - setup: (component, info) => { + setup: async (component, info) => { if (!this.#modalContext) return; - const modalContext = modalRegistration.routeSetup(this.#modalRouter, this.#modalContext, info.match.params); + const modalContext = await modalRegistration.routeSetup(this.#modalRouter, this.#modalContext, info.match.params); if (modalContext) { modalContext.onSubmit().then( () => {