@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.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";
@@ -4822,6 +4929,9 @@ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
4822
4929
  value,
4823
4930
  label: ROLLUP_AGG_LABEL[value]
4824
4931
  }));
4932
+ var FORMULA_SEPARATOR_LABEL = "Joined by";
4933
+ 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.";
4934
+ var FORMULA_DEFAULT_SEPARATOR = " \u2014 ";
4825
4935
  var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
4826
4936
  value,
4827
4937
  label: FORMULA_OP_LABEL[value],
@@ -4858,8 +4968,9 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4858
4968
  const [of, setOf] = useState15(String(field?.config?.["of"] ?? ""));
4859
4969
  const [relationTarget, setRelationTarget] = useState15(String(field?.relation_target ?? ""));
4860
4970
  const [display, setDisplay] = useState15(field?.display ?? null);
4861
- const [formulaOp, setFormulaOp] = useState15("concat");
4862
- const [formulaFields, setFormulaFields] = useState15([]);
4971
+ const [formulaOp, setFormulaOp] = useState15(() => formulaOpOf(field));
4972
+ const [formulaFields, setFormulaFields] = useState15(() => formulaFieldIdsOf(field));
4973
+ const [formulaSeparator, setFormulaSeparator] = useState15(() => formulaSeparatorOf(field));
4863
4974
  const [askingToRemove, setAskingToRemove] = useState15(false);
4864
4975
  const [notLanded, setNotLanded] = useState15(false);
4865
4976
  const others = useMemo12(
@@ -4933,7 +5044,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4933
5044
  ...type === "datetime" ? { kind: withTime ? "datetime" : "date" } : {},
4934
5045
  ...type === "lookup" ? { via, pick } : {},
4935
5046
  ...type === "rollup" ? { via, agg, ...agg === "count" ? {} : { of } } : {},
4936
- ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields) } : {}
5047
+ ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields, formulaSeparator) } : {}
4937
5048
  };
4938
5049
  const written = await mutation.declare({ table_id: tableId, spec });
4939
5050
  if (written === null) return;
@@ -4974,16 +5085,16 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4974
5085
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
4975
5086
  /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: "What it holds" }),
4976
5087
  /* @__PURE__ */ jsxs14(
4977
- Select3,
5088
+ Select4,
4978
5089
  {
4979
5090
  value: type,
4980
5091
  disabled: editing,
4981
5092
  onValueChange: (next) => setType(next),
4982
5093
  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: [
5094
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5095
+ /* @__PURE__ */ jsx18(SelectContent4, { children: FIELD_TYPE_GROUPS.map((group) => /* @__PURE__ */ jsxs14(SelectGroup, { children: [
4985
5096
  /* @__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))
5097
+ FIELD_TYPE_CHOICES.filter((c) => c.group === group).map((c) => /* @__PURE__ */ jsx18(SelectItem4, { value: c.id, children: c.label }, c.id))
4987
5098
  ] }, group)) })
4988
5099
  ]
4989
5100
  }
@@ -4991,6 +5102,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4991
5102
  /* @__PURE__ */ jsx18("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 ?? "" })
4992
5103
  ] })
4993
5104
  ] }),
5105
+ editing && type === "formula" ? /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: EDITING_A_WORKED_OUT_COLUMN }) : null,
4994
5106
  !editing || type === "select" || type === "multi_select" || type === "relation" ? /* @__PURE__ */ jsx18(
4995
5107
  TypeQuestions,
4996
5108
  {
@@ -5019,6 +5131,8 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5019
5131
  setFormulaOp,
5020
5132
  formulaFields,
5021
5133
  setFormulaFields,
5134
+ formulaSeparator,
5135
+ setFormulaSeparator,
5022
5136
  relations,
5023
5137
  others,
5024
5138
  existingOptionsTable: Boolean(field?.options_table_id ?? field?.config?.["options_table_id"])
@@ -5063,16 +5177,16 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5063
5177
  /* @__PURE__ */ jsxs14("div", { className: "grid gap-3 sm:grid-cols-2", children: [
5064
5178
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5065
5179
  /* @__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)) })
5180
+ /* @__PURE__ */ jsxs14(Select4, { value: sensitivity, onValueChange: setSensitivity, children: [
5181
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "Who can see what is in it", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5182
+ /* @__PURE__ */ jsx18(SelectContent4, { children: SENSITIVITIES.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5069
5183
  ] })
5070
5184
  ] }),
5071
5185
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5072
5186
  /* @__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)) })
5187
+ /* @__PURE__ */ jsxs14(Select4, { value: contextPolicy, onValueChange: setContextPolicy, children: [
5188
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "Can an assistant read it", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5189
+ /* @__PURE__ */ jsx18(SelectContent4, { children: AGENT_VISIBILITY.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5076
5190
  ] })
5077
5191
  ] })
5078
5192
  ] }),
