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:
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`}