Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/quiet-cell-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tailor-platform/app-shell": minor
---

Show cell-local error popovers by default when DataTable editing callbacks throw or return a rejected Promise. Keep pending and failed drafts editable, wait for a successful `onValueChange` check before confirmation, and clear drafts only after confirmation succeeds; Esc discards the draft and error together without trapping cell navigation.
The standard popover displays errors without a reset action. Customize its body and any consumer-owned actions with `edit.error.render({ row, error })`, or set `edit.error: false` to retain eager edit completion without callback interception. Data adoption, persistence, and rollback remain consumer-owned.
6 changes: 3 additions & 3 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,9 @@
],
"hashes": {
"typeSurface": "3ca8fce7d2efca2d",
"outline": "0b7a57e7d2fc04a5",
"outline": "6fa3b40f1fe0b561",
"snapshot": null,
"outputMd": "1073852822f9e535",
"outputMd": "354b21e48e9605d0",
"examples": "b3468077b9455521"
}
},
Expand Down Expand Up @@ -1751,7 +1751,7 @@
"packages/core/skills/app-shell-patterns/references/components/combobox.md": "dfb775c7c4307409",
"packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a",
"packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "1073852822f9e535",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "354b21e48e9605d0",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "bc69f14829004f9d",
"packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be",
"packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571",
Expand Down
93 changes: 73 additions & 20 deletions docs-src/components/data-table.docs.outline.md

Large diffs are not rendered by default.

93 changes: 73 additions & 20 deletions docs/components/data-table.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useRef, useState } from "react";
import {
Button,
DataTable,
Text,
createColumnHelper,
useDataTable,
} from "@tailor-platform/app-shell";

type ErrorDemoRow = {
id: string;
name: string;
note: string | null;
quantity: number | null;
code: string | null;
};

const { column } = createColumnHelper<ErrorDemoRow>();

function checkNote(value: string | null) {
if (value?.includes("!")) throw new Error("Remove ! from the note");
}

/** Callback failures use the table's default popover unless content is overridden. */
export function EditingErrorsDemo() {
const [rows, setRows] = useState<ErrorDemoRow[]>([
{ id: "error-demo", name: "Prototype row", note: "Ready", quantity: 5, code: "SKU-100" },
]);
const [savingCount, setSavingCount] = useState(0);
const [feedback, setFeedback] = useState("No action yet.");
const quantityRevision = useRef(0);
const update = (id: string, patch: Partial<ErrorDemoRow>) => {
setRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)));
};

const columns = [
column({ id: "name", label: "Item", type: "text", width: 160 }),
column({
id: "note",
label: "Draft note",
type: "text",
width: 200,
edit: {
onValueChange: (_row, draft) => checkNote(draft),
onValueCommitted: (row, value) => update(row.id, { note: value }),
},
}),
column({
id: "quantity",
label: "Async quantity",
type: "number",
width: 160,
edit: {
onValueChange: () => {
quantityRevision.current += 1;
},
onValueCommitted: async (row, value) => {
const revision = ++quantityRevision.current;
setSavingCount((count) => count + 1);
try {
await new Promise<void>((resolve) => setTimeout(resolve, 600));
if (value === 13) throw new Error("Quantity 13 was rejected by the simulated server");
if (revision === quantityRevision.current) {
update(row.id, { quantity: value });
}
} finally {
setSavingCount((count) => count - 1);
}
},
},
}),
column({
id: "code",
label: "Custom code",
type: "text",
width: 180,
edit: {
onValueCommitted: (row, value) => {
if (!value?.startsWith("SKU-")) throw new Error("Item code must start with SKU-");
update(row.id, { code: value });
},
error: {
render: ({ row, error }) => (
<div className="space-y-2">
<Text variant="label-md">Code issue for {row.name}</Text>
<Text variant="body-sm">
{error instanceof Error ? error.message : "The code could not be accepted."}
</Text>
<Button
variant="outline"
size="sm"
onClick={() => setFeedback("Use a code beginning with SKU-, for example SKU-101.")}
>
Show valid code example
</Button>
</div>
),
},
},
}),
];
const table = useDataTable({ columns, data: { rows } });

