Skip to content

Commit c3134cb

Browse files
authored
V15: Add abstraction for named entity detail workspaces (#17959)
* 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
1 parent b5e4806 commit c3134cb

File tree

17 files changed

+134
-32
lines changed

17 files changed

+134
-32
lines changed

src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,21 @@ export const queryFilter = (filterBy: string, value?: string) => {
3232
const query = filterBy.toLowerCase();
3333
return value.toLowerCase().includes(query);
3434
};
35+
36+
/**
37+
* Creates a problem details object.
38+
* @param {object} problemDetails The problem details object.
39+
* @param {string} problemDetails.title The title of the problem, which will be shown to the user.
40+
* @param {string} problemDetails.detail A human-readable explanation specific to this occurrence of the problem, which will be shown to the user.
41+
* @param {number} problemDetails.status The HTTP status code for this occurrence of the problem.
42+
* @param {string} problemDetails.type A URI reference that identifies the problem type.
43+
* @returns {object} The problem details object.
44+
*/
45+
export function createProblemDetails(problemDetails: {
46+
title: string;
47+
detail?: string;
48+
type?: string;
49+
status?: number;
50+
}): object {
51+
return problemDetails;
52+
}

src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
const { rest } = window.MockServiceWorker;
2+
import { createProblemDetails } from '../../data/utils.js';
23
import { umbPartialViewMockDB } from '../../data/partial-view/partial-view.db.js';
34
import { UMB_SLUG } from './slug.js';
45
import type {
5-
CreateStylesheetRequestModel,
6-
UpdateStylesheetRequestModel,
6+
CreatePartialViewRequestModel,
7+
UpdatePartialViewRequestModel,
78
} from '@umbraco-cms/backoffice/external/backend-api';
89
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
910

1011
export const detailHandlers = [
1112
rest.post(umbracoPath(UMB_SLUG), async (req, res, ctx) => {
12-
const requestBody = (await req.json()) as CreateStylesheetRequestModel;
13+
const requestBody = (await req.json()) as CreatePartialViewRequestModel;
1314
if (!requestBody) return res(ctx.status(400, 'no body found'));
15+
16+
// Validate name
17+
if (!requestBody.name) {
18+
return res(
19+
ctx.status(400, 'name is required'),
20+
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
21+
);
22+
}
23+
1424
const path = umbPartialViewMockDB.file.create(requestBody);
1525
const encodedPath = encodeURIComponent(path);
1626
return res(
@@ -39,7 +49,7 @@ export const detailHandlers = [
3949
rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => {
4050
const path = req.params.path as string;
4151
if (!path) return res(ctx.status(400));
42-
const requestBody = (await req.json()) as UpdateStylesheetRequestModel;
52+
const requestBody = (await req.json()) as UpdatePartialViewRequestModel;
4353
if (!requestBody) return res(ctx.status(400, 'no body found'));
4454
umbPartialViewMockDB.file.update(decodeURIComponent(path), requestBody);
4555
return res(ctx.status(200));

src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/rename.handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
const { rest } = window.MockServiceWorker;
22
import { umbPartialViewMockDB } from '../../data/partial-view/partial-view.db.js';
33
import { UMB_SLUG } from './slug.js';
4-
import type { RenameStylesheetRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
4+
import type { RenamePartialViewRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
55
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
66

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

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

1515
const newPath = umbPartialViewMockDB.file.rename(decodeURIComponent(path), requestBody.name);

src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
const { rest } = window.MockServiceWorker;
2+
import { createProblemDetails } from '../../data/utils.js';
23
import { umbScriptMockDb } from '../../data/script/script.db.js';
34
import { UMB_SLUG } from './slug.js';
4-
import type {
5-
CreateStylesheetRequestModel,
6-
UpdateStylesheetRequestModel,
7-
} from '@umbraco-cms/backoffice/external/backend-api';
5+
import type { CreateScriptRequestModel, UpdateScriptRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
86
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
97

108
export const detailHandlers = [
119
rest.post(umbracoPath(UMB_SLUG), async (req, res, ctx) => {
12-
const requestBody = (await req.json()) as CreateStylesheetRequestModel;
10+
const requestBody = (await req.json()) as CreateScriptRequestModel;
1311
if (!requestBody) return res(ctx.status(400, 'no body found'));
12+
13+
// Validate name
14+
if (!requestBody.name) {
15+
return res(
16+
ctx.status(400, 'name is required'),
17+
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
18+
);
19+
}
20+
1421
const path = umbScriptMockDb.file.create(requestBody);
1522
const encodedPath = encodeURIComponent(path);
1623
return res(
@@ -39,7 +46,7 @@ export const detailHandlers = [
3946
rest.put(umbracoPath(`${UMB_SLUG}/:path`), async (req, res, ctx) => {
4047
const path = req.params.path as string;
4148
if (!path) return res(ctx.status(400));
42-
const requestBody = (await req.json()) as UpdateStylesheetRequestModel;
49+
const requestBody = (await req.json()) as UpdateScriptRequestModel;
4350
if (!requestBody) return res(ctx.status(400, 'no body found'));
4451
umbScriptMockDb.file.update(decodeURIComponent(path), requestBody);
4552
return res(ctx.status(200));

src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { rest } = window.MockServiceWorker;
2+
import { createProblemDetails } from '../../data/utils.js';
23
import { umbStylesheetMockDb } from '../../data/stylesheet/stylesheet.db.js';
34
import { UMB_SLUG } from './slug.js';
45
import type {
@@ -14,6 +15,14 @@ export const detailHandlers = [
1415
const path = umbStylesheetMockDb.file.create(requestBody);
1516
const encodedPath = encodeURIComponent(path);
1617

18+
// Validate name
19+
if (!requestBody.name) {
20+
return res(
21+
ctx.status(400, 'name is required'),
22+
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name is required' })),
23+
);
24+
}
25+
1726
return res(
1827
ctx.status(201),
1928
ctx.set({

src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { rest } = window.MockServiceWorker;
2+
import { createProblemDetails } from '../../data/utils.js';
23
import { umbTemplateMockDb } from '../../data/template/template.db.js';
34
import { UMB_SLUG } from './slug.js';
45
import type {
@@ -12,6 +13,14 @@ export const detailHandlers = [
1213
const requestBody = (await req.json()) as CreateTemplateRequestModel;
1314
if (!requestBody) return res(ctx.status(400, 'no body found'));
1415

16+
// Validate name and alias
17+
if (!requestBody.name || !requestBody.alias) {
18+
return res(
19+
ctx.status(400, 'name and alias are required'),
20+
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name and alias are required' })),
21+
);
22+
}
23+
1524
const id = umbTemplateMockDb.detail.create(requestBody);
1625

1726
return res(
@@ -40,6 +49,15 @@ export const detailHandlers = [
4049
if (!id) return res(ctx.status(400));
4150
const requestBody = (await req.json()) as UpdateTemplateRequestModel;
4251
if (!requestBody) return res(ctx.status(400, 'no body found'));
52+
53+
// Validate name and alias
54+
if (!requestBody.name || !requestBody.alias) {
55+
return res(
56+
ctx.status(400, 'name and alias are required'),
57+
ctx.json(createProblemDetails({ title: 'Validation', detail: 'name and alias are required' })),
58+
);
59+
}
60+
4361
umbTemplateMockDb.detail.update(id, requestBody);
4462
return res(ctx.status(200));
4563
}),

src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ export interface UmbEntityModel {
44
unique: UmbEntityUnique;
55
entityType: string;
66
}
7+
8+
export interface UmbNamedEntityModel extends UmbEntityModel {
9+
name: string;
10+
}

src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1+
import type { UmbNamableWorkspaceContext } from '../../namable/namable-workspace-context.interface.js';
12
import type { UmbSubmittableWorkspaceContext } from './submittable-workspace-context.interface.js';
23
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
34
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
45
import type { UmbPropertyDatasetContext, UmbPropertyValueData } from '@umbraco-cms/backoffice/property';
56
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
67

7-
export interface UmbInvariantDatasetWorkspaceContext extends UmbSubmittableWorkspaceContext {
8-
// Name:
9-
name: Observable<string | undefined>;
10-
getName(): string | undefined;
11-
setName(name: string): void;
12-
8+
export interface UmbInvariantDatasetWorkspaceContext
9+
extends UmbSubmittableWorkspaceContext,
10+
UmbNamableWorkspaceContext {
1311
readonly values: Observable<Array<UmbPropertyValueData> | undefined>;
1412
getValues(): Promise<Array<UmbPropertyValueData> | undefined>;
1513

src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace.context-token.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export const UMB_ENTITY_DETAIL_WORKSPACE_CONTEXT = new UmbContextToken<
88
>(
99
'UmbWorkspaceContext',
1010
undefined,
11-
(context): context is UmbEntityDetailWorkspaceContextBase => (context as any).IS_ENTITY_DETAIL_WORKSPACE_CONTEXT,
11+
(context): context is UmbEntityDetailWorkspaceContextBase =>
12+
(context as UmbEntityDetailWorkspaceContextBase).IS_ENTITY_DETAIL_WORKSPACE_CONTEXT,
1213
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { UmbNamableWorkspaceContext } from '../types.js';
2+
import { UmbEntityDetailWorkspaceContextBase } from './entity-detail-workspace-base.js';
3+
import type { UmbEntityDetailWorkspaceContextCreateArgs } from './types.js';
4+
import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity';
5+
import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
6+
7+
export abstract class UmbEntityNamedDetailWorkspaceContextBase<
8+
NamedDetailModelType extends UmbNamedEntityModel = UmbNamedEntityModel,
9+
NamedDetailRepositoryType extends
10+
UmbDetailRepository<NamedDetailModelType> = UmbDetailRepository<NamedDetailModelType>,
11+
CreateArgsType extends
12+
UmbEntityDetailWorkspaceContextCreateArgs<NamedDetailModelType> = UmbEntityDetailWorkspaceContextCreateArgs<NamedDetailModelType>,
13+
>
14+
extends UmbEntityDetailWorkspaceContextBase<NamedDetailModelType, NamedDetailRepositoryType, CreateArgsType>
15+
implements UmbNamableWorkspaceContext
16+
{
17+
// Just for context token safety:
18+
public readonly IS_ENTITY_NAMED_DETAIL_WORKSPACE_CONTEXT = true;
19+
20+
readonly name = this._data.createObservablePartOfCurrent((data) => data?.name);
21+
22+
getName() {
23+
return this._data.getCurrent()?.name;
24+
}
25+
26+
setName(name: string | undefined) {
27+
// We have to cast to Partial because TypeScript doesn't understand that the model has a name property due to generic sub-types
28+
this._data.updateCurrent({ name } as Partial<NamedDetailModelType>);
29+
}
30+
}

0 commit comments

Comments
 (0)