@ai-matrx/records-ui 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.78.0
4
+
5
+ **The button that could not get rid of a table now can, and says what it does.** (lane
6
+ FIX-11A, VERIFIER-11 finding F3.)
7
+
8
+ `TableSettings` called `useRecordMutation().remove` with the TABLE's id — one unbounded
9
+ statement — so on a 1,430-record table it died twelve seconds in on `57014` and told the
10
+ person nothing had changed and to "try a smaller page". There is no page.
11
+
12
+ * The panel now drives `useTableArchive` from `@ai-matrx/records` 0.53.0: it previews
13
+ first (`chunk: 0`) so the confirm names the real number of records, then archives in
14
+ passes and shows "N of M records put away so far" while it works.
15
+ * **It never claims nothing happened.** A refused pass or a stopped run says what went
16
+ wrong, how many records are already archived and how many are left, and offers **Carry on
17
+ archiving**, which picks up exactly where it stopped. There is a **Stop** while it runs.
18
+ * When the store is busy the loop takes smaller passes, and the screen SAYS so rather than
19
+ going quietly four times slower.
20
+ * **The control's word matches what it does.** It said "Delete this table" over a sentence
21
+ explaining the store keeps everything for a year; the store's own verb is archive
22
+ (`table_archive`, `record_restore`, `read_records_archived`), so that is the word on the
23
+ button and the sentence says plainly it can be brought back.
24
+
25
+ Consumer action: none — the panel's props are unchanged; `onDeleted` still fires, now when
26
+ the Table itself has been archived.
27
+
3
28
  ## 0.77.0
4
29
 
