From 43c66bbd26e080c63999aedc285597f853d08c83 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 24 Sep 2024 11:48:37 +0200 Subject: [PATCH] add submittable workspace data manager --- .../src/packages/core/workspace/index.ts | 4 +- .../core/workspace/submittable/index.ts | 2 + .../submittable-workspace-context-base.ts | 164 ++++++++++++++++++ .../submittable-workspace-data-manager.ts | 107 ++++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-data-manager.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts index 0896610ff9..821b39bcee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts @@ -1,10 +1,12 @@ export * from './components/index.js'; export * from './contexts/index.js'; export * from './controllers/index.js'; +export * from './data-manager/index.js'; export * from './modals/index.js'; +export * from './paths.js'; +export * from './submittable/index.js'; export * from './workspace-property-dataset/index.js'; export * from './workspace.element.js'; -export * from './paths.js'; export type * from './conditions/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/index.ts new file mode 100644 index 0000000000..2889da8a28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/index.ts @@ -0,0 +1,2 @@ +export * from './submittable-workspace-data-manager.js'; +export * from './submittable-workspace-context-base.js'; 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 new file mode 100644 index 0000000000..2cdf2e0282 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -0,0 +1,164 @@ +import { UmbWorkspaceRouteManager } from '../controllers/workspace-route-manager.controller.js'; +import { UMB_WORKSPACE_CONTEXT } from '../contexts/tokens/workspace.context-token.js'; +import type { UmbSubmittableWorkspaceContext } from '../contexts/tokens/submittable-workspace-context.interface.js'; +import { UmbSubmittableWorkspaceDataManager } from './submittable-workspace-data-manager.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbValidationController } from '@umbraco-cms/backoffice/validation'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export abstract class UmbSubmittableWorkspaceContextBase + extends UmbContextBase> + implements UmbSubmittableWorkspaceContext +{ + protected readonly _data = new UmbSubmittableWorkspaceDataManager(this); + + public readonly workspaceAlias: string; + + // TODO: We could make a base type for workspace modal data, and use this here: As well as a base for the result, to make sure we always include the unique (instead of the object type) + public readonly modalContext?: UmbModalContext<{ preset: object }>; + + //public readonly validation = new UmbValidationContext(this); + #validationContexts: Array = []; + + /** + * Appends a validation context to the workspace. + * @param context + */ + addValidationContext(context: UmbValidationController) { + this.#validationContexts.push(context); + } + + #submitPromise: Promise | undefined; + #submitResolve: (() => void) | undefined; + #submitReject: (() => void) | undefined; + + abstract readonly unique: Observable; + + #isNew = new UmbBooleanState(undefined); + isNew = this.#isNew.asObservable(); + + readonly routes = new UmbWorkspaceRouteManager(this); + + /* + Concept notes: [NL] + Considerations are, if we bring a dirty state (observable) we need to maintain it all the time. + This might be too heavy process, so we might want to consider just having a get dirty state method. + */ + //#isDirty = new UmbBooleanState(undefined); + //isDirty = this.#isNew.asObservable(); + + constructor(host: UmbControllerHost, workspaceAlias: string) { + super(host, UMB_WORKSPACE_CONTEXT.toString()); + this.workspaceAlias = workspaceAlias; + // TODO: Consider if we can move this consumption to #resolveSubmit, just as a getContext, but it depends if others use the modalContext prop.. [NL] + this.consumeContext(UMB_MODAL_CONTEXT, (context) => { + (this.modalContext as UmbModalContext) = context; + }); + } + + protected resetState() { + //this.validation.reset(); + this.#validationContexts.forEach((context) => context.reset()); + this.#isNew.setValue(undefined); + } + + getIsNew() { + return this.#isNew.getValue(); + } + + protected setIsNew(isNew: boolean) { + this.#isNew.setValue(isNew); + } + + /** + * If a Workspace has multiple validation contexts, then this method can be overwritten to return the correct one. + * @returns Promise that resolves to void when the validation is complete. + */ + async validate(): Promise> { + //return this.validation.validate(); + return Promise.all(this.#validationContexts.map((context) => context.validate())); + } + + async requestSubmit(): Promise { + return this.validateAndSubmit( + () => this.submit(), + () => this.invalidSubmit(), + ); + } + + protected async validateAndSubmit(onValid: () => Promise, onInvalid: () => Promise): Promise { + if (this.#submitPromise) { + return this.#submitPromise; + } + this.#submitPromise = new Promise((resolve, reject) => { + this.#submitResolve = resolve; + this.#submitReject = reject; + }); + this.validate().then( + async () => { + onValid().then(this.#completeSubmit, this.#rejectSubmit); + }, + async () => { + onInvalid().then(this.#resolveSubmit, this.#rejectSubmit); + }, + ); + + return this.#submitPromise; + } + + #rejectSubmit = () => { + if (this.#submitPromise) { + // TODO: Capture the validation contexts messages on open, and then reset to them in this case. [NL] + + this.#submitReject?.(); + this.#submitPromise = undefined; + this.#submitResolve = undefined; + this.#submitReject = undefined; + } + }; + + #resolveSubmit = () => { + // Resolve the submit promise: + this.#submitResolve?.(); + this.#submitPromise = undefined; + this.#submitResolve = undefined; + this.#submitReject = undefined; + + // If we do not want to close a modal when saving something with errors, then move this part down to #completeSubmit method. [NL] + if (this.modalContext) { + this.modalContext?.setValue(this.getData()); + this.modalContext?.submit(); + } + }; + + #completeSubmit = () => { + this.#resolveSubmit(); + + // Calling reset on the validation context here. [NL] + // TODO: Capture the validation messages on open, and then reset to that. + //this.validation.reset(); + }; + + //abstract getIsDirty(): Promise; + abstract getUnique(): string | undefined; + abstract getEntityType(): string; + abstract getData(): WorkspaceDataModelType | undefined; + protected abstract submit(): Promise; + protected invalidSubmit(): Promise { + return Promise.reject(); + } +} + +/* + * @deprecated Use UmbSubmittableWorkspaceContextBase instead — Will be removed before RC. + * Rename `save` to `submit` and return a promise that resolves to true when save is complete. + * TODO: Delete before RC. + */ +export abstract class UmbEditableWorkspaceContextBase< + WorkspaceDataModelType, +> extends UmbSubmittableWorkspaceContextBase {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-data-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-data-manager.ts new file mode 100644 index 0000000000..571ed8f51f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-data-manager.ts @@ -0,0 +1,107 @@ +import type { UmbWorkspaceDataManager } from '../data-manager/workspace-data-manager.interface.js'; +import { jsonStringComparison, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbSubmittableWorkspaceDataManager + extends UmbControllerBase + implements UmbWorkspaceDataManager +{ + #persisted = new UmbObjectState(undefined); + #current = new UmbObjectState(undefined); + + public readonly current = this.#current.asObservable(); + + constructor(host: UmbControllerHost) { + super(host); + } + + /** + * Gets persisted data + * @returns {(ModelType | undefined)} + * @memberof UmbSubmittableWorkspaceDataManager + */ + getPersistedData() { + return this.#persisted.getValue(); + } + + /** + * Sets the persisted data + * @param {(ModelType | undefined)} data + * @memberof UmbSubmittableWorkspaceDataManager + */ + setPersistedData(data: ModelType | undefined) { + this.#persisted.setValue(data); + } + + /** + * Updates the persisted data + * @param {Partial} partialData + * @memberof UmbSubmittableWorkspaceDataManager + */ + updatePersistedData(partialData: Partial) { + this.#persisted.update(partialData); + } + + /** + * Gets the current data + * @returns {(ModelType | undefined)} + * @memberof UmbSubmittableWorkspaceDataManager + */ + getCurrentData() { + return this.#current.getValue(); + } + + /** + * Sets the current data + * @param {(ModelType | undefined)} data + * @memberof UmbSubmittableWorkspaceDataManager + */ + setCurrentData(data: ModelType | undefined) { + this.#current.setValue(data); + } + + /** + * Updates the current data + * @param {Partial} partialData + * @memberof UmbSubmittableWorkspaceDataManager + */ + updateCurrentData(partialData: Partial) { + this.#current.update(partialData); + } + + /** + * Checks if there are unpersisted changes + * @returns {*} + * @memberof UmbSubmittableWorkspaceDataManager + */ + hasUnpersistedChanges() { + const persisted = this.#persisted.getValue(); + const current = this.#current.getValue(); + return jsonStringComparison(persisted, current) === false; + } + + /** + * Resets the current data to the persisted data + * @memberof UmbSubmittableWorkspaceDataManager + */ + resetCurrentData() { + this.#current.setValue(this.#persisted.getValue()); + } + + /** + * Clears the data + * @memberof UmbSubmittableWorkspaceDataManager + */ + clearData() { + this.#persisted.setValue(undefined); + this.#current.setValue(undefined); + } + + override destroy() { + this.#persisted.destroy(); + this.#current.destroy(); + super.destroy(); + } +}