taler-typescript-core

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

commit f3060f724c1689c6ecd743bdda5ba3e1b4d0fafe
parent cffb8b76d96cd6321712824987abfdd6ba349cc5
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon,  1 Jun 2026 11:20:56 -0300

only one notification handler

Diffstat:
Mpackages/web-util/src/hooks/useNotifications.stories.tsx | 21+++++++++++----------
Mpackages/web-util/src/hooks/useNotifications.ts | 421++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Dpackages/web-util/src/hooks/useNotificationsDeprecated.stories.tsx | 353-------------------------------------------------------------------------------
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> - ); -}, {});