Skip to content

Commit 18fafaa

Browse files
iOvergaardclaude
andauthored
Content: Fix property variation change breaking document save via Infinite Editing (closes #21195) (#21221)
When changing a property's variation setting (Shared/Invariant ↔ Variant) via Infinite Editing, the document would fail to save with a 404 error. This fix: - Adds value migration fallback logic in UmbPropertyValuePresetVariantBuilderController to find values when culture/segment doesn't match exactly - Overrides reload() in UmbContentDetailWorkspaceContextBase to process incoming data through _processIncomingData() for proper value transformation - Detects property variation changes and triggers document reload to migrate values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2d35e32 commit 18fafaa

File tree

3 files changed

+207
-1
lines changed

3 files changed

+207
-1
lines changed

src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ export abstract class UmbContentDetailWorkspaceContextBase<
178178
#saveModalToken?: UmbModalToken<UmbContentVariantPickerData<VariantOptionModelType>, UmbContentVariantPickerValue>;
179179
#contentTypePropertyName: string;
180180

181+
/**
182+
* Cache of property variation settings to detect changes when structure is updated.
183+
* Used to trigger value migration when properties change from invariant to variant or vice versa.
184+
*/
185+
#propertyVariationCache = new Map<string, { variesByCulture: boolean; variesBySegment: boolean }>();
186+
181187
constructor(
182188
host: UmbControllerHost,
183189
args: UmbContentDetailWorkspaceContextArgs<
@@ -379,6 +385,16 @@ export abstract class UmbContentDetailWorkspaceContextBase<
379385
null,
380386
);
381387

388+
// Observe property variation changes to trigger value migration when properties change
389+
// from invariant to variant (or vice versa) via Infinite Editing
390+
this.observe(
391+
this.structure.contentTypeProperties,
392+
(properties: Array<UmbPropertyTypeModel>) => {
393+
this.#handlePropertyVariationChanges(properties);
394+
},
395+
null,
396+
);
397+
382398
this.loadLanguages();
383399
}
384400

@@ -1093,6 +1109,67 @@ export abstract class UmbContentDetailWorkspaceContextBase<
10931109
variantId: UmbVariantId,
10941110
): UmbContentPropertyDatasetContext<DetailModelType, ContentTypeDetailModelType, VariantModelType>;
10951111

