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
This commit is contained in:
Niels Lyngsø
2025-01-14 15:52:54 +01:00
committed by GitHub
parent 482af686a0
commit baecd565cc
9 changed files with 114 additions and 44 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -97,24 +97,19 @@ export abstract class UmbBlockEntriesContext<
settings: UmbBlockDataModel | undefined,
originData: BlockOriginData,
): Promise<boolean>;
//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
}

View File

@@ -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) {

View File

@@ -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<UmbValidationMessage>([], (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<UmbValidationMessage> {
//path = path.toLowerCase();
return this.#messages.getValue().filter((x) => MatchPathOrDescendantPath(x.path, path));
}
messagesOfPathAndDescendant(path: string): Observable<Array<UmbValidationMessage>> {
//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<Array<UmbValidationMessage>> {
@@ -43,14 +55,7 @@ export class UmbValidationMessagesManager {
hasMessagesOfPathAndDescendant(path: string): Observable<boolean> {
//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<string>): 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();

View File

@@ -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;

View File

@@ -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<string, string>} 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<string, string> {
// Object to hold property-value pairs
const propsMap: Record<string, string> = {};
let match;
// Iterate over all matches
while ((match = propValueRegex.exec(query)) !== null) {
propsMap[match[1]] = match[2];
}
return propsMap;
}

View File

@@ -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');
});
});

View File

@@ -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';