<umb-content-workspace-property> DX (#19399)

* introduce umb-content-workspace-property to improve dx

* make property responsible for observing the view guard

* Update src/Umbraco.Web.UI.Client/src/packages/content/content/global-components/content-workspace-property.element.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* context consumer update tests

* no need to import when exporting

* only observe aliases

* merge the two component for less complexity

* added property settings

* ensure this works with extension begin removed

---------

Co-authored-by: Niels Lyngsø <nsl@umbraco.dk>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
This commit is contained in:
Mads Rasmussen
2025-05-26 09:11:39 +02:00
committed by GitHub
parent 8d490783b5
commit 86bbdfe7d3
7 changed files with 151 additions and 147 deletions

View File

@@ -26,6 +26,7 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
// State which holds all the properties of the current container, this is a composition of all properties from the containers that matches our target [NL]
#propertyStructure = new UmbArrayState<UmbPropertyTypeModel>([], (x) => x.unique);
readonly propertyStructure = this.#propertyStructure.asObservable();
readonly propertyAliases = this.#propertyStructure.asObservablePart((x) => x.map((e) => e.alias));
constructor(host: UmbControllerHost) {
super(host);

View File

@@ -0,0 +1,136 @@
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '../constants.js';
import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation';
@customElement('umb-content-workspace-property')
export class UmbContentWorkspacePropertyElement extends UmbLitElement {
private _alias?: string | undefined;
@property({ type: String, attribute: 'alias' })
public get alias(): string | undefined {
return this._alias;
}
public set alias(value: string | undefined) {
this._alias = value;
this.#observePropertyType();
}
@state()
_datasetVariantId?: UmbVariantId;
@state()
_dataPath?: string;
@state()
_viewable?: boolean;
@state()
_writeable?: boolean;
@state()
_workspaceContext?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE;
@state()
_propertyType?: UmbPropertyTypeModel;
constructor() {
super();
// The Property Dataset is local to the active variant, we use this to retrieve the variant we like to gather the value from.
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => {
this._datasetVariantId = datasetContext?.getVariantId();
});
// The Content Workspace Context is used to retrieve the property type we like to observe.
// This gives us the configuration from the property type as part of the data type.
this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, async (workspaceContext) => {
this._workspaceContext = workspaceContext;
this.#observePropertyType();
});
}
async #observePropertyType() {
if (!this._alias || !this._workspaceContext) return;
this.observe(await this._workspaceContext?.structure.propertyStructureByAlias(this._alias), (propertyType) => {
this._propertyType = propertyType;
this.#checkViewGuard();
});
}
#checkViewGuard() {
if (!this._workspaceContext || !this._propertyType || !this._datasetVariantId) return;
const propertyVariantId = new UmbVariantId(
this._propertyType.variesByCulture ? this._datasetVariantId.culture : null,
this._propertyType.variesBySegment ? this._datasetVariantId.segment : null,
);
this.observe(
this._workspaceContext.propertyViewGuard.isPermittedForVariantAndProperty(
propertyVariantId,
this._propertyType,
this._datasetVariantId,
),
(permitted) => {
this._viewable = permitted;
},
`umbObservePropertyViewGuard`,
);
}
override willUpdate(changedProperties: Map<string, any>) {
super.willUpdate(changedProperties);
if (
changedProperties.has('_propertyType') ||
changedProperties.has('_datasetVariantId') ||
changedProperties.has('_workspaceContext')
) {
if (this._datasetVariantId && this._propertyType && this._workspaceContext) {
const propertyVariantId = new UmbVariantId(
this._propertyType.variesByCulture ? this._datasetVariantId.culture : null,
this._propertyType.variesBySegment ? this._datasetVariantId.segment : null,
);
this._dataPath = `$.values[${UmbDataPathPropertyValueQuery({
alias: this._propertyType.alias,
culture: propertyVariantId.culture,
segment: propertyVariantId.segment,
})}].value`;
this.observe(
this._workspaceContext.propertyWriteGuard.isPermittedForVariantAndProperty(
propertyVariantId,
this._propertyType,
propertyVariantId,
),
(write) => {
this._writeable = write;
},
'observeView',
);
}
}
}
override render() {
if (!this._viewable) return nothing;
if (!this._dataPath || this._writeable === undefined) return nothing;
return html`<umb-property-type-based-property
data-path=${this._dataPath}
.property=${this._propertyType}
?readonly=${!this._writeable}></umb-property-type-based-property>`;
}
}
export default UmbContentWorkspacePropertyElement;
declare global {
interface HTMLElementTagNameMap {
'umb-content-workspace-property': UmbContentWorkspacePropertyElement;
}
}

View File

@@ -0,0 +1 @@
export * from './content-workspace-property.element.js';

View File

@@ -2,9 +2,11 @@ export * from './collection/index.js';
export * from './components/index.js';
export * from './constants.js';
export * from './controller/merge-content-variant-data.controller.js';
export * from './global-components/index.js';
export * from './manager/index.js';
export * from './property-dataset-context/index.js';
export * from './workspace/index.js';
export type * from './repository/index.js';
export type * from './types.js';
export type * from './variant-picker/index.js';

View File

