From ea31e1dfb9a98fabae0e960f75070972d97d6530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2024 21:41:30 +0100 Subject: [PATCH] refactor to validation system --- .../core/form/context/form.context.ts | 6 -- ...property-editor-ui-number-range.element.ts | 13 ++- .../property/property/property.element.ts | 17 ++-- .../validation/context/validation.context.ts | 57 ++++++++++++- .../validation/events/validation.event.ts | 2 +- .../src/packages/core/validation/index.ts | 3 +- .../core/validation/interfaces/index.ts | 2 +- .../validation-manager.interface.ts | 4 - .../interfaces/validator.interface.ts | 22 +++++ .../validation/mixins/form-control.mixin.ts | 35 ++++---- .../validation/mixins/validity-state.class.ts | 22 ++--- .../validators/form-control.validator.ts | 71 ++++++++++++++++ .../core/validation/validators/index.ts | 1 + .../saveable-workspace-context-base.ts | 84 ++++++++----------- .../editable/editable-workspace.element.ts | 18 +--- .../workspace/data-type-workspace.context.ts | 23 ++--- ...installed-packages-section-view.element.ts | 4 +- 17 files changed, 258 insertions(+), 126 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validation-manager.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validator.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/form-control.validator.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/form/context/form.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/form/context/form.context.ts index f41dcb7089..b5b0a74246 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/form/context/form.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/form/context/form.context.ts @@ -13,7 +13,6 @@ export class UmbFormContext extends UmbContextBase { constructor(host: UmbControllerHost) { super(host, UMB_FORM_CONTEXT); - console.log('providing it self as', UMB_FORM_CONTEXT, this); } /** @@ -21,7 +20,6 @@ export class UmbFormContext extends UmbContextBase { * @param element {HTMLFormElement | null} - The Form element to be used for this context. */ setFormElement(element: HTMLFormElement | null) { - console.log('setFormElement', element, this); if (this.#formElement === element) return; if (this.#formElement) { this.#formElement.removeEventListener('submit', this.onSubmit); @@ -55,16 +53,12 @@ export class UmbFormContext extends UmbContextBase { * @description Triggered by the form, when it fires a submit event */ onSubmit = (event: SubmitEvent) => { - console.log('onSubmit', event); event?.preventDefault(); //this.dispatchEvent(new CustomEvent('submit-requested')); // Check client validation: const isClientValid = this.#formElement?.checkValidity(); - console.log('isClientValid', isClientValid); - // ask validation managers to validate the form. - const isValid = isClientValid ?? false; if (!isValid) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/number-range/property-editor-ui-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/number-range/property-editor-ui-number-range.element.ts index 4501a25d0c..8fb861168f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/number-range/property-editor-ui-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/number-range/property-editor-ui-number-range.element.ts @@ -1,17 +1,21 @@ import type { UmbInputNumberRangeElement } from '../../../components/input-number-range/input-number-range.element.js'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; import '../../../components/input-number-range/input-number-range.element.js'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-number-range */ @customElement('umb-property-editor-ui-number-range') -export class UmbPropertyEditorUINumberRangeElement extends UmbLitElement implements UmbPropertyEditorUiElement { +export class UmbPropertyEditorUINumberRangeElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ @property({ type: Object }) public set value(value: NumberRangeValueType | undefined) { this._value = value || { min: undefined, max: undefined }; @@ -39,6 +43,11 @@ export class UmbPropertyEditorUINumberRangeElement extends UmbLitElement impleme @state() _maxValue?: number; + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-number-range')!); + } + render() { return html`; #configObserver?: UmbObserverController; @@ -185,15 +187,20 @@ export class UmbPropertyElement extends UmbLitElement { // No need for a controller alias, as the clean is handled via the observer prop: this.#valueObserver = this.observe(this.#propertyContext.value, (value) => { //this._value = value;// This was not used currently [NL] - if (this._element) { - this._element.value = value; - } + this._element!.value = value; }); this.#configObserver = this.observe(this.#propertyContext.config, (config) => { - if (this._element && config) { - this._element.config = config; + if (config) { + this._element!.config = config; } }); + + if (this.#validator) { + this.#validator.destroy(); + } + if ('checkValidity' in this._element) { + this.#validator = new UmbFormControlValidator(this, this._element as any); + } } this.requestUpdate('element', oldElement); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts index e5d1eaeaca..540278fcbc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation.context.ts @@ -1,9 +1,64 @@ +import type { UmbValidator } from '../interfaces/validator.interface.js'; import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbValidationContext extends UmbContextBase { +export class UmbValidationContext extends UmbContextBase implements UmbValidator { + #validators: Array = []; + #isValid: boolean = false; + constructor(host: UmbControllerHost) { super(host, UMB_VALIDATION_CONTEXT); } + + get isValid(): boolean { + return this.#isValid; + } + + addValidator(validator: UmbValidator) { + this.#validators.push(validator); + //validator.addEventListener('change', this.#runValidate); + } + removeValidator(validator: UmbValidator) { + const index = this.#validators.indexOf(validator); + if (index !== -1) { + this.#validators.splice(index, 1); + //validator.removeEventListener('change', this.#runValidate); + } + } + + #runValidate = this.validate.bind(this); + + async validate(): Promise { + const results = await Promise.all(this.#validators.map((v) => v.validate())); + console.log('validators: ', this.#validators); + const isValid = results.every((r) => r); + this.#isValid = isValid; + + console.log('Context says valid: ', isValid); + + return isValid; + } + + getMessages(): string[] { + return this.#validators.reduce((acc, v) => acc.concat(v.getMessages()), [] as string[]); + } + + reset(): void { + this.#validators.forEach((v) => v.reset()); + } + + #destroyValidators() { + if (this.#validators === undefined || this.#validators.length === 0) return; + this.#validators.forEach((validator) => { + validator.destroy(); + //validator.removeEventListener('change', this.#runValidate); + }); + this.#validators = []; + } + + destroy(): void { + this.#destroyValidators(); + super.destroy(); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts index 62d03f34da..5b4948bf5f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/events/validation.event.ts @@ -1,5 +1,5 @@ export class UmbValidationEvent extends Event { public constructor(type: string) { - super(type, { bubbles: true, composed: true, cancelable: false }); + super(type, { bubbles: true, composed: false, cancelable: false }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts index a10074f553..588b35f52c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/index.ts @@ -1,4 +1,5 @@ +export * from './context/index.js'; export * from './events/index.js'; export * from './interfaces/index.js'; export * from './mixins/index.js'; -export * from './context/index.js'; +export * from './validators/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/index.ts index 76b00c66bc..f6545bdce6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/index.ts @@ -1 +1 @@ -export type * from './validation-manager.interface.js'; +export type * from './validator.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validation-manager.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validation-manager.interface.ts deleted file mode 100644 index 0bb167c36b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validation-manager.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface UmbValidationManager { - validate(): Promise; - // reset? -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validator.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validator.interface.ts new file mode 100644 index 0000000000..2f129b0337 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/interfaces/validator.interface.ts @@ -0,0 +1,22 @@ +export interface UmbValidator { + /** + * Validate the form, will return a promise that resolves to true if what the Validator represents is valid. + */ + validate(): Promise; + + /** + * Reset the validator to its initial state. + */ + reset(): void; + + /** + * Returns true if the validator is valid. + * This might represent last known state and might first be updated when validate() is called. + */ + isValid: boolean; + + //getMessage(): string; + getMessages(): string[]; // Should we enable bringing multiple messages? + + destroy(): void; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts index dd9368a7f8..6199632971 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts @@ -1,10 +1,10 @@ import { UmbValidationInvalidEvent } from '../events/validation-invalid.event.js'; import { UmbValidationValidEvent } from '../events/validation-valid.event.js'; -import { UmbValidityState } from './validity-state.class.js'; import { property, type LitElement } from '@umbraco-cms/backoffice/external/lit'; import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api'; -type NativeFormControlElement = HTMLInputElement | HTMLTextAreaElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement +type NativeFormControlElement = Pick & + HTMLElement; // Eventually use a specific interface or list multiple options like appending these types: ... | HTMLTextAreaElement | HTMLSelectElement /* FlagTypes type options originate from: * https://developer.mozilla.org/en-US/docs/Web/API/ValidityState @@ -31,7 +31,8 @@ interface Validator { } export interface UmbFormControlMixinInterface extends HTMLElement { - formAssociated: boolean; + //formAssociated: boolean; + getFormElement(): HTMLElement | undefined; get value(): ValueType | DefaultValueType; set value(newValue: ValueType | DefaultValueType); formResetCallback(): void; @@ -53,11 +54,11 @@ export declare abstract class UmbFormControlMixinElement string, checkMethod: () => boolean) => void; protected addFormControlElement(element: NativeFormControlElement): void; - formAssociated: boolean; + //formAssociated: boolean; + abstract getFormElement(): HTMLElement | undefined; get value(): ValueType | DefaultValueType; set value(newValue: ValueType | DefaultValueType); formResetCallback(): void; @@ -113,7 +114,8 @@ export const UmbFormControlMixin = < } // Validation - private _validityState = new UmbValidityState(); + //private _validityState = new UmbValidityState(); + #validity: any = {}; /** * Determines wether the form control has been touched or interacted with, this determines wether the validation-status of this form control should be made visible. @@ -147,7 +149,9 @@ export const UmbFormControlMixin = < * @method getFormElement * @returns {HTMLElement | undefined} */ - protected abstract getFormElement(): HTMLElement | undefined; + getFormElement(): HTMLElement | undefined { + return this.#formCtrlElements.find((el) => el.validity.valid === false); + } disconnectedCallback(): void { super.disconnectedCallback(); @@ -214,15 +218,16 @@ export const UmbFormControlMixin = < * Such are mainly properties that are not declared as a Lit state and or Lit property. */ protected _runValidators() { - this._validityState = new UmbValidityState(); + //this._validityState = new UmbValidityState(); + this.#validity = {}; // Loop through inner native form controls to adapt their validityState. this.#formCtrlElements.forEach((formCtrlEl) => { let key: keyof ValidityState; for (key in formCtrlEl.validity) { if (key !== 'valid' && formCtrlEl.validity[key]) { - this._validityState[key] = true; - this._internals.setValidity(this._validityState, formCtrlEl.validationMessage, formCtrlEl); + this.#validity[key] = true; + this._internals.setValidity(this.#validity, formCtrlEl.validationMessage, formCtrlEl); } } }); @@ -230,15 +235,15 @@ export const UmbFormControlMixin = < // Loop through custom validators, currently its intentional to have them overwritten native validity. but might need to be reconsidered (This current way enables to overwrite with custom messages) [NL] this.#validators.forEach((validator) => { if (validator.checkMethod()) { - this._validityState[validator.flagKey] = true; - this._internals.setValidity(this._validityState, validator.getMessageMethod(), this.getFormElement()); + this.#validity[validator.flagKey] = true; + this._internals.setValidity(this.#validity, validator.getMessageMethod(), this.getFormElement()); } }); - const hasError = Object.values(this._validityState).includes(true); + const hasError = Object.values(this.#validity).includes(true); // https://developer.mozilla.org/en-US/docs/Web/API/ValidityState#valid - this._validityState.valid = !hasError; + this.#validity.valid = !hasError; if (hasError) { this.dispatchEvent(new UmbValidationInvalidEvent()); @@ -292,7 +297,7 @@ export const UmbFormControlMixin = < // https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/validity public get validity(): ValidityState { - return this._validityState; + return this.#validity; } get validationMessage() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts index d62b087a0a..49058b041f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/validity-state.class.ts @@ -1,15 +1,15 @@ type Writeable = { -readonly [P in keyof T]: T[P] }; export class UmbValidityState implements Writeable { - badInput: boolean = false; - customError: boolean = false; - patternMismatch: boolean = false; - rangeOverflow: boolean = false; - rangeUnderflow: boolean = false; - stepMismatch: boolean = false; - tooLong: boolean = false; - tooShort: boolean = false; - typeMismatch: boolean = false; - valid: boolean = false; - valueMissing: boolean = false; + badInput: boolean = true; + customError: boolean = true; + patternMismatch: boolean = true; + rangeOverflow: boolean = true; + rangeUnderflow: boolean = true; + stepMismatch: boolean = true; + tooLong: boolean = true; + tooShort: boolean = true; + typeMismatch: boolean = true; + valid: boolean = true; + valueMissing: boolean = true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/form-control.validator.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/form-control.validator.ts new file mode 100644 index 0000000000..b3a793314c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/form-control.validator.ts @@ -0,0 +1,71 @@ +import type { UmbValidator } from '../interfaces/index.js'; +import { UMB_VALIDATION_CONTEXT } from '../context/validation.context-token.js'; +import type { UmbFormControlMixinInterface } from '../mixins/form-control.mixin.js'; +import { UmbValidationInvalidEvent, UmbValidationValidEvent } from '../index.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbFormControlValidator extends UmbControllerBase implements UmbValidator { + #context?: typeof UMB_VALIDATION_CONTEXT.TYPE; + + #control: UmbFormControlMixinInterface; + readonly controllerAlias: UmbControllerAlias; + + #isValid = false; + + constructor(host: UmbControllerHost, formControl: UmbFormControlMixinInterface) { + super(host); + this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => { + if (this.#context) { + this.#context.removeValidator(this); + } + this.#context = context; + context.addValidator(this); + }); + this.#control = formControl; + this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid); + this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid); + } + + get isValid(): boolean { + return this.#isValid; + } + #setIsValid(newVal: boolean) { + if (this.#isValid === newVal) return; + this.#isValid = newVal; + this.dispatchEvent(new CustomEvent('change')); + } + + #setInvalid = this.#setIsValid.bind(this, false); + #setValid = this.#setIsValid.bind(this, true); + + validate(): Promise { + //this.#validate(); + return Promise.resolve(this.#isValid); + } + + reset(): void { + this.#isValid = false; + } + + getMessages(): string[] { + return [this.#control.validationMessage]; + } + + focus(): void { + this.#control.getFormElement()?.focus(); + } + + destroy(): void { + if (this.#context) { + this.#context.removeValidator(this); + this.#context = undefined; + } + if (this.#control) { + this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid); + this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid); + this.#control = undefined as any; + } + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/index.ts new file mode 100644 index 0000000000..c83bb60b8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/validators/index.ts @@ -0,0 +1 @@ +export * from './form-control.validator.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts index ffc8ca415f..8dca36b911 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/saveable-workspace-context-base.ts @@ -1,14 +1,13 @@ import { UmbWorkspaceRouteManager } from '../controllers/workspace-route-manager.controller.js'; -//import { UmbValidationContext } from '../../validation/context/validation.context.js'; import { UMB_WORKSPACE_CONTEXT } from './tokens/workspace.context-token.js'; import type { UmbSaveableWorkspaceContext } from './tokens/saveable-workspace-context.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_FORM_CONTEXT } from '@umbraco-cms/backoffice/form'; 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 { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; export abstract class UmbSaveableWorkspaceContextBase extends UmbContextBase> @@ -19,11 +18,11 @@ export abstract class UmbSaveableWorkspaceContextBase // 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 }>; - //readonly #validation = new UmbValidationContext(this); - #form?: typeof UMB_FORM_CONTEXT.TYPE; - #savePromise: Promise | undefined; - #saveResolve: (() => void) | undefined; - #saveReject: (() => void) | undefined; + readonly #validation = new UmbValidationContext(this); + + #submitPromise: Promise | undefined; + #submitResolve: (() => void) | undefined; + #submitReject: (() => void) | undefined; abstract readonly unique: Observable; @@ -43,23 +42,10 @@ export abstract class UmbSaveableWorkspaceContextBase constructor(host: UmbControllerHost, workspaceAlias: string) { super(host, UMB_WORKSPACE_CONTEXT.toString()); this.workspaceAlias = workspaceAlias; - this.#performSubmitBind = this.submit.bind(this); + // TODO: Consider if we can turn this consumption to submitComplete, just as a getContext. [NL] this.consumeContext(UMB_MODAL_CONTEXT, (context) => { (this.modalContext as UmbModalContext) = context; }); - console.log('about to consume form context', UMB_FORM_CONTEXT); - this.consumeContext(UMB_FORM_CONTEXT, (context) => { - console.log('consume form context', context); - if (this.#form === context) return; - if (this.#form) { - this.#form.removeEventListener('submit', this.#performSubmitBind); - this.#form.removeEventListener('invalid', this.#invalidForm); - } - this.#form = context; - this.#form.addEventListener('submit', this.#performSubmitBind); - this.#form.addEventListener('invalid', this.#invalidForm); - this._gotFormContext(context); - }); } protected resetState() { @@ -74,41 +60,43 @@ export abstract class UmbSaveableWorkspaceContextBase this.#isNew.setValue(isNew); } - requestSubmit(): Promise { - if (this.#savePromise) { - return this.#savePromise; + async requestSubmit(): Promise { + if (this.#submitPromise) { + return this.#submitPromise; } - if (!this.#form) { - throw new Error('Form context not available'); - } - this.#savePromise = new Promise((resolve, reject) => { - this.#saveResolve = resolve; - this.#saveReject = reject; + this.#submitPromise = new Promise((resolve, reject) => { + this.#submitResolve = resolve; + this.#submitReject = reject; }); - console.log('REQUEST SUBMIT', this.#form); - this.#form.requestSubmit(); + console.log('request submit'); - return this.#savePromise; + this.#validation.validate().then((isValid) => { + if (isValid) { + this.submit(); + } else { + this.#failSubmit(); + } + }); + + return this.#submitPromise; } - #invalidForm = (event: Event) => { - console.log('workspace context got invalid form', event); - if (this.#savePromise) { - this.#saveReject?.(); - this.#savePromise = undefined; - this.#saveResolve = undefined; - this.#saveReject = undefined; + #failSubmit() { + if (this.#submitPromise) { + this.#submitReject?.(); + this.#submitPromise = undefined; + this.#submitResolve = undefined; + this.#submitReject = undefined; } - }; + } protected submitComplete(data: WorkspaceDataModelType | undefined) { // Resolve the save promise: - this.#saveResolve?.(); - // TODO: We need a way to fail the save promise.. - this.#savePromise = undefined; - this.#saveResolve = undefined; - this.#saveReject = undefined; + this.#submitResolve?.(); + this.#submitPromise = undefined; + this.#submitResolve = undefined; + this.#submitReject = undefined; if (this.modalContext) { if (data) { @@ -118,14 +106,10 @@ export abstract class UmbSaveableWorkspaceContextBase } } - protected _gotFormContext(context: typeof UMB_FORM_CONTEXT.TYPE): void {} - //abstract getIsDirty(): Promise; abstract getUnique(): string | undefined; abstract getEntityType(): string; abstract getData(): WorkspaceDataModelType | undefined; - - #performSubmitBind: () => void; protected abstract submit(): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts index 57847c13cf..aece53c85e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/kinds/editable/editable-workspace.element.ts @@ -1,15 +1,12 @@ import type { UmbSaveableWorkspaceContext } from '../../contexts/tokens/saveable-workspace-context.interface.js'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { html, customElement, state, css, type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, state, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbRoute } from '@umbraco-cms/backoffice/router'; import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbFormContext } from '@umbraco-cms/backoffice/form'; @customElement('umb-editable-workspace') export class UmbEditableWorkspaceElement extends UmbLitElement { - readonly #formContext = new UmbFormContext(this); - @state() _routes: UmbRoute[] = []; @@ -19,19 +16,8 @@ export class UmbEditableWorkspaceElement extends UmbLitElement { new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [api]); } - protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - - this.#formContext.setFormElement(this.shadowRoot!.querySelector('form')); - } - render() { - return html` -
- - -
-
`; + return html` `; } static styles = [ 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 cf1f2874f5..47175f1ba2 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 @@ -15,12 +15,12 @@ import { } from '@umbraco-cms/backoffice/workspace'; import { appendToFrozenArray, + mergeObservables, UmbArrayState, UmbObjectState, UmbStringState, } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { combineLatest, map } from '@umbraco-cms/backoffice/external/rxjs'; import type { PropertyEditorSettingsDefaultData, PropertyEditorSettingsProperty, @@ -281,17 +281,18 @@ export class UmbDataTypeWorkspaceContext async propertyValueByAlias(propertyAlias: string) { await this.#getDataPromise; - return combineLatest([ - this.#currentData.asObservablePart( - (data) => data?.values?.find((x) => x.alias === propertyAlias)?.value as ReturnType, - ), - this.#defaults.asObservablePart( - (defaults) => defaults?.find((x) => x.alias === propertyAlias)?.value as ReturnType, - ), - ]).pipe( - map(([value, defaultValue]) => { + return mergeObservables( + [ + this.#currentData.asObservablePart( + (data) => data?.values?.find((x) => x.alias === propertyAlias)?.value as ReturnType, + ), + this.#defaults.asObservablePart( + (defaults) => defaults?.find((x) => x.alias === propertyAlias)?.value as ReturnType, + ), + ], + ([value, defaultValue]) => { return value ?? defaultValue; - }), + }, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view.element.ts index 58a65558ce..2b3b4dfda7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view.element.ts @@ -1,12 +1,12 @@ import { UmbPackageRepository } from '../../../package/repository/package.repository.js'; import type { UmbPackageWithMigrationStatus } from '../../../types.js'; import { html, css, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { combineLatest } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbSectionViewElement } from '@umbraco-cms/backoffice/extension-registry'; import './installed-packages-section-view-item.element.js'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-installed-packages-section-view') export class UmbInstalledPackagesSectionViewElement extends UmbLitElement implements UmbSectionViewElement { @@ -35,7 +35,7 @@ export class UmbInstalledPackagesSectionViewElement extends UmbLitElement implem const [package$, migration$] = data; - this.observe(combineLatest([package$, migration$]), ([packages, migrations]) => { + this.observe(observeMultiple([package$, migration$]), ([packages, migrations]) => { this._installedPackages = packages.map((p) => { const migration = migrations.find((m) => m.packageName === p.name); if (migration) {