diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts index ed9a344d175d..180bee241130 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts @@ -14,9 +14,9 @@ import { UmbPropertyValuePresetBuilderController } from './property-value-preset @customElement('umb-test-controller-host') export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} -// TODO: Write test with config, investigate oppertunity to retrieve Config Object, for an simpler DX. [NL] +// TODO: Write test with config, investigate opportunity to retrieve Config Object, for an simpler DX. [NL] -// Test with async APIs, espcially where the first one is slower than the last one. +// Test with async APIs, especially where the first one is slower than the last one. export class TestPropertyValuePresetFirstApi implements UmbPropertyValuePreset { async processValue(value: undefined | string, config: UmbPropertyEditorConfig) { return value ? value + '_first' : 'first'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts index 84ccf2232bf4..ee0c11d6e5d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts @@ -523,6 +523,48 @@ describe('UmbPropertyValuePresetVariantBuilderController', () => { expect(result.some((r) => r.culture === null)).to.be.false; }); + it('produces one value per distinct segment when variant options include cultures but the property varies by segment only', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + // Variant options reflect a content type that varies by culture AND segment: a cultures × segments + // list plus the invariant option the caller appends for invariant properties. + ctrl.setVariantOptions([ + new UmbVariantId('cultureA', null), + new UmbVariantId('cultureA', 'segmentA'), + new UmbVariantId('cultureA', 'segmentB'), + new UmbVariantId('cultureB', null), + new UmbVariantId('cultureB', 'segmentA'), + new UmbVariantId('cultureB', 'segmentB'), + new UmbVariantId(null, null), + ]); + + // Property varies by segment but not by culture: each culture-bearing option must be projected + // to culture-null and deduped by segment, so one value per distinct segment is produced. + // If the logic instead filtered out everything with a non-null culture, only the invariant value + // would survive and segmented values would be silently dropped. + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesByCulture: false, variesBySegment: true }, + }, + ]; + + const result = await ctrl.create(propertyTypes, { + entityType: 'test', + entityUnique: 'some-unique', + }); + + expect(result.length).to.be.equal(3); + expect(result[0]?.culture).to.be.null; + expect(result[0]?.segment).to.be.null; + expect(result[1]?.culture).to.be.null; + expect(result[1]?.segment).to.be.equal('segmentA'); + expect(result[2]?.culture).to.be.null; + expect(result[2]?.segment).to.be.equal('segmentB'); + }); + it('excludes all variant options when property is invariant', async () => { const ctrlHost = new UmbTestControllerHostElement(); const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts index 432b555df281..16c4cbb39bc4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts @@ -104,9 +104,17 @@ export class UmbPropertyValuePresetVariantBuilderController extends UmbPropertyV } /** - * Filters variant options based on property type args (variesByCulture/variesBySegment). - * @param {UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel} propertyType - Property type model - * @returns {Array} Filtered array of UmbVariantId instances + * Derives the variant ids to generate values for, from the variant options supplied via + * {@link setVariantOptions}. Those options describe the document's variants (driven by the + * content type's variance: e.g. one entry per culture, or per culture × segment), which + * may not match how the property itself varies. + * + * Each option is projected onto the property's variance — the dimensions the property + * varies on (culture and/or segment) are kept, the rest are collapsed to null — and the + * projected ids are deduped. A culture-invariant option is dropped when the property + * varies by culture, because there is no culture to hoist it to. + * @param {UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel} propertyType - Property type model, whose variance drives the projection + * @returns {Array} The variant ids to generate values for */ #getFilteredVariantOptions( propertyType: UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel, @@ -115,30 +123,29 @@ export class UmbPropertyValuePresetVariantBuilderController extends UmbPropertyV return []; } - const variesByCulture = propertyType.typeArgs.variesByCulture; - const variesBySegment = propertyType.typeArgs.variesBySegment; + const variesByCulture = propertyType.typeArgs.variesByCulture ?? false; + const variesBySegment = propertyType.typeArgs.variesBySegment ?? false; - // Validate that cultures are available when property varies by culture if (variesByCulture && !this.#variantOptions.some((v) => v.culture !== null)) { throw new Error('Cultures must be set when varying by culture.'); } - // Filter options based on property variation settings - return this.#variantOptions.filter((variantId) => { - // If property doesn't vary by culture, only use culture-invariant options - if (!variesByCulture && variantId.culture !== null) { - return false; - } - // If property does vary by culture, exclude culture-invariant options + const seen = new Set(); + const result: Array = []; + for (const variantId of this.#variantOptions) { if (variesByCulture && variantId.culture === null) { - return false; + continue; } - // If property doesn't vary by segment, only use segment-invariant options - if (!variesBySegment && variantId.segment !== null) { - return false; + const projected = variantId.toVariant(variesByCulture, variesBySegment); + const key = projected.toString(); + if (seen.has(key)) { + continue; } - return true; - }); + seen.add(key); + result.push(projected); + } + + return result; } /**