diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index afa5eb3687e1..f78003f45092 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -946,7 +946,7 @@ export const data: Array = [ icon: 'icon-document', allowedAsRoot: true, variesByCulture: true, - variesBySegment: false, + variesBySegment: true, isElement: false, hasChildren: false, parent: null, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index c793e564315c..98ba2df69724 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -769,7 +769,7 @@ export const data: Array = [ { 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', @@ -777,6 +777,39 @@ export const data: Array = [ 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', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts index c1a3563664b3..9fbaebc38955 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts @@ -5,6 +5,7 @@ import { UMB_SLUG } from './slug.js'; import type { CreateDocumentRequestModel, DefaultReferenceResponseModel, + GetDocumentByIdAvailableSegmentOptionsResponse, GetDocumentByIdReferencedDescendantsResponse, PagedIReferenceResponseModel, UpdateDocumentRequestModel, @@ -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)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index f9ba1f351f9c..68d602c0ae83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -32,7 +32,6 @@ import { 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 { @@ -44,7 +43,7 @@ import { } 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 { @@ -56,11 +55,11 @@ import type { UmbEntityVariantModel, UmbEntityVariantOptionModel } from '@umbrac 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, - ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel, + ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel, VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] } ? DetailModelType['variants'][0] : never, @@ -93,7 +92,7 @@ export interface UmbContentDetailWorkspaceContextArgs< export abstract class UmbContentDetailWorkspaceContextBase< DetailModelType extends UmbContentDetailModel, DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository, - ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel, + ContentTypeDetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel, VariantModelType extends UmbEntityVariantModel = DetailModelType extends { variants: UmbEntityVariantModel[] } ? DetailModelType['variants'][0] : never, @@ -156,9 +155,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< */ public readonly languages = this.#languages.asObservable(); - #segmentRepository = new UmbSegmentCollectionRepository(this); - #segments = new UmbArrayState([], (x) => x.unique); - protected readonly _segments = this.#segments.asObservable(); + protected readonly _segments = new UmbArrayState([], (x) => x.alias); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -226,7 +223,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< ); 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 []; @@ -270,14 +267,16 @@ export abstract class UmbContentDetailWorkspaceContextBase< 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; }); @@ -295,14 +294,16 @@ export abstract class UmbContentDetailWorkspaceContextBase< 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; }); @@ -367,6 +368,11 @@ export abstract class UmbContentDetailWorkspaceContextBase< (varies) => { this._data.setVariesBySegment(varies); this.#variesBySegment = varies; + if (varies) { + this.loadSegments(); + } else { + this._segments.setValue([]); + } }, null, ); @@ -379,7 +385,6 @@ export abstract class UmbContentDetailWorkspaceContextBase< ); this.loadLanguages(); - this.#loadSegments(); } public async loadLanguages() { @@ -388,9 +393,11 @@ export abstract class UmbContentDetailWorkspaceContextBase< 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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts index 7d83defd7776..e119531113fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts @@ -34,9 +34,8 @@ export interface UmbEntityVariantOptionModel> { + 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 }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/index.ts new file mode 100644 index 000000000000..c85666786902 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/index.ts @@ -0,0 +1 @@ +export * from './document-segment.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/types.ts new file mode 100644 index 000000000000..0ea9dbe16e53 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/segment/types.ts @@ -0,0 +1,4 @@ +export interface UmbDocumentSegmentFilterModel { + skip?: number; + take?: number; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/types.ts new file mode 100644 index 000000000000..2f0e83e0be00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/types.ts @@ -0,0 +1 @@ +export type * from './segment/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts index 50f077041566..1e7a6912b320 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index a70051db9b6c..69d6445ee23b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -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, @@ -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, { @@ -207,6 +208,24 @@ export class UmbDocumentWorkspaceContext return response; } + protected override async loadSegments(): Promise { + 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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/segment/types.ts b/src/Umbraco.Web.UI.Client/src/packages/segment/types.ts index 6912635ed260..5087a609522c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/segment/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/segment/types.ts @@ -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 | null; +} + +export interface UmbSegmentResponseModel { + items: Array; + total: number; +}