5
30
  **A publish button that would hand out a dead link is not drawn.** (lane STORE-OFF, from
package/dist/index.cjs CHANGED
@@ -4476,6 +4476,16 @@ var import_design_system13 = require("@ai-matrx/design-system");
4476
4476
  var import_jsx_runtime16 = require("react/jsx-runtime");
4477
4477
  var BATCH = 250;
4478
4478
  var SAMPLES = 40;
4479
+ var MAPPING_COLUMNS = "minmax(9rem, 11rem) minmax(14rem, 17rem) minmax(13rem, 1fr) minmax(8rem, 10rem)";
4480
+ var ABSENT = "__absent__";
4481
+ function obviousTitleColumn(columns) {
4482
+ const says = /^(title|name|record|customer|client|company|job|item|subject|label|description)\b|(\bname|\btitle|\bnumber|\bid)$/i;
4483
+ const named = columns.find((c) => says.test(c.header.trim()));
4484
+ if (named) return named.header;
4485
+ const text = columns.find((c) => c.type === "text");
4486
+ if (text) return text.header;
4487
+ return columns[0]?.header ?? null;
4488
+ }
4479
4489
  async function sha256(bytes) {
4480
4490
  const subtle = globalThis.crypto?.subtle;
4481
4491
  if (!subtle) return null;
@@ -4510,6 +4520,7 @@ function ImportWizard({ tableId, onDone, className }) {
4510
4520
  const [alreadyRan, setAlreadyRan] = (0, import_react26.useState)(null);
4511
4521
  const [columnsAdded, setColumnsAdded] = (0, import_react26.useState)(null);
4512
4522
  const [show, setShow] = (0, import_react26.useState)("all");
4523
+ const [ledger, setLedger] = (0, import_react26.useState)(null);
4513
4524
  const declared = fields.data ?? [];
4514
4525
  const take = (0, import_react26.useCallback)(
4515
4526
  async (file) => {
@@ -4520,6 +4531,7 @@ function ImportWizard({ tableId, onDone, className }) {
4520
4531
  setProposals([]);
4521
4532
  setAlreadyRan(null);
4522
4533
  setProgress(0);
4534
+ setLedger(null);
4523
4535
  try {
4524
4536
  const xlsx = file.name.toLowerCase().endsWith(".xlsx");
4525
4537
  const bytes = await file.arrayBuffer();
@@ -4546,6 +4558,11 @@ function ImportWizard({ tableId, onDone, className }) {
4546
4558
  for (const column of answered.data.columns) {
4547
4559
  if (column.matched) next[column.header] = column.field_key;
4548
4560
  }
4561
+ const titleKey2 = table.data?.title_field ?? null;
4562
+ if (titleKey2 && !Object.values(next).includes(titleKey2)) {
4563
+ const guess = obviousTitleColumn(answered.data.columns);
4564
+ if (guess) next[guess] = titleKey2;
4565
+ }
4549
4566
  setMapping(next);
4550
4567
  setPhase("ready");
4551
4568
  } catch (thrown) {
@@ -4553,7 +4570,7 @@ function ImportWizard({ tableId, onDone, className }) {
4553
4570
  setPhase("waiting");
4554
4571
  }
4555
4572
  },
4556
- [client, declared, tableId]
4573
+ [client, declared, tableId, table.data?.title_field]
4557
4574
  );
4558
4575
  const run = (0, import_react26.useCallback)(async () => {
4559
4576
  if (!plan) return;
@@ -4561,6 +4578,7 @@ function ImportWizard({ tableId, onDone, className }) {
4561
4578
  setOutcomes([]);
4562
4579
  setColumnsAdded(null);
4563
4580
  setProgress(0);
4581
+ setLedger(null);
4564
4582
  if (unmapped === "create" && rights.admin) {
4565
4583
  setPhase("declaring");
4566
4584
  const made = await client.importDeclareColumns({ table_id: tableId, rows, mapping });
@@ -4596,6 +4614,7 @@ function ImportWizard({ tableId, onDone, className }) {
4596
4614
  }
4597
4615
  const importId = opened.data.import_id;
4598
4616
  const collected = [];
4617
+ const told = { seen: 0, written: 0, duplicate: 0, refused: 0 };
4599
4618
  let take2 = opened.data.rows_per_call ?? BATCH;
4600
4619
  let at = 0;
4601
4620
  while (at < rows.length) {
@@ -4604,11 +4623,17 @@ function ImportWizard({ tableId, onDone, className }) {
4604
4623
  if (!answered.ok) {
4605
4624
  setProblem(answered.error.message);
4606
4625
  setOutcomes([...collected]);
4626
+ setLedger({ ...told, final: false });
4607
4627
  setPhase("ready");
4608
4628
  return;
4609
4629
  }
4610
4630
  collected.push(...answered.data.outcomes);
4611
4631
  setOutcomes([...collected]);
4632
+ told.seen += answered.data.rows_seen;
4633
+ told.written += answered.data.rows_written;
4634
+ told.duplicate += answered.data.rows_duplicate;
4635
+ told.refused += answered.data.rows_refused;
4636
+ setLedger({ ...told, final: false });
4612
4637
  at += batch.length;
4613
4638
  setProgress(Math.min(at, rows.length));
4614
4639
  if (answered.data.rows_per_call) take2 = answered.data.rows_per_call;
@@ -4621,6 +4646,13 @@ function ImportWizard({ tableId, onDone, className }) {
4621
4646
  }
4622
4647
  setSummary(finished.data.message);
4623
4648
  setProposals(finished.data.proposals);
4649
+ setLedger({
4650
+ seen: finished.data.rows_seen,
4651
+ written: finished.data.rows_written,
4652
+ duplicate: finished.data.rows_duplicate,
4653
+ refused: finished.data.rows_refused,
4654
+ final: true
4655
+ });
4624
4656
  setPhase("done");
4625
4657
  onDone?.(finished.data.rows_written);
4626
4658
  }, [client, dedupeKey, fileBytes, fileHash, fileName, format, mapping, onDuplicate, onDone, plan, rows, tableId, unmapped]);
@@ -4640,6 +4672,10 @@ function ImportWizard({ tableId, onDone, className }) {
4640
4672
  [outcomes, show]
4641
4673
  );
4642
4674
  const mappedCount = Object.keys(mapping).length;
4675
+ const titleKey = table.data?.title_field ?? null;
4676
+ const titleMappedTo = titleKey ? Object.entries(mapping).find(([, key]) => key === titleKey)?.[0] ?? null : null;
4677
+ const titleMissing = Boolean(titleKey) && titleMappedTo === null;
4678
+ const titleFieldLabel = (titleKey ? plan?.fields.find((f) => f.key === titleKey)?.label : null) || (titleKey ? declared.find((f) => f.key === titleKey)?.label : null) || "the name column";
4643
4679
  const busy = phase === "reading" || phase === "planning" || phase === "declaring" || phase === "writing";
4644
4680
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: (0, import_design_system13.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
4645
4681
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
@@ -4672,55 +4708,107 @@ function ImportWizard({ tableId, onDone, className }) {
4672
4708
  phase === "reading" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: "Reading the file\u2026" }) : null,
4673
4709
  phase === "planning" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: "Working out what each column is\u2026" }) : null,
4674
4710
  plan && parsed ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4675
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "w-full overflow-x-auto rounded border", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("table", { className: "w-full min-w-[50rem] table-fixed", children: [
4676
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("colgroup", { children: [
4677
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("col", { className: "w-[10rem]" }),
4678
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("col", { className: "w-[17rem]" }),
4679
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("col", { className: "w-[14rem]" }),
4680
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("col", { className: "w-[9rem]" })
4681
- ] }),
4682
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("thead", { className: "bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { children: [
4683
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Column in the file" }),
4684
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Goes to" }),
4685
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Kind" }),
4686
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "First values" })
4687
- ] }) }),
4688
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("tbody", { children: plan.columns.map((column) => {
4711
+ titleKey ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-2 rounded border bg-muted/40 px-2 py-1.5", children: [
4712
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-medium", children: "What each record is called" }),
4713
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4714
+ import_design_system13.Select,
4715
+ {
4716
+ value: titleMappedTo ?? "",
4717
+ disabled: busy,
4718
+ onValueChange: (chosen) => {
4719
+ setMapping((m) => {
4720
+ const next = { ...m };
4721
+ for (const [header, key] of Object.entries(next)) {
4722
+ if (key === titleKey) delete next[header];
4723
+ }
4724
+ if (chosen) next[chosen] = titleKey;
4725
+ return next;
4726
+ });
4727
+ },
4728
+ children: [
4729
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4730
+ import_design_system13.SelectTrigger,
4731
+ {
4732
+ size: "sm",
4733
+ "aria-label": "Which column each record is named by",
4734
+ className: "min-w-[14rem]",
4735
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectValue, { placeholder: "\u2014 choose the column \u2014" })
4736
+ }
4737
+ ),
4738
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectContent, { children: plan.columns.map((c) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectItem, { value: c.header, children: c.header }, c.header)) })
4739
+ ]
4740
+ }
4741
+ ),
4742
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-muted-foreground", children: titleMappedTo ? `goes into ${titleFieldLabel}, so every record this file makes has a name you can read in the grid and on a chip.` : `Nothing goes into ${titleFieldLabel} yet, so every record this file makes would land with no name at all. Pick the column that says what each row IS.` })
4743
+ ] }) : null,
4744
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "w-full overflow-x-auto rounded border", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "min-w-[52rem]", children: [
4745
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4746
+ "div",
4747
+ {
4748
+ className: "grid items-center gap-x-3 border-b bg-muted px-2 py-1 font-medium",
4749
+ style: { gridTemplateColumns: MAPPING_COLUMNS },
4750
+ children: [
4751
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "Column in the file" }),
4752
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "Goes to" }),
4753
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "Kind" }),
4754
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "First values" })
4755
+ ]
4756
+ }
4757
+ ),
4758
+ plan.columns.map((column) => {
4689
4759
  const to = mapping[column.header] ?? "";
4690
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { className: "border-t align-top", children: [
4691
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "truncate px-2 py-1 font-medium", title: column.header, children: column.header }),
4692
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-2 py-1", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4693
- "select",
4694
- {
4695
- "aria-label": `Where ${column.header} goes`,
4696
- className: "w-full min-w-0 rounded border bg-background px-1 py-0.5",
4697
- value: to,
4698
- disabled: busy,
4699
- onChange: (e) => {
4700
- const chosen = e.target.value;
4701
- setMapping((m) => {
4702
- const next = { ...m };
4703
- if (chosen === "") delete next[column.header];
4704
- else next[column.header] = chosen;
4705
- return next;
4706
- });
4707
- },
4708
- children: [
4709
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("option", { value: "", children: unmapped === "ignore" ? "\u2014 leave it out \u2014" : "\u2014 offer it as a new column \u2014" }),
4710
- plan.fields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("option", { value: f.key, children: f.label || f.key }, f.field_id))
4711
- ]
4712
- }
4713
- ) }),
4714
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("td", { className: "px-2 py-1", children: [
4715
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Badge, { variant: column.matched ? "secondary" : "outline", className: "text-[11px] font-normal", children: typeWord(column.type) }),
4716
- column.unit ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-1 opacity-60", children: column.unit }) : null,
4717
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "mt-0.5 text-muted-foreground", children: column.why }),
4718
- column.ambiguous ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-destructive", children: "Check a date you know before you run this." }) : null,
4719
- column.collides_with ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-destructive", children: "Two columns in this file would make the same column." }) : null
4720
- ] }),
4721
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "truncate px-2 py-1 text-muted-foreground", children: (column.samples ?? []).slice(0, 3).map(String).join(" \xB7 ") })
4722
- ] }, column.header);
4723
- }) })
4760
+ const absent = unmapped === "ignore" ? "\u2014 leave it out \u2014" : "\u2014 offer it as a new column \u2014";
4761
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4762
+ "div",
4763
+ {
4764
+ className: "grid items-start gap-x-3 border-t px-2 py-1",
4765
+ style: { gridTemplateColumns: MAPPING_COLUMNS },
4766
+ children: [
4767
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate pt-1.5 font-medium", title: column.header, children: column.header }),
4768
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4769
+ import_design_system13.Select,
4770
+ {
4771
+ value: to || ABSENT,
4772
+ disabled: busy,
4773
+ onValueChange: (chosen) => {
4774
+ setMapping((m) => {
4775
+ const next = { ...m };
4776
+ if (chosen === ABSENT) delete next[column.header];
4777
+ else next[column.header] = chosen;
4778
+ return next;
4779
+ });
4780
+ },
4781
+ children: [
4782
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4783
+ import_design_system13.SelectTrigger,
4784
+ {
4785
+ size: "sm",
4786
+ "aria-label": `Where ${column.header} goes`,
4787
+ "data-goes-to": column.header,
4788
+ className: "w-full min-w-[14rem]",
4789
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectValue, {})
4790
+ }
4791
+ ),
4792
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_design_system13.SelectContent, { children: [
4793
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectItem, { value: ABSENT, children: absent }),
4794
+ plan.fields.map((f) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.SelectItem, { value: f.key, children: f.label || f.key }, f.field_id))
4795
+ ] })
4796
+ ]
4797
+ }
4798
+ ),
4799
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "pt-1", children: [
4800
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Badge, { variant: column.matched ? "secondary" : "outline", className: "text-[11px] font-normal", children: typeWord(column.type) }),
4801
+ column.unit ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "ml-1 opacity-60", children: column.unit }) : null,
4802
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "mt-0.5 text-muted-foreground", children: column.why }),
4803
+ column.ambiguous ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-destructive", children: "Check a date you know before you run this." }) : null,
4804
+ column.collides_with ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-destructive", children: "Two columns in this file would make the same column." }) : null
4805
+ ] }),
4806
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate pt-1.5 text-muted-foreground", children: (column.samples ?? []).slice(0, 3).map(String).join(" \xB7 ") })
4807
+ ]
4808
+ },
4809
+ column.header
4810
+ );
4811
+ })
4724
4812
  ] }) }),
