diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts index 7b2536ef50..d78db703ed 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/utils.ts @@ -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; +} diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts index 811f2f1a94..52fe83cd43 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/detail.handlers.ts @@ -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( @@ -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)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/rename.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/rename.handlers.ts index 61e3992f3d..8b6d10ab17 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/rename.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/partial-view/rename.handlers.ts @@ -1,7 +1,7 @@ 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 = [ @@ -9,7 +9,7 @@ export const renameHandlers = [ 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); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts index 2868cf8c14..03e6ab6c0e 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/script/detail.handlers.ts @@ -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( @@ -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)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts index 74bc982761..4d59dffd1b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/stylesheet/detail.handlers.ts @@ -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 { @@ -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({ diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts index eda0f749b1..732271025f 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/template/detail.handlers.ts @@ -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 { @@ -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( @@ -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)); }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts index dd96ef2420..f2fcb7dde9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts @@ -4,3 +4,7 @@ export interface UmbEntityModel { unique: UmbEntityUnique; entityType: string; } + +export interface UmbNamedEntityModel extends UmbEntityModel { + name: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts index 0ee14795cc..afcdfb69b9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/invariant-dataset-workspace-context.interface.ts @@ -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; - getName(): string | undefined; - setName(name: string): void; - +export interface UmbInvariantDatasetWorkspaceContext + extends UmbSubmittableWorkspaceContext, + UmbNamableWorkspaceContext { readonly values: Observable | undefined>; getValues(): Promise | undefined>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace.context-token.ts index a548bbb341..641ae9eefb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace.context-token.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts new file mode 100644 index 0000000000..25a7250eb8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace-base.ts @@ -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 = UmbDetailRepository, + CreateArgsType extends + UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs, + > + extends UmbEntityDetailWorkspaceContextBase + 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); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace.context-token.ts new file mode 100644 index 0000000000..caa79c7c5f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-named-detail-workspace.context-token.ts @@ -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, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/index.ts index b47a7be22f..b70ed5ecc8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/index.ts index 119ca06d86..2c3c5cd178 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/index.ts @@ -1,2 +1 @@ -export type * from './namable-workspace-context.interface.js'; export * from './namable-workspace.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/types.ts new file mode 100644 index 0000000000..9570219c9b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/namable/types.ts @@ -0,0 +1 @@ +export type * from './namable-workspace-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts index 82b14a3fec..76bb7815af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts @@ -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. diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index d6f481ded5..428adf4e39 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -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'; @@ -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 + extends UmbEntityNamedDetailWorkspaceContextBase 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); @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts index e290e01c33..aeff4bcdcf 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts @@ -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',