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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user