taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2bdf20e63fdf67bf978ab46f528f1a7ff9dcc89b
parent fa10ba63e84cdfdcc8f5023b1be53887e00529bc
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 14 May 2025 14:37:29 -0300

fix #9906

Diffstat:
Mpackages/aml-backoffice-ui/src/hooks/decision-request.ts | 64+++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mpackages/aml-backoffice-ui/src/pages/NewMeasure.tsx | 41+++++++++++++++++++++++++++++++----------
Mpackages/aml-backoffice-ui/src/pages/decision/Measures.tsx | 23+++++++++++++++++++++--
Mpackages/web-util/src/hooks/useLocalStorage.ts | 4++--
Mpackages/web-util/src/hooks/useMemoryStorage.ts | 2+-
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>();