return (
<div className="space-y-3">
<ul className="list-inside list-disc space-y-1 text-body-sm text-muted-foreground">
<li>
In <strong>Draft note</strong>, type <code>!</code> to throw from{" "}
<code>onValueChange</code>. The failed draft stays visible and cannot be confirmed until
you remove <code>!</code>. Press Esc to restore the adopted note and clear its error
together.
</li>
<li>
Set <strong>Async quantity</strong> to <code>13</code> and press Enter. The returned
Promise rejects after 600 ms; <code>13</code> stays visible while pending and after
failure. Enter retries the same draft. Change it to <code>14</code> to save successfully,
or press Esc to restore the adopted quantity. To test stale failures, confirm 13 and start
typing another value before the rejection arrives.
</li>
<li>
Set <strong>Custom code</strong> to <code>bad</code> and press Enter. Open its error
popover and use <strong>Show valid code example</strong> to try interactive custom
content. The failed <code>bad</code> draft remains editable; change it to{" "}
<code>SKU-101</code> and confirm to adopt a valid code.
</li>
</ul>
<Text variant="body-sm" className="text-muted-foreground">
Click an error icon or press Alt+ArrowDown in its editor to inspect the popover without
confirming or discarding the draft. Pending and failed drafts survive Tab or blur without
trapping focus. Only successful confirmation clears the draft; only the consumer adopts
successful values. The standard popover adds no reset action. Closing error details keeps
the draft; Esc in the editor cancels the draft and its error together.
</Text>
<DataTable.Root value={table}>
<DataTable.Table />
<DataTable.Footer>
<div className="space-y-1">
<Text variant="body-sm" className="text-muted-foreground" aria-live="polite">
Saves in flight: {savingCount}
</Text>
<Text variant="body-sm" render={<output />}>
{feedback}
</Text>
</div>
</DataTable.Footer>
</DataTable.Root>
</div>
);
}
13 changes: 13 additions & 0 deletions examples/vite-app/src/pages/showcase/data-table-lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { FlaskConical } from "lucide-react";
import { useMemo } from "react";
import { InlineEditingDemo } from "./inline-editing-demo";
import { EditingErrorsDemo } from "./editing-errors-demo";

// ─── Dummy data ──────────────────────────────────────────────────────────────
// 🧪 Dummy Data: Replace with a real GraphQL-backed source later.
Expand Down Expand Up @@ -574,6 +575,18 @@ const DataTableLabPage = () => {
<InlineEditingDemo />
</Section>

<Section
title="Editing errors — prototype"
description={
<>
Callback failures use a standard cell-error popover by default. Only the Custom code
column supplies <code>error.render</code>; the other columns need no error config.
</>
}
>
<EditingErrorsDemo />
</Section>

<Section
title="Toolbar — filters + column settings"
description={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ describe("DataTable inline editing", () => {
expect(input.closest("td")?.textContent).toContain("ABC");
});

it("does not keep optimistic cell state or wait for a returned promise", async () => {
it("retains a pending draft without adopting it or blocking focus movement", async () => {
let finish = noop;
const onValueCommitted = vi.fn(() => new Promise<void>((resolve) => (finish = resolve)));
const user = renderTable(
Expand All @@ -607,10 +607,12 @@ describe("DataTable inline editing", () => {
const [first, second] = screen.getAllByRole("textbox", { name: "Received" });
await retype(user, first, "20{Enter}");
expect(onValueCommitted).toHaveBeenCalledTimes(1);
expect((first as HTMLInputElement).value).toBe("12");
expect((first as HTMLInputElement).value).toBe("20");
expect(document.activeElement).toBe(first);
await user.tab();
expect(document.activeElement).toBe(second);
expect(onValueCommitted).toHaveBeenCalledTimes(1);
expect((first as HTMLInputElement).value).toBe("20");
await act(async () => finish());
expect((first as HTMLInputElement).value).toBe("12");
});
Expand Down
Loading
Loading