Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hotfix #18735 #18750

Merged
merged 18 commits into from
Mar 20, 2025
Merged
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -48,8 +48,9 @@ export class UmbContextProviderController<

public override destroy(): void {
if (this.#host) {
this.#host.removeUmbController(this);
const host = this.#host;
(this.#host as unknown) = undefined;
host.removeUmbController(this);
}
super.destroy();
}
Original file line number Diff line number Diff line change
@@ -173,23 +173,27 @@
this.observe(
this.#managerContext.layouts,
(layouts) => {
const validationMessagesToRemove: string[] = [];
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);
validationMessagesToRemove.push(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);
validationMessagesToRemove.push(message.key);
}
});

// Remove the messages after the loop to prevent changing the array while iterating over it.
this.#validationContext.messages.removeMessageByKeys(validationMessagesToRemove);

Check warning on line 196 in src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/15.3)

❌ Getting worse: Complex Method

UmbPropertyEditorUIBlockListElement.constructor already has high cyclomatic complexity, and now it increases in Lines of Code from 77 to 79. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
},
null,
);
Original file line number Diff line number Diff line change
@@ -33,6 +33,26 @@ export class UmbValidationMessagesManager {
this.messages.subscribe((x) => console.log(logName, x));
}

getMessages(): Array<UmbValidationMessage> {
return this.#messages.getValue();
}

#updateLock = 0;
initiateChange() {
this.#updateLock++;
this.#messages.mute();
// TODO: When ready enable this code will enable handling a finish automatically by this implementation 'using myState.initiatePropertyValueChange()' (Relies on TS support of Using) [NL]
/*return {
[Symbol.dispose]: this.finishPropertyValueChange,
};*/
}
finishChange() {
this.#updateLock--;
if (this.#updateLock === 0) {
this.#messages.unmute();
}
}

getHasAnyMessages(): boolean {
return this.#messages.getValue().length !== 0;
}
@@ -75,7 +95,9 @@ export class UmbValidationMessagesManager {
if (this.#messages.getValue().find((x) => x.type === type && x.path === path && x.body === body)) {
return;
}
this.initiateChange();
this.#messages.appendOne({ type, key, path, body: body });
this.finishChange();
}

addMessages(type: UmbValidationMessageType, path: string, bodies: Array<string>): void {
@@ -86,27 +108,42 @@ export class UmbValidationMessagesManager {
const newBodies = bodies.filter(
(message) => existingMessages.find((x) => x.type === type && x.path === path && x.body === message) === undefined,
);
this.initiateChange();
this.#messages.append(newBodies.map((body) => ({ type, key: UmbId.new(), path, body })));
this.finishChange();
}

removeMessageByKey(key: string): void {
this.initiateChange();
this.#messages.removeOne(key);
this.finishChange();
}
removeMessageByKeys(keys: Array<string>): void {
if (keys.length === 0) return;
this.initiateChange();
this.#messages.filter((x) => keys.indexOf(x.key) === -1);
this.finishChange();
}
removeMessagesByType(type: UmbValidationMessageType): void {
this.initiateChange();
this.#messages.filter((x) => x.type !== type);
this.finishChange();
}
removeMessagesByPath(path: string): void {
this.initiateChange();
this.#messages.filter((x) => x.path !== path);
this.finishChange();
}
removeMessagesAndDescendantsByPath(path: string): void {
this.initiateChange();
this.#messages.filter((x) => MatchPathOrDescendantPath(x.path, path));
this.finishChange();
}
removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void {
//path = path.toLowerCase();
this.initiateChange();
this.#messages.filter((x) => !(x.type === type && x.path === path));
this.finishChange();
}

#translatePath(path: string): string | undefined {
@@ -124,6 +161,7 @@ export class UmbValidationMessagesManager {

#translators: Array<UmbValidationMessageTranslator> = [];
addTranslator(translator: UmbValidationMessageTranslator): void {
this.initiateChange();
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
@@ -137,6 +175,7 @@ export class UmbValidationMessagesManager {
this.#messages.updateOne(msg.key, { path: newPath });
}
}
this.finishChange();
}

removeTranslator(translator: UmbValidationMessageTranslator): void {
Original file line number Diff line number Diff line change
@@ -35,8 +35,6 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
}
});
this.#control = formControl;
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
}

get isValid(): boolean {
@@ -82,12 +80,18 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal

override hostConnected(): void {
super.hostConnected();
this.#control.addEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.addEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
if (this.#context) {
this.#context.addValidator(this);
}
}
override hostDisconnected(): void {
super.hostDisconnected();
if (this.#control) {
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
}
if (this.#context) {
this.#context.removeValidator(this);
// Remove any messages that this validator has added:
@@ -99,11 +103,9 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal
}

override destroy(): void {
super.destroy();
if (this.#control) {
this.#control.removeEventListener(UmbValidationInvalidEvent.TYPE, this.#setInvalid);
this.#control.removeEventListener(UmbValidationValidEvent.TYPE, this.#setValid);
this.#control = undefined as any;
}
super.destroy();
}
}
Original file line number Diff line number Diff line change
@@ -122,11 +122,12 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
this.observe(
parent.messages.messagesOfPathAndDescendant(dataPath),
(msgs) => {
this.messages.initiateChange();
//this.messages.appendMessages(msgs);
if (this.#parentMessages) {
// Remove the local messages that does not exist in the parent anymore:
const toRemove = this.#parentMessages.filter((msg) => !msgs.find((m) => m.key === msg.key));
this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
this.messages.removeMessageByKeys(toRemove.map((msg) => msg.key));
}
this.#parentMessages = msgs;
msgs.forEach((msg) => {
@@ -139,6 +140,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
// Notice, the local message uses the same key. [NL]
this.messages.addMessage(msg.type, path, msg.body, msg.key);
});
this.messages.finishChange();
},
'observeParentMessages',
);
@@ -147,6 +149,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
this.messages.messages,
(msgs) => {
if (!this.#parent) return;

this.#parent!.messages.initiateChange();

//this.messages.appendMessages(msgs);
if (this.#localMessages) {
// Remove the parent messages that does not exist locally anymore:
@@ -165,13 +170,28 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
// Notice, the parent message uses the same key. [NL]
this.#parent!.messages.addMessage(msg.type, path, msg.body, msg.key);
});

this.#parent!.messages.finishChange();
},
'observeLocalMessages',
);
}).skipHost();
// Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL]
}

override hostConnected(): void {
super.hostConnected();
if (this.#parent) {
this.#parent.addValidator(this);
}
}
override hostDisconnected(): void {
super.hostDisconnected();
if (this.#parent) {
this.#parent.removeValidator(this);
}
}

/**
* Get if this context is valid.
* Notice this does not verify the validity.
@@ -229,18 +249,27 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
() => false,
);

if (!this.messages) {
/*if (this.#validators.length === 0 && resultsStatus === false) {
throw new Error('No validators to validate, but validation failed');
}*/

if (this.messages === undefined) {
// This Context has been destroyed while is was validating, so we should not continue.
return Promise.reject();
}

const hasMessages = this.messages.getHasAnyMessages();

// If we have any messages then we are not valid, otherwise lets check the validation results: [NL]
// This enables us to keep client validations though UI is not present anymore — because the client validations got defined as messages. [NL]
const isValid = this.messages.getHasAnyMessages() ? false : resultsStatus;
const isValid = hasMessages ? false : resultsStatus;

this.#isValid = isValid;

if (isValid === false) {
/*if (hasMessages === false && resultsStatus === false) {
throw new Error('Missing validation messages to represent why a child validation context is invalid.');
}*/
// Focus first invalid element:
this.focusFirstInvalidElement();
return Promise.reject();
@@ -278,6 +307,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal
}