@@ -1,5 +1,5 @@
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '../../content-workspace.context-token.js';
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type {
UmbContentTypeModel,
@@ -8,17 +8,10 @@ import type {
} from '@umbraco-cms/backoffice/content-type';
import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import './content-editor-property.element.js';
@customElement('umb-content-workspace-view-edit-properties')
export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement {
#workspaceContext?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE;
#propertyStructureHelper = new UmbContentTypePropertyStructureHelper<UmbContentTypeModel>(this);
#properties?: Array<UmbPropertyTypeModel>;
#visiblePropertiesUniques: Array<string> = [];
@property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | null | undefined {
@@ -29,86 +22,36 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
}
@state()
_datasetVariantId?: UmbVariantId;
_properties: Array<string> = [];
@state()
_visibleProperties?: Array<UmbPropertyTypeModel>;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => {
this._datasetVariantId = datasetContext?.getVariantId();
this.#processPropertyStructure();
});
this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (workspaceContext) => {
this.#workspaceContext = workspaceContext;
this.#propertyStructureHelper.setStructureManager(
// Assuming its the same content model type that we are working with here... [NL]
workspaceContext?.structure as unknown as UmbContentTypeStructureManager<UmbContentTypeModel>,
);
this.observe(
this.#propertyStructureHelper.propertyStructure,
this.#propertyStructureHelper.propertyAliases,
(properties) => {
this.#properties = properties;
this.#processPropertyStructure();
this._properties = properties;
},
'observePropertyStructure',
);
});
}
#processPropertyStructure() {
if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper || !this._datasetVariantId) {
return;
}
const propertyViewGuard = this.#workspaceContext.propertyViewGuard;
this.#properties.forEach((property) => {
const propertyVariantId = new UmbVariantId(
property.variesByCulture ? this._datasetVariantId?.culture : null,
property.variesBySegment ? this._datasetVariantId?.segment : null,
);
this.observe(
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._datasetVariantId!),
(permitted) => {
if (permitted) {
this.#visiblePropertiesUniques.push(property.unique);
this.#calculateVisibleProperties();
} else {
const index = this.#visiblePropertiesUniques.indexOf(property.unique);
if (index !== -1) {
this.#visiblePropertiesUniques.splice(index, 1);
this.#calculateVisibleProperties();
}
}
},
`propertyViewGuard-permittedForVariantAndProperty-${property.unique}`,
);
});
}
#calculateVisibleProperties() {
this._visibleProperties = this.#properties!.filter((property) =>
this.#visiblePropertiesUniques.includes(property.unique),
);
}
override render() {
return this._datasetVariantId && this._visibleProperties
? repeat(
this._visibleProperties,
(property) => property.alias,
(property) =>
html`<umb-content-workspace-view-edit-property
class="property"
.variantId=${this._datasetVariantId}
.property=${property}></umb-content-workspace-view-edit-property>`,
)
: nothing;
return repeat(
this._properties,
(property) => property,
(property) =>
html`<umb-content-workspace-property class="property" alias=${property}></umb-content-workspace-property>`,
);
}
static override styles = [

View File

@@ -1,79 +0,0 @@
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '../../content-workspace.context-token.js';
import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
@customElement('umb-content-workspace-view-edit-property')
export class UmbContentWorkspaceViewEditPropertyElement extends UmbLitElement {
//
@property({ attribute: false })
variantId?: UmbVariantId;
@property({ attribute: false })
property?: UmbPropertyTypeModel;
@state()
_dataPath?: string;
@state()
_writeable?: boolean;
@state()
_context?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (context) => {
this._context = context;
});
}
override willUpdate(changedProperties: Map<string, any>) {
super.willUpdate(changedProperties);
if (changedProperties.has('property') || changedProperties.has('variantId') || changedProperties.has('_context')) {
if (this.variantId && this.property && this._context) {
const propertyVariantId = new UmbVariantId(
this.property.variesByCulture ? this.variantId.culture : null,
this.property.variesBySegment ? this.variantId.segment : null,
);
this._dataPath = `$.values[${UmbDataPathPropertyValueQuery({
alias: this.property.alias,
culture: propertyVariantId.culture,
segment: propertyVariantId.segment,
})}].value`;
this.observe(
this._context.propertyWriteGuard.isPermittedForVariantAndProperty(
propertyVariantId,
this.property,
this.variantId,
),
(write) => {
this._writeable = write;
},
'observeView',
);
}
}
}
override render() {
if (!this._dataPath || this._writeable === undefined) return nothing;
return html`<umb-property-type-based-property
data-path=${this._dataPath}
.property=${this.property}
?readonly=${!this._writeable}></umb-property-type-based-property>`;
}
}
export default UmbContentWorkspaceViewEditPropertyElement;
declare global {
interface HTMLElementTagNameMap {
'umb-content-workspace-view-edit-property': UmbContentWorkspaceViewEditPropertyElement;
}
}

View File

@@ -31,7 +31,7 @@ export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase
// This method gets all extensions based on a type, then filters them based on the entity type. and then we get the alias of the first one [NL]
createObservablePart(
umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType),
(x) => x[0].alias,
(x) => x[0]?.alias,
),
(alias) => {
this.alias = alias;