Fix Allow Edit Invariant config in split view (#19320)

* temp implementation

* pass correct property variant id

* check for dataset variantId

* pass dataset variant id

* more explicit unique

* pass dataset variant id

* pass correct property variant id

* rename variable

* use !

* re-fit tests with new argument

* update rule check

---------

Co-authored-by: Niels Lyngsø <niels.lyngso@gmail.com>
This commit is contained in:
Mads Rasmussen
2025-05-16 09:23:29 +02:00
committed by GitHub
parent 8bc3de10a2
commit df56f1985b
8 changed files with 134 additions and 100 deletions

View File

@@ -39,7 +39,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
_dataOwner?: UmbBlockElementManager;
@state()
_variantId?: UmbVariantId;
_workspaceVariantId?: UmbVariantId;
@state()
_visibleProperties?: Array<UmbPropertyTypeModel>;
@@ -56,7 +56,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
this.observe(
workspaceContext?.variantId,
(variantId) => {
this._variantId = variantId;
this._workspaceVariantId = variantId;
this.#processPropertyStructure();
},
'observeVariantId',
@@ -83,16 +83,19 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
}
#processPropertyStructure() {
if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper) {
if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper || !this._workspaceVariantId) {
return;
}
const propertyViewGuard = this._dataOwner.propertyViewGuard;
this.#properties.forEach((property) => {
const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment);
const propertyVariantId = new UmbVariantId(
property.variesByCulture ? this._workspaceVariantId!.culture : null,
property.variesBySegment ? this._workspaceVariantId!.segment : null,
);
this.observe(
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property),
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._workspaceVariantId!),
(permitted) => {
if (permitted) {
this.#visiblePropertiesUniques.push(property.unique);
@@ -117,7 +120,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
}
override render() {
return this._variantId && this._visibleProperties
return this._workspaceVariantId && this._visibleProperties
? repeat(
this._visibleProperties,
(property) => property.alias,
@@ -126,7 +129,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
class="property"
.ownerContext=${this._dataOwner}
.ownerEntityType=${this._ownerEntityType}
.variantId=${this._variantId}
.variantId=${this._workspaceVariantId}
.property=${property}></umb-block-workspace-view-edit-property>`,
)
: nothing;

View File

@@ -38,7 +38,11 @@ export class UmbBlockWorkspaceViewEditPropertyElement extends UmbLitElement {
})}].value`;
this.observe(
this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property),
this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty(
propertyVariantId,
this.property,
this.variantId,
),
(write) => {
this._writeable = write;
},

View File

@@ -29,7 +29,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
}
@state()
_variantId?: UmbVariantId;
_datasetVariantId?: UmbVariantId;
@state()
_visibleProperties?: Array<UmbPropertyTypeModel>;
@@ -38,7 +38,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => {
this._variantId = datasetContext?.getVariantId();
this._datasetVariantId = datasetContext?.getVariantId();
this.#processPropertyStructure();
});
@@ -61,16 +61,19 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
}
#processPropertyStructure() {
if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper) {
if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper || !this._datasetVariantId) {
return;
}
const propertyViewGuard = this.#workspaceContext.propertyViewGuard;
this.#properties.forEach((property) => {
const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment);
const propertyVariantId = new UmbVariantId(
property.variesByCulture ? this._datasetVariantId?.culture : null,
property.variesBySegment ? this._datasetVariantId?.segment : null,
);
this.observe(
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property),
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._datasetVariantId!),
(permitted) => {
if (permitted) {
this.#visiblePropertiesUniques.push(property.unique);
@@ -95,14 +98,14 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
}
override render() {
return this._variantId && this._visibleProperties
return this._datasetVariantId && this._visibleProperties
? repeat(
this._visibleProperties,
(property) => property.alias,
(property) =>
html`<umb-content-workspace-view-edit-property
class="property"
.variantId=${this._variantId}
.variantId=${this._datasetVariantId}
.property=${property}></umb-content-workspace-view-edit-property>`,
)
: nothing;

View File

@@ -46,7 +46,11 @@ export class UmbContentWorkspaceViewEditPropertyElement extends UmbLitElement {
})}].value`;
this.observe(
this._context.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property),
this._context.propertyWriteGuard.isPermittedForVariantAndProperty(
propertyVariantId,
this.property,
this.variantId,
),
(write) => {
this._writeable = write;
},

View File

@@ -70,7 +70,7 @@ describe('UmbVariantPropertyGuardManager', () => {
it('is not permitted for a variant when no states', (done) => {
manager
.isPermittedForVariantAndProperty(invariantVariant, propB)
.isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -82,7 +82,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleEn);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.true;
done();
@@ -94,7 +94,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleInv);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -106,7 +106,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(statePropAInv);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -117,7 +117,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(statePropAInv);
manager
.isPermittedForVariantAndProperty(invariantVariant, propB)
.isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -129,7 +129,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(statePropAInv);
manager
.isPermittedForVariantAndProperty(englishVariant, propA)
.isPermittedForVariantAndProperty(englishVariant, propA, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -141,7 +141,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(rulePlain);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.true;
done();
@@ -154,7 +154,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleNoEn);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -166,7 +166,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleNoPlain);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -179,7 +179,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleEn);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -193,7 +193,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(ruleNoEn);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -206,7 +206,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(rulePlain);
manager
.isPermittedForVariantAndProperty(englishVariant, propB)
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -220,7 +220,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(statePropAInv);
manager
.isPermittedForVariantAndProperty(invariantVariant, propA)
.isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();
@@ -234,7 +234,7 @@ describe('UmbVariantPropertyGuardManager', () => {
manager.addRule(stateNoPropAInv);
manager
.isPermittedForVariantAndProperty(invariantVariant, propA)
.isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant)
.subscribe((value) => {
expect(value).to.be.false;
done();

View File

@@ -5,21 +5,39 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
import { UmbGuardManagerBase } from '@umbraco-cms/backoffice/utils';
export interface UmbVariantPropertyGuardRule extends UmbPropertyGuardRule {
/**
* @description - The variant id of the property.
* @type {UmbVariantId}
* @memberof UmbVariantPropertyGuardRule
*/
variantId?: UmbVariantId;
/**
* @description - The variant id of the dataset. This is used to determine if the rule applies to the current dataset.
* @type {UmbVariantId}
* @memberof UmbVariantPropertyGuardRule
*/
datasetVariantId?: UmbVariantId;
}
/**
*
* @param rule
* @param variantId
* @param propertyType
* @param {UmbVariantPropertyGuardRule} rule - The rule to check.
* @param {UmbVariantId} variantId - The property variant id to check.
* @param {UmbReferenceByUnique} propertyType - The property type to check.
* @param {UmbVariantId} datasetVariantId - The variant id of the dataset. This is used to determine if the rule applies to the current dataset.
* @returns {boolean} - Returns true if the rule applies to the given conditions.
*/
function findRule(rule: UmbVariantPropertyGuardRule, variantId: UmbVariantId, propertyType: UmbReferenceByUnique) {
function findRule(
rule: UmbVariantPropertyGuardRule,
variantId: UmbVariantId,
propertyType: UmbReferenceByUnique,
datasetVariantId: UmbVariantId,
) {
return (
(rule.variantId?.compare(variantId) && rule.propertyType?.unique === propertyType.unique) ||
(rule.variantId === undefined && rule.propertyType?.unique === propertyType.unique) ||
(rule.variantId?.compare(variantId) && rule.propertyType === undefined) ||
(rule.variantId === undefined && rule.propertyType === undefined)
(rule.variantId === undefined || rule.variantId.culture === variantId.culture) &&
(rule.propertyType === undefined || rule.propertyType.unique === propertyType.unique) &&
(rule.datasetVariantId === undefined || rule.datasetVariantId.culture === datasetVariantId.culture)
);
}
@@ -34,33 +52,54 @@ export class UmbVariantPropertyGuardManager extends UmbGuardManagerBase<UmbVaria
* Checks if the variant and propertyType is permitted.
* @param {UmbVariantId} variantId - The variant id to check.
* @param {UmbReferenceByUnique} propertyType - The property type to check.
* @param {UmbVariantId} datasetVariantId - The dataset variant id to check.
* @returns {Observable<boolean>} - Returns an observable that emits true if the variant and propertyType is permitted, false otherwise.
* @memberof UmbVariantPropertyGuardManager
*/
isPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): Observable<boolean> {
return this._rules.asObservablePart((rules) => this.#resolvePermission(rules, variantId, propertyType));
isPermittedForVariantAndProperty(
variantId: UmbVariantId,
propertyType: UmbReferenceByUnique,
datasetVariantId: UmbVariantId,
): Observable<boolean> {
return this._rules.asObservablePart((rules) =>
this.#resolvePermission(rules, variantId, propertyType, datasetVariantId),
);
}
/**
* Checks if the variant and propertyType is permitted.
* @param {UmbVariantId} variantId - The variant id to check.
* @param {UmbReferenceByUnique} propertyType - The property type to check.
* @param {UmbVariantId} datasetVariantId - The dataset variant id to check.
* @returns {boolean} - Returns true if the variant and propertyType is permitted, false otherwise.
* @memberof UmbVariantPropertyGuardManager
*/
getIsPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): boolean {
return this.#resolvePermission(this._rules.getValue(), variantId, propertyType);
getIsPermittedForVariantAndProperty(
variantId: UmbVariantId,
propertyType: UmbReferenceByUnique,
datasetVariantId: UmbVariantId,
): boolean {
return this.#resolvePermission(this._rules.getValue(), variantId, propertyType, datasetVariantId);
}
#resolvePermission(
rules: UmbVariantPropertyGuardRule[],
variantId: UmbVariantId,
propertyType: UmbReferenceByUnique,
datasetVariantId: UmbVariantId,
) {
if (rules.filter((x) => x.permitted === false).some((rule) => findRule(rule, variantId, propertyType))) {
if (
rules
.filter((x) => x.permitted === false)
.some((rule) => findRule(rule, variantId, propertyType, datasetVariantId))
) {
return false;
}
if (rules.filter((x) => x.permitted === true).some((rule) => findRule(rule, variantId, propertyType))) {
if (
rules
.filter((x) => x.permitted === true)
.some((rule) => findRule(rule, variantId, propertyType, datasetVariantId))
) {
return true;
}
return this._fallback;

View File

@@ -1,71 +1,15 @@
import type { UmbDocumentDetailModel, UmbDocumentVariantModel, UmbDocumentWorkspaceContext } from '../types.js';
import { UMB_DOCUMENT_CONFIGURATION_CONTEXT } from '../global-contexts/index.js';
import { UmbContentPropertyDatasetContext } from '@umbraco-cms/backoffice/content';
import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { DocumentConfigurationResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property';
export class UmbDocumentPropertyDatasetContext extends UmbContentPropertyDatasetContext<
UmbDocumentDetailModel,
UmbDocumentTypeDetailModel,
UmbDocumentVariantModel
> {
#dataSetVariantId?: UmbVariantId;
#documentConfiguration?: DocumentConfigurationResponseModel;
#allowEditInvariantFromNonDefault: boolean | undefined = undefined;
constructor(host: UmbControllerHost, dataOwner: UmbDocumentWorkspaceContext, variantId?: UmbVariantId) {
super(host, dataOwner, variantId);
this.#dataSetVariantId = variantId;
this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => {
if (!context) return;
this.#documentConfiguration = (await context?.getDocumentConfiguration()) ?? undefined;
this.#allowEditInvariantFromNonDefault = this.#documentConfiguration?.allowEditInvariantFromNonDefault;
if (this.#allowEditInvariantFromNonDefault === false) {
this.#preventEditInvariantFromNonDefault();
}
});
}
#preventEditInvariantFromNonDefault() {
this.observe(
observeMultiple([this._dataOwner.structure.contentTypeProperties, this._dataOwner.variantOptions]),
([properties, variantOptions]) => {
if (properties.length === 0) return;
if (variantOptions.length === 0) return;
const currentVariantOption = variantOptions.find(
(option) => option.culture === this.#dataSetVariantId?.culture,
);
const isDefaultLanguage = currentVariantOption?.language.isDefault;
properties.forEach((property) => {
const unique = 'UMB_PREVENT_EDIT_INVARIANT_FROM_NON_DEFAULT_' + property.unique;
// Clean up the rule if it exists before adding a new one
this._dataOwner.propertyWriteGuard.removeRule(unique);
// If the property is invariant and not in the default language, we need to add a rule
if (!property.variesByCulture && !isDefaultLanguage) {
const rule: UmbVariantPropertyGuardRule = {
unique,
message: 'Shared properties can only be edited in the default language',
propertyType: {
unique: property.unique,
},
permitted: false,
};
this._dataOwner.propertyWriteGuard.addRule(rule);
}
});
},
);
}
}

View File

@@ -38,6 +38,8 @@ import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin';
import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property';
type ContentModel = UmbDocumentDetailModel;
type ContentTypeModel = UmbDocumentTypeDetailModel;
@@ -89,6 +91,7 @@ export class UmbDocumentWorkspaceContext
this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => {
const config = await context?.getDocumentConfiguration();
const allowSegmentCreation = config?.allowNonExistingSegmentsCreation ?? false;
const allowEditInvariantFromNonDefault = config?.allowEditInvariantFromNonDefault ?? true;
this._variantOptionsFilter = (variantOption) => {
const isNotCreatedSegmentVariant = variantOption.segment && !variantOption.variant;
@@ -100,6 +103,10 @@ export class UmbDocumentWorkspaceContext
return true;
};
if (allowEditInvariantFromNonDefault === false) {
this.#preventEditInvariantFromNonDefault();
}
});
this.observe(
@@ -428,6 +435,36 @@ export class UmbDocumentWorkspaceContext
message,
});
}
#preventEditInvariantFromNonDefault() {
this.observe(
observeMultiple([this.structure.contentTypeProperties, this.variantOptions]),
([properties, variantOptions]) => {
if (properties.length === 0) return;
if (variantOptions.length === 0) return;
variantOptions.forEach((variantOption) => {
// Do not add a rule for the default language. It is always permitted to edit.
if (variantOption.language.isDefault) return;
const datasetVariantId = UmbVariantId.CreateFromPartial(variantOption);
const invariantVariantId = UmbVariantId.CreateInvariant();
const unique = `UMB_PREVENT_EDIT_INVARIANT_FROM_NON_DEFAULT_DATASET=${datasetVariantId.toString()}_PROPERTY_${invariantVariantId.toString()}`;
const rule: UmbVariantPropertyGuardRule = {
unique,
message: 'Shared properties can only be edited in the default language',
variantId: invariantVariantId,
datasetVariantId,
permitted: false,
};
this.propertyWriteGuard.addRule(rule);
console.log('Adding rule', rule);
});
},
);
}
}
export default UmbDocumentWorkspaceContext;