diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts index 99311cb3cf..96fbced51a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts @@ -1,2 +1,4 @@ export * from './validation.context.js'; export * from './validation.context-token.js'; +export * from './server-model-validation.context.js'; +export * from './server-model-validation.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context-token.ts new file mode 100644 index 0000000000..a37d4b48c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context-token.ts @@ -0,0 +1,6 @@ +import type { UmbServerModelValidationContext } from './server-model-validation.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_SERVER_MODEL_VALIDATION_CONTEXT = new UmbContextToken( + 'UmbServerModelValidationContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts new file mode 100644 index 0000000000..d46f58e6a1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts @@ -0,0 +1,103 @@ +import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js'; +import type { UmbValidator } from '../interfaces/validator.interface.js'; +import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js'; +import { UMB_SERVER_MODEL_VALIDATION_CONTEXT } from './server-model-validation.context-token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbServerModelValidationContext + extends UmbContextBase + implements UmbValidator +{ + #validatePromise?: Promise; + #validatePromiseResolve?: (valid: boolean) => void; + #validatePromiseReject?: () => void; + + #context?: typeof UMB_VALIDATION_CONTEXT.TYPE; + #isValid = true; + + #translators: Array = []; + + // Hold server feedback... + #serverFeedback: Record> = {}; + + constructor(host: UmbControllerHost) { + super(host, UMB_SERVER_MODEL_VALIDATION_CONTEXT); + this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => { + if (this.#context) { + this.#context.removeValidator(this); + } + this.#context = context; + context.addValidator(this); + + // Run translators? + }); + } + + async askServerForValidation(requestPromise: Promise<{ data: string | undefined }>): Promise { + this.#context?.messages.removeMessagesByType('server'); + + this.#validatePromiseReject?.(); + this.#validatePromise = new Promise((resolve, reject) => { + this.#validatePromiseResolve = resolve; + this.#validatePromiseReject = reject; + }); + this.#serverFeedback = {}; + // Ask the server for validation... + const { data } = await tryExecuteAndNotify(this, requestPromise); + + console.log('VALIDATE — Got server response:'); + console.log(data); + + this.#validatePromiseResolve?.(true); + this.#validatePromiseResolve = undefined; + this.#validatePromiseReject = undefined; + } + + addTranslator(translator: UmbValidationMessageTranslator): void { + if (this.#translators.indexOf(translator) === -1) { + this.#translators.push(translator); + } + } + + removeTranslator(translator: UmbValidationMessageTranslator): void { + const index = this.#translators.indexOf(translator); + if (index !== -1) { + this.#translators.splice(index, 1); + } + } + + get isValid(): boolean { + return this.#isValid; + } + validate(): Promise { + // TODO: Return to this decision once we have a bit more implementation to perspectives against: [NL] + // If we dont have a validatePromise, we valid cause then no one has called askServerForValidation(). [NL] (I might change my mind about this one, to then say we are invalid unless we have been validated by the server... ) + return this.#validatePromise ?? Promise.resolve(true); + } + + reset(): void {} + + focusFirstInvalidElement(): void {} + + hostConnected(): void { + super.hostConnected(); + if (this.#context) { + this.#context.addValidator(this); + } + } + hostDisconnected(): void { + super.hostDisconnected(); + if (this.#context) { + this.#context.removeValidator(this); + this.#context = undefined; + } + } + + destroy(): void { + // TODO: make sure we destroy things properly: + this.#translators = []; + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts index a2ce64c5c3..ff31f3939c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts @@ -1,4 +1,3 @@ -import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; @@ -12,26 +11,12 @@ export interface UmbValidationMessage { } export class UmbValidationMessagesManager { - #translators: Array = []; #messages = new UmbArrayState([], (x) => x.key); constructor() { this.#messages.asObservable().subscribe((x) => console.log('messages:', x)); } - addTranslator(translator: UmbValidationMessageTranslator): void { - if (this.#translators.indexOf(translator) === -1) { - this.#translators.push(translator); - } - } - - removeTranslator(translator: UmbValidationMessageTranslator): void { - const index = this.#translators.indexOf(translator); - if (index !== -1) { - this.#translators.splice(index, 1); - } - } - /* serializeMessages(fromPath: string, toPath: string): void { this.#messages.setValue( 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 715348f733..9e9b667fa8 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 @@ -3,3 +3,5 @@ export * from './controllers/index.js'; export * from './events/index.js'; export * from './interfaces/index.js'; export * from './mixins/index.js'; +export * from './translators/index.js'; +export * from './utils/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/index.ts new file mode 100644 index 0000000000..75453d257d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/index.ts @@ -0,0 +1 @@ +export * from './variant-values-validation-message-translator.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/variant-values-validation-message-translator.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/variant-values-validation-message-translator.controller.ts new file mode 100644 index 0000000000..3752450e82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/variant-values-validation-message-translator.controller.ts @@ -0,0 +1,35 @@ +import type { UmbValidationMessageTranslator } from '../interfaces/validation-message-translator.interface.js'; +import { UmbDataPathValueFilter } from '../utils/data-path-value-filter.function.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; + +export class UmbVariantValuesValidationMessageTranslator + extends UmbControllerBase + implements UmbValidationMessageTranslator +{ + // + #workspace: UmbVariantDatasetWorkspaceContext; + + constructor(host: UmbControllerHost, workspaceContext: UmbVariantDatasetWorkspaceContext) { + super(host); + this.#workspace = workspaceContext; + } + + match(message: string): boolean { + //return message.startsWith('values['); + // regex match, for "values[" and then a number: + return /^values\[\d+\]/.test(message); + } + translate(message: string): string { + /* + // retrieve the number from the message values index: + const index = parseInt(message.substring(7, message.indexOf(']'))); + // + this.#workspace.getCurrentData(); + // replace the values[ number ] with values [ number + 1 ], continues by the rest of the path: + return 'values[' + UmbDataPathValueFilter() + message.substring(message.indexOf(']')); + */ + return 'not done'; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/data-path-value-filter.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/data-path-value-filter.function.ts index 2622cdf810..d3bd97119b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/data-path-value-filter.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/data-path-value-filter.function.ts @@ -3,17 +3,17 @@ import type { UmbVariantableValueModel } from '@umbraco-cms/backoffice/models'; /** * write a JSON-Path filter similar to `?(@.alias = 'myAlias' && @.culture == 'en-us' && @.segment == 'mySegment')` * where culture and segment are optional - * @param property + * @param value * @returns */ -export function UmbDataPathValueFilter(property: Partial): string { +export function UmbDataPathValueFilter(value: Omit): string { // write a array of strings for each property, where alias must be present and culture and segment are optional - const filters: Array = [`@.alias = '${property.alias}'`]; - if (property.culture) { - filters.push(`@.culture == '${property.culture}'`); + const filters: Array = [`@.alias = '${value.alias}'`]; + if (value.culture) { + filters.push(`@.culture == '${value.culture}'`); } - if (property.segment) { - filters.push(`@.segment == '${property.segment}'`); + if (value.segment) { + filters.push(`@.segment == '${value.segment}'`); } return `?(${filters.join(' && ')})`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/index.ts index e69de29bb2..74a5d85100 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/index.ts @@ -0,0 +1 @@ +export * from './data-path-value-filter.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts index b78c9d80d2..0ac05ab410 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts @@ -3,6 +3,7 @@ import { html, customElement, state, ifDefined, repeat } from '@umbraco-cms/back import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { PropertyEditorSettingsProperty } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbDataPathValueFilter } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-config @@ -43,10 +44,10 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement { ? repeat( this._properties, (property) => property.alias, - (property, index) => + (property) => // TODO: Make a helper method to generate data-path entry for a property. html` { constructor(host: UmbControllerHost) { super(host, UmbDocumentServerDataSource, UMB_DOCUMENT_DETAIL_STORE_CONTEXT); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts index e8fb1638a3..904a3b62a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts @@ -128,7 +128,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource([], (x) => x.unique); public readonly languages = this.#languages.asObservable(); + #serverValidation = new UmbServerModelValidationContext(this); + #validationRepository?: UmbDocumentValidationRepository; + public isLoaded() { return this.#getDataPromise; } @@ -473,7 +478,7 @@ export class UmbDocumentWorkspaceContext if (variantIdsToParseForValues.some((x) => x.compare(value))) { return value; } else { - // If not we will find the value in the persisted data and use that instead. + // If not, then we will find the value in the persisted data and use that instead. return persistedData?.values.find( (x) => x.alias === value.alias && x.culture === value.culture && x.segment === value.segment, ); @@ -494,9 +499,7 @@ export class UmbDocumentWorkspaceContext }; } - async #performSaveOrCreate(selectedVariants: Array): Promise { - const saveData = this.#buildSaveData(selectedVariants); - + async #performSaveOrCreate(saveData: UmbDocumentDetailModel): Promise { if (this.getIsNew()) { const parent = this.#parent.getValue(); if (!parent) throw new Error('Parent is not set'); @@ -528,13 +531,13 @@ export class UmbDocumentWorkspaceContext this.#persistedData.setValue(data); this.#currentData.setValue(data); - const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbRequestReloadStructureForEntityEvent({ unique: this.getUnique()!, entityType: this.getEntityType(), }); - actionEventContext.dispatchEvent(event); + eventContext.dispatchEvent(event); } } @@ -570,24 +573,36 @@ export class UmbDocumentWorkspaceContext variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; } + const saveData = this.#buildSaveData(variantIds); + + this.#validationRepository ??= new UmbDocumentValidationRepository(this); + + if (this.getIsNew()) { + const parent = this.#parent.getValue(); + if (!parent) throw new Error('Parent is not set'); + this.#serverValidation.askServerForValidation(this.#validationRepository.validateCreate(saveData, parent.unique)); + } else { + this.#serverValidation.askServerForValidation(this.#validationRepository.validateSave(saveData)); + } + // TODO: Only validate the specified selection.. [NL] return this.validateAndSubmit( async () => { - return this.#performSaveAndPublish(variantIds); + return this.#performSaveAndPublish(variantIds, saveData); }, async () => { // If data of the selection is not valid Then just save: - await this.#performSaveOrCreate(variantIds); + await this.#performSaveOrCreate(saveData); // Reject even thought the save was successful, but we did not publish, which is what we want to symbolize here. [NL] return await Promise.reject(); }, ); } - async #performSaveAndPublish(variantIds: Array): Promise { + async #performSaveAndPublish(variantIds: Array, saveData: UmbDocumentDetailModel): Promise { const unique = this.getUnique(); if (!unique) throw new Error('Unique is missing'); - await this.#performSaveOrCreate(variantIds); + await this.#performSaveOrCreate(saveData); await this.publishingRepository.publish( unique, @@ -624,7 +639,8 @@ export class UmbDocumentWorkspaceContext variantIds = result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; } - return await this.#performSaveOrCreate(variantIds); + const saveData = this.#buildSaveData(variantIds); + return await this.#performSaveOrCreate(saveData); } public requestSubmit() {