taler-typescript-core

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

commit 60d0fe42e69f5e5f31a9897122f285ea7e32fe2f
parent 5d0e09b46049be4fab92a993acb4011462859046
Author: Florian Dold <dold@taler.net>
Date:   Tue,  3 Nov 2026 20:42:42 +0100

merchant web UI: reset errors when opening category dialogs

Diffstat:
Apackages/taler-merchant-webui/src/screens/InventoryScreen.state.test.tsx | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-merchant-webui/src/screens/InventoryScreen.tsx | 11+++++++++--
2 files changed, 116 insertions(+), 2 deletions(-)

diff --git a/packages/taler-merchant-webui/src/screens/InventoryScreen.state.test.tsx b/packages/taler-merchant-webui/src/screens/InventoryScreen.state.test.tsx @@ -0,0 +1,107 @@ +/* + This file is part of GNU Taler + (C) 2026 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. +*/ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { render } from "preact"; +import { act } from "preact/test-utils"; +import { InventoryScreen } from "./InventoryScreen.js"; + +function button(text: string, root: ParentNode = document): HTMLButtonElement { + const found = [...root.querySelectorAll("button")].find((b) => + b.textContent?.includes(text), + ); + assert.ok(found, `Missing button: ${text}`); + return found; +} +function field(id: string): HTMLInputElement { + const found = document.querySelector<HTMLInputElement>(`#${id}`); + assert.ok(found, `Missing field: ${id}`); + return found; +} +const settle = () => new Promise<void>((resolve) => setTimeout(resolve, 0)); +const click = (element: HTMLElement) => + act(async () => { + element.click(); + await settle(); + }); +const input = (element: HTMLInputElement, value: string) => + act(() => { + element.value = value; + element.dispatchEvent(new Event("input", { bubbles: true })); + }); +function submit(element: HTMLInputElement) { + element + .closest("form")! + .dispatchEvent(new Event("submit", { bubbles: true, cancelable: true })); +} +function mount() { + const root = document.createElement("div"); + document.body.append(root); + return root; +} +async function cleanup(root: HTMLElement) { + await act(() => render(null, root)); + root.remove(); +} + +for (const layout of ["mobile", "desktop"]) { + test(`category errors clear between create, rename and delete dialogs (${layout})`, async () => { + const root = mount(); + const fail = async () => { + throw new Error("Previous operation failed"); + }; + const openAction = async (label: string) => { + const triggers = root.querySelectorAll<HTMLButtonElement>( + 'button[aria-label="Actions for Coffee"]', + ); + await click(triggers[layout === "mobile" ? 0 : 1]!); + await click(button(label, document.querySelector('[role="menu"]')!)); + }; + try { + await act(() => + render( + <InventoryScreen + initialTab="categories" + categories={[{ id: 1, name: "Coffee", productCount: 0 }]} + onAddCategory={fail} + onUpdateCategory={fail} + onDeleteCategory={fail} + />, + root, + ), + ); + await click(button("Add a category")); + await input(field("cat_name_input"), "New"); + await act(async () => submit(field("cat_name_input"))); + assert.match( + document.querySelector('[role="dialog"]')!.textContent!, + /Previous operation failed/, + ); + await click(button("Cancel")); + for (const action of ["create", "rename", "delete"]) { + if (action === "create") await click(button("Add a category")); + else + await openAction( + action === "rename" ? "Rename category" : "Delete category", + ); + const dialog = document.querySelector('[role="dialog"]')!; + assert.doesNotMatch(dialog.textContent!, /Previous operation failed/); + if (action === "delete") await click(button("Delete Category", dialog)); + else { + await input(field("cat_name_input"), "Changed"); + await act(async () => submit(field("cat_name_input"))); + } + assert.match(dialog.textContent!, /Previous operation failed/); + await click(button("Cancel", dialog)); + } + } finally { + await cleanup(root); + } + }); +} diff --git a/packages/taler-merchant-webui/src/screens/InventoryScreen.tsx b/packages/taler-merchant-webui/src/screens/InventoryScreen.tsx @@ -138,17 +138,24 @@ export function InventoryScreen({ }; const handleOpenCreateCategory = () => { + setCategoryMutationError(undefined); setEditingCategory(null); setCategoryName(""); setIsCategoryModalOpen(true); }; const handleOpenEditCategory = (cat: CategoryItem) => { + setCategoryMutationError(undefined); setEditingCategory(cat); setCategoryName(cat.name); setIsCategoryModalOpen(true); }; + const handleOpenDeleteCategory = (cat: CategoryItem) => { + setCategoryMutationError(undefined); + setDeletingCategory(cat); + }; + const [categoryItems, setCategoryItems] = useState<CategoryItem[]>(categories); @@ -638,7 +645,7 @@ export function InventoryScreen({ >{t`Rename category`}</button> <button type="button" - onClick={() => setDeletingCategory(cat)} + onClick={() => handleOpenDeleteCategory(cat)} class={destructiveRowActionClass} >{t`Delete category`}</button> </RowActions> @@ -692,7 +699,7 @@ export function InventoryScreen({ </button> <button type="button" - onClick={() => setDeletingCategory(cat)} + onClick={() => handleOpenDeleteCategory(cat)} class={destructiveRowActionClass} > {t`Delete category`}