@@ -5095,13 +5209,13 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5095
5209
  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
5210
  rules.map((rule, index) => /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1", children: [
5097
5211
  /* @__PURE__ */ jsxs14(
5098
- Select3,
5212
+ Select4,
5099
5213
  {
5100
5214
  value: rule.kind,
5101
5215
  onValueChange: (kind) => setRules(rules.map((r, i) => i === index ? { ...r, kind } : r)),
5102
5216
  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)) })
5217
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", className: "w-52", "aria-label": `Check ${index + 1}`, children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5218
+ /* @__PURE__ */ jsx18(SelectContent4, { children: RULE_KINDS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.kind, children: row.label }, row.kind)) })
5105
5219
  ]
5106
5220
  }
5107
5221
  ),
@@ -5203,10 +5317,30 @@ function startingType(field) {
5203
5317
  if (fieldTypeChoice(kind)) return kind;
5204
5318
  return "text";
5205
5319
  }
5206
- function formulaExpression(op, fieldIds) {
5320
+ function formulaExpression(op, fieldIds, separator) {
5207
5321
  const args = fieldIds.filter((id) => id !== "").map((id) => ({ field: id }));
5208
- if (args.length === 0) return { op, args: [{ const: "" }] };
5209
- return { op, args };
5322
+ const joined = op === "concat" && separator !== "" ? { separator } : {};
5323
+ if (args.length === 0) return { op, ...joined, args: [{ const: "" }] };
5324
+ return { op, ...joined, args };
5325
+ }
5326
+ function formulaExprOf(field) {
5327
+ const expr = field?.config?.["expr"];
5328
+ return expr !== null && typeof expr === "object" ? expr : null;
5329
+ }
5330
+ function formulaOpOf(field) {
5331
+ const op = formulaExprOf(field)?.["op"];
5332
+ return typeof op === "string" && op !== "" ? op : "concat";
5333
+ }
5334
+ function formulaFieldIdsOf(field) {
5335
+ const args = formulaExprOf(field)?.["args"];
5336
+ if (!Array.isArray(args)) return [];
5337
+ return args.map((a) => a !== null && typeof a === "object" ? a["field"] : void 0).filter((id) => typeof id === "string" && id !== "");
5338
+ }
5339
+ function formulaSeparatorOf(field) {
5340
+ const expr = formulaExprOf(field);
5341
+ if (expr === null) return FORMULA_DEFAULT_SEPARATOR;
5342
+ const sep = expr["separator"];
5343
+ return typeof sep === "string" ? sep : "";
5210
5344
  }
5211
5345
  function TypeQuestions(props) {
5212
5346
  const { type } = props;
@@ -5277,9 +5411,9 @@ function TypeQuestions(props) {
5277
5411
  return /* @__PURE__ */ jsxs14("div", { className: "grid gap-3 sm:grid-cols-2", children: [
5278
5412
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5279
5413
  /* @__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: [
5414
+ /* @__PURE__ */ jsxs14(Select4, { value: props.via, onValueChange: props.setVia, children: [
5415
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": type === "rollup" ? "Across the records in" : "Read it from", children: /* @__PURE__ */ jsx18(SelectValue4, { placeholder: "Choose a column" }) }),
5416
+ /* @__PURE__ */ jsx18(SelectContent4, { children: usable.map((f) => /* @__PURE__ */ jsxs14(SelectItem4, { value: f.key, children: [
5283
5417
  fieldName(f),
5284
5418
  type === "rollup" && !f.multi ? " \u2014 holds one" : ""
5285
5419
  ] }, f.id)) })
@@ -5296,9 +5430,9 @@ function TypeQuestions(props) {
5296
5430
  }
5297
5431
  ) : /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5298
5432
  /* @__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)) })
5433
+ /* @__PURE__ */ jsxs14(Select4, { value: props.agg, onValueChange: (v) => props.setAgg(v), children: [
5434
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": "What to work out", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5435
+ /* @__PURE__ */ jsx18(SelectContent4, { children: ROLLUP_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5302
5436
  ] }),
5303
5437
  props.agg !== "count" ? /* @__PURE__ */ jsx18(
5304
5438
  ValueOnTheOtherSide,
@@ -5321,9 +5455,9 @@ function TypeQuestions(props) {
5321
5455
  return /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-2", children: [
5322
5456
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5323
5457
  /* @__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)) })
5458
+ /* @__PURE__ */ jsxs14(Select4, { value: props.formulaOp, onValueChange: props.setFormulaOp, children: [
5459
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", className: "max-w-[18rem]", "aria-label": "What to do", children: /* @__PURE__ */ jsx18(SelectValue4, {}) }),
5460
+ /* @__PURE__ */ jsx18(SelectContent4, { children: FORMULA_OPERATIONS.map((row) => /* @__PURE__ */ jsx18(SelectItem4, { value: row.value, children: row.label }, row.value)) })
5327
5461
  ] })
5328
5462
  ] }),
5329
5463
  /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
@@ -5345,7 +5479,21 @@ function TypeQuestions(props) {
5345
5479
  );
5346
5480
  }) }),
5347
5481
  /* @__PURE__ */ jsx18("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." })
5348
- ] })
5482
+ ] }),
5483
+ props.formulaOp === "concat" ? /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
5484
+ /* @__PURE__ */ jsx18(Label4, { className: "text-xs", children: FORMULA_SEPARATOR_LABEL }),
5485
+ /* @__PURE__ */ jsx18(
5486
+ BasicInput5,
5487
+ {
5488
+ value: props.formulaSeparator,
5489
+ "aria-label": FORMULA_SEPARATOR_LABEL,
5490
+ placeholder: "\u2014",
5491
+ className: "max-w-[10rem]",
5492
+ onChange: (e) => props.setFormulaSeparator(e.target.value)
5493
+ }
5494
+ ),
5495
+ /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: "What goes between the columns. Leave it empty to run them straight together." })
5496
+ ] }) : null
5349
5497
  ] });
5350
5498
  }
5351
5499
  return null;
@@ -5368,9 +5516,9 @@ function ValueOnTheOtherSide({
5368
5516
  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
5517
  fieldName(relation),
5370
5518
  " 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)) })
5519
+ ] }) : /* @__PURE__ */ jsxs14(Select4, { value, onValueChange: onChange, children: [
5520
+ /* @__PURE__ */ jsx18(SelectTrigger4, { size: "sm", "aria-label": label, children: /* @__PURE__ */ jsx18(SelectValue4, { placeholder: "Choose a column" }) }),
5521
+ /* @__PURE__ */ jsx18(SelectContent4, { children: offered.map((column) => /* @__PURE__ */ jsx18(SelectItem4, { value: column.key, children: column.name }, column.key)) })
5374
5522
  ] });
5375
5523
  if (compact) return /* @__PURE__ */ jsx18("div", { className: "mt-1 flex flex-col gap-1", children: body });
5376
5524
  return /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-1", children: [
@@ -5977,8 +6125,8 @@ import { useState as useState18 } from "react";
5977
6125
  import {
5978
6126
  useFieldMutation as useFieldMutation2,
5979
6127
  useFields as useFields8,
5980
- useRecordMutation,
5981
- useTable as useTable6
6128
+ useTable as useTable6,
6129
+ useTableArchive
5982
6130
  } from "@ai-matrx/records/react";
5983
6131
  import { Badge as Badge6, Button as Button16, Separator as Separator6, cn as cn16 } from "@ai-matrx/design-system";
5984
6132
  import { Fragment as Fragment10, jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
@@ -5993,8 +6141,8 @@ function TableSettings({
5993
6141
  const table = useTable6(tableId);
5994
6142
  const fields = useFields8(tableId);
5995
6143
  const rights = useTableRights(table.data);
5996
- const mutation = useRecordMutation();
5997
6144
  const shape = useFieldMutation2();
6145
+ const archive = useTableArchive(tableId);
5998
6146
  const [editing, setEditing] = useState18(null);
5999
6147
  const [askingToDelete, setAskingToDelete] = useState18(false);
6000
6148
  const [askingToRemove, setAskingToRemove] = useState18(null);
@@ -6019,10 +6167,16 @@ function TableSettings({
6019
6167
  table.reload();
6020
6168
  }
6021
6169
  };
6022
- const deleteTable = async () => {
6023
- const gone = await mutation.remove({ record_id: tableId });
6024
- setAskingToDelete(false);
6025
- if (gone !== null) onDeleted?.();
6170
+ const startArchive = async () => {
6171
+ const finished = await archive.start();
6172
+ if (finished?.done && finished.table_archived) {
6173
+ setAskingToDelete(false);
6174
+ onDeleted?.();
6175
+ }
6176
+ };
6177
+ const askToArchive = async () => {
6178
+ setAskingToDelete(true);
6179
+ await archive.preview();
6026
6180
  };
6027
6181
  if (enriching) {
6028
6182
  return /* @__PURE__ */ jsxs17("div", { className, children: [
@@ -6058,7 +6212,6 @@ function TableSettings({
6058
6212
  /* @__PURE__ */ jsx21("span", { className: "text-muted-foreground", children: rows.length }),
6059
6213
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", className: "ml-auto", onClick: () => setEditing("new"), children: "Add a field" })
6060
6214
  ] }),
6061
- mutation.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: mutation.error }) : null,
6062
6215
  shape.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: shape.error }) : null,
6063
6216
  askingToRemove ? /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-2 rounded border border-destructive/40 px-2 py-1.5", children: [
6064
6217
  /* @__PURE__ */ jsxs17("span", { className: "text-destructive", children: [
@@ -6149,21 +6302,32 @@ function TableSettings({
6149
6302
  /* @__PURE__ */ jsx21("span", { className: "font-medium", children: "This table" }),
6150
6303
  askingToDelete ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
6151
6304
  /* @__PURE__ */ jsxs17("p", { className: "text-muted-foreground", children: [
6152
- "Deleting ",
6305
+ "Archiving ",
6153
6306
  table.data ? tableName(table.data) : "this table",
6154
6307
  " takes it out of everyone's list, with its ",
6155
6308
  rows.length,
6156
6309
  " field",
6157
6310
  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."
6311
+ " and",
6312
+ " ",
6313
+ archive.progress ? `its ${archive.progress.remaining.toLocaleString()} record${archive.progress.remaining === 1 ? "" : "s"}` : "every record in it",
6314
+ ".",
6315
+ 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
6316
  ] }),
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" }),
6317
+ archive.progress && (archive.running || archive.passes > 0) ? /* @__PURE__ */ jsxs17("p", { className: archive.error ? "text-destructive" : "text-muted-foreground", children: [
6318
+ 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.`,
6319
+ archive.slowedDown ? " The store is busy, so it is taking smaller passes \u2014 it will still finish." : ""
6320
+ ] }) : null,
6321
+ 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,
6322
+ /* @__PURE__ */ jsx21("div", { className: "flex gap-1", children: archive.running ? /* @__PURE__ */ jsxs17(Fragment10, { children: [
6323
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "destructive", disabled: true, children: "Archiving\u2026" }),
6324
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "ghost", onClick: () => archive.stop(), children: "Stop" })
6325
+ ] }) : /* @__PURE__ */ jsxs17(Fragment10, { children: [
6326
+ /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "destructive", onClick: () => void startArchive(), children: archive.interrupted ? "Carry on archiving" : "Archive this table" }),
6163
6327
  /* @__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
6328
+ ] }) })
6329
+ ] }) : /* @__PURE__ */ jsx21("div", { children: /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", onClick: () => void askToArchive(), children: "Archive this table" }) }),
6330
+ archive.error ? /* @__PURE__ */ jsx21(RefusalNotice, { error: archive.error }) : null
6167
6331
  ] })
6168
6332
  ] });
6169
6333
  }
@@ -6305,7 +6469,7 @@ import { useEffect as useEffect11, useMemo as useMemo16, useState as useState21
6305
6469
  import {
6306
6470
  useFields as useFields9,
6307
6471
  useRecord,
6308
- useRecordMutation as useRecordMutation2
6472
+ useRecordMutation
6309
6473
  } from "@ai-matrx/records/react";
6310
6474
  import { predictWriteRefusals } from "@ai-matrx/records/core";
6311
6475
  import { Button as Button17, Separator as Separator7, cn as cn18 } from "@ai-matrx/design-system";
@@ -6494,7 +6658,7 @@ function RecordForm({
6494
6658
  const host = useRecordsUi();
6495
6659
  const fields = useFields9(tableId, recordType);
6496
6660
  const existing = useRecord(recordId ?? null);
6497
- const mutation = useRecordMutation2();
6661
+ const mutation = useRecordMutation();
6498
6662
  const [draft, setDraft] = useState21({});
6499
6663
  const [touched, setTouched] = useState21(false);
6500
6664
  const loaded = useRecordVersion(recordId ?? null);
@@ -7471,6 +7635,7 @@ function ViewSwitcher({
7471
7635
  )
7472
7636
  ] });
7473
7637
  }
7638
+ var UNUSUAL_FIELD_NOTE = "an unusual column to use here";
7474
7639
  function offerableFields(all, want) {
7475
7640
  return all.filter((field) => {
7476
7641
  const kind = editorKindFor(field);
@@ -7606,10 +7771,7 @@ function FieldPicker({
7606
7771
  children: [
7607
7772
  /* @__PURE__ */ jsx27("option", { value: "", children: "Choose a field" }),
7608
7773
  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
7774
+ unusual ? /* @__PURE__ */ jsx27("option", { value: unusual.key, children: `${fieldName(unusual)} \u2014 ${UNUSUAL_FIELD_NOTE}` }, unusual.key) : null
7613
7775
  ]
7614
7776
  }
7615
7777
  ),
@@ -13350,7 +13512,17 @@ function Drawing({
13350
13512
  // src/DashboardCanvas.tsx
13351
13513
  import { useCallback as useCallback30, useEffect as useEffect34, useMemo as useMemo28, useState as useState47 } from "react";
13352
13514
  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";
13515
+ import {
13516
+ BasicInput as BasicInput16,
13517
+ Button as Button42,
13518
+ Select as Select5,
13519
+ SelectContent as SelectContent5,
13520
+ SelectItem as SelectItem5,
13521
+ SelectTrigger as SelectTrigger5,
13522
+ SelectValue as SelectValue5,
13523
+ Skeleton as Skeleton26,
13524
+ cn as cn44
13525
+ } from "@ai-matrx/design-system";
13354
13526
  import { Fragment as Fragment21, jsx as jsx50, jsxs as jsxs46 } from "react/jsx-runtime";
13355
13527
  function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13356
13528
  const client = useRecordsClient33();
@@ -13424,8 +13596,8 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13424
13596
  [client, tableId, load, filter]
13425
13597
  );
13426
13598
  async function create() {
13427
- const groupable = (fields.data ?? []).filter((f) => f.type === "text" || f.type === "list");
13428
- const byField = groupable[0];
13599
+ const all = fields.data ?? [];
13600
+ const byField = offerableFields(all, "choice")[0] ?? all.find((f) => f.type === "text" || f.type === "list");
13429
13601
  const by = byField?.key;
13430
13602
  await declare2(
13431
13603
  {
@@ -13596,6 +13768,32 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13596
13768
  children: CHART_KINDS.map((kind) => /* @__PURE__ */ jsx50("option", { value: kind, children: CHART_KIND_LABEL[kind] }, kind))
13597
13769
  }
13598
13770
  ),
13771
+ (board.blocks[index].groupBy ?? []).length > 0 ? /* @__PURE__ */ jsx50(
13772
+ GroupingPicker,
13773
+ {
13774
+ block: board.blocks[index],
13775
+ fields: fields.data ?? [],
13776
+ onPick: (key) => void declare2(
13777
+ board,
13778
+ board.blocks.map((b, i) => {
13779
+ if (i !== index) return b;
13780
+ const picked = (fields.data ?? []).find((f) => f.key === key);
13781
+ const wasNamedAfterItsColumn = (fields.data ?? []).some(
13782
+ (f) => b.title === `By ${fieldName(f)}`
13783
+ );
13784
+ return {
13785
+ ...b,
13786
+ groupBy: [key],
13787
+ // A block titled after the column it grouped by
13788
+ // must not keep saying "By Job Number" once it
13789
+ // groups by Status. A title somebody wrote
13790
+ // themselves is left alone.
13791
+ ...wasNamedAfterItsColumn && picked ? { title: `By ${fieldName(picked)}` } : {}
13792
+ };
13793
+ })
13794
+ )
13795
+ }
13796
+ ) : null,
13599
13797
  /* @__PURE__ */ jsx50(
13600
13798
  Button42,
13601
13799
  {
@@ -13614,6 +13812,45 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
13614
13812
  ] })
13615
13813
  ] });
13616
13814
  }
13815
+ function groupingChoices(all) {
13816
+ const once = [];
13817
+ for (const field of all) if (!once.some((f) => f.key === field.key)) once.push(field);
13818
+ const sensible = offerableFields(once, "choice");
13819
+ const isSensible = (field) => sensible.some((s) => s.key === field.key);
13820
+ return [
13821
+ ...sensible.map((field) => ({ field, unusual: false })),
13822
+ ...once.filter((field) => !isSensible(field)).map((field) => ({ field, unusual: true }))
13823
+ ];
13824
+ }
13825
+ function GroupingPicker({
13826
+ block,
13827
+ fields,
13828
+ onPick
13829
+ }) {
13830
+ const choices = groupingChoices(fields);
13831
+ const current = block.groupBy?.[0] ?? "";
13832
+ if (choices.length === 0) {
13833
+ return /* @__PURE__ */ jsx50("span", { className: "text-muted-foreground", children: "This table has no columns yet, so there is nothing to group this block by." });
13834
+ }
13835
+ const known = choices.some((c) => c.field.key === current);
13836
+ return /* @__PURE__ */ jsxs46(Fragment21, { children: [
13837
+ /* @__PURE__ */ jsx50("label", { className: "text-muted-foreground", htmlFor: `block-group-${block.title}`, children: "Group by" }),
13838
+ /* @__PURE__ */ jsxs46(Select5, { value: known ? current : "", onValueChange: onPick, children: [
13839
+ /* @__PURE__ */ jsx50(
13840
+ SelectTrigger5,
13841
+ {
13842
+ size: "sm",
13843
+ id: `block-group-${block.title}`,
13844
+ "aria-label": `Which column ${block.title} is grouped by`,
13845
+ className: "h-7 min-w-[10rem] text-xs",
13846
+ children: /* @__PURE__ */ jsx50(SelectValue5, { placeholder: "Choose a column" })
13847
+ }
13848
+ ),
13849
+ /* @__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)) })
13850
+ ] }),
13851
+ 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." })
13852
+ ] });
13853
+ }
13617
13854
 
13618
13855
  // src/FormsPanel.tsx
13619
13856
  import { useCallback as useCallback31, useEffect as useEffect35, useState as useState48 } from "react";