Skip to content

Commit

Permalink
V15: Add abstraction for named entity detail workspaces (#17959)
Browse files Browse the repository at this point in the history
* chore: add validation to mocked endpoints

* feat: create new base context `UmbEntityNamedDetailWorkspaceContextBase` to use for named entities

* feat: extend from `UmbEntityNamedDetailWorkspaceContextBase` to be able to save some code

* feat: allow to pass on the generic parameters

* feat: add type-safety property

* chore: remove duplicate code by extending from correct interface

* chore: fix type casting

* feat: make class abstract and add explanatory comment
  • Loading branch information
iOvergaard authored Jan 14, 2025
1 parent b5e4806 commit c3134cb
Show file tree
Hide file tree
Showing 17 changed files with 134 additions and 32 deletions.
18 changes: 18 additions & 0 deletions src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,21 @@ export const queryFilter = (filterBy: string, value?: string) => {
const query = filterBy.toLowerCase();
return value.toLowerCase().includes(query);
};

/**
* Creates a problem details object.
* @param {object} problemDetails The problem details object.
* @param {string} problemDetails.title The title of the problem, which will be shown to the user.
* @param {string} problemDetails.detail A human-readable explanation specific to this occurrence of the problem, which will be shown to the user.
* @param {number} problemDetails.status The HTTP status code for this occurrence of the problem.
* @param {string} problemDetails.type A URI reference that identifies the problem type.
* @returns {object} The problem details object.
*/
export function createProblemDetails(problemDetails: {
title: string;
detail?: string;
type?: string;
status?: number;
}): object {
return problemDetails;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
const { rest } = window.MockServiceWorker;
import { createProblemDetails } from '../../data/utils.js';
import { umbPartialViewMockDB } from '../../data/partial-view/partial-view.db.js';
import { UMB_SLUG } from './slug.js';
import type {
CreateStylesheetRequestModel,
UpdateStylesheetRequestModel,
CreatePartialViewRequestModel,
UpdatePartialViewRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';

export const detailHandlers = [
rest.post(umbracoPath(UMB_SLUG), async (req, res, ctx) => {
const requestBody = (await req.json()) as CreateStylesheetRequestModel;
const requestBody = (await req.json()) as CreatePartialViewRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));

// Validate name
if (!requestBody.name) {
return res(
ctx.status(400, 'name is required'),
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
);
}

const path = umbPartialViewMockDB.file.create(requestBody);
const encodedPath = encodeURIComponent(path);
return res(
Expand Down Expand Up @@ -39,7 +49,7 @@ export const detailHandlers = [
rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => {
const path = req.params.path as string;
if (!path) return res(ctx.status(400));
const requestBody = (await req.json()) as UpdateStylesheetRequestModel;
const requestBody = (await req.json()) as UpdatePartialViewRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));
umbPartialViewMockDB.file.update(decodeURIComponent(path), requestBody);
return res(ctx.status(200));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const { rest } = window.MockServiceWorker;
import { umbPartialViewMockDB } from '../../data/partial-view/partial-view.db.js';
import { UMB_SLUG } from './slug.js';
import type { RenameStylesheetRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { RenamePartialViewRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';

export const renameHandlers = [
rest.put(umbracoPath(`${UMB_SLUG}/:path/rename`), async (req, res, ctx) => {
const path = req.params.path as string;
if (!path) return res(ctx.status(400));

const requestBody = (await req.json()) as RenameStylesheetRequestModel;
const requestBody = (await req.json()) as RenamePartialViewRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));

const newPath = umbPartialViewMockDB.file.rename(decodeURIComponent(path), requestBody.name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
const { rest } = window.MockServiceWorker;
import { createProblemDetails } from '../../data/utils.js';
import { umbScriptMockDb } from '../../data/script/script.db.js';
import { UMB_SLUG } from './slug.js';
import type {
CreateStylesheetRequestModel,
UpdateStylesheetRequestModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import type { CreateScriptRequestModel, UpdateScriptRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';

export const detailHandlers = [
rest.post(umbracoPath(UMB_SLUG), async (req, res, ctx) => {
const requestBody = (await req.json()) as CreateStylesheetRequestModel;
const requestBody = (await req.json()) as CreateScriptRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));

// Validate name
if (!requestBody.name) {
return res(
ctx.status(400, 'name is required'),
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
);
}

const path = umbScriptMockDb.file.create(requestBody);
const encodedPath = encodeURIComponent(path);
return res(
Expand Down Expand Up @@ -39,7 +46,7 @@ export const detailHandlers = [
rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => {
const path = req.params.path as string;
if (!path) return res(ctx.status(400));
const requestBody = (await req.json()) as UpdateStylesheetRequestModel;
const requestBody = (await req.json()) as UpdateScriptRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));
umbScriptMockDb.file.update(decodeURIComponent(path), requestBody);
return res(ctx.status(200));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { rest } = window.MockServiceWorker;
import { createProblemDetails } from '../../data/utils.js';
import { umbStylesheetMockDb } from '../../data/stylesheet/stylesheet.db.js';
import { UMB_SLUG } from './slug.js';
import type {
Expand All @@ -14,6 +15,14 @@ export const detailHandlers = [
const path = umbStylesheetMockDb.file.create(requestBody);
const encodedPath = encodeURIComponent(path);

// Validate name
if (!requestBody.name) {
return res(
ctx.status(400, 'name is required'),
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
);
}

return res(
ctx.status(201),
ctx.set({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { rest } = window.MockServiceWorker;
import { createProblemDetails } from '../../data/utils.js';
import { umbTemplateMockDb } from '../../data/template/template.db.js';
import { UMB_SLUG } from './slug.js';
import type {
Expand All @@ -12,6 +13,14 @@ export const detailHandlers = [
const requestBody = (await req.json()) as CreateTemplateRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));

// Validate name and alias
if (!requestBody.name || !requestBody.alias) {
return res(
ctx.status(400, 'name and alias are required'),
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name and alias are required' })),
);
}

const id = umbTemplateMockDb.detail.create(requestBody);

return res(
Expand Down Expand Up @@ -40,6 +49,15 @@ export const detailHandlers = [
if (!id) return res(ctx.status(400));
const requestBody = (await req.json()) as UpdateTemplateRequestModel;
if (!requestBody) return res(ctx.status(400, 'no body found'));

// Validate name and alias
if (!requestBody.name || !requestBody.alias) {
return res(
ctx.status(400, 'name and alias are required'),
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name and alias are required' })),
);
}

umbTemplateMockDb.detail.update(id, requestBody);
return res(ctx.status(200));
}),
Expand Down
4 changes: 4 additions & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ export interface UmbEntityModel {
unique: UmbEntityUnique;
entityType: string;
}

export interface UmbNamedEntityModel extends UmbEntityModel {
name: string;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { UmbNamableWorkspaceContext } from '../../namable/namable-workspace-context.interface.js';
import type { UmbSubmittableWorkspaceContext } from './submittable-workspace-context.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbPropertyDatasetContext, UmbPropertyValueData } from '@umbraco-cms/backoffice/property';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';

export interface UmbInvariantDatasetWorkspaceContext extends UmbSubmittableWorkspaceContext {
// Name:
name: Observable<string | undefined>;
getName(): string | undefined;
setName(name: string): void;

export interface UmbInvariantDatasetWorkspaceContext
extends UmbSubmittableWorkspaceContext,
UmbNamableWorkspaceContext {
readonly values: Observable<Array<UmbPropertyValueData> | undefined>;
getValues(): Promise<Array<UmbPropertyValueData> | undefined>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export const UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT = new UmbContextToken<
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbEntityDetailWorkspaceContextBase => (context as any).IS_ENTITY_DETAIL_WORKSPACE_CONTEXT,
(context): context is UmbEntityDetailWorkspaceContextBase =>
(context as UmbEntityDetailWorkspaceContextBase).IS_ENTITY_DETAIL_WORKSPACE_CONTEXT,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { UmbNamableWorkspaceContext } from '../types.js';
import { UmbEntityDetailWorkspaceContextBase } from './entity-detail-workspace-base.js';
import type { UmbEntityDetailWorkspaceContextCreateArgs } from './types.js';
import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity';
import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';

export abstract class UmbEntityNamedDetailWorkspaceContextBase<
NamedDetailModelType extends UmbNamedEntityModel = UmbNamedEntityModel,
NamedDetailRepositoryType extends
UmbDetailRepository<NamedDetailModelType> = UmbDetailRepository<NamedDetailModelType>,
CreateArgsType extends
UmbEntityDetailWorkspaceContextCreateArgs<NamedDetailModelType> = UmbEntityDetailWorkspaceContextCreateArgs<NamedDetailModelType>,
>
extends UmbEntityDetailWorkspaceContextBase<NamedDetailModelType, NamedDetailRepositoryType, CreateArgsType>
implements UmbNamableWorkspaceContext
{
// Just for context token safety:
public readonly IS_ENTITY_NAMED_DETAIL_WORKSPACE_CONTEXT = true;

readonly name = this._data.createObservablePartOfCurrent((data) => data?.name);

getName() {
return this._data.getCurrent()?.name;
}

setName(name: string | undefined) {
// We have to cast to Partial because TypeScript doesn't understand that the model has a name property due to generic sub-types
this._data.updateCurrent({ name } as Partial<NamedDetailModelType>);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { UmbWorkspaceContext } from '../workspace-context.interface.js';
import type { UmbEntityNamedDetailWorkspaceContextBase } from './entity-named-detail-workspace-base.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';

export const UMB_ENTITY_NAMED_DETAIL_WORKSPACE_CONTEXT = new UmbContextToken<
UmbWorkspaceContext,
UmbEntityNamedDetailWorkspaceContextBase
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbEntityNamedDetailWorkspaceContextBase =>
(context as UmbEntityNamedDetailWorkspaceContextBase).IS_ENTITY_NAMED_DETAIL_WORKSPACE_CONTEXT,
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ import './global-components/index.js';

export * from './entity-detail-workspace.context-token.js';
export * from './entity-detail-workspace-base.js';
export * from './entity-named-detail-workspace.context-token.js';
export * from './entity-named-detail-workspace-base.js';
export * from './global-components/index.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export type * from './namable-workspace-context.interface.js';
export * from './namable-workspace.context-token.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * from './namable-workspace-context.interface.js';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type * from './kinds/types.js';
export type * from './conditions/types.js';
export type * from './data-manager/types.js';
export type * from './workspace-context.interface.js';
export type * from './namable/types.js';

/**
* @deprecated Use `UmbEntityUnique`instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
import {
UmbInvariantWorkspacePropertyDatasetContext,
UmbWorkspaceIsNewRedirectController,
UmbEntityDetailWorkspaceContextBase,
UmbEntityNamedDetailWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { appendToFrozenArray, UmbArrayState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
Expand Down Expand Up @@ -42,10 +42,9 @@ type EntityType = UmbDataTypeDetailModel;
* - a new property editor ui is picked for a data-type, uses the data-type configuration to set the schema, if such is configured for the Property Editor UI. (The user picks the UI via the UI, the schema comes from the UI that the user picked, we store both on the data-type)
*/
export class UmbDataTypeWorkspaceContext
extends UmbEntityDetailWorkspaceContextBase<EntityType, UmbDataTypeDetailRepository>
extends UmbEntityNamedDetailWorkspaceContextBase<EntityType, UmbDataTypeDetailRepository>
implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext
{
readonly name = this._data.createObservablePartOfCurrent((data) => data?.name);
readonly propertyEditorUiAlias = this._data.createObservablePartOfCurrent((data) => data?.editorUiAlias);
readonly propertyEditorSchemaAlias = this._data.createObservablePartOfCurrent((data) => data?.editorAlias);

Expand Down Expand Up @@ -249,14 +248,6 @@ export class UmbDataTypeWorkspaceContext
return new UmbInvariantWorkspacePropertyDatasetContext(host, this);
}

getName() {
return this._data.getCurrent()?.name;
}

setName(name: string | undefined) {
this._data.updateCurrent({ name });
}

getPropertyEditorSchemaAlias() {
return this._data.getCurrent()?.editorAlias;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ export const foundConsts = [{
},
{
path: '@umbraco-cms/backoffice/workspace',
consts: ["UMB_WORKSPACE_SPLIT_VIEW_CONTEXT","UMB_WORKSPACE_HAS_COLLECTION_CONDITION_ALIAS","UMB_WORKSPACE_HAS_COLLECTION_CONDITION","UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS","UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION","UMB_WORKSPACE_CONDITION_ALIAS","UMB_ENTITY_WORKSPACE_CONTEXT","UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT","UMB_PUBLISHABLE_WORKSPACE_CONTEXT","UMB_ROUTABLE_WORKSPACE_CONTEXT","UMB_SUBMITTABLE_WORKSPACE_CONTEXT","UMB_VARIANT_WORKSPACE_CONTEXT","UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT","UMB_WORKSPACE_MODAL","UMB_NAMABLE_WORKSPACE_CONTEXT","UMB_WORKSPACE_PATH_PATTERN","UMB_WORKSPACE_VIEW_PATH_PATTERN","UMB_WORKSPACE_CONTEXT"]
consts: ["UMB_WORKSPACE_SPLIT_VIEW_CONTEXT","UMB_WORKSPACE_HAS_COLLECTION_CONDITION_ALIAS","UMB_WORKSPACE_HAS_COLLECTION_CONDITION","UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS","UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION","UMB_WORKSPACE_CONDITION_ALIAS","UMB_ENTITY_WORKSPACE_CONTEXT","UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT","UMB_PUBLISHABLE_WORKSPACE_CONTEXT","UMB_ROUTABLE_WORKSPACE_CONTEXT","UMB_SUBMITTABLE_WORKSPACE_CONTEXT","UMB_VARIANT_WORKSPACE_CONTEXT","UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT","UMB_ENTITY_NAMED_DETAIL_WORKSPACE_CONTEXT","UMB_WORKSPACE_MODAL","UMB_NAMABLE_WORKSPACE_CONTEXT","UMB_WORKSPACE_PATH_PATTERN","UMB_WORKSPACE_VIEW_PATH_PATTERN","UMB_WORKSPACE_CONTEXT"]
},
{
path: '@umbraco-cms/backoffice/external/backend-api',
Expand Down

0 comments on commit c3134cb

Please sign in to comment.