4725
4813
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Separator, {}),
4726
4814
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-3", children: [
@@ -4778,13 +4866,18 @@ function ImportWizard({ tableId, onDone, className }) {
4778
4866
  ] })
4779
4867
  ] }),
4780
4868
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4781
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Button, { size: "sm", disabled: busy || !rights.write || rows.length === 0, onClick: () => void run(), children: phase === "declaring" ? "Adding the columns\u2026" : phase === "writing" ? `Writing\u2026 ${progress} of ${rows.length}` : `Import ${rows.length} row${rows.length === 1 ? "" : "s"}` }),
4782
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-muted-foreground", children: [
4783
- mappedCount,
4784
- " of ",
4785
- plan.columns.length,
4786
- " columns go into this table",
4787
- fileHash ? "" : " \xB7 this browser cannot recognise the file again, so importing it twice would write it twice"
4869
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4870
+ import_design_system13.Button,
4871
+ {
4872
+ size: "sm",
4873
+ disabled: busy || !rights.write || rows.length === 0 || titleMissing,
4874
+ onClick: () => void run(),
4875
+ children: phase === "declaring" ? "Adding the columns\u2026" : phase === "writing" ? `Writing\u2026 ${progress} of ${rows.length}` : `Import ${rows.length} row${rows.length === 1 ? "" : "s"}`
4876
+ }
4877
+ ),
4878
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: (0, import_design_system13.cn)("text-muted-foreground", titleMissing && "text-destructive"), children: [
4879
+ titleMissing ? `Nothing goes into ${titleFieldLabel} yet, so these records would have no name. Choose the column above, then this will run.` : `${mappedCount} of ${plan.columns.length} columns go into this table`,
4880
+ fileHash || titleMissing ? "" : " \xB7 this browser cannot recognise the file again, so importing it twice would write it twice"
4788
4881
  ] })
