@ai-matrx/records-ui 0.77.0 → 0.79.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/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
  ] }) }),
@@ -5035,6 +5132,9 @@ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
5035
5132
  value,
5036
5133
  label: ROLLUP_AGG_LABEL[value]
5037
5134
  }));
5135
+ var FORMULA_SEPARATOR_LABEL = "Joined by";
5136
+ var EDITING_A_WORKED_OUT_COLUMN = "This column works out its answer from other columns. Which columns it uses, and what joins them, cannot be changed on this screen yet \u2014 remove it and add it again to change them.";
5137
+ var FORMULA_DEFAULT_SEPARATOR = " \u2014 ";
5038
5138
  var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
5039
5139
  value,
5040
5140
  label: FORMULA_OP_LABEL[value],
@@ -5071,8 +5171,9 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5071
5171
  const [of, setOf] = (0, import_react30.useState)(String(field?.config?.["of"] ?? ""));
5072
5172
  const [relationTarget, setRelationTarget] = (0, import_react30.useState)(String(field?.relation_target ?? ""));
5073
5173
  const [display, setDisplay] = (0, import_react30.useState)(field?.display ?? null);
5074
- const [formulaOp, setFormulaOp] = (0, import_react30.useState)("concat");
5075
- const [formulaFields, setFormulaFields] = (0, import_react30.useState)([]);
5174
+ const [formulaOp, setFormulaOp] = (0, import_react30.useState)(() => formulaOpOf(field));
5175
+ const [formulaFields, setFormulaFields] = (0, import_react30.useState)(() => formulaFieldIdsOf(field));
5176
+ const [formulaSeparator, setFormulaSeparator] = (0, import_react30.useState)(() => formulaSeparatorOf(field));
5076
5177
  const [askingToRemove, setAskingToRemove] = (0, import_react30.useState)(false);
5077
5178
  const [notLanded, setNotLanded] = (0, import_react30.useState)(false);
5078
5179
  const others = (0, import_react30.useMemo)(
@@ -5146,7 +5247,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5146
5247
  ...type === "datetime" ? { kind: withTime ? "datetime" : "date" } : {},
5147
5248
  ...type === "lookup" ? { via, pick } : {},
5148
5249
  ...type === "rollup" ? { via, agg, ...agg === "count" ? {} : { of } } : {},
5149
- ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields) } : {}
5250
+ ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields, formulaSeparator) } : {}
5150
5251
  };
5151
5252
  const written = await mutation.declare({ table_id: tableId, spec });
5152
5253
  if (written === null) return;
@@ -5204,6 +5305,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5204
5305
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: editing ? "What a column holds cannot be changed here \u2014 every value already saved would have to be converted. Add a new column and move the values across." : choice?.explanation ?? "" })
5205
5306
  ] })
5206
5307
  ] }),
5308
+ editing && type === "formula" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: EDITING_A_WORKED_OUT_COLUMN }) : null,
5207
5309
  !editing || type === "select" || type === "multi_select" || type === "relation" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5208
5310
  TypeQuestions,
5209
5311
  {
@@ -5232,6 +5334,8 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5232
5334
  setFormulaOp,
5233
5335
  formulaFields,
5234
5336
  setFormulaFields,
5337
+ formulaSeparator,
5338
+ setFormulaSeparator,
5235
5339
  relations,
5236
5340
  others,
5237
5341
  existingOptionsTable: Boolean(field?.options_table_id ?? field?.config?.["options_table_id"])
@@ -5416,10 +5520,30 @@ function startingType(field) {
5416
5520
  if (fieldTypeChoice(kind)) return kind;
5417
5521
  return "text";
5418
5522
  }
5419
- function formulaExpression(op, fieldIds) {
5523
+ function formulaExpression(op, fieldIds, separator) {
5420
5524
  const args = fieldIds.filter((id) => id !== "").map((id) => ({ field: id }));
5421
- if (args.length === 0) return { op, args: [{ const: "" }] };
5422
- return { op, args };
5525
+ const joined = op === "concat" && separator !== "" ? { separator } : {};
5526
+ if (args.length === 0) return { op, ...joined, args: [{ const: "" }] };
5527
+ return { op, ...joined, args };
5528
+ }
5529
+ function formulaExprOf(field) {
5530
+ const expr = field?.config?.["expr"];
5531
+ return expr !== null && typeof expr === "object" ? expr : null;
5532
+ }
5533
+ function formulaOpOf(field) {
5534
+ const op = formulaExprOf(field)?.["op"];
5535
+ return typeof op === "string" && op !== "" ? op : "concat";
5536
+ }
5537
+ function formulaFieldIdsOf(field) {
5538
+ const args = formulaExprOf(field)?.["args"];
5539
+ if (!Array.isArray(args)) return [];
5540
+ return args.map((a) => a !== null && typeof a === "object" ? a["field"] : void 0).filter((id) => typeof id === "string" && id !== "");
5541
+ }
5542
+ function formulaSeparatorOf(field) {
5543
+ const expr = formulaExprOf(field);
5544
+ if (expr === null) return FORMULA_DEFAULT_SEPARATOR;
5545
+ const sep = expr["separator"];
5546
+ return typeof sep === "string" ? sep : "";
5423
5547
  }
5424
5548
  function TypeQuestions(props) {
5425
5549
  const { type } = props;
@@ -5558,7 +5682,21 @@ function TypeQuestions(props) {
5558
5682
  );
5559
5683
  }) }),
