commit 2bdf20e63fdf67bf978ab46f528f1a7ff9dcc89b
parent fa10ba63e84cdfdcc8f5023b1be53887e00529bc
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 14 May 2025 14:37:29 -0300
fix #9906
Diffstat:
5 files changed, 106 insertions(+), 28 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -36,7 +36,7 @@ import {
FormErrors,
useLocalStorage,
} from "@gnu-taler/web-util/browser";
-import { useState } from "preact/hooks";
+import { useState, useMemo } from "preact/hooks";
export interface AccountAttributes {
data: object;
formId: string | undefined;
@@ -163,39 +163,77 @@ const DECISION_REQUEST_KEY = buildStorageKey(
codecForDecisionRequest(),
);
/**
+ * This helpers is used to add support for multple calls on the
+ * same update function that update and state with partial object.
+ *
+ */
+class ConcurrentUpdateHelper<T> {
+ prevValue: T | undefined;
+ public reset() {
+ this.prevValue = undefined;
+ }
+ public mergeWithLatestOrDefault(
+ defValue: T,
+ newValue: Partial<T>,
+ ): { old: T; merged: T } {
+ const latest = this.prevValue === undefined ? defValue : this.prevValue;
+ const mergedValue = { ...latest, ...newValue };
+ this.prevValue = mergedValue;
+ return { old: latest, merged: mergedValue };
+ }
+}
+
+const mark = new ConcurrentUpdateHelper<DecisionRequest>();
+/**
* User preferences.
*
*/
export function useCurrentDecisionRequest(): [
Readonly<DecisionRequest>,
- (l:string, s: Partial<DecisionRequest>) => void,
+ (l: string, s: Partial<DecisionRequest>) => void,
(s?: Partial<DecisionRequest>) => void,
() => void,
] {
const [currentDef, setDefault] = useState(DECISION_REQUEST_EMPTY);
- const { value, update } = useLocalStorage(
+ const { value: request, update: setRequest } = useLocalStorage(
DECISION_REQUEST_KEY,
DECISION_REQUEST_EMPTY,
);
+ mark.reset();
+
function updateValue(logLabel: string, newValue: Partial<DecisionRequest>) {
- const mergedValue = { ...value, ...newValue };
- console.log("UPDATING DECISON REQUEST", {logLabel, old: value, mergedValue})
- update(mergedValue);
+ /**
+ * "request" may not be te latest, it could happen that
+ * we already call "setRequest" but that call didn't update "request" yet.
+ * The caller didn't wait for a preact re-render.
+ *
+ * So we use the "mark" to get always an up-to-date "request". In this case
+ * is important since we are doing a merge update.
+ */
+ // const old = mark.getLatestOrDefault(request)
+ // const mergedValue = { ...old, ...newValue };
+ const { old, merged } = mark.mergeWithLatestOrDefault(request, newValue);
+ console.log("UPDATING DECISON REQUEST", {
+ logLabel,
+ old,
+ merged,
+ });
+ setRequest(merged);
}
function start(d: Partial<DecisionRequest> | undefined) {
- const v = d ?? DECISION_REQUEST_EMPTY
- const newDef = {...DECISION_REQUEST_EMPTY, ...v}
+ const v = d ?? DECISION_REQUEST_EMPTY;
+ const newDef = { ...DECISION_REQUEST_EMPTY, ...v };
setDefault(newDef);
- console.log("STARTING NEW DECISION REQUEST", newDef)
- updateValue("starting",newDef);
+ console.log("STARTING NEW DECISION REQUEST", newDef);
+ updateValue("starting", newDef);
}
function reset() {
- console.log("RESETTING TO DEFAULT")
- updateValue("reseting",currentDef);
+ console.log("RESETTING TO DEFAULT");
+ updateValue("reseting", currentDef);
}
- return [value, updateValue, start, reset];
+ return [request, updateValue, start, reset];
}
diff --git a/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx b/packages/aml-backoffice-ui/src/pages/NewMeasure.tsx
@@ -37,10 +37,16 @@ export function NewMeasure({
initial,
isNew,
onCancel,
+ onAdded,
+ onChanged,
+ onRemoved,
}: {
initial?: Partial<MeasureDefinition>;
isNew?: boolean;
onCancel: () => void;
+ onAdded: (name: string) => void;
+ onChanged: (name: string) => void;
+ onRemoved: (name: string) => void;
}): VNode {
const measures = useServerMeasures();
const { i18n } = useTranslationContext();
@@ -63,6 +69,9 @@ export function NewMeasure({
summary={summary}
initial={initial}
onCancel={onCancel}
+ onAdded={onAdded}
+ onChanged={onChanged}
+ onRemoved={onRemoved}
addingNew={isNew}
/>
);
@@ -71,6 +80,9 @@ export function NewMeasure({
export function MeasureForm({
summary,
onCancel,
+ onAdded,
+ onChanged,
+ onRemoved,
initial,
addingNew,
}: {
@@ -78,6 +90,9 @@ export function MeasureForm({
addingNew?: boolean;
summary: AvailableMeasureSummary;
onCancel: () => void;
+ onAdded: (name: string) => void;
+ onChanged: (name: string) => void;
+ onRemoved: (name: string) => void;
}) {
const { i18n } = useTranslationContext();
const [request, updateRequest] = useCurrentDecisionRequest();
@@ -157,7 +172,7 @@ export function MeasureForm({
onClick={() => {
const newMeasure = form.status.result as MeasureDefinition;
const currentMeasures = { ...request.custom_measures };
- currentMeasures[name!] = {
+ currentMeasures[newMeasure.name] = {
check_name: newMeasure.check,
prog_name: newMeasure.program,
context: (newMeasure.context ?? []).reduce(
@@ -171,7 +186,9 @@ export function MeasureForm({
updateRequest("add new measure", {
custom_measures: currentMeasures,
});
- onCancel();
+ if (onAdded) {
+ onAdded(newMeasure.name);
+ }
}}
class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
>
@@ -185,7 +202,7 @@ export function MeasureForm({
const newMeasure = form.status.result as MeasureDefinition;
const CURRENT_MEASURES = { ...request.custom_measures };
- CURRENT_MEASURES[name!] = {
+ CURRENT_MEASURES[newMeasure.name] = {
check_name: newMeasure.check,
prog_name: newMeasure.program,
context: (newMeasure.context ?? []).reduce(
@@ -199,7 +216,9 @@ export function MeasureForm({
updateRequest("update measure", {
custom_measures: CURRENT_MEASURES,
});
- onCancel();
+ if (onChanged) {
+ onChanged(newMeasure.name)
+ }
}}
class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
>
@@ -212,7 +231,9 @@ export function MeasureForm({
updateRequest("remove measure", {
custom_measures: currentMeasures,
});
- onCancel();
+ if (onRemoved) {
+ onRemoved(name!)
+ }
}}
class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700 disabled:bg-gray-600"
>
@@ -544,7 +565,7 @@ function getContextValueByType(type: string, value: string) {
return value;
}
-const REGEX_NUMER = /$[0-9]*^/;
+const REGEX_NUMER = /^[0-9]*$/;
function validateContextValueByType(
i18n: InternationalizationAPI,
@@ -559,10 +580,10 @@ function validateContextValueByType(
: Number.isNaN(num)
? i18n.str`Not a number`
: !Number.isFinite(num)
- ? i18n.str`It should be finite`
- : !Number.isSafeInteger(num)
- ? i18n.str`It should be a safe integer`
- : undefined;
+ ? i18n.str`It should be finite`
+ : !Number.isSafeInteger(num)
+ ? i18n.str`It should be a safe integer`
+ : undefined;
}
if (type === "boolean") {
if (value === "true" || value === "false") return undefined;
diff --git a/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx b/packages/aml-backoffice-ui/src/pages/decision/Measures.tsx
@@ -29,6 +29,7 @@ import { MeasureDefinition, NewMeasure } from "../NewMeasure.js";
* @returns
*/
export function Measures({}: {}): VNode {
+ const [request, updateRequest] = useCurrentDecisionRequest();
const [addMeasure, setAddMeasure] = useState<{
isNew: boolean;
template: Partial<MeasureDefinition>;
@@ -39,8 +40,26 @@ export function Measures({}: {}): VNode {
onCancel={() => {
setAddMeasure(undefined);
}}
+ onChanged={() => {
+ setAddMeasure(undefined);
+ }}
initial={addMeasure.template}
isNew={addMeasure.isNew}
+ onAdded={(m) => {
+ const nm = (request.new_measures ?? []).filter((v) => v !== m);
+ nm.push(m);
+ updateRequest("added active measure", {
+ new_measures: nm,
+ });
+ setAddMeasure(undefined);
+ }}
+ onRemoved={(m) => {
+ const nm = (request.new_measures ?? []).filter((v) => v !== m);
+ updateRequest("delete active measure", {
+ new_measures: nm,
+ });
+ setAddMeasure(undefined);
+ }}
/>
);
}
@@ -91,7 +110,7 @@ function ActiveMeasureForm(): VNode {
const form = useForm<FormType>(design, initValue);
onComponentUnload(() => {
- updateRequest("unload measure",{
+ updateRequest("unload active measure", {
new_measures: (form.status.result.measures ?? []) as string[],
});
});
@@ -177,7 +196,7 @@ function ShowAllMeasures({
key,
type: "json",
value: JSON.stringify(value),
- })),
+ })),
name: m.name,
program: m.type !== "info" ? m.programName : undefined,
});
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -52,7 +52,7 @@ export function buildStorageKey<Key = string>(
}
export interface StorageState<Type = string> {
- value?: Type;
+ value: currentRequest?: Type;
update: (s: Type) => void;
reset: () => void;
}
@@ -120,7 +120,7 @@ export function useLocalStorage<Type = string>(
};
return {
- value: current,
+ value: currentRequest: current,
update: setValue,
reset: () => {
setValue(defaultValue);
diff --git a/packages/web-util/src/hooks/useMemoryStorage.ts b/packages/web-util/src/hooks/useMemoryStorage.ts
@@ -21,7 +21,7 @@
import { useEffect, useState } from "preact/hooks";
import { ObservableMap, memoryMap } from "../utils/observable.js";
-import { StorageKey, StorageState } from "./useLocalStorage.js";
+import { StorageState } from "./useLocalStorage.js";
const storage: ObservableMap<string, any> = memoryMap<any>();