override destroy(): void {
this.#providerCtrl?.destroy();
this.#providerCtrl = undefined;
if (this.#parent) {
this.#parent.removeValidator(this);
Original file line number Diff line number Diff line change
@@ -4,8 +4,13 @@
* @param {string} path - the JSON path to the value that should be found
* @returns {unknown} - the found value.
*/
export function GetValueByJsonPath(data: unknown, path: string): unknown {
export function GetValueByJsonPath<ReturnType = unknown>(data: unknown, path: string): ReturnType | undefined {
if (path === '$') return data as ReturnType;
// strip $ from the path:
if (path.startsWith('$[')) {
return _GetNextArrayEntryFromPath(data as Array<unknown>, path.slice(2));
}

const strippedPath = path.startsWith('$.') ? path.slice(2) : path;
// get value from the path:
return GetNextPropertyValueFromPath(data, strippedPath);
@@ -33,46 +38,59 @@
const value = data[key];
// if there is no rest of the path, return the value:
if (rest === undefined) return value;

// if the value is an array, get the value at the index:
if (Array.isArray(value)) {
// get the value until the next ']', the value can be anything in between the brackets:
const lookupEnd = rest.match(/\]/);
if (!lookupEnd) return undefined;
// get everything before the match:
const entryPointer = rest.slice(0, lookupEnd.index);
return _GetNextArrayEntryFromPath(value, rest);
} else {
// continue with the rest of the path:
return GetNextPropertyValueFromPath(value, rest);
}
}

// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
// get the filter from the entryPointer:
// get the filter as a function:
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
// find the index of the value that matches the filter:
const index = value.findIndex(jsFilter[0]);
// if the index is -1, return undefined:
if (index === -1) return undefined;
// get the value at the index:
const data = value[index];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
return data;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
} else {
// get the value at the index:
const indexAsNumber = parseInt(entryPointer);
if (isNaN(indexAsNumber)) return undefined;
const data = value[indexAsNumber];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= rest.length) {
return data;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(data, rest.slice(lookupEnd.index + 2)) ?? data;
/**
* @private
* @param {object} array - object to traverse for the value.
* @param {string} path - the JSON path to the value that should be found, notice without the starting '['
* @returns {unknown} - the found value.
*/
function _GetNextArrayEntryFromPath(array: Array<any>, path: string): any {
if (!array) return undefined;

// get the value until the next ']', the value can be anything in between the brackets:
const lookupEnd = path.match(/\]/);
if (!lookupEnd) return undefined;
// get everything before the match:
const entryPointer = path.slice(0, lookupEnd.index);

// check if the entryPointer is a JSON Path Filter ( starting with ?( and ending with ) ):
if (entryPointer.startsWith('?(') && entryPointer.endsWith(')')) {
// get the filter from the entryPointer:
// get the filter as a function:
const jsFilter = JsFilterFromJsonPathFilter(entryPointer);
// find the index of the value that matches the filter:
const index = array.findIndex(jsFilter[0]);
// if the index is -1, return undefined:
if (index === -1) return undefined;
// get the value at the index:
const entryData = array[index];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
return entryData;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;
} else {
// get the value at the index:
const indexAsNumber = parseInt(entryPointer);
if (isNaN(indexAsNumber)) return undefined;
const entryData = array[indexAsNumber];
// Check for safety:
if (lookupEnd.index === undefined || lookupEnd.index + 1 >= path.length) {
return entryData;
}
// continue with the rest of the path:
return GetNextPropertyValueFromPath(value, rest);
return GetNextPropertyValueFromPath(entryData, path.slice(lookupEnd.index + 2)) ?? entryData;

Check notice on line 93 in src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/json-path.function.ts

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/15.3)

✅ No longer an issue: Complex Method

GetNextPropertyValueFromPath is no longer above the threshold for cyclomatic complexity. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check notice on line 93 in src/Umbraco.Web.UI.Client/src/packages/core/validation/utils/json-path.function.ts

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/15.3)

✅ No longer an issue: Bumpy Road Ahead

GetNextPropertyValueFromPath is no longer above the threshold for logical blocks with deeply nested code. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
}
}

Loading
Loading