Segmentation: The Backoffice should use the enhanced endpoint to fetch segment options for documents based on GUID (#20340)
* feat: adds new repository for document by id segment options * chore: mocks up the new endpoint * feat: all 'null' segments should appear on all languages * feat: uses new endpoint in content detail workspace base * feat: maps up the name of the segment * chore: mock segment data * feat: adds filter on available segments * feat: do not alter behavior depending on "undefined" and "null" * chore: updates mock handler * feat: ensures that the segments are loaded based on an override method (because they only work for documents) and that they use a generic type (to avoid circular imports) * feat: refines the segment filter * chore: updates deprecated model * feat: treats all culture-less segments as applying to everything * docs: updates console warn for developers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -769,7 +769,7 @@ 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',
|
||||
@@ -777,6 +777,39 @@ export const data: Array<UmbMockDocumentModel> = [
|
||||
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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<VariantModelType>,
|
||||
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<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,
|
||||
@@ -156,9 +155,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
*/
|
||||
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
|
||||
@@ -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<DetailModelType> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './document-segment.repository.js';
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UmbDocumentSegmentFilterModel {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type * from './segment/types.js';
|
||||
@@ -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';
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user