From 6de255f66710e35b68db5c12123a5b960c63a8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 7 May 2025 10:20:05 +0200 Subject: [PATCH] Saveable workspace interface + token (#19220) Co-authored-by: Mads Rasmussen --- .../content-detail-workspace-base.ts | 42 ++++++++------- .../workspace-action/common/index.ts | 1 + .../workspace-action/common/save/index.ts | 2 + .../common/save/save.action.ts | 52 +++++++++++++++++++ .../workspace-action/common/save/types.ts | 8 +++ .../common/submit/submit.action.ts | 5 -- .../core/workspace/contexts/tokens/index.ts | 2 + .../saveable-workspace-context.interface.ts | 5 ++ .../saveable-workspace.context-token.ts | 9 ++++ .../submittable-workspace-context-base.ts | 18 ++++--- .../workspace/actions/save.action.ts | 11 ++-- .../document-workspace.context-token.ts | 7 +-- .../workspace/document-workspace.context.ts | 8 +-- 13 files changed, 126 insertions(+), 44 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts 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 bbfa248cd6..3c257fbb1c 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 @@ -13,6 +13,7 @@ import { UmbWorkspaceSplitViewManager, type UmbEntityDetailWorkspaceContextArgs, type UmbEntityDetailWorkspaceContextCreateArgs, + type UmbSaveableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import { UmbContentTypeStructureManager, @@ -97,7 +98,9 @@ export abstract class UmbContentDetailWorkspaceContextBase< UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs, > extends UmbEntityDetailWorkspaceContextBase - implements UmbContentWorkspaceContext + implements + UmbContentWorkspaceContext, + UmbSaveableWorkspaceContext { public readonly IS_CONTENT_WORKSPACE_CONTEXT = true as const; @@ -778,6 +781,14 @@ export abstract class UmbContentDetailWorkspaceContextBase< return this._handleSubmit(); } + /** + * Request a save of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be saved. + * @returns {Promise} a promise which resolves once it has been completed. + */ + public requestSave() { + return this._handleSave(); + } + /** * Get the data to save * @param {Array} variantIds - The variant ids to save @@ -789,6 +800,10 @@ export abstract class UmbContentDetailWorkspaceContextBase< } protected async _handleSubmit() { + await this._handleSave(); + this._closeModal(); + } + protected async _handleSave() { const data = this.getData(); if (!data) { throw new Error('Data is missing'); @@ -818,8 +833,8 @@ export abstract class UmbContentDetailWorkspaceContextBase< variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; } else { - /* If there are multiple variants but no modal token is set - we will save the variants that would have been preselected in the modal. + /* If there are multiple variants but no modal token is set + we will save the variants that would have been preselected in the modal. These are based on the variants that have been edited */ variantIds = selected.map((x) => UmbVariantId.FromString(x)); } @@ -829,18 +844,13 @@ export abstract class UmbContentDetailWorkspaceContextBase< await this.runMandatoryValidationForSaveData(saveData, variantIds); if (this.#validateOnSubmit) { await this.askServerToValidate(saveData, variantIds); - return this.validateAndSubmit( - async () => { - return this.performCreateOrUpdate(variantIds, saveData); - }, - async (reason?: any) => { - if (this.#ignoreValidationResultOnSubmit) { - return this.performCreateOrUpdate(variantIds, saveData); - } else { - return this.invalidSubmit(reason); - } - }, + const valid = await this._validateAndLog().then( + () => true, + () => false, ); + if (valid || this.#ignoreValidationResultOnSubmit) { + return this.performCreateOrUpdate(variantIds, saveData); + } } else { await this.performCreateOrUpdate(variantIds, saveData); } @@ -915,8 +925,6 @@ export abstract class UmbContentDetailWorkspaceContextBase< }); eventContext.dispatchEvent(event); this.setIsNew(false); - - this._closeModal(); } async #update(variantIds: Array, saveData: DetailModelType) { @@ -966,8 +974,6 @@ export abstract class UmbContentDetailWorkspaceContextBase< }); eventContext.dispatchEvent(updatedEvent); - - this._closeModal(); } override resetState() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts index 744c315e61..44b7ca831a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts @@ -1 +1,2 @@ +export * from './save/index.js'; export * from './submit/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/index.ts new file mode 100644 index 0000000000..7c2f0d89e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/index.ts @@ -0,0 +1,2 @@ +export * from './save.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts new file mode 100644 index 0000000000..530fc13438 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts @@ -0,0 +1,52 @@ +import type { MetaWorkspaceAction } from '../../../../types.js'; +import { UMB_SAVEABLE_WORKSPACE_CONTEXT } from '../../../../contexts/tokens/index.js'; +import type { UmbSaveableWorkspaceContext } from '../../../../contexts/tokens/index.js'; +import { UmbWorkspaceActionBase } from '../../workspace-action-base.controller.js'; +import type { UmbSaveWorkspaceActionArgs } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbSaveWorkspaceAction< + ArgsMetaType extends MetaWorkspaceAction = MetaWorkspaceAction, + WorkspaceContextType extends UmbSaveableWorkspaceContext = UmbSaveableWorkspaceContext, +> extends UmbWorkspaceActionBase { + protected _retrieveWorkspaceContext: Promise; + protected _workspaceContext?: WorkspaceContextType; + + constructor(host: UmbControllerHost, args: UmbSaveWorkspaceActionArgs) { + super(host, args); + + this._retrieveWorkspaceContext = this.consumeContext( + args.workspaceContextToken ?? UMB_SAVEABLE_WORKSPACE_CONTEXT, + (context) => { + this._workspaceContext = context as WorkspaceContextType | undefined; + this.#observeUnique(); + this._gotWorkspaceContext(); + }, + ).asPromise(); + } + + #observeUnique() { + this.observe( + this._workspaceContext?.unique, + (unique) => { + // We can't save if we don't have a unique + if (unique === undefined) { + this.disable(); + } else { + // Dangerous, cause this could enable despite a class extension decided to disable it?. [NL] + this.enable(); + } + }, + 'saveWorkspaceActionUniqueObserver', + ); + } + + protected _gotWorkspaceContext() { + // Override in subclass + } + + override async execute() { + await this._retrieveWorkspaceContext; + return await this._workspaceContext?.requestSave(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/types.ts new file mode 100644 index 0000000000..2ec6ac99b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/types.ts @@ -0,0 +1,8 @@ +import type { UmbSaveableWorkspaceContext, UmbWorkspaceContext } from '../../../../contexts/index.js'; +import type { UmbWorkspaceActionArgs } from '../../types.js'; +import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export interface UmbSaveWorkspaceActionArgs + extends UmbWorkspaceActionArgs { + workspaceContextToken?: string | UmbContextToken; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/submit/submit.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/submit/submit.action.ts index 1cd6a6fefc..1b92d77816 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/submit/submit.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/submit/submit.action.ts @@ -51,8 +51,3 @@ export class UmbSubmitWorkspaceAction< return await this._workspaceContext!.requestSubmit(); } } - -/* - * @deprecated Use UmbSubmitWorkspaceAction instead - */ -export { UmbSubmitWorkspaceAction as UmbSaveWorkspaceAction }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts index 9c4fd2a4ed..4982a046ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts @@ -2,11 +2,13 @@ export * from './entity-workspace.context-token.js'; export * from './publishable-workspace.context-token.js'; export * from './routable-workspace.context-token.js'; export * from './submittable-workspace.context-token.js'; +export * from './saveable-workspace.context-token.js'; export * from './variant-workspace.context-token.js'; export type * from './entity-workspace-context.interface.js'; export type * from './invariant-dataset-workspace-context.interface.js'; export type * from './publishable-workspace-context.interface.js'; export type * from './routable-workspace-context.interface.js'; export type * from './submittable-workspace-context.interface.js'; +export type * from './saveable-workspace-context.interface.js'; export type * from './variant-dataset-workspace-context.interface.js'; export type * from '../../workspace-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts new file mode 100644 index 0000000000..ba4724c7ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace-context.interface.ts @@ -0,0 +1,5 @@ +import type { UmbSubmittableWorkspaceContext } from './submittable-workspace-context.interface.js'; + +export interface UmbSaveableWorkspaceContext extends UmbSubmittableWorkspaceContext { + requestSave(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts new file mode 100644 index 0000000000..8a1490dbeb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/saveable-workspace.context-token.ts @@ -0,0 +1,9 @@ +import type { UmbWorkspaceContext } from '../../types.js'; +import type { UmbSaveableWorkspaceContext } from './saveable-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_SAVEABLE_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbSaveableWorkspaceContext => 'requestSave' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts index be57abe1df..ec0933bb88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -84,6 +84,17 @@ export abstract class UmbSubmittableWorkspaceContextBase ); } + protected async _validateAndLog(): Promise { + await this.validate().catch(async () => { + // TODO: Implement developer-mode logging here. [NL] + console.warn( + 'Validation failed because of these validation messages still begin present: ', + this.#validationContexts.flatMap((x) => x.messages.getMessages()), + ); + return Promise.reject(); + }); + } + public async validateAndSubmit( onValid: () => Promise, onInvalid: (reason?: any) => Promise, @@ -95,16 +106,11 @@ export abstract class UmbSubmittableWorkspaceContextBase this.#submitResolve = resolve; this.#submitReject = reject; }); - this.validate().then( + this._validateAndLog().then( async () => { onValid().then(this.#completeSubmit, this.#rejectSubmit); }, async (error) => { - // TODO: Implement developer-mode logging here. [NL] - console.warn( - 'Validation failed because of these validation messages still begin present: ', - this.#validationContexts.flatMap((x) => x.messages.getMessages()), - ); onInvalid(error).then(this.#resolveSubmit, this.#rejectSubmit); }, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts index 3570f39f1e..b49f819c2f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts @@ -3,17 +3,20 @@ import type UmbDocumentWorkspaceContext from '../document-workspace.context.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { - UmbSubmitWorkspaceAction, + UmbSaveWorkspaceAction, type MetaWorkspaceAction, - type UmbSubmitWorkspaceActionArgs, + type UmbSaveWorkspaceActionArgs, type UmbWorkspaceActionDefaultKind, } from '@umbraco-cms/backoffice/workspace'; export class UmbDocumentSaveWorkspaceAction - extends UmbSubmitWorkspaceAction + extends UmbSaveWorkspaceAction implements UmbWorkspaceActionDefaultKind { - constructor(host: UmbControllerHost, args: UmbSubmitWorkspaceActionArgs) { + constructor( + host: UmbControllerHost, + args: UmbSaveWorkspaceActionArgs, + ) { super(host, { workspaceContextToken: UMB_DOCUMENT_WORKSPACE_CONTEXT, ...args }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts index 9cb87a9091..0efc3611cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context-token.ts @@ -1,12 +1,9 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import type { UmbDocumentWorkspaceContext } from './document-workspace.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; -export const UMB_DOCUMENT_WORKSPACE_CONTEXT = new UmbContextToken< - UmbSubmittableWorkspaceContext, - UmbDocumentWorkspaceContext ->( +export const UMB_DOCUMENT_WORKSPACE_CONTEXT = new UmbContextToken( 'UmbWorkspaceContext', undefined, (context): context is UmbDocumentWorkspaceContext => context.getEntityType?.() === UMB_DOCUMENT_ENTITY_TYPE, 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 56781134cd..b6b0e49287 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 @@ -341,17 +341,13 @@ export class UmbDocumentWorkspaceContext this._data.updateCurrent({ template: { unique: templateUnique } }); } - /** - * Request a submit of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be submitted. - * @returns {Promise} a promise which resolves once it has been completed. - */ - public override requestSubmit() { + protected override async _handleSave() { const elementStyle = (this.getHostElement() as HTMLElement).style; elementStyle.setProperty('--uui-color-invalid', 'var(--uui-color-warning)'); elementStyle.setProperty('--uui-color-invalid-emphasis', 'var(--uui-color-warning-emphasis)'); elementStyle.setProperty('--uui-color-invalid-standalone', 'var(--uui-color-warning-standalone)'); elementStyle.setProperty('--uui-color-invalid-contrast', 'var(--uui-color-warning-contrast)'); - return this._handleSubmit(); + await super._handleSave(); } public async saveAndPreview(): Promise {