4789
4882
  ] })
4790
4883
  ] }) : null,
@@ -4802,27 +4895,31 @@ function ImportWizard({ tableId, onDone, className }) {
4802
4895
  ] }) : null
4803
4896
  ] }) : null,
4804
4897
  alreadyRan ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: alreadyRan }) : null,
4805
- outcomes.length > 0 || summary ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4898
+ outcomes.length > 0 || summary || ledger ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4806
4899
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Separator, {}),
4807
4900
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
4808
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { children: [
4809
- counts.landed,
4810
- " landed \xB7 ",
4811
- counts.duplicate,
4812
- " already here \xB7 ",
4813
- counts.refused,
4814
- " refused"
4815
- ] }),
4816
- counts.refused + counts.duplicate > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4901
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: ledger ? `${ledger.written} landed \xB7 ${ledger.duplicate} already here \xB7 ${ledger.refused} refused, of ${ledger.seen} row${ledger.seen === 1 ? "" : "s"} offered` : `${counts.landed} landed \xB7 ${counts.duplicate} already here \xB7 ${counts.refused} refused` }),
4902
+ ledger && !ledger.final && phase !== "writing" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-destructive", children: "The run stopped here, so the rest of the file was never offered \u2014 these are the rows that were dealt with before it stopped, not the whole file." }) : null,
4903
+ (ledger ? ledger.refused + ledger.duplicate : counts.refused + counts.duplicate) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4817
4904
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: (0, import_design_system13.cn)("underline underline-offset-2", show === "all" && "font-medium"), onClick: () => setShow("all"), children: "everything that did not land" }),
4818
4905
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: (0, import_design_system13.cn)("underline underline-offset-2", show === "refused" && "font-medium"), onClick: () => setShow("refused"), children: "refused" }),
4819
4906
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: (0, import_design_system13.cn)("underline underline-offset-2", show === "duplicate" && "font-medium"), onClick: () => setShow("duplicate"), children: "already here" })
4820
4907
  ] }) : null
