fix: preserve folder rename drafts until save
This commit is contained in:
1 parent
725c836788
commit
e715160235
2 files changed
+62
-5
No files matched your search
@@ -43,7 +43,9 @@ test("nested folder can be created and renamed", async () => setup(async (contai
|
||||
await act(async () => { Array.from(container.querySelectorAll("button")).find(b => b.textContent === "Add folder")!.click(); });
|
||||
const services = store.get().folders.find(f => f.name === "Services")!;
|
||||
expect(services.parentId).toBe(housing.id);
|
||||
await act(async () => { input(container, "Name for Services", "Household"); });
|
||||
const field = container.querySelector('input[aria-label="Name for Services"]') as HTMLInputElement;
|
||||
act(() => { field.focus(); input(container, "Name for Services", "Household"); });
|
||||
await act(async () => field.blur());
|
||||
expect(store.get().folders.find(f => f.id === services.id)!.name).toBe("Household");
|
||||
await act(async () => { select(container, "Folder for Utilities", ""); });
|
||||
expect(store.get().folders[1]!.parentId).toBeNull();
|
||||
@@ -67,3 +69,35 @@ test("failed move reports error", async () => setup(async (container, data) => {
|
||||
expect(container.querySelector('[role="alert"]')?.textContent).toContain("Disk full");
|
||||
expect(JSON.stringify(store.get())).toBe(before);
|
||||
}, true));
|
||||
|
||||
|
||||
test("folder rename keeps multiple keystrokes until explicit save", async () => setup(async (container, data) => {
|
||||
const persist = store.mutate;
|
||||
const pending: Array<() => Promise<void>> = [];
|
||||
store.mutate = change => new Promise<void>((resolve, reject) => {
|
||||
pending.push(async () => { try { await persist(change); resolve(); } catch (error) { reject(error); } });
|
||||
});
|
||||
const field = container.querySelector('input[aria-label="Name for Housing"]') as HTMLInputElement;
|
||||
act(() => field.focus());
|
||||
for (const value of ["H", "Ho", "Home", "Home ", "Home bills"]) {
|
||||
act(() => input(container, "Name for Housing", value));
|
||||
expect(field.value).toBe(value);
|
||||
expect(store.get().folders[0]!.name).toBe("Housing");
|
||||
}
|
||||
expect(pending.length).toBe(0);
|
||||
act(() => field.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })));
|
||||
expect(pending.length).toBe(1);
|
||||
expect(store.get().folders[0]!.name).toBe("Housing");
|
||||
await act(async () => { await pending.shift()!(); });
|
||||
expect(store.get().folders[0]!.name).toBe("Home bills");
|
||||
expect(store.get().folders[0]!.id).toBe(data.folders[0]!.id);
|
||||
}));
|
||||
|
||||
test("failed folder rename retains draft for retry", async () => setup(async (container) => {
|
||||
const field = container.querySelector('input[aria-label="Name for Housing"]') as HTMLInputElement;
|
||||
act(() => { field.focus(); input(container, "Name for Housing", "Home bills"); });
|
||||
await act(async () => field.blur());
|
||||
expect(container.querySelector('[role="alert"]')?.textContent).toContain("Disk full");
|
||||
expect(store.get().folders[0]!.name).toBe("Housing");
|
||||
expect(field.value).toBe("Home bills");
|
||||
}, true));
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useRef, useState, type ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
@@ -13,6 +13,24 @@ export function BudgetFoldersSection() {
|
||||
const [name, setName] = useState("");
|
||||
const [parent, setParent] = useState<string | null>(null);
|
||||
|
||||
const [folderNames, setFolderNames] = useState<Record<string, string>>({});
|
||||
const savingNames = useRef(new Set<string>());
|
||||
|
||||
function saveFolderName(id: string, savedName: string) {
|
||||
const typed = folderNames[id];
|
||||
if (typed === undefined || typed === savedName || savingNames.current.has(id)) return;
|
||||
savingNames.current.add(id);
|
||||
void mutate(draft => renameBudgetFolder(draft, id, typed)).then(saved => {
|
||||
savingNames.current.delete(id);
|
||||
if (saved) setFolderNames(names => {
|
||||
if (names[id] !== typed) return names;
|
||||
const next = { ...names };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function folderPath(id: string): string {
|
||||
const folder = dataset.folders.find(folder => folder.id === id)!;
|
||||
return folder.parentId === null ? folder.name : `${folderPath(folder.parentId)} / ${folder.name}`;
|
||||
@@ -34,9 +52,14 @@ export function BudgetFoldersSection() {
|
||||
return <li key={`${key.kind}-${key.id}`}>
|
||||
<div data-budget-item={key.id} className="flex flex-wrap items-center gap-2 border-b border-budget-rule py-2">
|
||||
<div className="min-w-0 flex-1 basis-40">
|
||||
{key.kind === "folder" ? <Input className="h-8 w-full max-w-64" aria-label={`Name for ${item.name}`} value={item.name} onInput={event => {
|
||||
const name = event.currentTarget.value;
|
||||
if (name.trim()) void mutate(draft => renameBudgetFolder(draft, key.id, name));
|
||||
{key.kind === "folder" ? <Input className="h-8 w-full max-w-64" aria-label={`Name for ${item.name}`} value={folderNames[key.id] ?? item.name} onInput={event => {
|
||||
const typed = event.currentTarget.value;
|
||||
setFolderNames(names => ({ ...names, [key.id]: typed }));
|
||||
}} onBlur={() => saveFolderName(key.id, item.name)} onKeyDown={event => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
saveFolderName(key.id, item.name);
|
||||
}
|
||||
}} /> : <span className="break-words text-sm">{item.name}</span>}
|
||||
<span className="mt-1 block text-xs text-budget-ink-muted">{key.kind === "folder" ? "Folder" : key.kind === "post" ? "Post" : "Bill"}</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user