commit f3060f724c1689c6ecd743bdda5ba3e1b4d0fafe
parent cffb8b76d96cd6321712824987abfdd6ba349cc5
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 1 Jun 2026 11:20:56 -0300
only one notification handler
Diffstat:
3 files changed, 234 insertions(+), 561 deletions(-)
diff --git a/packages/web-util/src/hooks/useNotifications.stories.tsx b/packages/web-util/src/hooks/useNotifications.stories.tsx
@@ -168,7 +168,7 @@ export const sharedButton = tests.createExample(() => {
}, {});
export const conditionally = tests.createExample(() => {
- const { notification, showSuccess, clearOnStart } = useNotificationHandler();
+ const { notification, showSuccess, clear } = useNotificationHandler();
const safe = newSafeHandlerBuilder({});
const [count, setCount] = useState(0);
const [name, setName] = useState("");
@@ -183,7 +183,7 @@ export const conditionally = tests.createExample(() => {
},
name.length > 4 ? [name] : undefined,
);
- action.addListener(clearOnStart);
+ action.addListener((e) => (e === "start" ? clear() : undefined));
action.onSuccess = showSuccess(
(d) => `the name ${d} is a good name` as TranslatedString,
);
@@ -216,7 +216,7 @@ export const conditionally = tests.createExample(() => {
}, {});
export const messages = tests.createExample(() => {
- const { notification, showError, showSuccess, clearOnStart } =
+ const { notification, showError, showSuccess, clear } =
useNotificationHandler();
const safe = newSafeHandlerBuilder({});
const [name, setName] = useState("");
@@ -236,7 +236,7 @@ export const messages = tests.createExample(() => {
},
[name],
);
- action.addListener(clearOnStart);
+ action.addListener((e) => (e === "start" ? clear() : undefined));
action.onFail = showError(
"the operation failed" as TranslatedString,
(fail) => {
@@ -279,7 +279,7 @@ export const confirm = tests.createExample(() => {
const safe = newSafeHandlerBuilder({});
const [name, setName] = useState("");
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
- const { notification, showError, showSuccess, clearOnStart } =
+ const { notification, showError, showSuccess, clear } =
useNotificationHandler();
const [count, setCount] = useState(0);
function inc() {
@@ -301,7 +301,7 @@ export const confirm = tests.createExample(() => {
[name],
);
- action.addListener(clearOnStart);
+ action.addListener((e) => (e === "start" ? clear() : undefined));
action.onFail = showError(
"the operation failed" as TranslatedString,
(fail) => {
@@ -374,7 +374,7 @@ export const confirm = tests.createExample(() => {
}, {});
export const unhandledError = tests.createExample(() => {
- const { notification, displayError, displayInfo, clearOnStart } =
+ const { notification, displayError, displayInfo, clear } =
useNotificationHandler();
const safeHandler = newSafeHandlerBuilder({
onError: (error) => {
@@ -403,7 +403,7 @@ export const unhandledError = tests.createExample(() => {
return opFixedSuccess(name + id);
});
- action.addListener(clearOnStart);
+ action.addListener((e) => (e === "start" ? clear() : undefined));
const ok = action.withArgs("taler");
const fail = action.withArgs("qwe");
const error = action.withArgs("");
@@ -452,8 +452,9 @@ export const unhandledError = tests.createExample(() => {
);
}, {});
-function ShowMessage({ n }: { n?: Notification }): VNode {
- if (!n) return <Fragment />;
+function ShowMessage({ n: ns }: { n?: Notification[] }): VNode {
+ if (!ns || !ns.length) return <Fragment />;
+ const n = ns[0];
return (
<Attention
type={n.message.type}
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
@@ -67,31 +67,40 @@ function hash(msg: NotificationMessage): string {
}
export function useNotificationHandler() {
- const [notification, setNotification] = useState<Notification>();
+ const [notification, setNotification] = useState<Notification[]>([]);
+
+ function pushNotification(n:Notification) {
+ setNotification((ns)=>[n,...ns])
+ }
+ function ackNotification(when: AbsoluteTime) {
+ return ():void => setNotification((ns) => ns.filter(n => n.message.when.t_ms !== when.t_ms))
+ }
function displayError(
title: TranslatedString,
- debug: Error | TalerError | unknown | undefined,
+ debug: Error | TalerError | unknown | undefined,
...description: TranslatedString[]
) {
- setNotification({
+ const when = AbsoluteTime.now()
+ pushNotification({
message: {
title,
type: "error",
- when: AbsoluteTime.now(),
+ when,
description,
debug,
},
- acknowledge: () => setNotification(undefined),
+ acknowledge: ackNotification(when),
});
}
function displayInfo(title: TranslatedString) {
- setNotification({
+ const when = AbsoluteTime.now()
+ pushNotification({
message: {
title,
type: "info",
- when: AbsoluteTime.now(),
+ when,
},
- acknowledge: () => setNotification(undefined),
+ acknowledge: ackNotification(when),
});
}
@@ -120,19 +129,13 @@ export function useNotificationHandler() {
}) as T;
}
- function clearOnStart(e: "start" | "finish") {
- if (e === "start") {
- notification?.acknowledge();
- }
- }
-
return {
notification,
showError,
showSuccess,
displayError,
displayInfo,
- clearOnStart,
+ clear: () => setNotification([]),
};
}
@@ -158,160 +161,167 @@ export type FunctionThatMayFail<T extends any[]> = (
* into a function that will set the notification.
*
*/
-export function useLocalNotificationBetter(): [
- Notification | undefined,
- <Args extends any[], R extends OperationResult<any, any>>(
- opName: TranslatedString,
- doAction: (...args: Args) => Promise<R>,
- args?: Args,
- ) => SafeHandlerTemplate<Args, R>,
-] {
- const [value, save] = useState<NotificationMessage>();
- const notif = !value
- ? undefined
- : {
- message: value,
- acknowledge: () => {
- save(undefined);
- },
- };
-
- // FIXME: we should move this outside of logic
- const { i18n } = useTranslationContext();
-
- function safeFunctionHandler<
- Args extends any[],
- R extends OperationResult<any, any>,
- >(
- opName: TranslatedString,
- doAction: (...args: Args) => Promise<R>,
- args?: Args,
- ): SafeHandlerTemplate<Args, R> {
- function buildSafeHandler(
- a: Args | undefined,
- doAction: (...args: Args) => Promise<R>,
- ): SafeHandlerTemplate<Args, R> {
- const s = CancellationToken.create();
- let running = false;
- const thiz: SafeHandlerTemplate<Args, R> = {
- args: a,
- cancel: () => {
- if (running) {
- s.cancel();
- }
- },
- withArgs: (...newArgs) => {
- const r = buildSafeHandler(newArgs, doAction);
- r.onSuccess = thiz.onSuccess;
- r.onFail = thiz.onFail;
- return r;
- },
- lambda: (converter, init) => {
- type D = Parameters<typeof converter>;
- type SH = SafeHandlerTemplate<D, R>;
-
- const r = buildSafeHandler(
- init ? converter(...init) : undefined,
- doAction,
- );
- // @ts-expect-error
- r.withArgs = (...args: D) => {
- const d = converter(...args);
- if (!d) return thiz;
- const e = thiz.withArgs(...d);
- return e;
- };
- /**
- * FIXME: there is a problem with this
- *
- * adding onSuccess function after creating the lambda makes the withArgs
- * build handlers without onSuccess. consider this
- *
- * const h = safeHandler(handler).lambda((param) -> .. )
- * h.onSuccess = () => i18n.str`ok`
- * const button = h.withArgs(p);
- *
- * button.call()
- *
- * the onSuccess function is undefined when button is clicked.
- * But not if the lambda is created after the onSuccess assignment
- */
- r.onSuccess = thiz.onSuccess;
- r.onFail = thiz.onFail;
- return r as any as SH;
- },
- call: async (): Promise<void> => {
- if (!thiz.args || running) return;
- running = true;
- try {
- thiz.onStart.forEach((listener) => {
- listener();
- });
-
- const resp = await s.token.racePromise(doAction(...thiz.args));
- switch (resp.type) {
- case "ok": {
- const msg = thiz.onSuccess(resp.body, ...thiz.args);
- if (msg) {
- save(successWithTitle(msg));
- }
- return;
- }
- case "fail": {
- const error = thiz.onFail(resp as any, ...thiz.args);
- if (error) {
- save(failWithTitle(i18n, opName, resp, error, thiz.args));
- }
- return;
- }
- default: {
- assertUnreachable(resp);
- }
- }
- } catch (error: unknown) {
- if (error instanceof CancellationToken.CancellationError) {
- return;
- }
- // This functions should not throw, this is a problem.
- logBugForDevelopers(error);
- onUnexpected(
- i18n,
- i18n.str`Unexpected error trying to ${opName}`,
- save,
- )(error, thiz.args);
- return;
- } finally {
- thiz.onComplete.forEach((listener) => {
- listener();
- });
- running = false;
- }
- },
- onFail: (fail, ...rest) =>
- i18n.str`Unhandled failure trying to ${opName}. Code ${fail.case}`,
- onSuccess: () => undefined,
- onStart: [],
- onComplete: [],
- addCompleteListener: (h) => {
- thiz.onComplete.push(h);
- return thiz;
- },
- addStartListener: (h) => {
- thiz.onStart.push(h);
- return thiz;
- },
- };
- return thiz;
- }
- return buildSafeHandler(args, doAction);
- }
-
- return [notif, safeFunctionHandler];
-}
+// export function useLocalNotificationBetter(): [
+// Notification | undefined,
+// <Args extends any[], R extends OperationResult<any, any>>(
+// opName: TranslatedString,
+// doAction: (...args: Args) => Promise<R>,
+// args?: Args,
+// ) => SafeHandler<Args, R>,
+// ] {
+// const [value, save] = useState<NotificationMessage>();
+// const notif = !value
+// ? undefined
+// : {
+// message: value,
+// acknowledge: () => {
+// save(undefined);
+// },
+// };
+
+// // FIXME: we should move this outside of logic
+// const { i18n } = useTranslationContext();
+
+// function safeFunctionHandler<
+// Args extends any[],
+// R extends OperationResult<any, any>,
+// >(
+// opName: TranslatedString,
+// doAction: (...args: Args) => Promise<R>,
+// args?: Args,
+// ): SafeHandler<Args, R> {
+// function buildSafeHandler(
+// a: Args | undefined,
+// doAction: (...args: Args) => Promise<R>,
+// ): SafeHandler<Args, R> {
+// const s = CancellationToken.create();
+// let running = false;
+// const thiz: SafeHandler<Args, R> = {
+// args: a,
+// cancel: () => {
+// if (running) {
+// s.cancel();
+// }
+// },
+// withArgs: (...newArgs) => {
+// const r = buildSafeHandler(newArgs, doAction);
+// r.onSuccess = thiz.onSuccess;
+// r.onFail = thiz.onFail;
+// return r;
+// },
+// lambda: (converter, init) => {
+// type D = Parameters<typeof converter>;
+// type SH = SafeHandler<D, R>;
+
+// const r = buildSafeHandler(
+// init ? converter(...init) : undefined,
+// doAction,
+// );
+// // @ts-expect-error
+// r.withArgs = (...args: D) => {
+// const d = converter(...args);
+// if (!d) return thiz;
+// const e = thiz.withArgs(...d);
+// return e;
+// };
+// /**
+// * FIXME: there is a problem with this
+// *
+// * adding onSuccess function after creating the lambda makes the withArgs
+// * build handlers without onSuccess. consider this
+// *
+// * const h = safeHandler(handler).lambda((param) -> .. )
+// * h.onSuccess = () => i18n.str`ok`
+// * const button = h.withArgs(p);
+// *
+// * button.call()
+// *
+// * the onSuccess function is undefined when button is clicked.
+// * But not if the lambda is created after the onSuccess assignment
+// */
+// r.onSuccess = thiz.onSuccess;
+// r.onFail = thiz.onFail;
+// return r as any as SH;
+// },
+// call: async (): Promise<void> => {
+// if (!thiz.args || running) return;
+// running = true;
+// try {
+// thiz.onStart.forEach((listener) => {
+// listener();
+// });
+
+// const resp = await s.token.racePromise(doAction(...thiz.args));
+// switch (resp.type) {
+// case "ok": {
+// const msg = thiz.onSuccess(resp.body, ...thiz.args);
+// if (msg) {
+// save(successWithTitle(msg));
+// }
+// return;
+// }
+// case "fail": {
+// const error = thiz.onFail(resp as any, ...thiz.args);
+// if (error) {
+// save(failWithTitle(i18n, opName, resp, error, thiz.args));
+// }
+// return;
+// }
+// default: {
+// assertUnreachable(resp);
+// }
+// }
+// } catch (error: unknown) {
+// if (error instanceof CancellationToken.CancellationError) {
+// return;
+// }
+// // This functions should not throw, this is a problem.
+// logBugForDevelopers(error);
+// onUnexpected(
+// i18n,
+// i18n.str`Unexpected error trying to ${opName}`,
+// save,
+// )(error, thiz.args);
+// return;
+// } finally {
+// thiz.onComplete.forEach((listener) => {
+// listener();
+// });
+// running = false;
+// }
+// },
+// onFail: (fail, ...rest) =>
+// i18n.str`Unhandled failure trying to ${opName}. Code ${fail.case}`,
+// onSuccess: () => undefined,
+// onStart: [],
+// onComplete: [],
+// addCompleteListener: (h) => {
+// thiz.onComplete.push(h);
+// return thiz;
+// },
+// addStartListener: (h) => {
+// thiz.onStart.push(h);
+// return thiz;
+// },
+// };
+// return thiz;
+// }
+// return buildSafeHandler(args, doAction);
+// }
+
+// return [notif, safeFunctionHandler];
+// }
export interface SafeHandler<Args extends any[], OpType> {
+ /**
+ * Be careful of not settings the args with always-changing values
+ * like `[]` or `{}` (which returns new instance every time)
+ *
+ * The list of arguments is subject to be used as an useEffect dependency array
+ * for re-render purpose
+ */
readonly args: Args | undefined;
- readonly listeners: ((e: "start" | "finish") => void)[];
+ readonly listeners: ((e: "start" | "success" | "fail" | "finish") => void)[];
call(): Promise<void>;
cancel(): void;
@@ -322,7 +332,11 @@ export interface SafeHandler<Args extends any[], OpType> {
withArgs(...args: Args): SafeHandler<Args, Error>;
/**
- * derive another handler but convert the arguments before calling
+ * Derive a new handler with different calling interface.
+ *
+ * The converter will be called with previoulsy set arguments plus the
+ * new arguments which should return the merged result.
+ *
*/
lambda<OtherArgs extends any[]>(
convert: (
@@ -336,7 +350,7 @@ export interface SafeHandler<Args extends any[], OpType> {
onFail: OnOperationFailReturnType_NG<OpType, Args>;
addListener: (
- h: (event: "start" | "finish") => void,
+ h: (event: "start" | "success" | "fail" | "finish") => void,
) => SafeHandler<Args, Error>;
}
@@ -357,7 +371,7 @@ export function newSafeHandlerBuilder<
onSuccess?: (result: unknown, ...args: any[]) => void;
onFail?: (fail: unknown, ...args: any[]) => void;
onError?: (e: unknown, ...args: any[]) => void;
- listeners?: (SafeHandler<unknown[],unknown>["listeners"]);
+ listeners?: SafeHandler<unknown[], unknown>["listeners"];
} = {},
) {
return function newSafeHandler<Args extends A, R extends B>(
@@ -428,10 +442,16 @@ export function newSafeHandlerBuilder<
);
switch (resp.type) {
case "ok": {
+ thiz.listeners.forEach((listener) => {
+ listener("success");
+ });
thiz.onSuccess(resp.body, ...thiz.args);
return;
}
case "fail": {
+ thiz.listeners.forEach((listener) => {
+ listener("fail");
+ });
thiz.onFail(resp as any, ...thiz.args);
return;
}
@@ -440,6 +460,9 @@ export function newSafeHandlerBuilder<
}
}
} catch (error: unknown) {
+ thiz.listeners.forEach((listener) => {
+ listener("fail");
+ });
if (error instanceof CancellationToken.CancellationError) {
return;
}
@@ -455,7 +478,9 @@ export function newSafeHandlerBuilder<
},
onFail: opts.onFail ?? noop,
onSuccess: opts.onSuccess ?? noop,
- listeners: opts.listeners ?? [],
+ // we save a copy because we don't want further
+ // references to affect the original handler
+ listeners: opts.listeners?.slice() ?? [],
addListener: (h) => {
thiz.listeners.push(h);
return thiz;
@@ -641,34 +666,34 @@ function sanitizeFunctionArguments(args: any[]): string {
*
*
*/
-export interface SafeHandlerTemplate<Args extends any[], Errors> {
- readonly args: Args | undefined;
- readonly onStart: (() => void)[];
- readonly onComplete: (() => void)[];
- /**
- * call the action with the arguments
- */
- call(): Promise<void>;
- cancel(): void;
- /**
- * creates another handler for the same actions but different arguments
- * @param e
- */
- lambda<OtherArgs extends any[]>(
- e: (...d: OtherArgs) => Args | undefined,
- init?: OtherArgs,
- ): SafeHandlerTemplate<OtherArgs, Errors>;
- /**
- * creates another handler with new arguments
- * @param args
- */
- withArgs(...args: Args): SafeHandlerTemplate<Args, Error>;
-
- onSuccess: OnOperationSuccesReturnType<Errors, Args>;
- onFail: OnOperationFailReturnType<Errors, Args>;
- addStartListener: (h: () => void) => SafeHandlerTemplate<Args, Error>;
- addCompleteListener: (h: () => void) => SafeHandlerTemplate<Args, Error>;
-}
+// export interface SafeHandler<Args extends any[], Errors> {
+// readonly args: Args | undefined;
+// readonly onStart: (() => void)[];
+// readonly onComplete: (() => void)[];
+// /**
+// * call the action with the arguments
+// */
+// call(): Promise<void>;
+// cancel(): void;
+// /**
+// * creates another handler for the same actions but different arguments
+// * @param e
+// */
+// lambda<OtherArgs extends any[]>(
+// e: (...d: OtherArgs) => Args | undefined,
+// init?: OtherArgs,
+// ): SafeHandler<OtherArgs, Errors>;
+// /**
+// * creates another handler with new arguments
+// * @param args
+// */
+// withArgs(...args: Args): SafeHandler<Args, Error>;
+
+// onSuccess: OnOperationSuccesReturnType<Errors, Args>;
+// onFail: OnOperationFailReturnType<Errors, Args>;
+// addStartListener: (h: () => void) => SafeHandler<Args, Error>;
+// addCompleteListener: (h: () => void) => SafeHandler<Args, Error>;
+// }
function successWithTitle(title: TranslatedString): NotificationMessage {
return {
diff --git a/packages/web-util/src/hooks/useNotificationsDeprecated.stories.tsx b/packages/web-util/src/hooks/useNotificationsDeprecated.stories.tsx
@@ -1,353 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import {
- opEmptySuccess,
- opFixedSuccess,
- opKnownFailure,
- TranslatedString,
-} from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import {
- Attention,
- ButtonBetter,
- delayMs,
- ToastBanner,
-} from "../index.browser.js";
-import * as tests from "../tests/hook.js";
-import {
- Notification,
- useLocalNotificationBetter,
-} from "./useNotifications.js";
-
-export default {
- title: "UseNotificationsDeprecated",
-};
-
-export namespace Simplest {
- export interface Form {
- comment: string;
- }
-}
-
-export const autoTriggered = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [count, setCount] = useState(0);
- function inc() {
- setCount((n) => n + 1);
- }
- const action = safe(
- "label" as TranslatedString,
- async () => {
- await delayMs(1500);
- inc();
- return opEmptySuccess();
- },
- [],
- );
- useEffect(() => {
- const id = setTimeout(() => {
- action.call();
- }, 3_000);
- return () => {
- clearTimeout(id);
- };
- });
- return (
- <div>
- <div>
- This button is going to be cliked programatically every 3 seconds,
- unless is already working. While the action is active it can't be
- trigered agian until completes.
- </div>
- <div>click {count} times</div>
- <div class="grid gap-2 w-40">
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action}
- >
- button
- </ButtonBetter>
- <button
- class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
- onClick={() => action.call()}
- >
- call directly
- </button>
- </div>
- </div>
- );
-}, {});
-
-export const withCancel = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [count, setCount] = useState(0);
- function inc() {
- setCount((n) => n + 1);
- }
- const action = safe(
- "label" as TranslatedString,
- async () => {
- await delayMs(5_000);
- inc();
- return opEmptySuccess();
- },
- [],
- );
- return (
- <div>
- <div>This button will take 5 secs to complete but can be cancelled</div>
- <div>click {count} times</div>
- <div class="grid gap-2 w-40">
- <button
- class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
- onClick={action.cancel}
- >
- cancel
- </button>
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action}
- >
- button
- </ButtonBetter>
- </div>
- </div>
- );
-}, {});
-
-export const sharedButton = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [count, setCount] = useState(0);
- function inc(d: number) {
- setCount((n) => n + d);
- }
- const action = safe("label" as TranslatedString, async (size: number) => {
- await delayMs(1_000);
- inc(size);
- return opEmptySuccess();
- });
- return (
- <div>
- <div>These buttons tiggers the same action but with different args</div>
- <div>click {count} times</div>
- <div class="grid gap-2 w-40">
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action.withArgs(1)}
- >
- button +1
- </ButtonBetter>
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action.withArgs(2)}
- >
- button +2
- </ButtonBetter>
- </div>
- </div>
- );
-}, {});
-
-export const conditionally = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [count, setCount] = useState(0);
- const [name, setName] = useState("");
- function inc() {
- setCount((n) => n + 1);
- }
- const action = safe(
- "label" as TranslatedString,
- async (s: string) => {
- await delayMs(500);
- inc();
- return opFixedSuccess(s);
- },
- name.length > 4 ? [name] : undefined,
- );
- action.onSuccess = (d) => `the name ${d} is a good name` as TranslatedString;
- return (
- <div>
- <div>Only with the field is valid</div>
- <ShowMessage n={notif} />
- <input
- class="block w-full rounded-md border-0 p-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 "
- onChange={(e) => {
- setName(e.currentTarget.value);
- }}
- placeholder={"The name should be greater than 4 characters"}
- />
- <div>click {count} times</div>
- <div class="grid gap-2 w-40">
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action}
- >
- button
- </ButtonBetter>
- </div>
- </div>
- );
-}, {});
-
-export const messages = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [name, setName] = useState("");
- const [count, setCount] = useState(0);
- function inc() {
- setCount((n) => n + 1);
- return count + 1;
- }
- const action = safe(
- "label" as TranslatedString,
- async function (name: string) {
- await delayMs(500);
- if (!name.length) return opKnownFailure("no-name");
- if (name.length < 4) return opKnownFailure("short-name");
- const id = inc();
-
- return opFixedSuccess(id);
- },
- [name],
- );
- action.onFail = (fail) => {
- switch (fail.case) {
- case "no-name":
- return "please enter a name" as TranslatedString;
- case "short-name":
- return "the name is not long enough" as TranslatedString;
- }
- };
- action.onSuccess = (s) => {
- return `person updated, operation id: ${s}` as TranslatedString;
- };
- return (
- <div>
- <div>Fail if the input is invalid, shows success otherwise</div>
- <ToastBanner />
- <input
- class="block w-full rounded-md border-0 p-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 "
- onChange={(e) => {
- setName(e.currentTarget.value);
- }}
- placeholder={"The name should be greater than 4 characters"}
- />
- <div>click {count} times</div>
- <div class="grid gap-2 w-40">
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action}
- >
- button
- </ButtonBetter>
- </div>
- </div>
- );
-}, {});
-
-function ShowMessage({ n }: { n?: Notification }): VNode {
- if (!n) return <Fragment />;
- return (
- <Attention
- type={n.message.type}
- title={n.message.title}
- onClose={() => {
- n.acknowledge();
- }}
- >
- {n.message.type === "error" ? n.message.description : undefined}
- </Attention>
- );
-}
-
-export const confirm = tests.createExample(() => {
- const [notif, safe] = useLocalNotificationBetter();
- const [name, setName] = useState("");
- const [showConfirmDialog, setShowConfirmDialog] = useState(false);
- const [count, setCount] = useState(0);
- function inc() {
- setCount((n) => n + 1);
- return count + 1;
- }
- const action = safe(
- "label" as TranslatedString,
- async function (name: string, confirm?: boolean) {
- await delayMs(500);
- if (!name.length) return opKnownFailure("no-name");
- if (name.length < 4) return opKnownFailure("short-name");
- if (!confirm) {
- return opKnownFailure("confirm");
- }
- const id = inc();
-
- return opFixedSuccess(name + id);
- },
- [name],
- );
- action.onFail = (fail) => {
- switch (fail.case) {
- case "no-name":
- return "please enter a name" as TranslatedString;
- case "short-name":
- return "the name is not long enough" as TranslatedString;
- case "confirm":
- setShowConfirmDialog(true);
- return undefined;
- }
- };
- action.onSuccess = (s) => {
- setShowConfirmDialog(false);
- return `person updated, operation id: ${s}` as TranslatedString;
- };
- const confirm = action.lambda(() => [action.args![0], true], []);
-
- return (
- <div>
- <div>It will ask for a confirmaton if didn't fail and before succed</div>
- <ShowMessage n={notif} />
-
- <input
- class="block w-full rounded-md border-0 p-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 "
- onChange={(e) => {
- setName(e.currentTarget.value);
- }}
- placeholder={"The name should be greater than 4 characters"}
- />
- <div>click {count} times</div>
- {!showConfirmDialog ? (
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={action}
- >
- agree
- </ButtonBetter>
- ) : (
- <ButtonBetter
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
- onClick={confirm}
- >
- confirm
- </ButtonBetter>
- )}
- </div>
- );
-}, {});