4821
4908
  ] }),
4822
4909
  summary ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: summary }) : null,
4910
+ ledger && show === "all" && ledger.refused + ledger.duplicate > interesting.length ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-muted-foreground", children: [
4911
+ interesting.length,
4912
+ " of the ",
4913
+ ledger.refused + ledger.duplicate,
4914
+ " row",
4915
+ ledger.refused + ledger.duplicate === 1 ? " that" : "s that",
4916
+ " did not land",
4917
+ interesting.length === 1 ? " is" : " are",
4918
+ " listed below by their line in your file \u2014 the store keeps the rest with this run and does not hand every one of them back at once."
4919
+ ] }) : null,
4823
4920
  interesting.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "max-h-64 overflow-auto rounded border", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("table", { className: "w-full", children: [
4824
4921
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("thead", { className: "sticky top-0 bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { children: [
4825
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Row" }),
4922
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Line in your file" }),
4826
4923
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "What happened" }),
4827
4924
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "The row in your file" })
4828
4925
  ] }) }),
@@ -6201,8 +6298,8 @@ function TableSettings({
6201
6298
  const table = (0, import_react36.useTable)(tableId);
6202
6299
  const fields = (0, import_react36.useFields)(tableId);
6203
6300
  const rights = useTableRights(table.data);
6204
- const mutation = (0, import_react36.useRecordMutation)();
6205
6301
  const shape = (0, import_react36.useFieldMutation)();
6302
+ const archive = (0, import_react36.useTableArchive)(tableId);
6206
6303
  const [editing, setEditing] = (0, import_react35.useState)(null);
6207
6304
  const [askingToDelete, setAskingToDelete] = (0, import_react35.useState)(false);
6208
6305
  const [askingToRemove, setAskingToRemove] = (0, import_react35.useState)(null);
@@ -6227,10 +6324,16 @@ function TableSettings({
6227
6324
  table.reload();
6228
6325
  }
6229
6326
  };
6230
- const deleteTable = async () => {
6231
- const gone = await mutation.remove({ record_id: tableId });
6232
- setAskingToDelete(false);
6233
- if (gone !== null) onDeleted?.();
6327
+ const startArchive = async () => {
6328
+ const finished = await archive.start();
6329
+ if (finished?.done && finished.table_archived) {
6330
+ setAskingToDelete(false);
6331
+ onDeleted?.();
6332
+ }
6333
+ };
6334
+ const askToArchive = async () => {
6335
+ setAskingToDelete(true);
6336
+ await archive.preview();
6234
6337
  };
6235
6338
  if (enriching) {
6236
6339
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className, children: [
@@ -6266,7 +6369,6 @@ function TableSettings({
6266
6369
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-muted-foreground", children: rows.length }),
6267
6370
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "outline", className: "ml-auto", onClick: () => setEditing("new"), children: "Add a field" })
6268
6371
  ] }),
6269
- mutation.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: mutation.error }) : null,
6270
6372
  shape.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: shape.error }) : null,
