Порівняти коміти

...
Автор SHA1 Повідомлення Дата
l0ng-ai 2bb14c9704 fix(database): keep unsubmitted result edits while another result tab is shown
The result pane remounts when another result tab is selected, so its pending
cell edits, added and deleted rows were dropped. They are now kept by the rows
they were made on, so switching back restores them; a re-run or another page
still starts over.
2026-11-13 17:00:14 +08:00
2 змінених файлів з 37 додано та 6 видалено

No files matched your search

+14 -6
Переглянути файл
@@ -200,6 +200,9 @@ function Pager({ tab }: { tab: ResultTab }) {
);
}
/** Unsubmitted edits by the rows they were made on, kept while another result tab is shown. */
const keptEdits = new WeakMap<DbQueryResult, PendingEdits>();
function Result({ tab }: { tab: ResultTab }) {
const [view, setView] = useState<"table" | "chart">("table");
const [panel, setPanel] = useState<DataPanel | null>(null);
@@ -275,14 +278,19 @@ function Result({ tab }: { tab: ResultTab }) {
sources.some((s) => s !== null);
// The pending edits belong to the result they were made on: fresh rows (a
// re-run, another page) start over, without an effect.
const [pending, setPending] = useState<{ outcome: DbQueryResult | null; edits: PendingEdits }>({
outcome: null,
edits: emptyEdits(),
});
// re-run, another page) start over, without an effect. They outlive the pane,
// which remounts when another result tab is selected.
const [pending, setPending] = useState<{ outcome: DbQueryResult | null; edits: PendingEdits }>(() => ({
outcome: r ?? null,
edits: (r && keptEdits.get(r)) ?? emptyEdits(),
}));
const edits = pending.outcome === r ? pending.edits : emptyEdits();
const setEdits = (f: (e: PendingEdits) => PendingEdits) =>
setPending((previous) => ({ outcome: r, edits: f(previous.outcome === r ? previous.edits : emptyEdits()) }));
setPending((previous) => {
const next = f(previous.outcome === r ? previous.edits : emptyEdits());
if (r) keptEdits.set(r, next);
return { outcome: r ?? null, edits: next };
});
const loaded = r?.rows.length ?? 0;
const rowCount = loaded + edits.inserts.length;
const value = useCallback(
+23
Переглянути файл
@@ -235,6 +235,29 @@ describe("editable result grid", () => {
expect(backend.applyEdits).not.toHaveBeenCalled();
});
it("keeps unsubmitted edits while another result tab is shown", async () => {
const first = show("SELECT * FROM users WHERE id < 10", [{ name: "id" }, { name: "name" }], [["1", "Ann"]]);
const second: ResultTab = {
...first,
id: nextId++,
title: "Result 2",
outcome: { ...first.outcome, result: { ...first.outcome.result! } },
};
useDatabase.setState((s) => ({ results: [...s.results, second] }));
await act(async () => render(<ResultsView />));
fireEvent.doubleClick(await screen.findByRole("gridcell", { name: "Ann" }));
const input = screen.getByRole("textbox", { name: "Edit name" });
fireEvent.change(input, { target: { value: "Bob" } });
fireEvent.keyDown(input, { key: "Enter" });
await act(async () => useDatabase.setState({ activeResult: second.id }));
expect(screen.getByRole("button", { name: "Submit" }).hasAttribute("disabled")).toBe(true);
await act(async () => useDatabase.setState({ activeResult: first.id }));
expect(screen.getByRole("gridcell", { name: "Bob" })).toBeTruthy();
expect(screen.getByRole("button", { name: "Submit" }).hasAttribute("disabled")).toBe(false);
});
it("keeps an aliased column read-only while the plain ones stay editable", async () => {
show("SELECT id, name AS n FROM users", [{ name: "id" }, { name: "n" }], [["1", "Ann"]]);
await act(async () => render(<ResultsView />));