@ai-matrx/records-ui 0.78.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.81.0
4
+
5
+ **The relation picker stops cutting a customer's name in half, and a grid mount stops
6
+ screaming at the console.** (lane FIX-14, from FIX-13's "named, not fixed" list.)
7
+
8
+ **1. `Create “Marisol Okonkwo — 418 Calle Pu`.** The picker pasted a per-surface `w-64`
9
+ over the design system's popover, and the Create offer had no truncation of its own, so the
10
+ one control that exists to show the name you are about to create cut it mid-word. Fixed
11
+ where it belongs: `@ai-matrx/design-system` 0.35.0 gained `sizing="content"` and decides
12
+ the width; this package sets no width at all. The offer and every candidate row truncate
13
+ with a real ellipsis and carry the whole name as a `title`, and `RecordChip` does the same
14
+ everywhere it is drawn. Measured headlessly against the live store: 408px showing the whole
15
+ name at 1600×1000, 343px with a real `text-overflow: ellipsis` at 375 wide, inside the
16
+ viewport both times, 0 page errors.
17
+
18
+ **2. `custom.enrich_cells … does not exist` on every grid mount.** The DOOR was never
19
+ missing — `custom.enrich_cells(uuid, uuid, text[], uuid[])` is live, granted to
20
+ `authenticated`, and answers 320 rows for a real organization from the seat. `demo/branch-server.ts`
21
+ formatted every non-scalar argument as `'<json>'::jsonb`, because `typeof [] === "object"`,
22
+ so no door with a Postgres array parameter could ever be matched; that 42883 was recorded
23
+ into `demo/fixtures/doors.json` and replayed on every mount. The seam now asks `pg_proc`
24
+ what each parameter is declared as and formats by that — the class, not the instance — and
25
+ an array whose type it cannot read is named loudly instead of quietly re-sent as jsonb.
26
+
27
+ * New guard `demo/a-grid-mount-logs-no-console-error.test.tsx`: mounts `Grid` against the
28
+ LIVE store and fails on ANY `console.error`, with one named exclusion (React's jsdom-only
29
+ `act()` warning). RED on the pre-fix bytes with exactly the 42883 above, GREEN on these.
30
+ * The demo harness seeds a PLAIN relation column ("Related proposal", a self-relation on
31
+ Northbay Commercial Solar's proposals table) — until now every relation it seeded carried
32
+ `parity: "member"` and routed to `PersonPicker`, so `RelationPicker` had no browser
33
+ surface at all and its only coverage was jsdom, which has no geometry.
34
+
35
+ Suite: 51 files / 319 tests green, `tsc --noEmit` clean.
36
+
37
+ ## 0.80.0
38
+
39
+ **An import's report outlives the run, and its refusals outlive the panel.** (lane FIX-12,
40
+ finding F2 from VERIFIER-12; published by PUBLISH-SWEEP-2, aidream `675cc61fe8`.)
41
+
42
+ VERIFIER-12 watched a 40-row file land 28 rows and refuse 12 against the live store and saw
43
+ NOTHING on screen: no summary, no refusal list, and afterwards nothing on any of that
44
+ table's screens naming the twelve rows. The store had been right the whole time — the
45
+ ledger said seen 40 / written 28 / refusals 12, and every refused row was kept with a
46
+ sentence a person understands. The report was not the defect. `importFinish` called
47
+ `onDone`, every host wires `onDone` to close the rail, and a run that fits in ONE call
48
+ finishes between two paints — so the report was destroyed in the tick it became true. A long
49
+ file only looked better because there was time to read the progress.
50
+
51
+ * The wizard tells the host `onWrote` when rows land; `onDone` is now the person pressing
52
+ **Done** on the report, and nothing else closes the panel.
53
+ * The report says where the refused rows now live.
54
+ * **"Imports into this table"** — every run this table has had, newest first, off
55
+ `custom.io_imports`; a run with refusals opens them off `custom.io_import_report`, with
56
+ the store's own reason and the line of the file. Both doors have existed since lane
57
+ IMPORT; no screen had ever asked.
58
+ * Guard: two clauses in `src/import-wizard.test.tsx`, mounted the way a HOST mounts the
59
+ wizard (closing on `onDone`). RED on the old bytes ("expected 'the import is closed' not
60
+ to contain 'the import is closed'"; the past run never on screen), GREEN on these.
61
+
62
+ Built against `@ai-matrx/records@0.54.0`. Suite at publish: 317/317 across 50 files,
63
+ including `demo/live-smoke.test.tsx`, this package's blocking live publish gate.
64
+
65
+ ## 0.79.0
66
+
67
+ **A joined column says what joins it.** (lane STORE-TAILS-2, VERIFIER-11 finding F5.)
68
+
69
+ Greenline Landscaping Crew's Jobs board, "Job label" joining Job Number and Service Type,
70
+ read `GL-2034mow_edge` on all 35 rows — beside a cell reading `Mow & Edge`. Two wrongs in
71
+ one string. The machine's word is the store's half and is fixed there (`custom.rule_eval`'s
72
+ concat arm now resolves a choice to its label and a relation to its display words through
73
+ `custom.field_words`, while every comparison keeps the stored value). The missing separator
74
+ is this package's: nothing on the field panel ever asked for one, so there was no way to
75
+ send one.
76
+
77
+ * **"Joined by"** — a new box on the worked-out-column builder, offered only for *Join them
78
+ together*, defaulting to ` — `. It is sent as `expr.separator`, which the store has
79
+ accepted since the migration above. Emptying it sends no key at all, which is exactly what
80
+ every formula written before today already holds, so nothing saved changes its answer.
81
+ * A sum, a product or a difference is not offered the box and never sends the key — a
82
+ control that set something the evaluator ignores would be a setting that silently does
83
+ nothing.
84
+ * **The panel reads an existing worked-out column back** — its operation, its columns and
85
+ its separator — instead of starting at "concat" with nothing picked whatever the column
86
+ actually held.
87
+ * **And it says out loud what it still cannot do.** Opening an existing worked-out column
88
+ draws no formula controls (this panel never has), and now it says so in a sentence instead
89
+ of standing silent over the one thing that column is. Re-aiming an existing formula needs
90
+ `expr` on `FieldPatch` in `@ai-matrx/records`; the store door already takes it.
91
+
92
+ Consumer action: none.
93
+
3
94
  ## 0.78.0
4
95
 
5
96
  **The button that could not get rid of a table now can, and says what it does.** (lane
package/dist/index.cjs CHANGED
@@ -1082,7 +1082,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1082
1082
  )),
1083
1083
  disabled ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system2.Popover, { open, onOpenChange: setOpen, children: [
1084
1084
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system2.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_design_system2.Button, { id, type: "button", size: "sm", variant: "outline", "aria-label": `Pick for ${fieldName(field)}`, children: picked.length === 0 ? "Pick" : "Change" }) }),
1085
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system2.PopoverContent, { className: "w-64 p-1", align: "start", children: [
1085
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_design_system2.PopoverContent, { sizing: "content", className: "p-1", align: "start", children: [
1086
1086
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1087
1087
  import_design_system2.BasicInput,
1088
1088
  {
@@ -1106,6 +1106,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1106
1106
  {
1107
1107
  type: "button",
1108
1108
  onClick: () => toggle(row.id),
1109
+ title: titleOf(row.id),
1109
1110
  className: (0, import_design_system2.cn)(
1110
1111
  "w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
1111
1112
  picked.includes(row.id) ? "bg-accent text-accent-foreground" : ""
@@ -1151,6 +1152,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1151
1152
  type: "button",
1152
1153
  size: "sm",
1153
1154
  variant: "ghost",
1155
+ title: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`,
1154
1156
  className: "w-full justify-start text-xs",
1155
1157
  disabled: busy,
1156
1158
  onClick: () => {
@@ -1160,7 +1162,7 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1160
1162
  setCreating(true);
1161
1163
  }
1162
1164
  },
1163
- children: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`
1165
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "block w-full truncate text-left", children: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}` })
1164
1166
  }
1165
1167
  ),
1166
1168
  createRefusal ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "p-2 text-xs text-destructive", children: createRefusal }) : null
@@ -1421,10 +1423,17 @@ function RecordChip({
1421
1423
  onRemove,
1422
1424
  className
1423
1425
  }) {
1424
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_design_system3.Badge, { variant: "secondary", className: (0, import_design_system3.cn)("gap-1 text-[11px]", className), children: [
1425
- title,
1426
- onRemove ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", onClick: onRemove, "aria-label": `Remove ${title}`, className: "opacity-60 hover:opacity-100", children: "\xD7" }) : null
1427
- ] });
1426
+ return (
1427
+ // A chip carries a RECORD'S OWN TITLE, which nobody here chose the length of —
1428
+ // "Marisol Okonkwo 418 Calle Puerto Vallarta, Camarillo" is an ordinary customer
1429
+ // name. So it caps at its container and ends in an ellipsis with the whole name in
1430
+ // its tooltip, rather than being cut mid-word by whatever box it landed in
1431
+ // (lane FIX-14, 2026-09-22).
1432
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_design_system3.Badge, { variant: "secondary", className: (0, import_design_system3.cn)("max-w-full gap-1 text-[11px]", className), title, children: [
1433
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "min-w-0 truncate", children: title }),
1434
+ onRemove ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", onClick: onRemove, "aria-label": `Remove ${title}`, className: "shrink-0 opacity-60 hover:opacity-100", children: "\xD7" }) : null
1435
+ ] })
1436
+ );
1428
1437
  }
1429
1438
 
1430
1439
  // src/PersonPicker.tsx
@@ -1570,7 +1579,7 @@ function PersonPicker({ field, value, onChange, disabled, id, className }) {
1570
1579
  children: picked.length === 0 ? "Pick a person" : "Change"
1571
1580
  }
1572
1581
  ) }),
1573
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_design_system4.PopoverContent, { className: "w-64 p-1", align: "start", children: [
1582
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_design_system4.PopoverContent, { sizing: "content", className: "p-1", align: "start", children: [
1574
1583
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1575
1584
  import_design_system4.BasicInput,
1576
1585
  {
@@ -3691,6 +3700,9 @@ function Grid({
3691
3700
  [client, host, refreshEnrich]
3692
3701
  );
3693
3702
  const [pastePlan, setPastePlan] = (0, import_react20.useState)(null);
3703
+ const [deleting, setDeleting] = (0, import_react20.useState)(null);
3704
+ const [deleteBusy, setDeleteBusy] = (0, import_react20.useState)(false);
3705
+ const [archived, setArchived] = (0, import_react20.useState)(null);
3694
3706
  const choiceOptions = (0, import_react20.useRef)(/* @__PURE__ */ new Map());
3695
3707
  const loadChoiceOptions = (0, import_react20.useCallback)(
3696
3708
  async (fieldList) => {
@@ -3978,6 +3990,21 @@ function Grid({
3978
3990
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ml-auto flex items-center gap-1", children: toolbarActions })
3979
3991
  ] }),
3980
3992
  records.error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(RefusalNotice, { error: records.error }) : null,
3993
+ archived ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3994
+ "p",
3995
+ {
3996
+ "data-records-archived-notice": "",
3997
+ role: "status",
3998
+ className: "flex items-center gap-2 rounded border border-border bg-muted/40 px-2 py-1 text-xs",
3999
+ children: [
4000
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { children: [
4001
+ archived,
4002
+ " is archived. Nothing was destroyed \u2014 it is in this table\u2019s Archived items, where Restore brings it back."
4003
+ ] }),
4004
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.Button, { size: "sm", variant: "ghost", className: "ml-auto h-6", onClick: () => setArchived(null), children: "Dismiss" })
4005
+ ]
4006
+ }
4007
+ ) : null,
3981
4008
  editing.rowError ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3982
4009
  RefusalNotice,
3983
4010
  {
@@ -4094,11 +4121,13 @@ function Grid({
4094
4121
  }
4095
4122
  ) : null,
4096
4123
  canWrite && mayWriteRow(row.id) ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4097
- DeleteRecordButton,
4124
+ import_design_system10.Button,
4098
4125
  {
4099
- row,
4100
- retentionDays: table.data?.retention_days ?? null,
4101
- onDelete: () => editing.remove(row.id)
4126
+ size: "sm",
4127
+ variant: "ghost",
4128
+ title: "Delete this record",
4129
+ onClick: () => setDeleting(row),
4130
+ children: "Delete"
4102
4131
  }
4103
4132
  ) : null
4104
4133
  ] })
@@ -4141,7 +4170,32 @@ function Grid({
4141
4170
  onPage: setPage
4142
4171
  }
4143
4172
  )
4144
- ] })
4173
+ ] }),
4174
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4175
+ import_design_system10.ConfirmDialog,
4176
+ {
4177
+ open: deleting !== null,
4178
+ onOpenChange: (next) => {
4179
+ if (!next && !deleteBusy) setDeleting(null);
4180
+ },
4181
+ variant: "destructive",
4182
+ title: `Delete ${deleting ? recordNameIn(table.data, deleting.document ?? {}) : "this record"}?`,
4183
+ description: table.data?.retention_days ? `It leaves the table and waits in Archived items. Nothing is destroyed: Restore brings it back, for ${table.data.retention_days} days.` : "It leaves the table and waits in Archived items. Nothing is destroyed: Restore brings it back.",
4184
+ confirmLabel: "Delete",
4185
+ cancelLabel: "Keep",
4186
+ busy: deleteBusy,
4187
+ onConfirm: async () => {
4188
+ const row = deleting;
4189
+ if (!row) return;
4190
+ const name = recordNameIn(table.data, row.document ?? {});
4191
+ setDeleteBusy(true);
4192
+ const failed = await editing.remove(row.id);
4193
+ setDeleteBusy(false);
4194
+ setDeleting(null);
4195
+ if (!failed) setArchived(name);
4196
+ }
4197
+ }
4198
+ )
4145
4199
  ] }) });
4146
4200
  }
4147
4201
  function columnLabel(fields, columnId) {
@@ -4161,42 +4215,6 @@ function AddFieldButton({ onClick }) {
4161
4215
  }
4162
4216
  );
4163
4217
  }
4164
- function DeleteRecordButton({
4165
- row,
4166
- retentionDays,
4167
- onDelete
4168
- }) {
4169
- const [asking, setAsking] = (0, import_react20.useState)(false);
4170
- if (!asking) {
4171
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4172
- import_design_system10.Button,
4173
- {
4174
- size: "sm",
4175
- variant: "ghost",
4176
- title: "Delete this record",
4177
- onClick: () => setAsking(true),
4178
- children: "Delete"
4179
- }
4180
- );
4181
- }
4182
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "flex items-center gap-1 text-[11px]", children: [
4183
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-muted-foreground", children: retentionDays ? `Removes it from the table. Restorable for ${retentionDays} days.` : "Removes it from the table. It can be restored from the record's history." }),
4184
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4185
- import_design_system10.Button,
4186
- {
4187
- size: "sm",
4188
- variant: "destructive",
4189
- onClick: () => {
4190
- setAsking(false);
4191
- void onDelete();
4192
- },
4193
- children: "Delete"
4194
- }
4195
- ),
4196
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.Button, { size: "sm", variant: "ghost", onClick: () => setAsking(false), children: "Keep" }),
4197
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "sr-only", children: row.id })
4198
- ] });
4199
- }
4200
4218
  function Pager({
4201
4219
  page,
4202
4220
  pageSize,
@@ -4495,7 +4513,7 @@ async function sha256(bytes) {
4495
4513
  function typeWord(t) {
4496
4514
  return fieldTypeChoice(t)?.label ?? (t === "relation" ? "Points at another table" : t);
4497
4515
  }
4498
- function ImportWizard({ tableId, onDone, className }) {
4516
+ function ImportWizard({ tableId, onDone, onWrote, className }) {
4499
4517
  const client = (0, import_react27.useRecordsClient)();
4500
4518
  const table = (0, import_react27.useTable)(tableId);
4501
4519
  const fields = (0, import_react27.useFields)(tableId);
@@ -4521,6 +4539,46 @@ function ImportWizard({ tableId, onDone, className }) {
4521
4539
  const [columnsAdded, setColumnsAdded] = (0, import_react26.useState)(null);
4522
4540
  const [show, setShow] = (0, import_react26.useState)("all");
4523
4541
  const [ledger, setLedger] = (0, import_react26.useState)(null);
4542
+ const [runs, setRuns] = (0, import_react26.useState)(null);
4543
+ const [openRun, setOpenRun] = (0, import_react26.useState)(null);
4544
+ const [runRows, setRunRows] = (0, import_react26.useState)(null);
4545
+ const [runProblem, setRunProblem] = (0, import_react26.useState)(null);
4546
+ const loadRuns = (0, import_react26.useCallback)(async () => {
4547
+ const answered = await client.imports({ table_id: tableId, limit: 10 });
4548
+ if (answered.ok) setRuns(answered.data);
4549
+ }, [client, tableId]);
4550
+ (0, import_react26.useEffect)(() => {
4551
+ void loadRuns();
4552
+ }, [loadRuns]);
4553
+ const openRefusals = (0, import_react26.useCallback)(
4554
+ async (importId) => {
4555
+ setRunProblem(null);
4556
+ if (openRun === importId) {
4557
+ setOpenRun(null);
4558
+ setRunRows(null);
4559
+ return;
4560
+ }
4561
+ setOpenRun(importId);
4562
+ setRunRows(null);
4563
+ const answered = await client.importReport({
4564
+ import_id: importId,
4565
+ state: "refused",
4566
+ limit: 200
4567
+ });
4568
+ if (!answered.ok) {
4569
+ setRunProblem(answered.error.message);
4570
+ return;
4571
+ }
4572
+ setRunRows({
4573
+ import_id: answered.data.import_id,
4574
+ rows: answered.data.rows,
4575
+ kept: answered.data.kept,
4576
+ total: answered.data.total,
4577
+ note: answered.data.note
4578
+ });
4579
+ },
4580
+ [client, openRun]
4581
+ );
4524
4582
  const declared = fields.data ?? [];
4525
4583
  const take = (0, import_react26.useCallback)(
4526
4584
  async (file) => {
@@ -4654,8 +4712,9 @@ function ImportWizard({ tableId, onDone, className }) {
4654
4712
  final: true
4655
4713
  });
4656
4714
  setPhase("done");
4657
- onDone?.(finished.data.rows_written);
4658
- }, [client, dedupeKey, fileBytes, fileHash, fileName, format, mapping, onDuplicate, onDone, plan, rows, tableId, unmapped]);
4715
+ onWrote?.(finished.data.rows_written);
4716
+ void loadRuns();
4717
+ }, [client, dedupeKey, fileBytes, fileHash, fileName, format, loadRuns, mapping, onDuplicate, onWrote, plan, rows, tableId, unmapped]);
4659
4718
  const counts = (0, import_react26.useMemo)(() => {
4660
4719
  let landed = 0;
4661
4720
  let duplicate = 0;
@@ -4932,7 +4991,73 @@ function ImportWizard({ tableId, onDone, className }) {
4932
4991
  proposals.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-0.5", children: proposals.map((p) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("li", { className: "text-muted-foreground", children: [
4933
4992
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-medium text-foreground", children: p.column }),
4934
4993
  p.state === "waiting" ? " is waiting in the approvals inbox." : p.state === "accepted" ? " was added as a column." : p.state === "ignored" ? " was left out." : p.state === "refused" ? ` could not be added: ${p.reason ?? ""}` : ` \u2014 ${p.state}`
4935
- ] }, p.column)) }) : null
4994
+ ] }, p.column)) }) : null,
4995
+ phase === "done" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
4996
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Button, { size: "sm", onClick: () => onDone?.(ledger?.written ?? 0), children: "Done" }),
4997
+ (ledger?.refused ?? 0) > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-muted-foreground", children: [
4998
+ "The ",
4999
+ ledger?.refused,
5000
+ " row",
5001
+ ledger?.refused === 1 ? "" : "s",
5002
+ " that ",
5003
+ ledger?.refused === 1 ? "was" : "were",
5004
+ " refused",
5005
+ " ",
5006
+ "are kept with this import \u2014 open them again from the list of imports below, with the store's reason and the line from your file on each."
5007
+ ] }) : null
5008
+ ] }) : null
5009
+ ] }) : null,
5010
+ runs && runs.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
5011
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Separator, {}),
5012
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "font-medium", children: "Imports into this table" }),
5013
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", children: runs.map((r) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("li", { className: "flex flex-col gap-1", children: [
5014
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
5015
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "font-medium", children: r.source_name ?? "a file" }),
5016
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-muted-foreground", children: [
5017
+ new Date(r.opened_at).toLocaleString(),
5018
+ " \xB7 ",
5019
+ r.rows_written,
5020
+ " landed \xB7",
5021
+ " ",
5022
+ r.rows_duplicate,
5023
+ " already here \xB7 ",
5024
+ r.rows_refused,
5025
+ " refused, of ",
5026
+ r.rows_seen,
5027
+ " row",
5028
+ r.rows_seen === 1 ? "" : "s",
5029
+ " offered"
5030
+ ] }),
5031
+ r.rows_refused > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5032
+ "button",
5033
+ {
5034
+ type: "button",
5035
+ className: "underline underline-offset-2",
5036
+ "aria-expanded": openRun === r.import_id,
5037
+ onClick: () => void openRefusals(r.import_id),
5038
+ children: openRun === r.import_id ? "hide the rows it refused" : `see the ${r.rows_refused} row${r.rows_refused === 1 ? "" : "s"} it refused`
5039
+ }
5040
+ ) : null
5041
+ ] }),
5042
+ openRun === r.import_id ? runProblem ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-destructive", children: runProblem }) : runRows ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
5043
+ runRows.note ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: runRows.note }) : null,
5044
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "max-h-64 overflow-auto rounded border", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("table", { className: "w-full", children: [
5045
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("thead", { className: "sticky top-0 bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { children: [
5046
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "Line in your file" }),
5047
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "What happened" }),
5048
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { className: "px-2 py-1 text-left font-medium", children: "The row in your file" })
5049
+ ] }) }),
5050
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("tbody", { children: runRows.rows.map((row, i) => {
5051
+ const source = row.source ?? {};
5052
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { className: "border-t align-top", children: [
5053
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-2 py-1", children: String(row.row ?? "") }),
5054
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-2 py-1 text-destructive", children: String(row.reason ?? "") }),
5055
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "max-w-96 px-2 py-1 text-muted-foreground", children: Object.entries(source).map(([k, v]) => `${k}: ${String(v ?? "")}`).join(" \xB7 ") })
5056
+ ] }, `${r.import_id}-${String(row.row ?? i)}`);
5057
+ }) })
5058
+ ] }) })
5059
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-muted-foreground", children: "Reading the rows this import refused\u2026" }) : null
5060
+ ] }, r.import_id)) })
4936
5061
  ] }) : null,
4937
5062
  table.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RefusalNotice, { error: table.error }) : null
4938
5063
  ] });
@@ -5132,6 +5257,9 @@ var ROLLUP_OPERATIONS = ROLLUP_AGG_VALUES.map((value) => ({
5132
5257
  value,
5133
5258
  label: ROLLUP_AGG_LABEL[value]
5134
5259
  }));
5260
+ var FORMULA_SEPARATOR_LABEL = "Joined by";
5261
+ 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.";
5262
+ var FORMULA_DEFAULT_SEPARATOR = " \u2014 ";
5135
5263
  var FORMULA_OPERATIONS = FORMULA_OP_VALUES.map((value) => ({
5136
5264
  value,
5137
5265
  label: FORMULA_OP_LABEL[value],
@@ -5168,8 +5296,9 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5168
5296
  const [of, setOf] = (0, import_react30.useState)(String(field?.config?.["of"] ?? ""));
5169
5297
  const [relationTarget, setRelationTarget] = (0, import_react30.useState)(String(field?.relation_target ?? ""));
5170
5298
  const [display, setDisplay] = (0, import_react30.useState)(field?.display ?? null);
5171
- const [formulaOp, setFormulaOp] = (0, import_react30.useState)("concat");
5172
- const [formulaFields, setFormulaFields] = (0, import_react30.useState)([]);
5299
+ const [formulaOp, setFormulaOp] = (0, import_react30.useState)(() => formulaOpOf(field));
5300
+ const [formulaFields, setFormulaFields] = (0, import_react30.useState)(() => formulaFieldIdsOf(field));
5301
+ const [formulaSeparator, setFormulaSeparator] = (0, import_react30.useState)(() => formulaSeparatorOf(field));
5173
5302
  const [askingToRemove, setAskingToRemove] = (0, import_react30.useState)(false);
5174
5303
  const [notLanded, setNotLanded] = (0, import_react30.useState)(false);
5175
5304
  const others = (0, import_react30.useMemo)(
@@ -5243,7 +5372,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5243
5372
  ...type === "datetime" ? { kind: withTime ? "datetime" : "date" } : {},
5244
5373
  ...type === "lookup" ? { via, pick } : {},
5245
5374
  ...type === "rollup" ? { via, agg, ...agg === "count" ? {} : { of } } : {},
5246
- ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields) } : {}
5375
+ ...type === "formula" ? { expr: formulaExpression(formulaOp, formulaFields, formulaSeparator) } : {}
5247
5376
  };
5248
5377
  const written = await mutation.declare({ table_id: tableId, spec });
5249
5378
  if (written === null) return;
@@ -5301,6 +5430,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5301
5430
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: editing ? "What a column holds cannot be changed here \u2014 every value already saved would have to be converted. Add a new column and move the values across." : choice?.explanation ?? "" })
5302
5431
  ] })
5303
5432
  ] }),
5433
+ editing && type === "formula" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: EDITING_A_WORKED_OUT_COLUMN }) : null,
5304
5434
  !editing || type === "select" || type === "multi_select" || type === "relation" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5305
5435
  TypeQuestions,
5306
5436
  {
@@ -5329,6 +5459,8 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
5329
5459
  setFormulaOp,
5330
5460
  formulaFields,
5331
5461
  setFormulaFields,
5462
+ formulaSeparator,
5463
+ setFormulaSeparator,
5332
5464
  relations,
5333
5465
  others,
5334
5466
  existingOptionsTable: Boolean(field?.options_table_id ?? field?.config?.["options_table_id"])
@@ -5513,10 +5645,30 @@ function startingType(field) {
5513
5645
  if (fieldTypeChoice(kind)) return kind;
5514
5646
  return "text";
5515
5647
  }
5516
- function formulaExpression(op, fieldIds) {
5648
+ function formulaExpression(op, fieldIds, separator) {
5517
5649
  const args = fieldIds.filter((id) => id !== "").map((id) => ({ field: id }));
5518
- if (args.length === 0) return { op, args: [{ const: "" }] };
5519
- return { op, args };
5650
+ const joined = op === "concat" && separator !== "" ? { separator } : {};
5651
+ if (args.length === 0) return { op, ...joined, args: [{ const: "" }] };
5652
+ return { op, ...joined, args };
5653
+ }
5654
+ function formulaExprOf(field) {
5655
+ const expr = field?.config?.["expr"];
5656
+ return expr !== null && typeof expr === "object" ? expr : null;
5657
+ }
5658
+ function formulaOpOf(field) {
5659
+ const op = formulaExprOf(field)?.["op"];
5660
+ return typeof op === "string" && op !== "" ? op : "concat";
5661
+ }
5662
+ function formulaFieldIdsOf(field) {
5663
+ const args = formulaExprOf(field)?.["args"];
5664
+ if (!Array.isArray(args)) return [];
5665
+ return args.map((a) => a !== null && typeof a === "object" ? a["field"] : void 0).filter((id) => typeof id === "string" && id !== "");
5666
+ }
5667
+ function formulaSeparatorOf(field) {
5668
+ const expr = formulaExprOf(field);
5669
+ if (expr === null) return FORMULA_DEFAULT_SEPARATOR;
5670
+ const sep = expr["separator"];
5671
+ return typeof sep === "string" ? sep : "";
5520
5672
  }
5521
5673
  function TypeQuestions(props) {
5522
5674
  const { type } = props;
@@ -5655,7 +5807,21 @@ function TypeQuestions(props) {
5655
5807
  );
5656
5808
  }) }),
5657
5809
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "In the order you pick them. Columns are remembered by which column they are, so renaming one never breaks this." })
5658
- ] })
5810
+ ] }),
5811
+ props.formulaOp === "concat" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-1", children: [
5812
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_design_system15.Label, { className: "text-xs", children: FORMULA_SEPARATOR_LABEL }),
5813
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5814
+ import_design_system15.BasicInput,
5815
+ {
5816
+ value: props.formulaSeparator,
5817
+ "aria-label": FORMULA_SEPARATOR_LABEL,
5818
+ placeholder: "\u2014",
5819
+ className: "max-w-[10rem]",
5820
+ onChange: (e) => props.setFormulaSeparator(e.target.value)
5821
+ }
5822
+ ),
5823
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "What goes between the columns. Leave it empty to run them straight together." })
5824
+ ] }) : null
5659
5825
  ] });
5660
5826
  }
5661
5827
  return null;
@@ -7351,8 +7517,8 @@ function PipelineBoard({
7351
7517
  const asked = await client.pipelineTransitionRefusal({ record_id: recordId, to });
7352
7518
  if (!alive.current) return;
7353
7519
  if (!asked.ok) {
7354
- setError(asked.error);
7355
- setPending({ kind: "none" });
7520
+ setPending({ kind: "clash", recordId, to, error: asked.error });
7521
+ onMoved?.();
7356
7522
  return;
7357
7523
  }
7358
7524
  const verdict = asked.data;
@@ -7365,8 +7531,8 @@ function PipelineBoard({
7365
7531
  const moved = await client.pipelineMove({ record_id: recordId, to });
7366
7532
  if (!alive.current) return;
7367
7533
  if (!moved.ok) {
7368
- setPending({ kind: "none" });
7369
- setError(moved.error);
7534
+ setPending({ kind: "clash", recordId, to, error: moved.error });
7535
+ onMoved?.();
7370
7536
  return;
7371
7537
  }
7372
7538
  const result = moved.data;
@@ -7387,21 +7553,22 @@ function PipelineBoard({
7387
7553
  const moved = await client.pipelineMove({ record_id: recordId, to });
7388
7554
  if (!alive.current) return;
7389
7555
  if (!moved.ok) {
7390
- setPending({ kind: "none" });
7391
- setError(moved.error);
7556
+ setPending({ kind: "clash", recordId, to, error: moved.error });
7557
+ onMoved?.();
7392
7558
  return;
7393
7559
  }
7394
7560
  setPending({ kind: "filed", recordId, to, result: moved.data });
7395
7561
  void reload();
7396
7562
  },
7397
- [client, reload]
7563
+ [client, onMoved, reload]
7398
7564
  );
7399
7565
  const moveWith = (0, import_react47.useCallback)(
7400
7566
  async (recordId, to, also) => {
7401
7567
  const moved = await client.pipelineMove({ record_id: recordId, to, also });
7402
7568
  if (!alive.current) return;
7403
7569
  if (!moved.ok) {
7404
- setError(moved.error);
7570
+ setPending({ kind: "clash", recordId, to, error: moved.error });
7571
+ onMoved?.();
7405
7572
  return;
7406
7573
  }
7407
7574
  setPending({ kind: "none" });
@@ -7529,6 +7696,16 @@ function Held({
7529
7696
  if (pending.kind === "asking") {
7530
7697
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "px-2 py-1 text-xs text-muted-foreground", children: "Asking\u2026" });
7531
7698
  }
7699
+ if (pending.kind === "clash") {
7700
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
7701
+ RefusalNotice,
7702
+ {
7703
+ className: "mt-1",
7704
+ error: pending.error,
7705
+ actions: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "mt-1", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_design_system23.Button, { size: "sm", variant: "ghost", className: "h-6", onClick: onCancel, children: "Close" }) })
7706
+ }
7707
+ );
7708
+ }
7532
7709
  if (pending.kind === "refused") {
7533
7710
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "mt-1 rounded border border-destructive/50 bg-destructive/5 p-2 text-xs", children: [
7534
7711
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { children: pending.verdict.why }),