6271
6373
  askingToRemove ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-wrap items-center gap-2 rounded border border-destructive/40 px-2 py-1.5", children: [
6272
6374
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "text-destructive", children: [
@@ -6357,21 +6459,32 @@ function TableSettings({
6357
6459
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "font-medium", children: "This table" }),
6358
6460
  askingToDelete ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
6359
6461
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "text-muted-foreground", children: [
6360
- "Deleting ",
6462
+ "Archiving ",
6361
6463
  table.data ? tableName(table.data) : "this table",
6362
6464
  " takes it out of everyone's list, with its ",
6363
6465
  rows.length,
6364
6466
  " field",
6365
6467
  rows.length === 1 ? "" : "s",
6366
- " and every record in it.",
6367
- table.data?.retention_days ? ` The store keeps them for ${table.data.retention_days} days, so it can still be restored.` : " The store keeps them, so it can still be restored."
6468
+ " and",
6469
+ " ",
6470
+ archive.progress ? `its ${archive.progress.remaining.toLocaleString()} record${archive.progress.remaining === 1 ? "" : "s"}` : "every record in it",
6471
+ ".",
6472
+ table.data?.retention_days ? ` The store keeps them for ${table.data.retention_days} days, so it can still be brought back.` : " The store keeps them, so it can still be brought back."
6368
6473
  ] }),
