-
Notifications
You must be signed in to change notification settings - Fork 3
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
[ASSB-1334] Detect Actual Changes and Display Changed Flag at field, sub field, protocol , sections #973
base: master
Are you sure you want to change the base?
Conversation
…changed flag only for genuine modifications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks good, thanks.
-
Remove debug / commented out console logs
-
Some refactorings for readability
-
It would be good to see some test cases added for hasDatabaseChanges, hasSectionChangedDeep, and hasChangedFields. There are some existing tests in https://github.com/UKHomeOffice/asl-projects/tree/master/test/specs as an example.
-
Remove debug / commented out console logs :
All unnecessary debug statements and commented-out logs have been removed. -
Some refactorings for readability :
Some refactorings have been applied as per your suggestions.
However, in certain places, explicit checks ("", 0, false, null, undefined) were necessary due to the way our data structure works. These explicit conditions ensure accuracy when handling falsy values that could otherwise be misinterpreted as unchanged. -
It would be good to see some test cases added for hasDatabaseChanges, hasSectionChangedDeep, and hasChangedFields. There are some existing tests in https://github.com/UKHomeOffice/asl-projects/tree/master/test/specs as an example.
hasDatabaseChanges - I agree with you passing values as param would be better code design
I will implement this in the second part of the task as part of the structural improvement along with test cases you have mention.
const actualCurrentValue = currentValue !== undefined && currentValue !== null | ||
? currentValue | ||
: values[name] !== undefined | ||
? values[name] | ||
: null; // Fallback to `null` if no value is provided |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is hard to read and can use the nullish coalescing operator to make it clearer
const actualCurrentValue = currentValue !== undefined && currentValue !== null | |
? currentValue | |
: values[name] !== undefined | |
? values[name] | |
: null; // Fallback to `null` if no value is provided | |
const actualCurrentValue = currentValue ?? values[name] ?? null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jeff, I initially used the nullish operator for readability, but the explicit ternary check ensures falsy values like 0, "", and false are preserved, preventing unintended fallbacks, so I had to keep it this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does ??
- it only uses the RHS if the LHS is null
or undefined
. ||
has the behaviour you're describing, but that isn't what i'm suggesting
It looks like there is a bug in eslint causing the linting to fail (https://drone-gh.acp.homeoffice.gov.uk/UKHomeOffice/asl-projects/4773/1/3). It looks like this might be fixed in more recent versions eslint/eslint#14172 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to see some test cases added for hasDatabaseChanges, hasSectionChangedDeep, and hasChangedFields. There are some existing tests in https://github.com/UKHomeOffice/asl-projects/tree/master/test/specs as an example.
I'd still like to see some unit tests for the main change detection functions.
It looks like there is a bug in eslint causing the linting to fail (https://drone-gh.acp.homeoffice.gov.uk/UKHomeOffice/asl-projects/4773/1/3). It looks like this might be fixed in more recent versions eslint/eslint#14172
The eslint issue still needs fixing
/** | ||
* Normalise a given value into a consistent string format. | ||
* | ||
* - If the value is `null` or `undefined`, it returns an empty string. | ||
* - If the value is an object, it converts it into a JSON string. | ||
* - For all other types, it converts the value into a string and trims any extra whitespace. | ||
* | ||
* @param {any} value - The input value to normalise. | ||
* @returns {string} - A normalised string representation of the input value. | ||
*/ | ||
function normaliseValue(value) { | ||
|
||
if (value === null || value === undefined) { | ||
return ""; // Return an empty string for null or undefined values. | ||
} | ||
|
||
if (typeof value === "object") { | ||
return JSON.stringify(value); // Convert objects to their JSON string representation. | ||
} | ||
|
||
return String(value).trim(); // Convert other types to string and trim whitespace. | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've only addressed the first of my points here. Please see the others.
- Clean and normalise are doing two parts of one thing, and shouldn't be separate.
- Comments don't need to be included if they are repeating trivial code - adding redundant comments makes code harder to read, and makes useful comments harder to pick out from the noise.
String(value).trim().replace(/["]+/g, "")
is equivalent toJSON.stringify(value).replace(/["]+/g, "").trim();
so we can unify the two branches.value === null || value === undefined
is equivalent tovalue == null
which is the more idiomatic way of doing that check.
/** | |
* Normalise a given value into a consistent string format. | |
* | |
* - If the value is `null` or `undefined`, it returns an empty string. | |
* - If the value is an object, it converts it into a JSON string. | |
* - For all other types, it converts the value into a string and trims any extra whitespace. | |
* | |
* @param {any} value - The input value to normalise. | |
* @returns {string} - A normalised string representation of the input value. | |
*/ | |
function normaliseValue(value) { | |
if (value === null || value === undefined) { | |
return ""; // Return an empty string for null or undefined values. | |
} | |
if (typeof value === "object") { | |
return JSON.stringify(value); // Convert objects to their JSON string representation. | |
} | |
return String(value).trim(); // Convert other types to string and trim whitespace. | |
} | |
/** | |
* Normalise a given value into a consistent string format. | |
* | |
* - If the value is `null` or `undefined`, it returns an empty string. | |
* - For other values it uses JSON stringify, removing double quotes. | |
* | |
* @param {any} value - The input value to normalise. | |
* @returns {string} - A normalised string representation of the input value. | |
*/ | |
function normaliseValue(value) { | |
if (value == null) { | |
return ""; | |
} | |
return JSON.stringify(value) | |
.replace(/["]+/g, "") | |
.trim(); | |
} |
New question: do we need to be doing some form of deep normalisation?
Say for example we have a section with an object like:
const old = {"property": "value "}; // normalises to "{property:value }"
const new = {"property": "value"}; // normalises to "{property:value}" - different
but when calculating the difference between the two values for the change badge for that field we end up comparing value
vs value
because the }
is no longer present to block the .trim()
. This then results in a section that shows a change flag, but none of the inputs in that section show as changed.
See also my comment below about merging the two normalise functions and using json-stable-stringify
instead of JSON.stringify()
.
// Special handling for array-based fields (e.g., checkboxes, multi-selects) | ||
if (Array.isArray(currentValue) || Array.isArray(initialValue)) { | ||
const currentArray = Array.isArray(currentValue) ? currentValue : []; | ||
const initialArray = Array.isArray(initialValue) ? initialValue : []; | ||
const changed = JSON.stringify(currentArray.sort()) !== JSON.stringify(initialArray.sort()); | ||
|
||
return changed; | ||
} | ||
|
||
// Special handling for boolean fields (e.g., checkboxes, radios) | ||
if (typeof currentValue === "boolean" || typeof initialValue === "boolean") { | ||
const changed = currentValue !== initialValue; | ||
|
||
return changed; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After normalisation current value and initial value are always strings so this is redundant and misleading.
// Special handling for array-based fields (e.g., checkboxes, multi-selects) | |
if (Array.isArray(currentValue) || Array.isArray(initialValue)) { | |
const currentArray = Array.isArray(currentValue) ? currentValue : []; | |
const initialArray = Array.isArray(initialValue) ? initialValue : []; | |
const changed = JSON.stringify(currentArray.sort()) !== JSON.stringify(initialArray.sort()); | |
return changed; | |
} | |
// Special handling for boolean fields (e.g., checkboxes, radios) | |
if (typeof currentValue === "boolean" || typeof initialValue === "boolean") { | |
const changed = currentValue !== initialValue; | |
return changed; | |
} |
const normaliseValue = value => { | ||
if (value === null || value === undefined || (Array.isArray(value) && value.length === 0)) { | ||
// Normalise `null`, `undefined`, and empty arrays to a consistent empty state | ||
return null; | ||
} | ||
if (Array.isArray(value)) { | ||
// Sort arrays to ensure order doesn't affect comparison | ||
return value.sort(); | ||
} | ||
if (typeof value === 'object' && value !== null) { | ||
// Ensure consistent ordering of object keys for generic objects | ||
return JSON.stringify(value, Object.keys(value).sort()); | ||
} | ||
return value; // Return all other values as-is | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is different to normaliseValue
in application-summary.js. We should probably extract both into a library method and have a consistent normalised form.
if (typeof value === 'object' && value !== null) {
// Ensure consistent ordering of object keys for generic objects
return JSON.stringify(value, Object.keys(value).sort());
}
This is a shallow normalisation, any nested objects could still have inconsistent ordering of keys.
We already depend on json-stable-stringify
via shasum
to calculate if there's a mismatch between server and client versions when applying a patch. (https://github.com/UKHomeOffice/asl-projects/blob/master/client/actions/projects.js#L325-L331) we could add this as a direct dependency and use that rather than writing our own.
const normaliseDuration = value => { | ||
if (value && typeof value === 'object' && ('years' in value || 'months' in value)) { | ||
// Normalise duration objects specifically | ||
return { | ||
years: value.years ?? 5, // Default to 5 if undefined or null | ||
months: value.months ?? 0 // Default to 0 if undefined or null | ||
}; | ||
} | ||
|
||
// Default for completely missing duration | ||
return { years: 5, months: 0 }; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const normaliseDuration = value => { | |
if (value && typeof value === 'object' && ('years' in value || 'months' in value)) { | |
// Normalise duration objects specifically | |
return { | |
years: value.years ?? 5, // Default to 5 if undefined or null | |
months: value.months ?? 0 // Default to 0 if undefined or null | |
}; | |
} | |
// Default for completely missing duration | |
return { years: 5, months: 0 }; | |
}; | |
const normaliseDuration = value => ({ | |
years: value?.years ?? 5, | |
months: value?.months ?? 0 | |
}); |
const actualCurrentValue = currentValue !== undefined && currentValue !== null | ||
? currentValue | ||
: values[name] !== undefined | ||
? values[name] | ||
: null; // Fallback to `null` if no value is provided |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does ??
- it only uses the RHS if the LHS is null
or undefined
. ||
has the behaviour you're describing, but that isn't what i'm suggesting
// Adjust storedValue for duration fields | ||
const adjustedStoredValue = | ||
name === 'duration' ? normaliseDuration(storedValue) : storedValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant because of lines 79 and 81.
// Adjust storedValue for duration fields | |
const adjustedStoredValue = | |
name === 'duration' ? normaliseDuration(storedValue) : storedValue; |
// Detect changes by comparing normalized values | ||
let hasChange = !isEqual(normalisedStoredValue, normalisedCurrentValue); | ||
|
||
|
||
// Add logic for species-based dynamic fields | ||
const species = values?.species || []; | ||
if (species.length === 0) { | ||
|
||
} else { | ||
species.forEach(speciesName => { | ||
const fieldName = `reduction-quantities-${speciesName}`; | ||
const speciesStoredValue = values.storedValue?.[fieldName] || null; | ||
const speciesCurrentValue = values[fieldName] || null; | ||
|
||
if (!isEqual(normaliseValue(speciesStoredValue), normaliseValue(speciesCurrentValue))) { | ||
|
||
// any change in dynamic fields sets hasChange to true | ||
hasChange = true; | ||
} else { | ||
|
||
} | ||
}); | ||
} | ||
|
||
return hasChange; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Detect changes by comparing normalized values | |
let hasChange = !isEqual(normalisedStoredValue, normalisedCurrentValue); | |
// Add logic for species-based dynamic fields | |
const species = values?.species || []; | |
if (species.length === 0) { | |
} else { | |
species.forEach(speciesName => { | |
const fieldName = `reduction-quantities-${speciesName}`; | |
const speciesStoredValue = values.storedValue?.[fieldName] || null; | |
const speciesCurrentValue = values[fieldName] || null; | |
if (!isEqual(normaliseValue(speciesStoredValue), normaliseValue(speciesCurrentValue))) { | |
// any change in dynamic fields sets hasChange to true | |
hasChange = true; | |
} else { | |
} | |
}); | |
} | |
return hasChange; | |
if (!isEqual(normalizedStoredValue, normalizedCurrentValue)) { | |
return true | |
} | |
const hasSpeciesChange = (speciesName) => { | |
const fieldName = `reduction-quantities-${speciesName}`; | |
const speciesStoredValue = values.storedValue?.[fieldName] || null; | |
const speciesCurrentValue = values[fieldName] || null; | |
return !isEqual(normalizeValue(speciesStoredValue), normalizeValue(speciesCurrentValue)); | |
} | |
const species = values?.species || []; | |
return species.find(species => hasSpeciesChange(species)) != null; |
Also future work: create a ticket to move field specific overrides like this to the schema for that field.
const fieldsWithoutValues = []; | ||
|
||
section.fields?.forEach((field, index) => { | ||
console.log("Processing field:", field.name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These logs are going to be very noisy in opensearch when doing server-side rendering of the page, and could potentially be logging PII which would be a security risk
console.log("Processing field:", field.name); |
fieldValue = rawValue || null; | ||
} | ||
|
||
console.log(`Field ${index + 1}: ${field.name} = ${fieldValue}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These logs are going to be very noisy in opensearch when doing server-side rendering of the page, and could potentially be logging PII which would be a security risk
|
||
console.log("Fields with values:", fieldsWithValues); | ||
console.log("Fields without values:", fieldsWithoutValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These logs are going to be very noisy in opensearch when doing server-side rendering of the page, and could potentially be logging PII which would be a security risk
This PR implements the following changes: