@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/dist/index.js CHANGED
@@ -4240,10 +4240,30 @@ import {
4240
4240
  importCsv,
4241
4241
  importXlsx
4242
4242
  } from "@ai-matrx/records/core";
4243
- import { Badge as Badge5, Button as Button11, Separator as Separator2, cn as cn12 } from "@ai-matrx/design-system";
4243
+ import {
4244
+ Badge as Badge5,
4245
+ Button as Button11,
4246
+ Select as Select3,
4247
+ SelectContent as SelectContent3,
4248
+ SelectItem as SelectItem3,
4249
+ SelectTrigger as SelectTrigger3,
4250
+ SelectValue as SelectValue3,
4251
+ Separator as Separator2,
4252
+ cn as cn12
4253
+ } from "@ai-matrx/design-system";
4244
4254
  import { Fragment as Fragment7, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
4245
4255
  var BATCH = 250;
4246
4256
  var SAMPLES = 40;
4257
+ var MAPPING_COLUMNS = "minmax(9rem, 11rem) minmax(14rem, 17rem) minmax(13rem, 1fr) minmax(8rem, 10rem)";
4258
+ var ABSENT = "__absent__";
4259
+ function obviousTitleColumn(columns) {
4260
+ const says = /^(title|name|record|customer|client|company|job|item|subject|label|description)\b|(\bname|\btitle|\bnumber|\bid)$/i;
4261
+ const named = columns.find((c) => says.test(c.header.trim()));
4262
+ if (named) return named.header;
4263
+ const text = columns.find((c) => c.type === "text");
4264
+ if (text) return text.header;
4265
+ return columns[0]?.header ?? null;
4266
+ }
4247
4267
  async function sha256(bytes) {
4248
4268
  const subtle = globalThis.crypto?.subtle;
4249
4269
  if (!subtle) return null;
@@ -4278,6 +4298,7 @@ function ImportWizard({ tableId, onDone, className }) {
4278
4298
  const [alreadyRan, setAlreadyRan] = useState13(null);
4279
4299
  const [columnsAdded, setColumnsAdded] = useState13(null);
4280
4300
  const [show, setShow] = useState13("all");
4301
+ const [ledger, setLedger] = useState13(null);
4281
4302
  const declared = fields.data ?? [];
4282
4303
  const take = useCallback8(
4283
4304
  async (file) => {
@@ -4288,6 +4309,7 @@ function ImportWizard({ tableId, onDone, className }) {
4288
4309
  setProposals([]);
4289
4310
  setAlreadyRan(null);
4290
4311
  setProgress(0);
4312
+ setLedger(null);
4291
4313
  try {
4292
4314
  const xlsx = file.name.toLowerCase().endsWith(".xlsx");
4293
4315
  const bytes = await file.arrayBuffer();
@@ -4314,6 +4336,11 @@ function ImportWizard({ tableId, onDone, className }) {
4314
4336
  for (const column of answered.data.columns) {
4315
4337
  if (column.matched) next[column.header] = column.field_key;
4316
4338
  }
4339
+ const titleKey2 = table.data?.title_field ?? null;
4340
+ if (titleKey2 && !Object.values(next).includes(titleKey2)) {
4341
+ const guess = obviousTitleColumn(answered.data.columns);
4342
+ if (guess) next[guess] = titleKey2;
4343
+ }
4317
4344
  setMapping(next);
4318
4345
  setPhase("ready");
4319
4346
  } catch (thrown) {
@@ -4321,7 +4348,7 @@ function ImportWizard({ tableId, onDone, className }) {
4321
4348
  setPhase("waiting");
4322
4349
  }
4323
4350
  },
4324
- [client, declared, tableId]
4351
+ [client, declared, tableId, table.data?.title_field]
4325
4352
  );
4326
4353
  const run = useCallback8(async () => {
4327
4354
  if (!plan) return;
@@ -4329,6 +4356,7 @@ function ImportWizard({ tableId, onDone, className }) {
4329
4356
  setOutcomes([]);
4330
4357
  setColumnsAdded(null);
4331
4358
  setProgress(0);
4359
+ setLedger(null);
4332
4360
  if (unmapped === "create" && rights.admin) {
4333
4361
  setPhase("declaring");
4334
4362
  const made = await client.importDeclareColumns({ table_id: tableId, rows, mapping });
@@ -4364,6 +4392,7 @@ function ImportWizard({ tableId, onDone, className }) {
4364
4392
  }
4365
4393
  const importId = opened.data.import_id;
4366
4394
  const collected = [];
4395
+ const told = { seen: 0, written: 0, duplicate: 0, refused: 0 };
4367
4396
  let take2 = opened.data.rows_per_call ?? BATCH;
4368
4397
  let at = 0;
4369
4398
  while (at < rows.length) {
@@ -4372,11 +4401,17 @@ function ImportWizard({ tableId, onDone, className }) {
4372
4401
  if (!answered.ok) {
4373
4402
  setProblem(answered.error.message);
4374
4403
  setOutcomes([...collected]);
4404
+ setLedger({ ...told, final: false });
4375
4405
  setPhase("ready");
4376
4406
  return;
4377
4407
  }
4378
4408
  collected.push(...answered.data.outcomes);
4379
4409
  setOutcomes([...collected]);
4410
+ told.seen += answered.data.rows_seen;
4411
+ told.written += answered.data.rows_written;
4412
+ told.duplicate += answered.data.rows_duplicate;
4413
+ told.refused += answered.data.rows_refused;
4414
+ setLedger({ ...told, final: false });
4380
4415
  at += batch.length;
4381
4416
  setProgress(Math.min(at, rows.length));
4382
4417
  if (answered.data.rows_per_call) take2 = answered.data.rows_per_call;
@@ -4389,6 +4424,13 @@ function ImportWizard({ tableId, onDone, className }) {
4389
4424
  }
4390
4425
  setSummary(finished.data.message);
4391
4426
  setProposals(finished.data.proposals);
4427
+ setLedger({
4428
+ seen: finished.data.rows_seen,
4429
+ written: finished.data.rows_written,
4430
+ duplicate: finished.data.rows_duplicate,
4431
+ refused: finished.data.rows_refused,
4432
+ final: true
4433
+ });
4392
4434
  setPhase("done");
4393
4435
  onDone?.(finished.data.rows_written);
4394
4436
  }, [client, dedupeKey, fileBytes, fileHash, fileName, format, mapping, onDuplicate, onDone, plan, rows, tableId, unmapped]);
@@ -4408,6 +4450,10 @@ function ImportWizard({ tableId, onDone, className }) {
4408
4450
  [outcomes, show]
4409
4451
  );
4410
4452
  const mappedCount = Object.keys(mapping).length;
4453
+ const titleKey = table.data?.title_field ?? null;
4454
+ const titleMappedTo = titleKey ? Object.entries(mapping).find(([, key]) => key === titleKey)?.[0] ?? null : null;
4455
+ const titleMissing = Boolean(titleKey) && titleMappedTo === null;
4456
+ const titleFieldLabel = (titleKey ? plan?.fields.find((f) => f.key === titleKey)?.label : null) || (titleKey ? declared.find((f) => f.key === titleKey)?.label : null) || "the name column";
4411
4457
  const busy = phase === "reading" || phase === "planning" || phase === "declaring" || phase === "writing";
4412
4458
  return /* @__PURE__ */ jsxs12("div", { className: cn12("flex min-w-0 flex-col gap-2 text-xs", className), children: [
4413
4459
  /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
@@ -4440,55 +4486,107 @@ function ImportWizard({ tableId, onDone, className }) {
4440
4486
  phase === "reading" ? /* @__PURE__ */ jsx16("p", { className: "text-muted-foreground", children: "Reading the file\u2026" }) : null,
4441
4487
  phase === "planning" ? /* @__PURE__ */ jsx16("p", { className: "text-muted-foreground", children: "Working out what each column is\u2026" }) : null,
4442
4488
  plan && parsed ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
4443
- /* @__PURE__ */ jsx16("div", { className: "w-full overflow-x-auto rounded border", children: /* @__PURE__ */ jsxs12("table", { className: "w-full min-w-[50rem] table-fixed", children: [
4444
- /* @__PURE__ */ jsxs12("colgroup", { children: [
4445
- /* @__PURE__ */ jsx16("col", { className: "w-[10rem]" }),
4446
- /* @__PURE__ */ jsx16("col", { className: "w-[17rem]" }),
4447
- /* @__PURE__ */ jsx16("col", { className: "w-[14rem]" }),
4448
- /* @__PURE__ */ jsx16("col", { className: "w-[9rem]" })
4449
- ] }),
4450
- /* @__PURE__ */ jsx16("thead", { className: "bg-muted", children: /* @__PURE__ */ jsxs12("tr", { children: [
4451
- /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "Column in the file" }),
4452
- /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "Goes to" }),
4453
- /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "Kind" }),
4454
- /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "First values" })
4455
- ] }) }),
4456
- /* @__PURE__ */ jsx16("tbody", { children: plan.columns.map((column) => {
4489
+ titleKey ? /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-2 rounded border bg-muted/40 px-2 py-1.5", children: [
4490
+ /* @__PURE__ */ jsx16("span", { className: "font-medium", children: "What each record is called" }),
4491
+ /* @__PURE__ */ jsxs12(
4492
+ Select3,
4493
+ {
4494
+ value: titleMappedTo ?? "",
4495
+ disabled: busy,
4496
+ onValueChange: (chosen) => {
4497
+ setMapping((m) => {
4498
+ const next = { ...m };
4499
+ for (const [header, key] of Object.entries(next)) {
4500
+ if (key === titleKey) delete next[header];
4501
+ }
4502
+ if (chosen) next[chosen] = titleKey;
4503
+ return next;
4504
+ });
4505
+ },
4506
+ children: [
4507
+ /* @__PURE__ */ jsx16(
4508
+ SelectTrigger3,
4509
+ {
4510
+ size: "sm",
4511
+ "aria-label": "Which column each record is named by",
4512
+ className: "min-w-[14rem]",
4513
+ children: /* @__PURE__ */ jsx16(SelectValue3, { placeholder: "\u2014 choose the column \u2014" })
4514
+ }
4515
+ ),
4516
+ /* @__PURE__ */ jsx16(SelectContent3, { children: plan.columns.map((c) => /* @__PURE__ */ jsx16(SelectItem3, { value: c.header, children: c.header }, c.header)) })
4517
+ ]
4518
+ }
4519
+ ),
4520
+ /* @__PURE__ */ jsx16("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.` })
4521
+ ] }) : null,
4522
+ /* @__PURE__ */ jsx16("div", { className: "w-full overflow-x-auto rounded border", children: /* @__PURE__ */ jsxs12("div", { className: "min-w-[52rem]", children: [
4523
+ /* @__PURE__ */ jsxs12(
4524
+ "div",
4525
+ {
4526
+ className: "grid items-center gap-x-3 border-b bg-muted px-2 py-1 font-medium",
4527
+ style: { gridTemplateColumns: MAPPING_COLUMNS },
4528
+ children: [
4529
+ /* @__PURE__ */ jsx16("span", { children: "Column in the file" }),
4530
+ /* @__PURE__ */ jsx16("span", { children: "Goes to" }),
4531
+ /* @__PURE__ */ jsx16("span", { children: "Kind" }),
4532
+ /* @__PURE__ */ jsx16("span", { children: "First values" })
4533
+ ]
4534
+ }
4535
+ ),
4536
+ plan.columns.map((column) => {
4457
4537
  const to = mapping[column.header] ?? "";
4458
- return /* @__PURE__ */ jsxs12("tr", { className: "border-t align-top", children: [
4459
- /* @__PURE__ */ jsx16("td", { className: "truncate px-2 py-1 font-medium", title: column.header, children: column.header }),
4460
- /* @__PURE__ */ jsx16("td", { className: "px-2 py-1", children: /* @__PURE__ */ jsxs12(
4461
- "select",
4462
- {
4463
- "aria-label": `Where ${column.header} goes`,
4464
- className: "w-full min-w-0 rounded border bg-background px-1 py-0.5",
4465
- value: to,
4466
- disabled: busy,
4467
- onChange: (e) => {
4468
- const chosen = e.target.value;
4469
- setMapping((m) => {
4470
- const next = { ...m };
4471
- if (chosen === "") delete next[column.header];
4472
- else next[column.header] = chosen;
4473
- return next;
4474
- });
4475
- },
4476
- children: [
4477
- /* @__PURE__ */ jsx16("option", { value: "", children: unmapped === "ignore" ? "\u2014 leave it out \u2014" : "\u2014 offer it as a new column \u2014" }),
4478
- plan.fields.map((f) => /* @__PURE__ */ jsx16("option", { value: f.key, children: f.label || f.key }, f.field_id))
4479
- ]
4480
- }
4481
- ) }),
4482
- /* @__PURE__ */ jsxs12("td", { className: "px-2 py-1", children: [
4483
- /* @__PURE__ */ jsx16(Badge5, { variant: column.matched ? "secondary" : "outline", className: "text-[11px] font-normal", children: typeWord(column.type) }),
4484
- column.unit ? /* @__PURE__ */ jsx16("span", { className: "ml-1 opacity-60", children: column.unit }) : null,
4485
- /* @__PURE__ */ jsx16("p", { className: "mt-0.5 text-muted-foreground", children: column.why }),
4486
- column.ambiguous ? /* @__PURE__ */ jsx16("p", { className: "text-destructive", children: "Check a date you know before you run this." }) : null,
4487
- column.collides_with ? /* @__PURE__ */ jsx16("p", { className: "text-destructive", children: "Two columns in this file would make the same column." }) : null
4488
- ] }),
4489
- /* @__PURE__ */ jsx16("td", { className: "truncate px-2 py-1 text-muted-foreground", children: (column.samples ?? []).slice(0, 3).map(String).join(" \xB7 ") })
4490
- ] }, column.header);
4491
- }) })
4538
+ const absent = unmapped === "ignore" ? "\u2014 leave it out \u2014" : "\u2014 offer it as a new column \u2014";
4539
+ return /* @__PURE__ */ jsxs12(
4540
+ "div",
4541
+ {
4542
+ className: "grid items-start gap-x-3 border-t px-2 py-1",
4543
+ style: { gridTemplateColumns: MAPPING_COLUMNS },
4544
+ children: [
4545
+ /* @__PURE__ */ jsx16("span", { className: "truncate pt-1.5 font-medium", title: column.header, children: column.header }),
4546
+ /* @__PURE__ */ jsxs12(
4547
+ Select3,
4548
+ {
4549
+ value: to || ABSENT,
4550
+ disabled: busy,
4551
+ onValueChange: (chosen) => {
4552
+ setMapping((m) => {
4553
+ const next = { ...m };
4554
+ if (chosen === ABSENT) delete next[column.header];
4555
+ else next[column.header] = chosen;
4556
+ return next;
4557
+ });
4558
+ },
4559
+ children: [
4560
+ /* @__PURE__ */ jsx16(
4561
+ SelectTrigger3,
4562
+ {
4563
+ size: "sm",
4564
+ "aria-label": `Where ${column.header} goes`,
4565
+ "data-goes-to": column.header,
4566
+ className: "w-full min-w-[14rem]",
4567
+ children: /* @__PURE__ */ jsx16(SelectValue3, {})
4568
+ }
4569
+ ),
4570
+ /* @__PURE__ */ jsxs12(SelectContent3, { children: [
4571
+ /* @__PURE__ */ jsx16(SelectItem3, { value: ABSENT, children: absent }),
4572
+ plan.fields.map((f) => /* @__PURE__ */ jsx16(SelectItem3, { value: f.key, children: f.label || f.key }, f.field_id))
4573
+ ] })
4574
+ ]
4575
+ }
4576
+ ),
4577
+ /* @__PURE__ */ jsxs12("div", { className: "pt-1", children: [
4578
+ /* @__PURE__ */ jsx16(Badge5, { variant: column.matched ? "secondary" : "outline", className: "text-[11px] font-normal", children: typeWord(column.type) }),
4579
+ column.unit ? /* @__PURE__ */ jsx16("span", { className: "ml-1 opacity-60", children: column.unit }) : null,
4580
+ /* @__PURE__ */ jsx16("p", { className: "mt-0.5 text-muted-foreground", children: column.why }),
4581
+ column.ambiguous ? /* @__PURE__ */ jsx16("p", { className: "text-destructive", children: "Check a date you know before you run this." }) : null,
4582
+ column.collides_with ? /* @__PURE__ */ jsx16("p", { className: "text-destructive", children: "Two columns in this file would make the same column." }) : null
4583
+ ] }),
4584
+ /* @__PURE__ */ jsx16("span", { className: "truncate pt-1.5 text-muted-foreground", children: (column.samples ?? []).slice(0, 3).map(String).join(" \xB7 ") })
4585
+ ]
4586
+ },
4587
+ column.header
4588
+ );
4589
+ })
4492
4590
  ] }) }),
4493
4591
  /* @__PURE__ */ jsx16(Separator2, {}),
4494
4592
  /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-3", children: [
@@ -4546,13 +4644,18 @@ function ImportWizard({ tableId, onDone, className }) {
4546
4644
  ] })
4547
4645
  ] }),
4548
4646
  /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
4549
- /* @__PURE__ */ jsx16(Button11, { 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"}` }),
4550
- /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground", children: [
4551
- mappedCount,
4552
- " of ",
4553
- plan.columns.length,
4554
- " columns go into this table",
4555
- fileHash ? "" : " \xB7 this browser cannot recognise the file again, so importing it twice would write it twice"
4647
+ /* @__PURE__ */ jsx16(
4648
+ Button11,
4649
+ {
4650
+ size: "sm",
4651
+ disabled: busy || !rights.write || rows.length === 0 || titleMissing,
4652
+ onClick: () => void run(),
4653
+ children: phase === "declaring" ? "Adding the columns\u2026" : phase === "writing" ? `Writing\u2026 ${progress} of ${rows.length}` : `Import ${rows.length} row${rows.length === 1 ? "" : "s"}`
4654
+ }
4655
+ ),
4656
+ /* @__PURE__ */ jsxs12("span", { className: cn12("text-muted-foreground", titleMissing && "text-destructive"), children: [
4657
+ 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`,
4658
+ fileHash || titleMissing ? "" : " \xB7 this browser cannot recognise the file again, so importing it twice would write it twice"
4556
4659
  ] })
4557
4660
  ] })
4558
4661
  ] }) : null,
@@ -4570,27 +4673,31 @@ function ImportWizard({ tableId, onDone, className }) {
4570
4673
  ] }) : null
4571
4674
  ] }) : null,
4572
4675
  alreadyRan ? /* @__PURE__ */ jsx16("p", { className: "text-muted-foreground", children: alreadyRan }) : null,
4573
- outcomes.length > 0 || summary ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
4676
+ outcomes.length > 0 || summary || ledger ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
4574
4677
  /* @__PURE__ */ jsx16(Separator2, {}),
4575
4678
  /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap items-center gap-2", children: [
4576
- /* @__PURE__ */ jsxs12("span", { children: [
4577
- counts.landed,
4578
- " landed \xB7 ",
4579
- counts.duplicate,
4580
- " already here \xB7 ",
4581
- counts.refused,
4582
- " refused"
4583
- ] }),
4584
- counts.refused + counts.duplicate > 0 ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
4679
+ /* @__PURE__ */ jsx16("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` }),
4680
+ ledger && !ledger.final && phase !== "writing" ? /* @__PURE__ */ jsx16("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,
4681
+ (ledger ? ledger.refused + ledger.duplicate : counts.refused + counts.duplicate) > 0 ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
4585
4682
  /* @__PURE__ */ jsx16("button", { type: "button", className: cn12("underline underline-offset-2", show === "all" && "font-medium"), onClick: () => setShow("all"), children: "everything that did not land" }),
4586
4683
  /* @__PURE__ */ jsx16("button", { type: "button", className: cn12("underline underline-offset-2", show === "refused" && "font-medium"), onClick: () => setShow("refused"), children: "refused" }),
4587
4684
  /* @__PURE__ */ jsx16("button", { type: "button", className: cn12("underline underline-offset-2", show === "duplicate" && "font-medium"), onClick: () => setShow("duplicate"), children: "already here" })
4588
4685
  ] }) : null
4589
4686
  ] }),
4590
4687
  summary ? /* @__PURE__ */ jsx16("p", { className: "text-muted-foreground", children: summary }) : null,
4688
+ ledger && show === "all" && ledger.refused + ledger.duplicate > interesting.length ? /* @__PURE__ */ jsxs12("p", { className: "text-muted-foreground", children: [
4689
+ interesting.length,
4690
+ " of the ",
4691
+ ledger.refused + ledger.duplicate,
4692
+ " row",
4693
+ ledger.refused + ledger.duplicate === 1 ? " that" : "s that",
4694
+ " did not land",
4695
+ interesting.length === 1 ? " is" : " are",
4696
+ " 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."
4697
+ ] }) : null,
4591
4698
  interesting.length > 0 ? /* @__PURE__ */ jsx16("div", { className: "max-h-64 overflow-auto rounded border", children: /* @__PURE__ */ jsxs12("table", { className: "w-full", children: [
4592
4699
  /* @__PURE__ */ jsx16("thead", { className: "sticky top-0 bg-muted", children: /* @__PURE__ */ jsxs12("tr", { children: [
4593
- /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "Row" }),
4700
+ /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "Line in your file" }),
4594
4701
  /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "What happened" }),
4595
4702
  /* @__PURE__ */ jsx16("th", { className: "px-2 py-1 text-left font-medium", children: "The row in your file" })
4596
4703
  ] }) }),
@@ -4803,13 +4910,13 @@ import {
4803
4910
  Button as Button13,
4804
4911
  Checkbox as Checkbox4,
4805
4912
  Label as Label4,
4806
- Select as Select3,
4807
- SelectContent as SelectContent3,
4913
+ Select as Select4,
4914
+ SelectContent as SelectContent4,
4808
4915
  SelectGroup,
4809
- SelectItem as SelectItem3,
4916
+ SelectItem as SelectItem4,
4810
4917
  SelectLabel,
4811
- SelectTrigger as SelectTrigger3,
4812
- SelectValue as SelectValue3,
4918
+ SelectTrigger as SelectTrigger4,
4919
+ SelectValue as SelectValue4,
4813
4920
  Separator as Separator4,
4814
4921
  cn as cn14
4815
4922
  } from "@ai-matrx/design-system";
@@ -4974,16 +5081,16 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4974
5081
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
4975
5082
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "What it holds" }),
4976
5083
  /* @__PURE__ */ jsxs14(
4977
- Select3,
5084
+ Select4,
4978
5085
  {
4979
5086
  value: type,
4980
5087
  disabled: editing,
4981
5088
  onValueChange: (next) => setType(next),
4982
5089
  children: [
4983
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
4984
- /* @__PURE__ */ jsx18(SelectContent3, { children: FIELD_TYPE_GROUPS.map((group) => /* @__PURE__ */ jsxs14(SelectGroup, { children: [
5090
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5091
+ /* @__PURE__ */ jsx18(SelectContent4, { children: FIELD_TYPE_GROUPS.map((group) => /* @__PURE__ */ jsxs14(SelectGroup, { children: [
4985
5092
  /* @__PURE__ */ jsx18(SelectLabel, { children: group }),
4986
- FIELD_TYPE_CHOICES.filter((c) => c.group === group).map((c) => /* @__PURE__ */ jsx18(SelectItem3, { value: c.id, children: c.label }, c.id))
5093
+ FIELD_TYPE_CHOICES.filter((c) => c.group === group).map((c) => /* @__PURE__ */ jsx18(SelectItem4, { value: c.id, children: c.label }, c.id))
4987
5094
  ] }, group)) })
4988
5095
  ]
4989
5096
  }
@@ -5063,16 +5170,16 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5063
5170
  /* @__PURE__ */ jsxs14("div", { className: "grid gap-3 sm:grid-cols-2", children: [
5064
5171
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5065
5172
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "Who can see what is in it" }),
5066
- /* @__PURE__ */ jsxs14(Select3, { value: sensitivity, onValueChange: setSensitivity, children: [
5067
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": "Who can see what is in it", children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
5068
- /* @__PURE__ */ jsx18(SelectContent3, { children: SENSITIVITIES.map((row) => /* @__PURE__ */ jsx18(SelectItem3, { value: row.value, children: row.label }, row.value)) })
5173
+ /* @__PURE__ */ jsxs14(Select4, { value: sensitivity, onValueChange: setSensitivity, children: [
5174
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "Who can see what is in it", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5175
+ /* @__PURE__ */ jsx18(SelectContent4, { children: SENSITIVITIES.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5069
5176
  ] })
5070
5177
  ] }),
5071
5178
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5072
5179
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "Can an assistant read it" }),
5073
- /* @__PURE__ */ jsxs14(Select3, { value: contextPolicy, onValueChange: setContextPolicy, children: [
5074
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": "Can an assistant read it", children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
5075
- /* @__PURE__ */ jsx18(SelectContent3, { children: AGENT_VISIBILITY.map((row) => /* @__PURE__ */ jsx18(SelectItem3, { value: row.value, children: row.label }, row.value)) })
5180
+ /* @__PURE__ */ jsxs14(Select4, { value: contextPolicy, onValueChange: setContextPolicy, children: [
5181
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "Can an assistant read it", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5182
+ /* @__PURE__ */ jsx18(SelectContent4, { children: AGENT_VISIBILITY.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5076
5183
  ] })
5077
5184
  ] })
5078
5185
  ] }),
@@ -5095,13 +5202,13 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5095
5202
  rules.length === 0 ? /* @__PURE__ */ jsx18("p", { className: "text-muted-foreground", children: "No checks. Anything this kind of column can hold will be accepted." }) : null,
5096
5203
  rules.map((rule, index) => /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", children: [
5097
5204
  /* @__PURE__ */ jsxs14(
5098
- Select3,
5205
+ Select4,
5099
5206
  {
5100
5207
  value: rule.kind,
5101
5208
  onValueChange: (kind) => setRules(rules.map((r, i) => i === index ? { ...r, kind } : r)),
5102
5209
  children: [
5103
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", className: "w-52", "aria-label": `Check ${index + 1}`, children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
5104
- /* @__PURE__ */ jsx18(SelectContent3, { children: RULE_KINDS.map((row) => /* @__PURE__ */ jsx18(SelectItem3, { value: row.kind, children: row.label }, row.kind)) })
5210
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", className: "w-52", "aria-label": `Check ${index + 1}`, children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5211
+ /* @__PURE__ */ jsx18(SelectContent4, { children: RULE_KINDS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.kind, children: row.label }, row.kind)) })
5105
5212
  ]
5106
5213
  }
5107
5214
  ),
@@ -5277,9 +5384,9 @@ function TypeQuestions(props) {
5277
5384
  return /* @__PURE__ */ jsxs14("div", { className: "grid gap-3 sm:grid-cols-2", children: [
5278
5385
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5279
5386
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: type === "rollup" ? "Across the records in" : "Read it from" }),
5280
- /* @__PURE__ */ jsxs14(Select3, { value: props.via, onValueChange: props.setVia, children: [
5281
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": type === "rollup" ? "Across the records in" : "Read it from", children: /* @__PURE__ */ jsx18(SelectValue3, { placeholder: "Choose a column" }) }),
5282
- /* @__PURE__ */ jsx18(SelectContent3, { children: usable.map((f) => /* @__PURE__ */ jsxs14(SelectItem3, { value: f.key, children: [
5387
+ /* @__PURE__ */ jsxs14(Select4, { value: props.via, onValueChange: props.setVia, children: [
5388
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": type === "rollup" ? "Across the records in" : "Read it from", children: /* @__PURE__ */ jsx18(SelectValue4, { placeholder: "Choose a column" }) }),
5389
+ /* @__PURE__ */ jsx18(SelectContent4, { children: usable.map((f) => /* @__PURE__ */ jsxs14(SelectItem4, { value: f.key, children: [
5283
5390
  fieldName(f),
5284
5391
  type === "rollup" && !f.multi ? " \u2014 holds one" : ""
5285
5392
  ] }, f.id)) })
@@ -5296,9 +5403,9 @@ function TypeQuestions(props) {
5296
5403
  }
5297
5404
  ) : /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5298
5405
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "What to work out" }),
5299
- /* @__PURE__ */ jsxs14(Select3, { value: props.agg, onValueChange: (v) => props.setAgg(v), children: [
5300
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": "What to work out", children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
5301
- /* @__PURE__ */ jsx18(SelectContent3, { children: ROLLUP_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem3, { value: row.value, children: row.label }, row.value)) })
5406
+ /* @__PURE__ */ jsxs14(Select4, { value: props.agg, onValueChange: (v) => props.setAgg(v), children: [
5407
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "What to work out", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5408
+ /* @__PURE__ */ jsx18(SelectContent4, { children: ROLLUP_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5302
5409
  ] }),
5303
5410
  props.agg !== "count" ? /* @__PURE__ */ jsx18(
5304
5411
  ValueOnTheOtherSide,
@@ -5321,9 +5428,9 @@ function TypeQuestions(props) {
5321
5428
  return /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-2", children: [
5322
5429
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5323
5430
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "What to do" }),
5324
- /* @__PURE__ */ jsxs14(Select3, { value: props.formulaOp, onValueChange: props.setFormulaOp, children: [
5325
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", className: "max-w-[18rem]", "aria-label": "What to do", children: /* @__PURE__ */ jsx18(SelectValue3, {}) }),
5326
- /* @__PURE__ */ jsx18(SelectContent3, { children: FORMULA_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem3, { value: row.value, children: row.label }, row.value)) })
5431
+ /* @__PURE__ */ jsxs14(Select4, { value: props.formulaOp, onValueChange: props.setFormulaOp, children: [
5432
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", className: "max-w-[18rem]", "aria-label": "What to do", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5433
+ /* @__PURE__ */ jsx18(SelectContent4, { children: FORMULA_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5327
5434
  ] })
5328
5435
  ] }),
5329
5436
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
@@ -5368,9 +5475,9 @@ function ValueOnTheOtherSide({
5368
5475
  const body = !relation ? /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: "Choose the column above first." }) : stillAsking ? /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: "Reading that table's columns\u2026" }) : offered.length === 0 ? /* @__PURE__ */ jsxs14("p", { className: "text-[11px] text-muted-foreground", children: [
5369
5476
  fieldName(relation),
5370
5477
  " points at a table that has no columns to read yet. Add one there first."
5371
- ] }) : /* @__PURE__ */ jsxs14(Select3, { value, onValueChange: onChange, children: [
5372
- /* @__PURE__ */ jsx18(SelectTrigger3, { size: "sm", "aria-label": label, children: /* @__PURE__ */ jsx18(SelectValue3, { placeholder: "Choose a column" }) }),
5373
- /* @__PURE__ */ jsx18(SelectContent3, { children: offered.map((column) => /* @__PURE__ */ jsx18(SelectItem3, { value: column.key, children: column.name }, column.key)) })
5478
+ ] }) : /* @__PURE__ */ jsxs14(Select4, { value, onValueChange: onChange, children: [
5479
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": label, children: /* @__PURE__ */ jsx18(SelectValue4, { placeholder: "Choose a column" }) }),
5480
+ /* @__PURE__ */ jsx18(SelectContent4, { children: offered.map((column) => /* @__PURE__ */ jsx18(SelectItem4, { value: column.key, children: column.name }, column.key)) })
5374
5481
  ] });
5375
5482
  if (compact) return /* @__PURE__ */ jsx18("div", { className: "mt-1 flex flex-col gap-1", children: body });
5376
5483
  return /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
@@ -5977,8 +6084,8 @@ import { useState as useState18 } from "react";
5977
6084
  import {
5978
6085
  useFieldMutation as useFieldMutation2,
5979
6086
  useFields as useFields8,
5980
- useRecordMutation,
5981
- useTable as useTable6
6087
+ useTable as useTable6,
6088
+ useTableArchive
5982
6089
  } from "@ai-matrx/records/react";
5983
6090
  import { Badge as Badge6, Button as Button16, Separator as Separator6, cn as cn16 } from "@ai-matrx/design-system";
5984
6091
  import { Fragment as Fragment10, jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
@@ -5993,8 +6100,8 @@ function TableSettings({
5993
6100
  const table = useTable6(tableId);
5994
6101
  const fields = useFields8(tableId);
5995
6102
  const rights = useTableRights(table.data);
5996
- const mutation = useRecordMutation();
5997
6103
  const shape = useFieldMutation2();
6104
+ const archive = useTableArchive(tableId);
5998
6105
  const [editing, setEditing] = useState18(null);
5999
6106
  const [askingToDelete, setAskingToDelete] = useState18(false);
6000
6107
  const [askingToRemove, setAskingToRemove] = useState18(null);
@@ -6019,10 +6126,16 @@ function TableSettings({
6019
6126
  table.reload();
6020
6127
  }
6021
6128
  };
6022
- const deleteTable = async () => {
6023
- const gone = await mutation.remove({ record_id: tableId });
6024
- setAskingToDelete(false);
6025
- if (gone !== null) onDeleted?.();
6129
+ const startArchive = async () => {
6130
+ const finished = await archive.start();
6131
+ if (finished?.done && finished.table_archived) {
6132
+ setAskingToDelete(false);
6133
+ onDeleted?.();
6134
+ }
6135
+ };
6136
+ const askToArchive = async () => {
6137
+ setAskingToDelete(true);
6138
+ await archive.preview();
6026
6139
  };
6027
6140
  if (enriching) {
6028
6141
  return /* @__PURE__ */ jsxs17("div", { className, children: [
@@ -6058,7 +6171,6 @@ function TableSettings({
6058
6171
  /* @__PURE__ */ jsx21("span", { className: "text-muted-foreground", children: rows.length }),
6059
6172
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", className: "ml-auto", onClick: () => setEditing("new"), children: "Add a field" })
6060
6173
  ] }),
6061
- mutation.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: mutation.error }) : null,
6062
6174
  shape.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: shape.error }) : null,
6063
6175
  askingToRemove ? /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-2 rounded border border-destructive/40 px-2 py-1.5", children: [
6064
6176
  /* @__PURE__ */ jsxs17("span", { className: "text-destructive", children: [
@@ -6149,21 +6261,32 @@ function TableSettings({
6149
6261
  /* @__PURE__ */ jsx21("span", { className: "font-medium", children: "This table" }),
6150
6262
  askingToDelete ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
6151
6263
  /* @__PURE__ */ jsxs17("p", { className: "text-muted-foreground", children: [
6152
- "Deleting ",
6264
+ "Archiving ",
6153
6265
  table.data ? tableName(table.data) : "this table",
6154
6266
  " takes it out of everyone's list, with its ",
6155
6267
  rows.length,
6156
6268
  " field",
6157
6269
  rows.length === 1 ? "" : "s",
6158
- " and every record in it.",
6159
- 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."
6270
+ " and",
6271
+ " ",
6272
+ archive.progress ? `its ${archive.progress.remaining.toLocaleString()} record${archive.progress.remaining === 1 ? "" : "s"}` : "every record in it",
6273
+ ".",
6274
+ 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."
6160
6275
  ] }),
6161
- /* @__PURE__ */ jsxs17("div", { className: "flex gap-1", children: [
6162
- /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "destructive", disabled: mutation.saving, onClick: () => void deleteTable(), children: mutation.saving ? "Deleting\u2026" : "Delete this table" }),
6276
+ archive.progress && (archive.running || archive.passes > 0) ? /* @__PURE__ */ jsxs17("p", { className: archive.error ? "text-destructive" : "text-muted-foreground", children: [
6277
+ 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.`,
6278
+ archive.slowedDown ? " The store is busy, so it is taking smaller passes \u2014 it will still finish." : ""
6279
+ ] }) : null,
6280
+ archive.interrupted && archive.progress ? /* @__PURE__ */ jsx21("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,
6281
+ /* @__PURE__ */ jsx21("div", { className: "flex gap-1", children: archive.running ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
6282
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "destructive", disabled: true, children: "Archiving\u2026" }),
6283
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "ghost", onClick: () => archive.stop(), children: "Stop" })
6284
+ ] }) : /* @__PURE__ */ jsxs17(Fragment10, { children: [
6285
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "destructive", onClick: () => void startArchive(), children: archive.interrupted ? "Carry on archiving" : "Archive this table" }),
6163
6286
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "ghost", onClick: () => setAskingToDelete(false), children: "Keep it" })
6164
- ] })
6165
- ] }) : /* @__PURE__ */ jsx21("div", { children: /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", onClick: () => setAskingToDelete(true), children: "Delete this table" }) }),
6166
- mutation.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: mutation.error }) : null
6287
+ ] }) })
6288
+ ] }) : /* @__PURE__ */ jsx21("div", { children: /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", onClick: () => void askToArchive(), children: "Archive this table" }) }),
6289
+ archive.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: archive.error }) : null
6167
6290
  ] })
6168
6291
  ] });
6169
6292
  }
@@ -6305,7 +6428,7 @@ import { useEffect as useEffect11, useMemo as useMemo16, useState as useState21
6305
6428
  import {
6306
6429
  useFields as useFields9,
6307
6430
  useRecord,
6308
- useRecordMutation as useRecordMutation2
6431
+ useRecordMutation
6309
6432
  } from "@ai-matrx/records/react";
6310
6433
  import { predictWriteRefusals } from "@ai-matrx/records/core";
6311
6434
  import { Button as Button17, Separator as Separator7, cn as cn18 } from "@ai-matrx/design-system";
@@ -6494,7 +6617,7 @@ function RecordForm({
6494
6617
  const host = useRecordsUi();
6495
6618
  const fields = useFields9(tableId, recordType);
6496
6619
  const existing = useRecord(recordId ?? null);
6497
- const mutation = useRecordMutation2();
6620
+ const mutation = useRecordMutation();
6498
6621
  const [draft, setDraft] = useState21({});
6499
6622
  const [touched, setTouched] = useState21(false);
6500
6623
  const loaded = useRecordVersion(recordId ?? null);
@@ -7471,6 +7594,7 @@ function ViewSwitcher({
7471
7594
  )
7472
7595
  ] });
7473
7596
  }
7597
+ var UNUSUAL_FIELD_NOTE = "an unusual column to use here";
7474
7598
  function offerableFields(all, want) {
7475
7599
  return all.filter((field) => {
7476
7600
  const kind = editorKindFor(field);
@@ -7606,10 +7730,7 @@ function FieldPicker({
7606
7730
  children: [
7607
7731
  /* @__PURE__ */ jsx27("option", { value: "", children: "Choose a field" }),
7608
7732
  offerable.map((field) => /* @__PURE__ */ jsx27("option", { value: field.key, children: fieldName(field) }, field.key)),
7609
- unusual ? /* @__PURE__ */ jsxs23("option", { value: unusual.key, children: [
7610
- fieldName(unusual),
7611
- " \u2014 an unusual column to use here"
7612
- ] }, unusual.key) : null
7733
+ unusual ? /* @__PURE__ */ jsx27("option", { value: unusual.key, children: `${fieldName(unusual)} \u2014 ${UNUSUAL_FIELD_NOTE}` }, unusual.key) : null
7613
7734
  ]
7614
7735
  }
7615
7736
  ),
@@ -13350,7 +13471,17 @@ function Drawing({
13350
13471
  // src/DashboardCanvas.tsx
13351
13472
  import { useCallback as useCallback30, useEffect as useEffect34, useMemo as useMemo28, useState as useState47 } from "react";
13352
13473
  import { useFields as useFields21, useRecordsClient as useRecordsClient33, useTable as useTable18 } from "@ai-matrx/records/react";
13353
- import { BasicInput as BasicInput16, Button as Button42, Skeleton as Skeleton26, cn as cn44 } from "@ai-matrx/design-system";
13474
+ import {
13475
+ BasicInput as BasicInput16,
13476
+ Button as Button42,
13477
+ Select as Select5,
13478
+ SelectContent as SelectContent5,
13479
+ SelectItem as SelectItem5,
13480
+ SelectTrigger as SelectTrigger5,
13481
+ SelectValue as SelectValue5,
13482
+ Skeleton as Skeleton26,
13483
+ cn as cn44
13484
+ } from "@ai-matrx/design-system";
13354
13485
  import { Fragment as Fragment21, jsx as jsx50, jsxs as jsxs46 } from "react/jsx-runtime";
13355
13486
  function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13356
13487
  const client = useRecordsClient33();
@@ -13424,8 +13555,8 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13424
13555
  [client, tableId, load, filter]
13425
13556
  );
13426
13557
  async function create() {
13427
- const groupable = (fields.data ?? []).filter((f) => f.type === "text" || f.type === "list");
13428
- const byField = groupable[0];
13558
+ const all = fields.data ?? [];
13559
+ const byField = offerableFields(all, "choice")[0] ?? all.find((f) => f.type === "text" || f.type === "list");
13429
13560
  const by = byField?.key;
13430
13561
  await declare2(
13431
13562
  {
@@ -13596,6 +13727,32 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13596
13727
  children: CHART_KINDS.map((kind) => /* @__PURE__ */ jsx50("option", { value: kind, children: CHART_KIND_LABEL[kind] }, kind))
13597
13728
  }
13598
13729
  ),
13730
+ (board.blocks[index].groupBy ?? []).length > 0 ? /* @__PURE__ */ jsx50(
13731
+ GroupingPicker,
13732
+ {
13733
+ block: board.blocks[index],
13734
+ fields: fields.data ?? [],
13735
+ onPick: (key) => void declare2(
13736
+ board,
13737
+ board.blocks.map((b, i) => {
13738
+ if (i !== index) return b;
13739
+ const picked = (fields.data ?? []).find((f) => f.key === key);
13740
+ const wasNamedAfterItsColumn = (fields.data ?? []).some(
13741
+ (f) => b.title === `By ${fieldName(f)}`
13742
+ );
13743
+ return {
13744
+ ...b,
13745
+ groupBy: [key],
13746
+ // A block titled after the column it grouped by
13747
+ // must not keep saying "By Job Number" once it
13748
+ // groups by Status. A title somebody wrote
13749
+ // themselves is left alone.
13750
+ ...wasNamedAfterItsColumn && picked ? { title: `By ${fieldName(picked)}` } : {}
13751
+ };
13752
+ })
13753
+ )
13754
+ }
13755
+ ) : null,
13599
13756
  /* @__PURE__ */ jsx50(
13600
13757
  Button42,
13601
13758
  {
@@ -13614,6 +13771,45 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13614
13771
  ] })
13615
13772
  ] });
13616
13773
  }
13774
+ function groupingChoices(all) {
13775
+ const once = [];
13776
+ for (const field of all) if (!once.some((f) => f.key === field.key)) once.push(field);
13777
+ const sensible = offerableFields(once, "choice");
13778
+ const isSensible = (field) => sensible.some((s) => s.key === field.key);
13779
+ return [
13780
+ ...sensible.map((field) => ({ field, unusual: false })),
13781
+ ...once.filter((field) => !isSensible(field)).map((field) => ({ field, unusual: true }))
13782
+ ];
13783
+ }
13784
+ function GroupingPicker({
13785
+ block,
13786
+ fields,
13787
+ onPick
13788
+ }) {
13789
+ const choices = groupingChoices(fields);
13790
+ const current = block.groupBy?.[0] ?? "";
13791
+ if (choices.length === 0) {
13792
+ return /* @__PURE__ */ jsx50("span", { className: "text-muted-foreground", children: "This table has no columns yet, so there is nothing to group this block by." });
13793
+ }
13794
+ const known = choices.some((c) => c.field.key === current);
13795
+ return /* @__PURE__ */ jsxs46(Fragment21, { children: [
13796
+ /* @__PURE__ */ jsx50("label", { className: "text-muted-foreground", htmlFor: `block-group-${block.title}`, children: "Group by" }),
13797
+ /* @__PURE__ */ jsxs46(Select5, { value: known ? current : "", onValueChange: onPick, children: [
13798
+ /* @__PURE__ */ jsx50(
13799
+ SelectTrigger5,
13800
+ {
13801
+ size: "sm",
13802
+ id: `block-group-${block.title}`,
13803
+ "aria-label": `Which column ${block.title} is grouped by`,
13804
+ className: "h-7 min-w-[10rem] text-xs",
13805
+ children: /* @__PURE__ */ jsx50(SelectValue5, { placeholder: "Choose a column" })
13806
+ }
13807
+ ),
13808
+ /* @__PURE__ */ jsx50(SelectContent5, { children: choices.map(({ field, unusual }) => /* @__PURE__ */ jsx50(SelectItem5, { value: field.key, children: unusual ? `${fieldName(field)} \u2014 ${UNUSUAL_FIELD_NOTE}` : fieldName(field) }, field.key)) })
13809
+ ] }),
13810
+ known ? null : /* @__PURE__ */ jsx50("span", { className: "text-muted-foreground", children: "The column this block was grouped by is no longer on this table \u2014 choose another." })
13811
+ ] });
13812
+ }
13617
13813
 
13618
13814
  // src/FormsPanel.tsx
13619
13815
  import { useCallback as useCallback31, useEffect as useEffect35, useState as useState48 } from "react";