6369
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex gap-1", children: [
6370
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "destructive", disabled: mutation.saving, onClick: () => void deleteTable(), children: mutation.saving ? "Deleting\u2026" : "Delete this table" }),
6474
+ archive.progress && (archive.running || archive.passes > 0) ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: archive.error ? "text-destructive" : "text-muted-foreground", children: [
6475
+ archive.progress.total - archive.progress.remaining === archive.progress.total ? `All ${archive.progress.total.toLocaleString()} record${archive.progress.total === 1 ? "" : "s"} are put away.` : `${(archive.progress.total - archive.progress.remaining).toLocaleString()} of ${archive.progress.total.toLocaleString()} record${archive.progress.total === 1 ? "" : "s"} put away so far.`,
6476
+ archive.slowedDown ? " The store is busy, so it is taking smaller passes \u2014 it will still finish." : ""
6477
+ ] }) : null,
6478
+ archive.interrupted && archive.progress ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-muted-foreground", children: archive.error ? `That pass did not finish, so the rest is still here. ${archive.progress.archived_total.toLocaleString()} record${archive.progress.archived_total === 1 ? " is" : "s are"} already archived and ${archive.progress.remaining.toLocaleString()} ${archive.progress.remaining === 1 ? "is" : "are"} left. Nothing was lost \u2014 carry on and it picks up where it stopped.` : `Stopped. ${archive.progress.archived_total.toLocaleString()} record${archive.progress.archived_total === 1 ? " is" : "s are"} archived and ${archive.progress.remaining.toLocaleString()} ${archive.progress.remaining === 1 ? "is" : "are"} left. Carry on whenever you like \u2014 it picks up where it stopped.` }) : null,
6479
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex gap-1", children: archive.running ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
6480
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "destructive", disabled: true, children: "Archiving\u2026" }),
6481
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "ghost", onClick: () => archive.stop(), children: "Stop" })
6482
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
6483
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "destructive", onClick: () => void startArchive(), children: archive.interrupted ? "Carry on archiving" : "Archive this table" }),
6371
6484
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "ghost", onClick: () => setAskingToDelete(false), children: "Keep it" })
6372
- ] })
6373
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "outline", onClick: () => setAskingToDelete(true), children: "Delete this table" }) }),
6374
- mutation.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: mutation.error }) : null
6485
+ ] }) })
6486
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "outline", onClick: () => void askToArchive(), children: "Archive this table" }) }),
6487
+ archive.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: archive.error }) : null
6375
6488
  ] })
6376
6489
  ] });
6377
6490
  }
@@ -7671,6 +7784,7 @@ function ViewSwitcher({
7671
7784
  )
7672
7785
  ] });
7673
7786
  }
7787
+ var UNUSUAL_FIELD_NOTE = "an unusual column to use here";
7674
7788
  function offerableFields(all, want) {
7675
7789
  return all.filter((field) => {
7676
7790
  const kind = editorKindFor(field);
@@ -7806,10 +7920,7 @@ function FieldPicker({
7806
7920
  children: [
7807
7921
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: "", children: "Choose a field" }),
7808
7922
  offerable.map((field) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: field.key, children: fieldName(field) }, field.key)),
7809
- unusual ? /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("option", { value: unusual.key, children: [
7810
- fieldName(unusual),
7811
- " \u2014 an unusual column to use here"
7812
- ] }, unusual.key) : null
7923
+ unusual ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: unusual.key, children: `${fieldName(unusual)} \u2014 ${UNUSUAL_FIELD_NOTE}` }, unusual.key) : null
7813
7924
  ]
7814
7925
  }
7815
7926
  ),
@@ -13568,8 +13679,8 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13568
13679
  [client, tableId, load, filter]
13569
13680
  );
