Skip to content

Commit baecd56

Browse files
authored
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
1 parent 482af68 commit baecd56

File tree

9 files changed

+114
-44
lines changed

9 files changed

+114
-44
lines changed

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
3636
public get contentKey(): string | undefined {
3737
return this._contentKey;
3838
}
39-
public set contentKey(value: string | undefined) {
40-
if (!value || value === this._contentKey) return;
41-
this._contentKey = value;
42-
this._blockViewProps.contentKey = value;
43-
this.setAttribute('data-element-key', value);
44-
this.#context.setContentKey(value);
39+
public set contentKey(key: string | undefined) {
40+
if (!key || key === this._contentKey) return;
41+
this._contentKey = key;
42+
this._blockViewProps.contentKey = key;
43+
this.setAttribute('data-element-key', key);
44+
this.#context.setContentKey(key);
4545

4646
new UmbObserveValidationStateController(
4747
this,
48-
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: value })}]`,
48+
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: key })}]`,
4949
(hasMessages) => {
5050
this._contentInvalid = hasMessages;
5151
this._blockViewProps.contentInvalid = hasMessages;

src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type';
2222

2323
import '../../components/block-list-entry/index.js';
2424
import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
25-
import { UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
25+
import { ExtractJsonQueryProps, UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation';
2626
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
2727
import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs';
2828

@@ -184,6 +184,27 @@ export class UmbPropertyEditorUIBlockListElement
184184
'observePropertyAlias',
185185
);
186186

187+
// Observe Blocks and clean up validation messages for content/settings that are not in the block list anymore:
188+
this.observe(this.#managerContext.layouts, (layouts) => {
189+
const contentKeys = layouts.map((x) => x.contentKey);
190+
this.#validationContext.messages.getMessagesOfPathAndDescendant('$.contentData').forEach((message) => {
191+
// get the KEY from this string: $.contentData[?(@.key == 'KEY')]
192+
const key = ExtractJsonQueryProps(message.path).key;
193+
if (key && contentKeys.indexOf(key) === -1) {
194+
this.#validationContext.messages.removeMessageByKey(message.key);
195+
}
196+
});
197+
198+
const settingsKeys = layouts.map((x) => x.settingsKey).filter((x) => x !== undefined) as string[];
199+
this.#validationContext.messages.getMessagesOfPathAndDescendant('$.settingsData').forEach((message) => {
200+
// get the key from this string: $.settingsData[?(@.key == 'KEY')]
201+
const key = ExtractJsonQueryProps(message.path).key;
202+
if (key && settingsKeys.indexOf(key) === -1) {
203+
this.#validationContext.messages.removeMessageByKey(message.key);
204+
}
205+
});
206+
});
207+
187208
this.observe(
188209
observeMultiple([
189210
this.#managerContext.layouts,

src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entries.context.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,19 @@ export abstract class UmbBlockEntriesContext<
9797
settings: UmbBlockDataModel | undefined,
9898
originData: BlockOriginData,
9999
): Promise<boolean>;
100-
//edit?
101-
//editSettings
102100

103-
// Idea: should we return true if it was successful?
104101
public async delete(contentKey: string) {
105102
await this._retrieveManager;
106103
const layout = this._layoutEntries.value.find((x) => x.contentKey === contentKey);
107104
if (!layout) {
108105
throw new Error(`Cannot delete block, missing layout for ${contentKey}`);
109106
}
107+
this._layoutEntries.removeOne(contentKey);
110108

109+
this._manager!.removeOneContent(contentKey);
111110
if (layout.settingsKey) {
112111
this._manager!.removeOneSettings(layout.settingsKey);
113112
}
114-
this._manager!.removeOneContent(contentKey);
115113
this._manager!.removeExposesOf(contentKey);
116-
117-
this._layoutEntries.removeOne(contentKey);
118114
}
119-
//copy
120115
}

src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,18 @@ export abstract class UmbBlockManagerContext<
120120
constructor(host: UmbControllerHost) {
121121
super(host, UMB_BLOCK_MANAGER_CONTEXT);
122122

123-
this.observe(this.blockTypes, (blockTypes) => {
124-
blockTypes.forEach((x) => {
125-
this.#ensureContentType(x.contentElementTypeKey);
126-
if (x.settingsElementTypeKey) {
127-
this.#ensureContentType(x.settingsElementTypeKey);
128-
}
129-
});
130-
});
123+
this.observe(
124+
this.blockTypes,
125+
(blockTypes) => {
126+
blockTypes.forEach((x) => {
127+
this.#ensureContentType(x.contentElementTypeKey);
128+
if (x.settingsElementTypeKey) {
129+
this.#ensureContentType(x.settingsElementTypeKey);
130+
}
131+
});
132+
},
133+
null,
134+
);
131135
}
132136

133137
async #ensureContentType(unique: string) {

src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,40 @@ export interface UmbValidationMessage {
1111
body: string;
1212
}
1313

14+
/**
15+
* Matches a path or a descendant path.
16+
* @param {string} source The path to check.
17+
* @param {string} match The path to match against, the source must forfill all of the match, but the source can be further specific.
18+
* @returns {boolean} True if the path matches or is a descendant path.
19+
*/
20+
function MatchPathOrDescendantPath(source: string, match: string): boolean {
21+
// 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:
22+
return (
23+
source.indexOf(match) === 0 &&
24+
(source.length === match.length || source[match.length] === '.' || source[match.length] === '[')
25+
);
26+
}
27+
1428
export class UmbValidationMessagesManager {
1529
#messages = new UmbArrayState<UmbValidationMessage>([], (x) => x.key);
1630
messages = this.#messages.asObservable();
1731

1832
debug(logName: string) {
19-
this.#messages.asObservable().subscribe((x) => console.log(logName, x));
33+
this.messages.subscribe((x) => console.log(logName, x));
2034
}
2135

2236
getHasAnyMessages(): boolean {
2337
return this.#messages.getValue().length !== 0;
2438
}
2539

40+
getMessagesOfPathAndDescendant(path: string): Array<UmbValidationMessage> {
41+
//path = path.toLowerCase();
42+
return this.#messages.getValue().filter((x) => MatchPathOrDescendantPath(x.path, path));
43+
}
44+
2645
messagesOfPathAndDescendant(path: string): Observable<Array<UmbValidationMessage>> {
2746
//path = path.toLowerCase();
28-
// 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:
29-
return this.#messages.asObservablePart((msgs) =>
30-
msgs.filter(
31-
(x) =>
32-
x.path.indexOf(path) === 0 &&
33-
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
34-
),
35-
);
47+
return this.#messages.asObservablePart((msgs) => msgs.filter((x) => MatchPathOrDescendantPath(x.path, path)));
3648
}
3749

3850
messagesOfTypeAndPath(type: UmbValidationMessageType, path: string): Observable<Array<UmbValidationMessage>> {
@@ -43,14 +55,7 @@ export class UmbValidationMessagesManager {
4355

4456
hasMessagesOfPathAndDescendant(path: string): Observable<boolean> {
4557
//path = path.toLowerCase();
46-
return this.#messages.asObservablePart((msgs) =>
47-
// 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]
48-
msgs.some(
49-
(x) =>
50-
x.path.indexOf(path) === 0 &&
51-
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
52-
),
53-
);
58+
return this.#messages.asObservablePart((msgs) => msgs.some((x) => MatchPathOrDescendantPath(x.path, path)));
5459
}
5560
getHasMessagesOfPathAndDescendant(path: string): boolean {
5661
//path = path.toLowerCase();
@@ -90,13 +95,19 @@ export class UmbValidationMessagesManager {
9095
removeMessageByKeys(keys: Array<string>): void {
9196
this.#messages.filter((x) => keys.indexOf(x.key) === -1);
9297
}
98+
removeMessagesByType(type: UmbValidationMessageType): void {
99+
this.#messages.filter((x) => x.type !== type);
100+
}
101+
removeMessagesByPath(path: string): void {
102+
this.#messages.filter((x) => x.path !== path);
103+
}
104+
removeMessagesAndDescendantsByPath(path: string): void {
105+
this.#messages.filter((x) => MatchPathOrDescendantPath(x.path, path));
106+
}
93107
removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void {
94108
//path = path.toLowerCase();
95109
this.#messages.filter((x) => !(x.type === type && x.path === path));
96110
}
97-
removeMessagesByType(type: UmbValidationMessageType): void {
98-
this.#messages.filter((x) => x.type !== type);
99-
}
100111

101112
#translatePath(path: string): string | undefined {
102113
//path = path.toLowerCase();

src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
7676
* @param translator
7777
*/
7878
async removeTranslator(translator: UmbValidationMessageTranslator) {
79-
this.messages.removeTranslator(translator);
79+
// 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]
80+
this.messages?.removeTranslator(translator);
8081
}
8182

8283
#currentProvideHost?: UmbClassInterface;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const propValueRegex = /@\.([a-zA-Z_$][\w$]*)\s*==\s*['"]([^'"]*)['"]/g;
2+
3+
/**
4+
* Extracts properties and their values from a JSON path query.
5+
* @param {string} query - The JSON path query.
6+
* @returns {Record<string, string>} An object containing the properties and their values.
7+
* @example
8+
* ```ts
9+
* const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`;
10+
* const props = ExtractJsonQueryProps(query);
11+
* console.log(props); // { culture: 'en-us', segment: 'mySegment' }
12+
* ```
13+
*/
14+
export function ExtractJsonQueryProps(query: string): Record<string, string> {
15+
// Object to hold property-value pairs
16+
const propsMap: Record<string, string> = {};
17+
let match;
18+
19+
// Iterate over all matches
20+
while ((match = propValueRegex.exec(query)) !== null) {
21+
propsMap[match[1]] = match[2];
22+
}
23+
24+
return propsMap;
25+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect } from '@open-wc/testing';
2+
import { ExtractJsonQueryProps } from './extract-json-query-properties.function.js';
3+
4+
describe('UmbJsonPathFunctions', () => {
5+
it('retrieve property value', () => {
6+
const query = `?(@.culture == 'en-us' && @.segment == 'mySegment')`;
7+
const result = ExtractJsonQueryProps(query);
8+
9+
expect(result.culture).to.eq('en-us');
10+
expect(result.segment).to.eq('mySegment');
11+
});
12+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './data-path-property-value-query.function.js';
22
export * from './data-path-variant-query.function.js';
3+
export * from './extract-json-query-properties.function.js';
34
export * from './json-path.function.js';

0 commit comments

Comments
 (0)