5560
5684
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "In the order you pick them. Columns are remembered by which column they are, so renaming one never breaks this." })
5561
- ] })
5685
+ ] }),
5686
+ props.formulaOp === "concat" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-1", children: [
5687
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_design_system15.Label, { className: "text-xs", children: FORMULA_SEPARATOR_LABEL }),
5688
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5689
+ import_design_system15.BasicInput,
5690
+ {
5691
+ value: props.formulaSeparator,
5692
+ "aria-label": FORMULA_SEPARATOR_LABEL,
5693
+ placeholder: "\u2014",
5694
+ className: "max-w-[10rem]",
5695
+ onChange: (e) => props.setFormulaSeparator(e.target.value)
5696
+ }
5697
+ ),
5698
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "What goes between the columns. Leave it empty to run them straight together." })
5699
+ ] }) : null
5562
5700
  ] });
5563
5701
  }
5564
5702
  return null;
@@ -6201,8 +6339,8 @@ function TableSettings({
6201
6339
  const table = (0, import_react36.useTable)(tableId);
6202
6340
  const fields = (0, import_react36.useFields)(tableId);
6203
6341
  const rights = useTableRights(table.data);
6204
- const mutation = (0, import_react36.useRecordMutation)();
6205
6342
  const shape = (0, import_react36.useFieldMutation)();
6343
+ const archive = (0, import_react36.useTableArchive)(tableId);
6206
6344
  const [editing, setEditing] = (0, import_react35.useState)(null);
6207
6345
  const [askingToDelete, setAskingToDelete] = (0, import_react35.useState)(false);
6208
6346
  const [askingToRemove, setAskingToRemove] = (0, import_react35.useState)(null);
@@ -6227,10 +6365,16 @@ function TableSettings({
6227
6365
  table.reload();
6228
6366
  }
6229
6367
  };
6230
- const deleteTable = async () => {
6231
- const gone = await mutation.remove({ record_id: tableId });
6232
- setAskingToDelete(false);
6233
- if (gone !== null) onDeleted?.();
6368
+ const startArchive = async () => {
6369
+ const finished = await archive.start();
6370
+ if (finished?.done && finished.table_archived) {
6371
+ setAskingToDelete(false);
6372
+ onDeleted?.();
6373
+ }
6374
+ };
6375
+ const askToArchive = async () => {
6376
+ setAskingToDelete(true);
6377
+ await archive.preview();
6234
6378
  };
6235
6379
  if (enriching) {
6236
6380
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className, children: [
@@ -6266,7 +6410,6 @@ function TableSettings({
6266
6410
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-muted-foreground", children: rows.length }),
6267
6411
  /* @__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
6412
  ] }),
6269
- mutation.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: mutation.error }) : null,
6270
6413
  shape.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: shape.error }) : null,
6271
6414
  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
6415
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "text-destructive", children: [
@@ -6357,21 +6500,32 @@ function TableSettings({
6357
6500
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "font-medium", children: "This table" }),
6358
6501
  askingToDelete ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
6359
6502
  /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: "text-muted-foreground", children: [
6360
- "Deleting ",
6503
+ "Archiving ",
6361
6504
  table.data ? tableName(table.data) : "this table",
6362
6505
  " takes it out of everyone's list, with its ",
6363
6506
  rows.length,
6364
6507
  " field",
6365
6508
  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."
6509
+ " and",
6510
+ " ",
6511
+ archive.progress ? `its ${archive.progress.remaining.toLocaleString()} record${archive.progress.remaining === 1 ? "" : "s"}` : "every record in it",
6512
+ ".",
6513
+ 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
6514
  ] }),
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" }),
6515
+ archive.progress && (archive.running || archive.passes > 0) ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("p", { className: archive.error ? "text-destructive" : "text-muted-foreground", children: [
6516
+ 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.`,
6517
+ archive.slowedDown ? " The store is busy, so it is taking smaller passes \u2014 it will still finish." : ""
6518
+ ] }) : null,
6519
+ 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,
6520
+ /* @__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: [
6521
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "destructive", disabled: true, children: "Archiving\u2026" }),
6522
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_design_system18.Button, { size: "sm", variant: "ghost", onClick: () => archive.stop(), children: "Stop" })
6523
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
6524
+ /* @__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
6525
  /* @__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
6526
+ ] }) })
6527
+ ] }) : /* @__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" }) }),
6528
+ archive.error ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(RefusalNotice, { error: archive.error }) : null
6375
6529
  ] })
6376
6530
  ] });
6377
6531
  }
@@ -7671,6 +7825,7 @@ function ViewSwitcher({
7671
7825
  )
7672
7826
  ] });
7673
7827
  }
7828
+ var UNUSUAL_FIELD_NOTE = "an unusual column to use here";
7674
7829
  function offerableFields(all, want) {
7675
7830
  return all.filter((field) => {
7676
7831
  const kind = editorKindFor(field);
@@ -7806,10 +7961,7 @@ function FieldPicker({
7806
7961
  children: [
7807
7962
  /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: "", children: "Choose a field" }),
7808
7963
  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
7964
+ unusual ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("option", { value: unusual.key, children: `${fieldName(unusual)} \u2014 ${UNUSUAL_FIELD_NOTE}` }, unusual.key) : null
7813
7965
  ]
7814
7966
  }
7815
7967
  ),
@@ -13568,8 +13720,8 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13568
13720
  [client, tableId, load, filter]
13569
13721
  );
13570
13722
  async function create() {
13571
- const groupable = (fields.data ?? []).filter((f) => f.type === "text" || f.type === "list");
13572
- const byField = groupable[0];
13723
+ const all = fields.data ?? [];
13724
+ const byField = offerableFields(all, "choice")[0] ?? all.find((f) => f.type === "text" || f.type === "list");
13573
13725
  const by = byField?.key;
13574
13726
  await declare2(
13575
13727
  {
@@ -13740,6 +13892,32 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13740
13892
  children: CHART_KINDS.map((kind) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("option", { value: kind, children: CHART_KIND_LABEL[kind] }, kind))
13741
13893
  }
13742
13894
  ),
13895
+ (board.blocks[index].groupBy ?? []).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13896
+ GroupingPicker,
13897
+ {
13898
+ block: board.blocks[index],
13899
+ fields: fields.data ?? [],
13900
+ onPick: (key) => void declare2(
13901
+ board,
13902
+ board.blocks.map((b, i) => {
13903
+ if (i !== index) return b;
13904
+ const picked = (fields.data ?? []).find((f) => f.key === key);
13905
+ const wasNamedAfterItsColumn = (fields.data ?? []).some(
13906
+ (f) => b.title === `By ${fieldName(f)}`
13907
+ );
13908
+ return {
13909
+ ...b,
13910
+ groupBy: [key],
13911
+ // A block titled after the column it grouped by
13912
+ // must not keep saying "By Job Number" once it
13913
+ // groups by Status. A title somebody wrote
13914
+ // themselves is left alone.
13915
+ ...wasNamedAfterItsColumn && picked ? { title: `By ${fieldName(picked)}` } : {}
13916
+ };
13917
+ })
13918
+ )
13919
+ }
13920
+ ) : null,
13743
13921
  /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13744
13922
  import_design_system47.Button,
13745
13923
  {
@@ -13758,6 +13936,45 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13758
13936
  ] })
13759
13937
  ] });
13760
13938
  }
13939
+ function groupingChoices(all) {
13940
+ const once = [];
13941
+ for (const field of all) if (!once.some((f) => f.key === field.key)) once.push(field);
13942
+ const sensible = offerableFields(once, "choice");
13943
+ const isSensible = (field) => sensible.some((s) => s.key === field.key);
13944
+ return [
13945
+ ...sensible.map((field) => ({ field, unusual: false })),
13946
+ ...once.filter((field) => !isSensible(field)).map((field) => ({ field, unusual: true }))
13947
+ ];
13948
+ }
13949
+ function GroupingPicker({
13950
+ block,
13951
+ fields,
13952
+ onPick
13953
+ }) {
13954
+ const choices = groupingChoices(fields);
13955
+ const current = block.groupBy?.[0] ?? "";
13956
+ if (choices.length === 0) {
13957
+ 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." });
13958
+ }
13959
+ const known = choices.some((c) => c.field.key === current);
13960
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_jsx_runtime50.Fragment, { children: [
13961
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("label", { className: "text-muted-foreground", htmlFor: `block-group-${block.title}`, children: "Group by" }),
13962
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_design_system47.Select, { value: known ? current : "", onValueChange: onPick, children: [
13963
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
13964
+ import_design_system47.SelectTrigger,
13965
+ {
13966
+ size: "sm",
13967
+ id: `block-group-${block.title}`,
13968
+ "aria-label": `Which column ${block.title} is grouped by`,
13969
+ className: "h-7 min-w-[10rem] text-xs",
13970
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_design_system47.SelectValue, { placeholder: "Choose a column" })
13971
+ }
13972
+ ),
13973
+ /* @__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)) })
13974
+ ] }),
13975
+ 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." })
13976
+ ] });
13977
+ }
13761
13978
 
13762
13979
  // src/FormsPanel.tsx
13763
13980
  var import_react98 = require("react");