From baecd565cc55a2e452adfa572d52e92db5f0e71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Jan 2025 15:52:54 +0100 Subject: [PATCH] Fix: 17428 (#17976) * method to extract json query properties * fix issue when validation context has been destroyed * method to remove and get validation messages * param key * do not assign a controller alias to this observation * clean up delete method * clean up validation messages * remove unused imports --- .../block-grid-entry.element.ts | 14 ++--- .../property-editor-ui-block-list.element.ts | 23 ++++++++- .../block/context/block-entries.context.ts | 9 +--- .../block/context/block-manager.context.ts | 20 +++++--- .../context/validation-messages.manager.ts | 51 +++++++++++-------- .../controllers/validation.controller.ts | 3 +- .../extract-json-query-properties.function.ts | 25 +++++++++ .../extract-json-query-properties.test.ts | 12 +++++ .../packages/core/validation/utils/index.ts | 1 + 9 files changed, 114 insertions(+), 44 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.function.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index e9bf5f7f37..654ef8ff0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -36,16 +36,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper public get contentKey(): string | undefined { return this._contentKey; } - public set contentKey(value: string | undefined) { - if (!value || value === this._contentKey) return; - this._contentKey = value; - this._blockViewProps.contentKey = value; - this.setAttribute('data-element-key', value); - this.#context.setContentKey(value); + public set contentKey(key: string | undefined) { + if (!key || key === this._contentKey) return; + this._contentKey = key; + this._blockViewProps.contentKey = key; + this.setAttribute('data-element-key', key); + this.#context.setContentKey(key); new UmbObserveValidationStateController( this, - `$.contentData[${UmbDataPathBlockElementDataQuery({ key: value })}]`, + `$.contentData[${UmbDataPathBlockElementDataQuery({ key: key })}]`, (hasMessages) => { this._contentInvalid = hasMessages; this._blockViewProps.contentInvalid = hasMessages; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index 06d3e28bd1..727501c998 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -22,7 +22,7 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; import '../../components/block-list-entry/index.js'; import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import { UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { ExtractJsonQueryProps, UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; @@ -184,6 +184,27 @@ export class UmbPropertyEditorUIBlockListElement 'observePropertyAlias', ); + // Observe Blocks and clean up validation messages for content/settings that are not in the block list anymore: + this.observe(this.#managerContext.layouts, (layouts) => { + const contentKeys = layouts.map((x) => x.contentKey); + this.#validationContext.messages.getMessagesOfPathAndDescendant('$.contentData').forEach((message) => { + // get the KEY from this string: $.contentData[?(@.key == 'KEY')] + const key = ExtractJsonQueryProps(message.path).key; + if (key && contentKeys.indexOf(key) === -1) { + this.#validationContext.messages.removeMessageByKey(message.key); + } + }); + + const settingsKeys = layouts.map((x) => x.settingsKey).filter((x) => x !== undefined) as string[]; + this.#validationContext.messages.getMessagesOfPathAndDescendant('$.settingsData').forEach((message) => { + // get the key from this string: $.settingsData[?(@.key == 'KEY')] + const key = ExtractJsonQueryProps(message.path).key; + if (key && settingsKeys.indexOf(key) === -1) { + this.#validationContext.messages.removeMessageByKey(message.key); + } + }); + }); + this.observe( observeMultiple([ this.#managerContext.layouts, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entries.context.ts index 5e7c44ae15..f1a0e198ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entries.context.ts @@ -97,24 +97,19 @@ export abstract class UmbBlockEntriesContext< settings: UmbBlockDataModel | undefined, originData: BlockOriginData, ): Promise; - //edit? - //editSettings - // Idea: should we return true if it was successful? public async delete(contentKey: string) { await this._retrieveManager; const layout = this._layoutEntries.value.find((x) => x.contentKey === contentKey); if (!layout) { throw new Error(`Cannot delete block, missing layout for ${contentKey}`); } + this._layoutEntries.removeOne(contentKey); + this._manager!.removeOneContent(contentKey); if (layout.settingsKey) { this._manager!.removeOneSettings(layout.settingsKey); } - this._manager!.removeOneContent(contentKey); this._manager!.removeExposesOf(contentKey); - - this._layoutEntries.removeOne(contentKey); } - //copy } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 0f4cd984a4..c08b9d6726 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -120,14 +120,18 @@ export abstract class UmbBlockManagerContext< constructor(host: UmbControllerHost) { super(host, UMB_BLOCK_MANAGER_CONTEXT); - this.observe(this.blockTypes, (blockTypes) => { - blockTypes.forEach((x) => { - this.#ensureContentType(x.contentElementTypeKey); - if (x.settingsElementTypeKey) { - this.#ensureContentType(x.settingsElementTypeKey); - } - }); - }); + this.observe( + this.blockTypes, + (blockTypes) => { + blockTypes.forEach((x) => { + this.#ensureContentType(x.contentElementTypeKey); + if (x.settingsElementTypeKey) { + this.#ensureContentType(x.settingsElementTypeKey); + } + }); + }, + null, + ); } async #ensureContentType(unique: string) { 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 be8e796acb..dbd64593e2 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 @@ -11,28 +11,40 @@ export interface UmbValidationMessage { body: string; } +/** + * Matches a path or a descendant path. + * @param {string} source The path to check. + * @param {string} match The path to match against, the source must forfill all of the match, but the source can be further specific. + * @returns {boolean} True if the path matches or is a descendant path. + */ +function MatchPathOrDescendantPath(source: string, match: string): boolean { + // Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. using a more performant way than Regex: + return ( + source.indexOf(match) === 0 && + (source.length === match.length || source[match.length] === '.' || source[match.length] === '[') + ); +} + export class UmbValidationMessagesManager { #messages = new UmbArrayState([], (x) => x.key); messages = this.#messages.asObservable(); debug(logName: string) { - this.#messages.asObservable().subscribe((x) => console.log(logName, x)); + this.messages.subscribe((x) => console.log(logName, x)); } getHasAnyMessages(): boolean { return this.#messages.getValue().length !== 0; } + getMessagesOfPathAndDescendant(path: string): Array { + //path = path.toLowerCase(); + return this.#messages.getValue().filter((x) => MatchPathOrDescendantPath(x.path, path)); + } + messagesOfPathAndDescendant(path: string): Observable> { //path = path.toLowerCase(); - // Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. using a more performant way than Regex: - return this.#messages.asObservablePart((msgs) => - msgs.filter( - (x) => - x.path.indexOf(path) === 0 && - (x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['), - ), - ); + return this.#messages.asObservablePart((msgs) => msgs.filter((x) => MatchPathOrDescendantPath(x.path, path))); } messagesOfTypeAndPath(type: UmbValidationMessageType, path: string): Observable> { @@ -43,14 +55,7 @@ export class UmbValidationMessagesManager { hasMessagesOfPathAndDescendant(path: string): Observable { //path = path.toLowerCase(); - return this.#messages.asObservablePart((msgs) => - // Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. Using a more performant way than Regex: [NL] - msgs.some( - (x) => - x.path.indexOf(path) === 0 && - (x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['), - ), - ); + return this.#messages.asObservablePart((msgs) => msgs.some((x) => MatchPathOrDescendantPath(x.path, path))); } getHasMessagesOfPathAndDescendant(path: string): boolean { //path = path.toLowerCase(); @@ -90,13 +95,19 @@ export class UmbValidationMessagesManager { removeMessageByKeys(keys: Array): void { this.#messages.filter((x) => keys.indexOf(x.key) === -1); } + removeMessagesByType(type: UmbValidationMessageType): void { + this.#messages.filter((x) => x.type !== type); + } + removeMessagesByPath(path: string): void { + this.#messages.filter((x) => x.path !== path); + } + removeMessagesAndDescendantsByPath(path: string): void { + this.#messages.filter((x) => MatchPathOrDescendantPath(x.path, path)); + } removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void { //path = path.toLowerCase(); this.#messages.filter((x) => !(x.type === type && x.path === path)); } - removeMessagesByType(type: UmbValidationMessageType): void { - this.#messages.filter((x) => x.type !== type); - } #translatePath(path: string): string | undefined { //path = path.toLowerCase(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts index 5ab675af49..8e7cbd3635 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts @@ -76,7 +76,8 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal * @param translator */ async removeTranslator(translator: UmbValidationMessageTranslator) { - this.messages.removeTranslator(translator); + // Because this may have been destroyed at this point. and because we do not know if a context has been destroyed, then we allow this call, but let it soft-fail if messages does not exists. [NL] + this.messages?.removeTranslator(translator); } #currentProvideHost?: UmbClassInterface; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.function.ts new file mode 100644 index 0000000000..c5f02a6f65 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.function.ts @@ -0,0 +1,25 @@ +const propValueRegex = /@\.([a-zA-Z_$][\w$]*)\s*==\s*['"]([^'"]*)['"]/g; + +/** + * Extracts properties and their values from a JSON path query. + * @param {string} query - The JSON path query. + * @returns {Record} An object containing the properties and their values. + * @example + * ```ts + * const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`; + * const props = ExtractJsonQueryProps(query); + * console.log(props); // { culture: 'en-us', segment: 'mySegment' } + * ``` + */ +export function ExtractJsonQueryProps(query: string): Record { + // Object to hold property-value pairs + const propsMap: Record = {}; + let match; + + // Iterate over all matches + while ((match = propValueRegex.exec(query)) !== null) { + propsMap[match[1]] = match[2]; + } + + return propsMap; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.test.ts new file mode 100644 index 0000000000..a7ce999c23 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/extract-json-query-properties.test.ts @@ -0,0 +1,12 @@ +import { expect } from '@open-wc/testing'; +import { ExtractJsonQueryProps } from './extract-json-query-properties.function.js'; + +describe('UmbJsonPathFunctions', () => { + it('retrieve property value', () => { + const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`; + const result = ExtractJsonQueryProps(query); + + expect(result.culture).to.eq('en-us'); + expect(result.segment).to.eq('mySegment'); + }); +}); 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 52e7d2c3c4..c30ff59612 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 @@ -1,3 +1,4 @@ export * from './data-path-property-value-query.function.js'; export * from './data-path-variant-query.function.js'; +export * from './extract-json-query-properties.function.js'; export * from './json-path.function.js';