@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 +91 -0
- package/dist/index.cjs +244 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +354 -177
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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: "
|
|
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
|
|
1425
|
-
|
|
1426
|
-
|
|
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: "
|
|
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
|
-
|
|
4124
|
+
import_design_system10.Button,
|
|
4098
4125
|
{
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
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
|
-
|
|
4658
|
-
|
|
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)(
|
|
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
|
-
|
|
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
|
-
|
|
7355
|
-
|
|
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: "
|
|
7369
|
-
|
|
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: "
|
|
7391
|
-
|
|
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
|
-
|
|
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 }),
|