13570
13681
  async function create() {
13571
- const groupable = (fields.data ?? []).filter((f) => f.type === "text" || f.type === "list");
13572
- const byField = groupable[0];
13682
+ const all = fields.data ?? [];
13683
+ const byField = offerableFields(all, "choice")[0] ?? all.find((f) => f.type === "text" || f.type === "list");
13573
13684
  const by = byField?.key;
13574
13685
  await declare2(
13575
13686
  {
@@ -13740,6 +13851,32 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13740
13851
  children: CHART_KINDS.map((kind) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("option", { value: kind, children: CHART_KIND_LABEL[kind] }, kind))
13741
13852
  }
13742
13853
  ),
13854
+ (board.blocks[index].groupBy ?? []).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13855
+ GroupingPicker,
13856
+ {
13857
+ block: board.blocks[index],
13858
+ fields: fields.data ?? [],
13859
+ onPick: (key) => void declare2(
13860
+ board,
13861
+ board.blocks.map((b, i) => {
13862
+ if (i !== index) return b;
13863
+ const picked = (fields.data ?? []).find((f) => f.key === key);
13864
+ const wasNamedAfterItsColumn = (fields.data ?? []).some(
13865
+ (f) => b.title === `By ${fieldName(f)}`
13866
+ );
13867
+ return {
13868
+ ...b,
13869
+ groupBy: [key],
13870
+ // A block titled after the column it grouped by
13871
+ // must not keep saying "By Job Number" once it
13872
+ // groups by Status. A title somebody wrote
13873
+ // themselves is left alone.
13874
+ ...wasNamedAfterItsColumn && picked ? { title: `By ${fieldName(picked)}` } : {}
13875
+ };
13876
+ })
13877
+ )
13878
+ }
13879
+ ) : null,
13743
13880
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13744
13881
  import_design_system47.Button,
13745
13882
  {
@@ -13758,6 +13895,45 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13758
13895
  ] })
13759
13896
  ] });
13760
13897
  }
13898
+ function groupingChoices(all) {
13899
+ const once = [];
13900
+ for (const field of all) if (!once.some((f) => f.key === field.key)) once.push(field);
13901
+ const sensible = offerableFields(once, "choice");
13902
+ const isSensible = (field) => sensible.some((s) => s.key === field.key);
13903
+ return [
13904
+ ...sensible.map((field) => ({ field, unusual: false })),
13905
+ ...once.filter((field) => !isSensible(field)).map((field) => ({ field, unusual: true }))
13906
+ ];
13907
+ }
13908
+ function GroupingPicker({
13909
+ block,
13910
+ fields,
13911
+ onPick
13912
+ }) {
13913
+ const choices = groupingChoices(fields);
13914
+ const current = block.groupBy?.[0] ?? "";
13915
+ if (choices.length === 0) {
13916
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "text-muted-foreground", children: "This table has no columns yet, so there is nothing to group this block by." });
13917
+ }
13918
+ const known = choices.some((c) => c.field.key === current);
13919
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
13920
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("label", { className: "text-muted-foreground", htmlFor: `block-group-${block.title}`, children: "Group by" }),
13921
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_design_system47.Select, { value: known ? current : "", onValueChange: onPick, children: [
13922
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13923
+ import_design_system47.SelectTrigger,
13924
+ {
13925
+ size: "sm",
13926
+ id: `block-group-${block.title}`,
13927
+ "aria-label": `Which column ${block.title} is grouped by`,
13928
+ className: "h-7 min-w-[10rem] text-xs",
13929
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_design_system47.SelectValue, { placeholder: "Choose a column" })
13930
+ }
13931
+ ),
13932
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_design_system47.SelectContent, { children: choices.map(({ field, unusual }) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_design_system47.SelectItem, { value: field.key, children: unusual ? `${fieldName(field)} \u2014 ${UNUSUAL_FIELD_NOTE}` : fieldName(field) }, field.key)) })
13933
+ ] }),
13934
+ known ? null : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { className: "text-muted-foreground", children: "The column this block was grouped by is no longer on this table \u2014 choose another." })
13935
+ ] });
13936
+ }
13761
13937
 
13762
13938
  // src/FormsPanel.tsx
13763
13939
  var import_react98 = require("react");