Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8100911
feat: adds new repository for document by id segment options
iOvergaard Oct 1, 2025
f1f3109
chore: mocks up the new endpoint
iOvergaard Oct 1, 2025
b8c2273
feat: all 'null' segments should appear on all languages
iOvergaard Oct 1, 2025
d1f2fdc
feat: uses new endpoint in content detail workspace base
iOvergaard Oct 1, 2025
1ad62fe
feat: maps up the name of the segment
iOvergaard Oct 1, 2025
d231c6a
chore: mock segment data
iOvergaard Oct 1, 2025
8dec40d
feat: adds filter on available segments
iOvergaard Oct 1, 2025
166f116
Merge remote-tracking branch 'origin/v17/dev' into v16/feature/docume…
iOvergaard Oct 1, 2025
cf27515
feat: do not alter behavior depending on "undefined" and "null"
iOvergaard Oct 1, 2025
9687810
chore: updates mock handler
iOvergaard Oct 1, 2025
6173a36
Merge branch 'v17/dev' into v16/feature/document-segment-options
iOvergaard Oct 1, 2025
b13a46a
Merge branch 'v17/dev' into v16/feature/document-segment-options
iOvergaard Oct 2, 2025
99b995d
feat: ensures that the segments are loaded based on an override metho…
iOvergaard Oct 2, 2025
98104fc
feat: refines the segment filter
iOvergaard Oct 2, 2025
37a06ed
Merge branch 'v17/dev' into v16/feature/document-segment-options
iOvergaard Oct 2, 2025
03c60ce
chore: updates deprecated model
iOvergaard Oct 2, 2025
975f0c4
feat: treats all culture-less segments as applying to everything
iOvergaard Oct 2, 2025
45bbae5
Merge remote-tracking branch 'origin/v17/dev' into v16/feature/docume…
iOvergaard Oct 3, 2025
48a254b
docs: updates console warn for developers
iOvergaard Oct 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,7 @@ export const data: Array<UmbMockDocumentTypeModel> = [
icon: 'icon-document',
allowedAsRoot: true,
variesByCulture: true,
variesBySegment: false,
variesBySegment: true,
isElement: false,
hasChildren: false,
parent: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -769,14 +769,47 @@ export const data: Array<UmbMockDocumentModel> = [
{
state: DocumentVariantStateModel.PUBLISHED,
publishDate: '2023-02-06T15:31:51.354764',
culture: 'da-dk',
culture: 'da',
segment: null,
name: 'Artikel på Dansk',
createDate: '2023-02-06T15:31:46.876902',
updateDate: '2023-02-06T15:31:51.354764',
id: 'artikel-pa-dansk',
flags: [],
},
{
state: DocumentVariantStateModel.PUBLISHED,
publishDate: '2023-02-06T15:31:51.354764',
culture: 'da',
segment: 'vip',
name: 'VIP: Artikel på Dansk',
createDate: '2023-02-06T15:31:46.876902',
updateDate: '2023-02-06T15:31:51.354764',
id: 'artikel-pa-dansk-vip',
flags: [],
},
{
state: DocumentVariantStateModel.PUBLISHED,
publishDate: '2023-02-06T15:31:51.354764',
culture: null,
segment: 'vip-invariant',
name: 'Invariant VIP Segmented Article',
createDate: '2023-02-06T15:31:46.876902',
updateDate: '2023-02-06T15:31:51.354764',
id: 'invariant-vip-segmented-article',
flags: [],
},
{
state: DocumentVariantStateModel.PUBLISHED,
publishDate: '2023-02-06T15:31:51.354764',
culture: null,
segment: 'generic',
name: 'Generic VIP Segmented Article',
createDate: '2023-02-06T15:31:46.876902',
updateDate: '2023-02-06T15:31:51.354764',
id: 'generic-vip-segmented-article',
flags: [],
},
{
state: DocumentVariantStateModel.PUBLISHED,
publishDate: '2023-02-06T15:31:51.354764',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UMB_SLUG } from './slug.js';
import type {
CreateDocumentRequestModel,
DefaultReferenceResponseModel,
GetDocumentByIdAvailableSegmentOptionsResponse,
GetDocumentByIdReferencedDescendantsResponse,
PagedIReferenceResponseModel,
UpdateDocumentRequestModel,
Expand Down Expand Up @@ -73,6 +74,43 @@ export const detailHandlers = [
return res(ctx.status(200), ctx.json(ReferencedDescendantsResponse));
}),

rest.get(umbracoPath(`${UMB_SLUG}/:id/available-segment-options`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return res(ctx.status(400));
const document = umbDocumentMockDb.detail.read(id);
if (!document) return res(ctx.status(404));

const availableSegments = document.variants.filter((v) => !!v.segment).map((v) => v.segment!) ?? [];

const response: GetDocumentByIdAvailableSegmentOptionsResponse = {
total: availableSegments.length,
items: availableSegments.map((alias) => {
// If the segment is generic (i.e. not tied to any culture) we show the segment on all cultures
const isGeneric = alias.includes('generic');
const whichCulturesHaveThisSegment: string[] | undefined = isGeneric
? undefined
: document.variants.filter((v) => v.segment === alias).map((v) => v.culture!);

let availableSegmentOptions: string[] | null = whichCulturesHaveThisSegment ?? null;

if (whichCulturesHaveThisSegment) {
const hasNull = whichCulturesHaveThisSegment.some((c) => c === null);
if (hasNull) {
availableSegmentOptions = null;
}
}

return {
alias,
name: `Segment: ${alias}`,
cultures: availableSegmentOptions,
};
}),
};

return res(ctx.status(200), ctx.json(response));
}),

rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`, 'v1.1'), (_req, res, ctx) => {
const id = _req.params.id as string;
if (!id) return res(ctx.status(400));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
UmbPropertyValuePresetVariantBuilderController,
UmbVariantPropertyGuardManager,
} from '@umbraco-cms/backoffice/property';
import { UmbSegmentCollectionRepository } from '@umbraco-cms/backoffice/segment';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
Expand All @@ -44,7 +43,7 @@
} from '@umbraco-cms/backoffice/validation';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbContentTypeDetailModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbraco-cms/backoffice/repository';
import type {
Expand All @@ -56,11 +55,11 @@
import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import type { UmbPropertyTypePresetModel, UmbPropertyTypePresetModelTypeModel } from '@umbraco-cms/backoffice/property';
import type { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import type { UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment';
import type { UmbSegmentModel } from '@umbraco-cms/backoffice/segment';

export interface UmbContentDetailWorkspaceContextArgs<
DetailModelType extends UmbContentDetailModel<VariantModelType>,
ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel,
ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel,
VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] }
? DetailModelType['variants'][0]
: never,
Expand Down Expand Up @@ -93,7 +92,7 @@
export abstract class UmbContentDetailWorkspaceContextBase<
DetailModelType extends UmbContentDetailModel<VariantModelType>,
DetailRepositoryType extends UmbDetailRepository<DetailModelType> = UmbDetailRepository<DetailModelType>,
ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel,
ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel,
VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] }
? DetailModelType['variants'][0]
: never,
Expand Down Expand Up @@ -156,9 +155,7 @@
*/
public readonly languages = this.#languages.asObservable();

#segmentRepository = new UmbSegmentCollectionRepository(this);
#segments = new UmbArrayState<UmbSegmentCollectionItemModel>([], (x) => x.unique);
protected readonly _segments = this.#segments.asObservable();
protected readonly _segments = new UmbArrayState<UmbSegmentModel>([], (x) => x.alias);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -226,160 +223,168 @@
);

this.variantOptions = mergeObservables(
[this.variesByCulture, this.variesBySegment, this.variants, this.languages, this._segments],
[this.variesByCulture, this.variesBySegment, this.variants, this.languages, this._segments.asObservable()],
([variesByCulture, variesBySegment, variants, languages, segments]) => {
if ((variesByCulture || variesBySegment) === undefined) {
return [];
}

const varies = variesByCulture || variesBySegment;

// No variation
if (!varies) {
return [
{
variant: variants.find((x) => new UmbVariantId(x.culture, x.segment).isInvariant()),
language: languages.find((x) => x.isDefault),
culture: null,
segment: null,
unique: new UmbVariantId().toString(),
} as VariantOptionModelType,
];
}

// Only culture variation
if (variesByCulture && !variesBySegment) {
return languages.map((language) => {
return {
variant: variants.find((x) => x.culture === language.unique),
language,
culture: language.unique,
segment: null,
unique: new UmbVariantId(language.unique).toString(),
} as VariantOptionModelType;
});
}

// Only segment variation
if (!variesByCulture && variesBySegment) {
const invariantCulture = {
variant: variants.find((x) => new UmbVariantId(x.culture, x.segment).isInvariant()),
language: languages.find((x) => x.isDefault),
culture: null,
segment: null,
unique: new UmbVariantId().toString(),
} as VariantOptionModelType;

const segmentsForInvariantCulture = segments.map((segment) => {
// Find all segments that are either generic (undefined) or invariant (null)
const availableSegments = segments.filter((s) => !s.cultures);
const segmentsForInvariantCulture = availableSegments.map((segment) => {
return {
variant: variants.find((x) => x.culture === null && x.segment === segment.unique),
variant: variants.find((x) => x.culture === null && x.segment === segment.alias),
language: languages.find((x) => x.isDefault),
segmentInfo: segment,
culture: null,
segment: segment.unique,
unique: new UmbVariantId(null, segment.unique).toString(),
segment: segment.alias,
unique: new UmbVariantId(null, segment.alias).toString(),
} as VariantOptionModelType;
});

return [invariantCulture, ...segmentsForInvariantCulture] as Array<VariantOptionModelType>;
}

// Culture and segment variation
if (variesByCulture && variesBySegment) {
return languages.flatMap((language) => {
const culture = {
variant: variants.find((x) => x.culture === language.unique && x.segment === null),
language,
culture: language.unique,
segment: null,
unique: new UmbVariantId(language.unique).toString(),
} as VariantOptionModelType;

const segmentsForCulture = segments.map((segment) => {
// Find all segments that are either generic (undefined) or that contains this culture
const availableSegments = segments.filter((s) => !s.cultures || s.cultures.includes(language.unique));
const segmentsForCulture = availableSegments.map((segment) => {
return {
variant: variants.find((x) => x.culture === language.unique && x.segment === segment.unique),
variant: variants.find((x) => x.culture === language.unique && x.segment === segment.alias),
language,
segmentInfo: segment,
culture: language.unique,
segment: segment.unique,
unique: new UmbVariantId(language.unique, segment.unique).toString(),
segment: segment.alias,
unique: new UmbVariantId(language.unique, segment.alias).toString(),
} as VariantOptionModelType;
});

return [culture, ...segmentsForCulture] as Array<VariantOptionModelType>;
});
}

return [] as Array<VariantOptionModelType>;
},
).pipe(map((options) => options.filter((option) => this._variantOptionsFilter(option))));

this.observe(
this.variantOptions,
(variantOptions) => {
variantOptions.forEach((variantOption) => {
const missingThis = !this.#variantValidationContexts.some((x) => {
const variantId = x.getVariantId();
if (!variantId) return;
return variantId.culture === variantOption.culture && variantId.segment === variantOption.segment;
});
if (missingThis) {
const context = new UmbValidationController(this);
context.inheritFrom(this.validationContext, '$');
context.setVariantId(UmbVariantId.Create(variantOption));
context.autoReport();
this.#variantValidationContexts.push(context);
}
});
},
null,
);

this.observe(
observeMultiple([this.splitView.activeVariantByIndex(0), this.variants]),
([activeVariant, variants]) => {
const variantName = variants.find(
(v) => v.culture === activeVariant?.culture && v.segment === activeVariant?.segment,
)?.name;
this.view.setTitle(variantName);
},
null,
);

this.observe(
this.varies,
(varies) => {
this._data.setVaries(varies);
this.#varies = varies;
},
null,
);
this.observe(
this.variesByCulture,
(varies) => {
this._data.setVariesByCulture(varies);
this.#variesByCulture = varies;
},
null,
);
this.observe(
this.variesBySegment,
(varies) => {
this._data.setVariesBySegment(varies);
this.#variesBySegment = varies;
if (varies) {
this.loadSegments();
} else {
this._segments.setValue([]);
}
},
null,
);
this.observe(
this.structure.contentTypeDataTypeUniques,
(dataTypeUniques: Array<string>) => {
this.#dataTypeItemManager.setUniques(dataTypeUniques);
},
null,
);

this.loadLanguages();

Check warning on line 387 in src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/dev)

❌ Getting worse: Complex Method

constructor increases in cyclomatic complexity from 26 to 28, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
this.#loadSegments();
}

public async loadLanguages() {
Expand All @@ -388,9 +393,11 @@
this.#languages.setValue(data?.items ?? []);
}

async #loadSegments() {
const { data } = await this.#segmentRepository.requestCollection({});
this.#segments.setValue(data?.items ?? []);
protected async loadSegments() {
console.warn(
`UmbContentDetailWorkspaceContextBase: Segments are not implemented in the workspace context for "${this.getEntityType()}" types.`,
);
this._segments.setValue([]);
}

protected override async _scaffoldProcessData(data: DetailModelType): Promise<DetailModelType> {
Expand Down
3 changes: 1 addition & 2 deletions src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ export interface UmbEntityVariantOptionModel<VariantType extends UmbEntityVarian
language: UmbLanguageDetailModel;
segmentInfo?: {
alias: string;
entityType: string;
name: string;
unique: string;
cultures?: string[] | null;
};
/**
* The unique identifier is a VariantId string.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { UmbDocumentDetailRepository } from './detail/index.js';
export { UmbDocumentPreviewRepository } from './preview/index.js';
export { UmbDocumentSegmentRepository } from './segment/index.js';

export * from './constants.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { UmbDocumentSegmentFilterModel } from './types.js';
import { UmbRepositoryBase, type UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository';
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import type { UmbSegmentModel, UmbSegmentResponseModel } from '@umbraco-cms/backoffice/segment';

export class UmbDocumentSegmentRepository extends UmbRepositoryBase {
/**
* Get available segment options for a document by its ID.
* @param {string} unique The unique identifier of the document.
* @param {UmbDocumentSegmentFilterModel} filter The filter options to apply.
* @returns A promise that resolves with the available segment options.
*/
async getDocumentByIdSegmentOptions(
unique: string,
filter: UmbDocumentSegmentFilterModel,
): Promise<UmbRepositoryResponse<UmbSegmentResponseModel>> {
const { data, error } = await tryExecute(
this,
// eslint-disable-next-line @typescript-eslint/no-deprecated
DocumentService.getDocumentByIdAvailableSegmentOptions({ path: { id: unique }, query: filter }),
);

if (data) {
const items = data.items.map((item) => {
const model: UmbSegmentModel = {
alias: item.alias,
name: item.name,
// eslint-disable-next-line @typescript-eslint/no-deprecated
cultures: item.cultures,
};

return model;
});

return { data: { items, total: data.total } };
}

return { error };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './document-segment.repository.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface UmbDocumentSegmentFilterModel {
skip?: number;
take?: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * from './segment/types.js';
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type * from './item/types.js';
export type * from './modals/types.js';
export type * from './publishing/types.js';
export type * from './recycle-bin/types.js';
export type * from './repository/types.js';
export type * from './tree/types.js';
export type * from './url/types.js';
export type * from './user-permissions/types.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UmbDocumentTypeDetailRepository } from '../../document-types/repository/detail/document-type-detail.repository.js';
import { UmbDocumentPropertyDatasetContext } from '../property-dataset-context/document-property-dataset.context.js';
import type { UmbDocumentDetailRepository } from '../repository/index.js';
import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js';
import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, UmbDocumentSegmentRepository } from '../repository/index.js';
import type { UmbDocumentDetailModel, UmbDocumentVariantModel } from '../types.js';
import {
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN,
Expand Down Expand Up @@ -63,6 +63,7 @@ export class UmbDocumentWorkspaceContext
readonly templateId = this._data.createObservablePartOfCurrent((data) => data?.template?.unique || null);

#isTrashedContext = new UmbIsTrashedEntityContext(this);
#documentSegmentRepository = new UmbDocumentSegmentRepository(this);

constructor(host: UmbControllerHost) {
super(host, {
Expand Down Expand Up @@ -207,6 +208,24 @@ export class UmbDocumentWorkspaceContext
return response;
}

protected override async loadSegments(): Promise<void> {
this.observe(
this.unique,
async (unique) => {
if (!unique) {
this._segments.setValue([]);
return;
}
const { data } = await this.#documentSegmentRepository.getDocumentByIdSegmentOptions(unique, {
skip: 0,
take: 9999,
});
this._segments.setValue(data?.items ?? []);
},
'_loadSegmentsUnique',
);
}

async create(parent: UmbEntityModel, documentTypeUnique: string, blueprintUnique?: string) {
if (blueprintUnique) {
const blueprintRepository = new UmbDocumentBlueprintDetailRepository(this);
Expand Down
24 changes: 24 additions & 0 deletions src/Umbraco.Web.UI.Client/src/packages/segment/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
export type * from './entity.js';
export type * from './collection/types.js';

export interface UmbSegmentModel {
/**
* The unique alias of the segment.
*/
alias: string;

/**
* The name of the segment used for display purposes.
*/
name: string;

/**
* An optional list of culture codes that the segment applies to.
* If null, the segment applies to the invariant culture.
* If undefined, the segment is considered generic and applies to all cultures.
*/
cultures?: Array<string> | null;
}

export interface UmbSegmentResponseModel {
items: Array<UmbSegmentModel>;
total: number;
}
Loading