1112+
/**
1113+
* Handles property variation changes when the document type is updated via Infinite Editing.
1114+
* When a property's variation setting changes (e.g., from shared/invariant to variant or vice versa),
1115+
* this method reloads the document to get properly migrated values from the server.
1116+
*/
1117+
#handlePropertyVariationChanges(properties: Array<UmbPropertyTypeModel>): void {
1118+
// Skip if no current data or if this is initial load
1119+
const currentData = this._data.getCurrent();
1120+
if (!currentData || this.#propertyVariationCache.size === 0) {
1121+
// Initial load - just populate the cache
1122+
for (const property of properties) {
1123+
this.#propertyVariationCache.set(property.alias, {
1124+
variesByCulture: property.variesByCulture,
1125+
variesBySegment: property.variesBySegment,
1126+
});
1127+
}
1128+
return;
1129+
}
1130+
1131+
// Check for variation setting changes
1132+
let hasChanges = false;
1133+
for (const property of properties) {
1134+
const cached = this.#propertyVariationCache.get(property.alias);
1135+
if (cached) {
1136+
if (
1137+
cached.variesByCulture !== property.variesByCulture ||
1138+
cached.variesBySegment !== property.variesBySegment
1139+
) {
1140+
hasChanges = true;
1141+
}
1142+
}
1143+
// Update cache
1144+
this.#propertyVariationCache.set(property.alias, {
1145+
variesByCulture: property.variesByCulture,
1146+
variesBySegment: property.variesBySegment,
1147+
});
1148+
}
1149+
1150+
// If variation settings changed, reload to get properly migrated values
1151+
if (hasChanges) {
1152+
this.reload();
1153+
}
1154+
}
1155+
1156+
/**
1157+
* Override reload to process incoming data through the value migration pipeline.
1158+
* This ensures property values are properly transformed when variation settings change.
1159+
*/
1160+
public override async reload(): Promise<void> {
1161+
const unique = this.getUnique();
1162+
if (!unique) throw new Error('Unique is not set');
1163+
const { data } = await this._detailRepository!.requestByUnique(unique);
1164+
1165+
if (data) {
1166+
// Process the data through _processIncomingData to handle value migration
1167+
const processedData = await this._processIncomingData(data);
1168+
this._data.setPersisted(processedData);
1169+
this._data.setCurrent(processedData);
1170+
}
1171+
}
1172+
10961173
public override destroy(): void {
10971174
this.structure.destroy();
10981175
this.#languageRepository.destroy();

src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,5 +230,91 @@ describe('UmbPropertyValuePresetVariantBuilderController', () => {
230230
expect(result[5]?.culture).to.be.equal('cultureB');
231231
expect(result[5]?.segment).to.be.equal('segmentB');
232232
});
233+
234+
it('migrates invariant value to variant when variation changes', async () => {
235+
const ctrlHost = new UmbTestControllerHostElement();
236+
const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost);
237+
ctrl.setCultures(['en-US', 'da-DK']);
238+
239+
// Set existing values as invariant (culture: null) - simulating before variation change
240+
ctrl.setValues([
241+
{
242+
alias: 'test',
243+
value: 'invariant value',
244+
culture: null,
245+
segment: null,
246+
editorAlias: 'test-editor-schema',
247+
},
248+
]);
249+
250+
// Property now varies by culture (after variation change)
251+
const propertyTypes: Array<UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel> = [
252+
{
253+
alias: 'test',
254+
propertyEditorUiAlias: 'test-editor-ui',
255+
propertyEditorSchemaAlias: 'test-editor-schema',
256+
config: [],
257+
typeArgs: { variesByCulture: true },
258+
},
259+
];
260+
261+
const result = await ctrl.create(propertyTypes, {
262+
entityType: 'test',
263+
entityUnique: 'some-unique',
264+
});
265+
266+
// Both culture variants should inherit the invariant value
267+
// Note: When a value exists, the preset API preserves it without modification
268+
expect(result.length).to.be.equal(2);
269+
expect(result[0]?.value).to.be.equal('invariant value');
270+
expect(result[0]?.culture).to.be.equal('en-US');
271+
expect(result[1]?.value).to.be.equal('invariant value');
272+
expect(result[1]?.culture).to.be.equal('da-DK');
273+
});
274+
275+
it('migrates variant value to invariant when variation changes', async () => {
276+
const ctrlHost = new UmbTestControllerHostElement();
277+
const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost);
278+
279+
// Set existing values as variant (culture-specific) - simulating before variation change
280+
ctrl.setValues([
281+
{
282+
alias: 'test',
283+
value: 'en-US value',
284+
culture: 'en-US',
285+
segment: null,
286+
editorAlias: 'test-editor-schema',
287+
},
288+
{
289+
alias: 'test',
290+
value: 'da-DK value',
291+
culture: 'da-DK',
292+
segment: null,
293+
editorAlias: 'test-editor-schema',
294+
},
295+
]);
296+
297+
// Property is now invariant (after variation change)
298+
const propertyTypes: Array<UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel> = [
299+
{
300+
alias: 'test',
301+
propertyEditorUiAlias: 'test-editor-ui',
302+
propertyEditorSchemaAlias: 'test-editor-schema',
303+
config: [],
304+
typeArgs: { variesByCulture: false },
305+
},
306+
];
307+
308+
const result = await ctrl.create(propertyTypes, {
309+
entityType: 'test',
310+
entityUnique: 'some-unique',
311+
});
312+
313+
// Invariant value should use first culture-specific value found
314+
// Note: When a value exists, the preset API preserves it without modification
315+
expect(result.length).to.be.equal(1);
316+
expect(result[0]?.value).to.be.equal('en-US value');
317+
expect(result[0]?.culture).to.be.null;
318+
});
233319
});
234320
});

src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,51 @@ export class UmbPropertyValuePresetVariantBuilderController extends UmbPropertyV
101101
const variantId = new UmbVariantId(culture, segment);
102102
const args: Partial<UmbPropertyValuePresetApiCallArgs> = {
103103
variantId,
104-
value: this._existingValues?.find((x) => x.alias === alias && variantId.compare(x))?.value,
104+
value: this.#findExistingValue(alias, variantId),
105105
};
106106
return args;
107107
}
108+
109+
/**
110+
* Finds an existing value for a property, with fallback logic for variation setting changes.
111+
* When a property changes from invariant to variant (or vice versa), the existing values
112+
* may have a different culture/segment than what we're looking for. This method handles
113+
* value migration by trying fallback lookups.
114+
*/
115+
#findExistingValue(alias: string, variantId: UmbVariantId): unknown {
116+
if (!this._existingValues) return undefined;
117+
118+
// First, try exact match (same alias + same culture/segment)
119+
const exactMatch = this._existingValues.find((x) => x.alias === alias && variantId.compare(x));
120+
if (exactMatch) {
121+
return exactMatch.value;
122+
}
123+
124+
// No exact match - try fallback for variation setting changes
125+
// Get all values for this property alias
126+
const valuesForAlias = this._existingValues.filter((x) => x.alias === alias);
127+
if (valuesForAlias.length === 0) {
128+
return undefined;
129+
}
130+
131+
// Fallback 1: If looking for a culture-specific value, try to use the invariant value
132+
// This handles: invariant → variant migration
133+
if (variantId.culture !== null) {
134+
const invariantValue = valuesForAlias.find((x) => x.culture === null && x.segment === variantId.segment);
135+
if (invariantValue) {
136+
return invariantValue.value;
137+
}
138+
}
139+
140+
// Fallback 2: If looking for an invariant value, try to use the first culture-specific value
141+
// This handles: variant → invariant migration
142+
if (variantId.culture === null) {
143+
const anyVariantValue = valuesForAlias.find((x) => x.culture !== null && x.segment === variantId.segment);
144+
if (anyVariantValue) {
145+
return anyVariantValue.value;
146+
}
147+
}
148+
149+
return undefined;
150+
}
108151
}

0 commit comments

Comments
 (0)