commit a85af3425d935c8d454676a177f8fe72a150a336
parent ab605e4088c8f789bfbc62d72e3b22189b56b497
Author: Sebastian <sebasjm@gmail.com>
Date: Sun, 19 Jan 2025 15:40:38 -0300
events impl
Diffstat:
6 files changed, 286 insertions(+), 70 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx
@@ -112,15 +112,8 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
* ui props and state
*/
-export function ExchangeAmlFrame({
- children,
- officer,
-}: {
- officer?: OfficerState;
- children?: ComponentChildren;
-}): VNode {
+function useErrorReport() {
const { i18n } = useTranslationContext();
-
const [error] = useErrorBoundary();
useEffect(() => {
@@ -137,6 +130,18 @@ export function ExchangeAmlFrame({
// resetError()
}
}, [error]);
+}
+
+export function ExchangeAmlFrame({
+ children,
+ officer,
+}: {
+ officer?: OfficerState;
+ children?: ComponentChildren;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ useErrorReport();
const [preferences, updatePreferences] = usePreferences();
const settings = useUiSettingsContext();
@@ -207,7 +212,7 @@ export function ExchangeAmlFrame({
<div class="fixed z-20 w-full">
<div class="mx-auto w-4/5">
- <ToastBanner />
+ <ToastBanner debug={preferences.showDebugInfo} />
</div>
</div>
diff --git a/packages/aml-backoffice-ui/src/hooks/decision-request.ts b/packages/aml-backoffice-ui/src/hooks/decision-request.ts
@@ -165,7 +165,6 @@ export function useCurrentDecisionRequest(): [
v: DecisionRequest[T],
) {
const newValue = { ...value, [k]: v };
- console.log("===", v, k);
update(newValue);
}
return [value, updateField, update];
diff --git a/packages/aml-backoffice-ui/src/hooks/preferences.ts b/packages/aml-backoffice-ui/src/hooks/preferences.ts
@@ -18,7 +18,7 @@ import {
Codec,
TranslatedString,
buildCodecForObject,
- codecForBoolean
+ codecForBoolean,
} from "@gnu-taler/taler-util";
import {
buildStorageKey,
@@ -30,18 +30,21 @@ interface Preferences {
showDebugInfo: boolean;
allowInsecurePassword: boolean;
keepSessionAfterReload: boolean;
+ testingDialect: boolean;
}
export const codecForPreferences = (): Codec<Preferences> =>
buildCodecForObject<Preferences>()
- .property("allowInsecurePassword", (codecForBoolean()))
- .property("showDebugInfo", codecForBoolean())
- .property("keepSessionAfterReload", (codecForBoolean()))
+ .property("allowInsecurePassword", codecForBoolean())
+ .property("showDebugInfo", codecForBoolean())
+ .property("testingDialect", codecForBoolean())
+ .property("keepSessionAfterReload", codecForBoolean())
.build("Preferences");
const defaultPreferences: Preferences = {
allowInsecurePassword: false,
showDebugInfo: false,
+ testingDialect: false,
keepSessionAfterReload: false,
};
@@ -75,6 +78,7 @@ export function getAllBooleanPreferences(): Array<keyof Preferences> {
"showDebugInfo",
"allowInsecurePassword",
"keepSessionAfterReload",
+ "testingDialect",
];
}
@@ -83,8 +87,13 @@ export function getLabelForPreferences(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString {
switch (k) {
- case "showDebugInfo": return i18n.str`Show debug info`
- case "allowInsecurePassword": return i18n.str`Allow Insecure password`
- case "keepSessionAfterReload": return i18n.str`Keep session after reload`
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ case "testingDialect":
+ return i18n.str`Use testing dialect`;
+ case "allowInsecurePassword":
+ return i18n.str`Allow Insecure password`;
+ case "keepSessionAfterReload":
+ return i18n.str`Keep session after reload`;
}
}
diff --git a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
@@ -41,6 +41,7 @@ import { useAccountActiveDecision } from "../hooks/decisions.js";
import { ShowDecisionLimitInfo, ShowMeasuresToSelect } from "./CaseDetails.js";
import { useEffect, useRef } from "preact/hooks";
import { useServerMeasures } from "../hooks/account.js";
+import { usePreferences } from "../hooks/preferences.js";
export type WizardSteps =
| "rules" // define the limits
@@ -338,31 +339,31 @@ function Rules({ account }: { account?: string }): VNode {
}
type PropertiesForm = {
- defined: { [name: string]: boolean };
- custom: { [name: string]: boolean };
+ defined: { [name: string]: string };
+ custom: { name: string; value: string }[];
};
const propertiesForm = (
i18n: InternationalizationAPI,
- fields: UIFormElementConfig[],
+ props: UIFormElementConfig[],
): FormDesign<PropertiesForm> => ({
type: "double-column",
sections: [
{
title: i18n.str`Properties`,
- description: i18n.str`props.`,
- fields: fields.map((f) =>
+ description: i18n.str`Default properties are defined by the server dialect`,
+ fields: props.map((f) =>
"id" in f ? { ...f, id: ("defined." + f.id) as UIHandlerId } : f,
),
},
{
title: i18n.str`Custom properties`,
- description: i18n.str`add properties not listed above`,
+ description: i18n.str`Add more properties that not listed above.`,
fields: [
{
id: "custom" as UIHandlerId,
type: "array",
- label: i18n.str`Fields`,
+ label: i18n.str`New properties`,
labelFieldId: "name" as UIHandlerId,
fields: [
{
@@ -382,7 +383,7 @@ const propertiesForm = (
],
});
-function fieldsByDialect(
+function propertiesByDialect(
i18n: InternationalizationAPI,
dialect: string | undefined,
): UIFormElementConfig[] {
@@ -467,21 +468,40 @@ function fieldsByDialect(
*/
function Properties({}: {}): VNode {
const { i18n } = useTranslationContext();
- const [request, updateRequest] = useCurrentDecisionRequest();
+ const [request, _, updateRequest] = useCurrentDecisionRequest();
const { config } = useExchangeApiContext();
+ const [pref] = usePreferences();
const design = propertiesForm(
i18n,
- fieldsByDialect(i18n, config.config.aml_spa_dialect),
+ propertiesByDialect(
+ i18n,
+ pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
+ ),
);
const form = useForm<PropertiesForm>(design, {
defined: request.properties,
- custom: request.custom_properties,
+ custom: Object.entries(request.custom_properties ?? {}).map(
+ ([name, value]) => {
+ return { name, value };
+ },
+ ),
});
onComponentUnload(() => {
- updateRequest("properties", form.status.result.defined);
- updateRequest("custom_properties", form.status.result.custom);
+ updateRequest({
+ ...request,
+ properties: form.status.result.defined ?? {},
+ custom_properties: (form.status.result.custom ?? []).reduce(
+ (prev, cur) => {
+ if (!cur || !cur.name || !cur.value) return prev;
+ console.log(cur);
+ prev[cur.name] = cur.value;
+ return prev;
+ },
+ {} as Record<string, string>,
+ ),
+ });
});
return (
@@ -492,28 +512,136 @@ function Properties({}: {}): VNode {
}
type EventsForm = {
- trigger: { [name: string]: boolean };
+ trigger: { name: string }[];
inhibit: { [name: string]: boolean };
};
const eventsForm = (
i18n: InternationalizationAPI,
- props: string[],
+ defaultEvents: UIFormElementConfig[],
): FormDesign<MeasureForm> => ({
type: "double-column",
sections: [
{
- title: i18n.str`Custom events`,
- description: i18n.str`This events will be triggered by default unless you marked to skip it.`,
- fields: [],
+ title: i18n.str`Inhibit default events`,
+ description: i18n.str`Use this form to prevent events to be triggered by the current status.`,
+ fields: !defaultEvents.length
+ ? [
+ {
+ type: "caption",
+ label: i18n.str`No default events calculated.`,
+ },
+ ]
+ : defaultEvents.map((f) =>
+ "id" in f ? { ...f, id: ("inhibit." + f.id) as UIHandlerId } : f,
+ ),
},
{
- title: i18n.str`Inhibit`,
- description: i18n.str`This events will be triggered by default unless you marked to skip it.`,
- fields: [],
+ title: i18n.str`Custom event`,
+ description: i18n.str`Add more events to be triggered by this request.`,
+ fields: [
+ {
+ id: "trigger" as UIHandlerId,
+ type: "array",
+ label: i18n.str`Event list`,
+ labelFieldId: "name" as UIHandlerId,
+ fields: [
+ {
+ type: "text",
+ label: i18n.str`Name`,
+ id: "name" as UIHandlerId,
+ },
+ ],
+ },
+ ],
},
],
+ // fields: [
+ // {
+ // id: "trigger" as UIHandlerId,
+ // type: "array",
+ // labelFieldId: "name" as UIHandlerId,
+ // label: i18n.str`Trigger`,
+ // fields: [],
+ // },
+ // {
+ // id: "inhibit" as UIHandlerId,
+ // type: "array",
+ // labelFieldId: "name" as UIHandlerId,
+ // label: i18n.str`Inhibit`,
+ // fields: [],
+ // },
+ // ],
});
+
+function eventsByDialect(
+ i18n: InternationalizationAPI,
+ dialect: string | undefined,
+ properties: object,
+): UIFormElementConfig[] {
+ if (!dialect) return [];
+ const result: UIFormElementConfig[] = [];
+ switch (dialect) {
+ case "testing": {
+ const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+ if (props.ACCOUNT_FROZEN) {
+ result.push({
+ id: "ACCOUNT_FROZEN" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+ label: i18n.str`Is froozen?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ });
+ }
+ if (props.ACCOUNT_SANCTIONED) {
+ result.push({
+ id: "ACCOUNT_SANCTIONED" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+ label: i18n.str`Is sacntioned?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ });
+ }
+ if (props.ACCOUNT_HIGH_RISK) {
+ result.push({
+ id: "ACCOUNT_HIGH_RISK" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+ label: i18n.str`Is high risk?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ });
+ }
+ break;
+ }
+ case "gls": {
+ const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+ if (props.ACCOUNT_FROZEN) {
+ result.push({
+ id: "ACCOUNT_FROZEN" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+ label: i18n.str`Is frozen?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ });
+ }
+ break;
+ }
+ case "tops": {
+ const props = properties as TalerFormAttributes.AccountProperties_TOPS;
+ if (props.ACCOUNT_HIGH_RISK) {
+ result.push({
+ id: "ACCOUNT_HIGH_RISK" satisfies keyof TalerFormAttributes.AccountProperties_TOPS as UIHandlerId,
+ label: i18n.str`Is high risk?`,
+ // gana_type: "Boolean",
+ type: "toggle",
+ required: true,
+ });
+ }
+ break;
+ }
+ }
+ return result;
+}
/**
* Trigger additional events
* @param param0
@@ -521,8 +649,57 @@ const eventsForm = (
*/
function Events({}: {}): VNode {
const { i18n } = useTranslationContext();
- const [request] = useCurrentDecisionRequest();
- return <div> not yet impltemented: events</div>;
+ const [request, _, updateRequest] = useCurrentDecisionRequest();
+ const [pref] = usePreferences();
+ const { config } = useExchangeApiContext();
+
+ const calculatedProps = {
+ ...(request.properties ?? {}),
+ ...(request.custom_properties ?? {}),
+ };
+
+ const calculatedEvents = eventsByDialect(
+ i18n,
+ pref.testingDialect ? "testing" : config.config.aml_spa_dialect,
+ calculatedProps,
+ );
+
+ const design = eventsForm(i18n, calculatedEvents);
+
+ const form = useForm<EventsForm>(design, {
+ inhibit: calculatedEvents.reduce(
+ (prev, cur) => {
+ if (cur.type !== "toggle") return prev;
+ const isInhibit =
+ request.inhibit_events !== undefined &&
+ request.inhibit_events.indexOf(cur.id) !== -1;
+ prev[cur.id] = isInhibit;
+ return prev;
+ },
+ {} as EventsForm["inhibit"],
+ ),
+ trigger: !request.custom_events
+ ? []
+ : request.custom_events.map((name) => ({ name })),
+ });
+
+ onComponentUnload(() => {
+ updateRequest({
+ ...request,
+ custom_events: !form.status.result.trigger
+ ? []
+ : form.status.result.trigger.map((t) => t?.name!),
+ inhibit_events: Object.entries(form.status.result.inhibit ?? {})
+ .filter(([key, inhibit]) => !!inhibit)
+ .map(([key]) => key),
+ });
+ });
+
+ return (
+ <div>
+ <FormUI design={design} handler={form.handler} />
+ </div>
+ );
}
type MeasureForm = {
@@ -566,7 +743,7 @@ const measureForm = (
*/
function Measures({}: {}): VNode {
const { i18n } = useTranslationContext();
- const [request, updateRequest] = useCurrentDecisionRequest();
+ const [request, _, updateRequest] = useCurrentDecisionRequest();
const measures = useServerMeasures();
const measureList =
!measures || measures instanceof TalerError || measures.type === "fail"
@@ -586,8 +763,8 @@ function Measures({}: {}): VNode {
: (form.status.result.paths.map(
(path) => path?.steps ?? [],
) as string[][]);
- updateRequest("next_measure", r);
- updateRequest("custom_measures", {});
+
+ updateRequest({ ...request, next_measure: r, custom_measures: {} });
});
return (
diff --git a/packages/web-util/src/components/ToastBanner.tsx b/packages/web-util/src/components/ToastBanner.tsx
@@ -13,49 +13,76 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Fragment, VNode, h } from "preact"
-import { Attention, GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT, Notification, useNotifications } from "../index.browser.js"
+import { Fragment, VNode, h } from "preact";
+import {
+ Attention,
+ GLOBAL_NOTIFICATION_TIMEOUT as GLOBAL_TOAST_TIMEOUT,
+ Notification,
+ useNotifications,
+} from "../index.browser.js";
+import { Duration } from "@gnu-taler/taler-util";
/**
* Toasts should be considered when displaying these types of information to the user:
- *
+ *
* Low attention messages that do not require user action
* Singular status updates
* Confirmations
* Information that does not need to be followed up
- *
+ *
* Do not use toasts if the information contains the following:
- *
+ *
* High attention and crtitical information
* Time-sensitive information
* Requires user action or input
* Batch updates
- *
- * @returns
+ *
+ * @returns
*/
-export function ToastBanner(): VNode {
- const notifs = useNotifications()
- if (notifs.length === 0) return <Fragment />
- const show = notifs.filter(e => !e.message.ack && !e.message.timeout)
- if (show.length === 0) return <Fragment />
- return <AttentionByType msg={show[0]} />
+export function ToastBanner({ debug }: { debug?: boolean }): VNode {
+ const notifs = useNotifications();
+ if (notifs.length === 0) return <Fragment />;
+ const show = notifs.filter((e) => !e.message.ack && !e.message.timeout);
+ if (show.length === 0) return <Fragment />;
+ return <AttentionByType msg={show[0]} debug={debug} />;
}
-function AttentionByType({ msg }: { msg: Notification }) {
+function AttentionByType({
+ msg,
+ debug,
+}: {
+ debug?: boolean;
+ msg: Notification;
+}) {
switch (msg.message.type) {
case "error":
- return <Attention type="danger" title={msg.message.title} onClose={() => {
- msg.acknowledge()
- }} timeout={GLOBAL_TOAST_TIMEOUT}>
- {msg.message.description &&
- <div class="mt-2 text-sm text-red-700">
- {msg.message.description}
- </div>
- }
- </Attention>
+ return (
+ <Attention
+ type="danger"
+ title={msg.message.title}
+ onClose={() => {
+ msg.acknowledge();
+ }}
+ timeout={debug ? Duration.getForever() : GLOBAL_TOAST_TIMEOUT}
+ >
+ {msg.message.description && (
+ <div class="mt-2 text-sm text-red-700">
+ {msg.message.description}
+ </div>
+ )}
+ {!debug ? undefined : <pre>{msg.message.debug}</pre>}
+ </Attention>
+ );
case "info":
- return <Attention type="success" title={msg.message.title} onClose={() => {
- msg.acknowledge();
- }} timeout={GLOBAL_TOAST_TIMEOUT} />
+ return (
+ <Attention
+ type="success"
+ title={msg.message.title}
+ onClose={() => {
+ msg.acknowledge();
+ }}
+ timeout={GLOBAL_TOAST_TIMEOUT}
+ />
+ );
}
}
diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx
@@ -213,7 +213,6 @@ export function InputArray<T extends object, K extends keyof T>(
<button
type="button"
- disabled={selected !== undefined}
onClick={() => {
const newValue = [...list];
newValue.splice(selectedIndex, 1);