@ai-matrx/records-ui 0.93.133 → 0.93.142
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 +77 -1
- package/dist/index.cjs +10614 -6634
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +672 -165
- package/dist/index.d.ts +672 -165
- package/dist/index.js +10001 -5972
- package/dist/index.js.map +1 -1
- package/dist/object-actions.cjs +240 -0
- package/dist/object-actions.cjs.map +1 -0
- package/dist/object-actions.d.cts +2 -0
- package/dist/object-actions.d.ts +2 -0
- package/dist/object-actions.js +218 -0
- package/dist/object-actions.js.map +1 -0
- package/dist/objectActions-BSbblgf-.d.cts +168 -0
- package/dist/objectActions-BSbblgf-.d.ts +168 -0
- package/dist/pickers.cjs +4529 -0
- package/dist/pickers.cjs.map +1 -0
- package/dist/pickers.d.cts +137 -0
- package/dist/pickers.d.ts +137 -0
- package/dist/pickers.js +4556 -0
- package/dist/pickers.js.map +1 -0
- package/dist/table-shape.cjs +5 -10
- package/dist/table-shape.cjs.map +1 -1
- package/dist/table-shape.d.cts +1 -1
- package/dist/table-shape.d.ts +1 -1
- package/dist/table-shape.js +7 -10
- package/dist/table-shape.js.map +1 -1
- package/package.json +27 -1
package/dist/pickers.js
ADDED
|
@@ -0,0 +1,4556 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/TablePickers.tsx
|
|
4
|
+
import { useEffect as useEffect12, useMemo as useMemo7, useState as useState15 } from "react";
|
|
5
|
+
import { RecordsProvider as RecordsProvider2, useFields, useRecords, useTable } from "@ai-matrx/records/react";
|
|
6
|
+
import { Button as Button9, CreatablePicker } from "@ai-matrx/design-system";
|
|
7
|
+
|
|
8
|
+
// src/editors.tsx
|
|
9
|
+
import { useCallback as useCallback4, useEffect as useEffect10, useRef as useRef9, useState as useState13 } from "react";
|
|
10
|
+
import { allowsOtherValues, choiceValuesOf, coerceTypedAnswer, newerKindOf as newerKindOf4, decideTypedChoice, isTheChosen, optionKey as optionKey2, rankTypedMatches as rankTypedMatches3, readTypedTime as readTypedTime2, typedDateText as typedDateText2, typedTimeText as typedTimeText2 } from "@ai-matrx/records";
|
|
11
|
+
import { useOptionalRecordsClient as useOptionalRecordsClient6 } from "@ai-matrx/records/react";
|
|
12
|
+
import {
|
|
13
|
+
BasicInput as BasicInput6,
|
|
14
|
+
BasicTextarea as BasicTextarea2,
|
|
15
|
+
Badge as Badge3,
|
|
16
|
+
Button as Button8,
|
|
17
|
+
Checkbox,
|
|
18
|
+
Label,
|
|
19
|
+
Popover as Popover4,
|
|
20
|
+
PopoverContent as PopoverContent4,
|
|
21
|
+
PopoverTrigger as PopoverTrigger4,
|
|
22
|
+
Select,
|
|
23
|
+
SelectContent,
|
|
24
|
+
SelectItem,
|
|
25
|
+
SelectTrigger,
|
|
26
|
+
SelectValue,
|
|
27
|
+
cn as cn11
|
|
28
|
+
} from "@ai-matrx/design-system";
|
|
29
|
+
|
|
30
|
+
// src/dates.ts
|
|
31
|
+
import { formatFieldValue } from "@ai-matrx/design-system/field-formats";
|
|
32
|
+
var BARE_CALENDAR_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ]00:00(?::00(?:\.0+)?)?)?$/;
|
|
33
|
+
function systemStampOf(field) {
|
|
34
|
+
if (field.type !== "formula") return null;
|
|
35
|
+
const system = String((field.config ?? {})["system"] ?? "");
|
|
36
|
+
return system === "autonumber" || system === "created_time" || system === "modified_time" ? system : null;
|
|
37
|
+
}
|
|
38
|
+
function carriesTimeOfDay(field, value) {
|
|
39
|
+
if (value !== void 0 && value !== null && value !== "") return hasTimePart(value);
|
|
40
|
+
const stamp = systemStampOf(field);
|
|
41
|
+
if (stamp === "created_time" || stamp === "modified_time") return true;
|
|
42
|
+
return (field.format ?? "").toLowerCase() === "datetime";
|
|
43
|
+
}
|
|
44
|
+
function hasTimePart(value) {
|
|
45
|
+
if (value instanceof Date || typeof value === "number") return true;
|
|
46
|
+
if (typeof value !== "string") return false;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (BARE_CALENDAR_DATE.test(trimmed)) return false;
|
|
49
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d/.test(trimmed);
|
|
50
|
+
}
|
|
51
|
+
function isDateField(field) {
|
|
52
|
+
const stamp = systemStampOf(field);
|
|
53
|
+
if (stamp === "created_time" || stamp === "modified_time") return true;
|
|
54
|
+
if (field.type !== "range") return false;
|
|
55
|
+
const kind = String((field.config ?? {})["kind"] ?? "");
|
|
56
|
+
return kind === "date" || kind === "datetime";
|
|
57
|
+
}
|
|
58
|
+
function dateFormatFor(field, value) {
|
|
59
|
+
if (!isDateField(field)) return void 0;
|
|
60
|
+
return carriesTimeOfDay(field, value) ? { id: "datetime" } : { id: "date" };
|
|
61
|
+
}
|
|
62
|
+
function dateText(field, value) {
|
|
63
|
+
const format = dateFormatFor(field, value) ?? (isDateLike(value) ? { id: hasTimePart(value) ? "datetime" : "date" } : void 0);
|
|
64
|
+
if (!format) return null;
|
|
65
|
+
const result = formatFieldValue(value, format);
|
|
66
|
+
return result.ok && !result.empty ? result.text : null;
|
|
67
|
+
}
|
|
68
|
+
function isDateLike(value) {
|
|
69
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value.trim());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/newerKindValues.tsx
|
|
73
|
+
import { useEffect, useRef, useState } from "react";
|
|
74
|
+
import { codeSpanText, findInlineCodeSpans } from "@ai-matrx/content-ir/source";
|
|
75
|
+
import {
|
|
76
|
+
ADDRESS_PARTS,
|
|
77
|
+
NOT_A_MEMBER_WORDS,
|
|
78
|
+
STATUS_GROUP_WORDS,
|
|
79
|
+
addressMapUrl,
|
|
80
|
+
addressText,
|
|
81
|
+
durationText,
|
|
82
|
+
isAddressValue,
|
|
83
|
+
isPersonStamp,
|
|
84
|
+
newerKindOf,
|
|
85
|
+
readTypedDuration,
|
|
86
|
+
statusGroupOf
|
|
87
|
+
} from "@ai-matrx/records";
|
|
88
|
+
import { BasicInput, cn } from "@ai-matrx/design-system";
|
|
89
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
90
|
+
var own = (event) => event.stopPropagation();
|
|
91
|
+
function newerKindValue(field, value, opts = {}) {
|
|
92
|
+
switch (newerKindOf(field)) {
|
|
93
|
+
case "duration": {
|
|
94
|
+
const words = durationText(value);
|
|
95
|
+
return words === null ? void 0 : /* @__PURE__ */ jsx("span", { className: "tabular-nums", "data-records-duration": "", children: words });
|
|
96
|
+
}
|
|
97
|
+
case "created_by":
|
|
98
|
+
case "modified_by":
|
|
99
|
+
return isPersonStamp(value) ? /* @__PURE__ */ jsx(PersonStampChip, { name: value.name }) : void 0;
|
|
100
|
+
case "address":
|
|
101
|
+
return isAddressValue(value) || typeof value === "string" ? /* @__PURE__ */ jsx(AddressLink, { value }) : void 0;
|
|
102
|
+
case "barcode":
|
|
103
|
+
return typeof value === "string" ? /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1 font-mono text-xs", "data-records-barcode": "", title: value, children: [
|
|
104
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u25AE\u25AF\u25AE" }),
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: value })
|
|
106
|
+
] }) : void 0;
|
|
107
|
+
case "rich_text":
|
|
108
|
+
return typeof value === "string" ? /* @__PURE__ */ jsx(SafeMarkdown, { text: value, clipped: opts.inCell === true }) : void 0;
|
|
109
|
+
case "count":
|
|
110
|
+
return typeof value === "number" ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: value.toLocaleString() }) : void 0;
|
|
111
|
+
default:
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function PersonStampChip({ name }) {
|
|
116
|
+
const words = name && name.trim() !== "" ? name : NOT_A_MEMBER_WORDS;
|
|
117
|
+
const initial = name && name.trim() !== "" ? name.trim()[0].toUpperCase() : "?";
|
|
118
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-1.5", "data-records-person-stamp": "", children: [
|
|
119
|
+
/* @__PURE__ */ jsx(
|
|
120
|
+
"span",
|
|
121
|
+
{
|
|
122
|
+
"aria-hidden": "true",
|
|
123
|
+
className: "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-foreground",
|
|
124
|
+
children: initial
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
/* @__PURE__ */ jsx("span", { className: cn("truncate", name ? "" : "italic text-muted-foreground"), children: words })
|
|
128
|
+
] });
|
|
129
|
+
}
|
|
130
|
+
function AddressLink({ value }) {
|
|
131
|
+
const words = addressText(value);
|
|
132
|
+
const href = addressMapUrl(value);
|
|
133
|
+
if (!words || !href) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
134
|
+
return /* @__PURE__ */ jsxs(
|
|
135
|
+
"a",
|
|
136
|
+
{
|
|
137
|
+
href,
|
|
138
|
+
target: "_blank",
|
|
139
|
+
rel: "noopener noreferrer",
|
|
140
|
+
className: "inline-flex min-w-0 items-center gap-1 text-primary hover:underline",
|
|
141
|
+
title: "Open in maps",
|
|
142
|
+
"data-records-address": "",
|
|
143
|
+
onClick: own,
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2316" }),
|
|
146
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: words })
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
var STATUS_GROUP_MARK = { todo: "\u25CB", in_progress: "\u25D0", done: "\u25CF" };
|
|
152
|
+
function statusMarkOf(field, value) {
|
|
153
|
+
if (newerKindOf(field) !== "status" || typeof value !== "string") return null;
|
|
154
|
+
const group = statusGroupOf(field, value);
|
|
155
|
+
return { mark: STATUS_GROUP_MARK[group], group, words: STATUS_GROUP_WORDS[group] };
|
|
156
|
+
}
|
|
157
|
+
var SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
158
|
+
function inline(text, key) {
|
|
159
|
+
const out = [];
|
|
160
|
+
let at = 0;
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (const r of findInlineCodeSpans(text)) {
|
|
163
|
+
if (r.start < at) continue;
|
|
164
|
+
if (r.start > at) out.push(...marks(text.slice(at, r.start), `${key}-m${n++}`));
|
|
165
|
+
out.push(/* @__PURE__ */ jsx("code", { className: "rounded bg-muted px-1 font-mono text-[0.9em]", children: codeSpanText(text.slice(r.start, r.end)) }, `${key}-c${n++}`));
|
|
166
|
+
at = r.end;
|
|
167
|
+
}
|
|
168
|
+
if (at < text.length) out.push(...marks(text.slice(at), `${key}-m${n++}`));
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
function marks(text, key) {
|
|
172
|
+
const out = [];
|
|
173
|
+
const re = /(\*\*[^*]+\*\*|__[^_]+__|\[[^\]]+\]\([^)\s]+\)|\*[^*\s][^*]*\*|_[^_\s][^_]*_)/g;
|
|
174
|
+
let last = 0;
|
|
175
|
+
let i = 0;
|
|
176
|
+
for (const m of text.matchAll(re)) {
|
|
177
|
+
const at = m.index ?? 0;
|
|
178
|
+
if (at > last) out.push(text.slice(last, at));
|
|
179
|
+
const tok = m[0];
|
|
180
|
+
const k = `${key}-${i++}`;
|
|
181
|
+
if (tok.startsWith("**") || tok.startsWith("__")) out.push(/* @__PURE__ */ jsx("strong", { children: tok.slice(2, -2) }, k));
|
|
182
|
+
else if (tok.startsWith("[")) {
|
|
183
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(tok);
|
|
184
|
+
const href = link?.[2] ?? "";
|
|
185
|
+
out.push(
|
|
186
|
+
SAFE_HREF.test(href) ? /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline-offset-2 hover:underline", onClick: own, children: link?.[1] }, k) : link?.[1] ?? tok
|
|
187
|
+
);
|
|
188
|
+
} else out.push(/* @__PURE__ */ jsx("em", { children: tok.slice(1, -1) }, k));
|
|
189
|
+
last = at + tok.length;
|
|
190
|
+
}
|
|
191
|
+
if (last < text.length) out.push(text.slice(last));
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function SafeMarkdown({ text, clipped }) {
|
|
195
|
+
const blocks = [];
|
|
196
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
197
|
+
let list = null;
|
|
198
|
+
let para = [];
|
|
199
|
+
const flushPara = () => {
|
|
200
|
+
if (para.length > 0) {
|
|
201
|
+
blocks.push(/* @__PURE__ */ jsx("p", { children: inline(para.join(" "), `p${blocks.length}`) }, `p${blocks.length}`));
|
|
202
|
+
para = [];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const flushList = () => {
|
|
206
|
+
if (list) {
|
|
207
|
+
const items = list.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: inline(item, `l${blocks.length}-${i}`) }, i));
|
|
208
|
+
blocks.push(
|
|
209
|
+
list.ordered ? /* @__PURE__ */ jsx("ol", { className: "ml-4 list-decimal", children: items }, `o${blocks.length}`) : /* @__PURE__ */ jsx("ul", { className: "ml-4 list-disc", children: items }, `u${blocks.length}`)
|
|
210
|
+
);
|
|
211
|
+
list = null;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
for (const raw of lines) {
|
|
215
|
+
const line = raw.trimEnd();
|
|
216
|
+
const bullet = /^\s*[-*+]\s+(.*)$/.exec(line);
|
|
217
|
+
const numbered = /^\s*\d+[.)]\s+(.*)$/.exec(line);
|
|
218
|
+
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
219
|
+
if (bullet || numbered) {
|
|
220
|
+
flushPara();
|
|
221
|
+
const ordered = Boolean(numbered);
|
|
222
|
+
if (list && list.ordered !== ordered) flushList();
|
|
223
|
+
if (!list) list = { ordered, items: [] };
|
|
224
|
+
list.items.push((bullet ?? numbered)[1]);
|
|
225
|
+
} else if (heading) {
|
|
226
|
+
flushPara();
|
|
227
|
+
flushList();
|
|
228
|
+
blocks.push(/* @__PURE__ */ jsx("p", { className: "font-semibold", children: inline(heading[2], `h${blocks.length}`) }, `h${blocks.length}`));
|
|
229
|
+
} else if (line.trim() === "") {
|
|
230
|
+
flushPara();
|
|
231
|
+
flushList();
|
|
232
|
+
} else {
|
|
233
|
+
flushList();
|
|
234
|
+
para.push(line.trim());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
flushPara();
|
|
238
|
+
flushList();
|
|
239
|
+
return /* @__PURE__ */ jsx(
|
|
240
|
+
"span",
|
|
241
|
+
{
|
|
242
|
+
className: cn("block min-w-0 space-y-1 whitespace-normal text-sm", clipped ? "max-h-[4.5em] overflow-hidden" : ""),
|
|
243
|
+
"data-records-rich-text": "",
|
|
244
|
+
children: blocks
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function newerKindControl(props) {
|
|
249
|
+
switch (newerKindOf(props.field)) {
|
|
250
|
+
case "duration":
|
|
251
|
+
return /* @__PURE__ */ jsx(DurationControl, { ...props });
|
|
252
|
+
case "address":
|
|
253
|
+
return /* @__PURE__ */ jsx(AddressControl, { ...props });
|
|
254
|
+
case "barcode":
|
|
255
|
+
return /* @__PURE__ */ jsx(BarcodeControl, { ...props });
|
|
256
|
+
case "created_by":
|
|
257
|
+
case "modified_by":
|
|
258
|
+
case "count":
|
|
259
|
+
return /* @__PURE__ */ jsx("div", { id: props.id, className: "rounded border border-dashed px-2 py-1.5 text-xs text-muted-foreground", children: props.value === null || props.value === void 0 ? "\u2014" : newerKindValue(props.field, props.value) });
|
|
260
|
+
default:
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function DurationControl({ value, onChange, disabled, id }) {
|
|
265
|
+
const [text, setText] = useState(() => durationText(value) ?? "");
|
|
266
|
+
const [wrong, setWrong] = useState(false);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
setText(durationText(value) ?? "");
|
|
269
|
+
}, [value]);
|
|
270
|
+
return /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-1", children: [
|
|
271
|
+
/* @__PURE__ */ jsx(
|
|
272
|
+
BasicInput,
|
|
273
|
+
{
|
|
274
|
+
id,
|
|
275
|
+
value: text,
|
|
276
|
+
disabled,
|
|
277
|
+
inputMode: "numeric",
|
|
278
|
+
placeholder: "45 min",
|
|
279
|
+
"aria-invalid": wrong || void 0,
|
|
280
|
+
className: "tabular-nums",
|
|
281
|
+
onChange: (e) => {
|
|
282
|
+
setText(e.target.value);
|
|
283
|
+
if (e.target.value.trim() === "") {
|
|
284
|
+
setWrong(false);
|
|
285
|
+
onChange(null);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const seconds = readTypedDuration(e.target.value);
|
|
289
|
+
setWrong(seconds === null);
|
|
290
|
+
if (seconds !== null) onChange(seconds);
|
|
291
|
+
},
|
|
292
|
+
onBlur: () => {
|
|
293
|
+
if (!wrong) setText(durationText(value) ?? "");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
wrong ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-destructive", children: "Write it like 45 min, 1:30 or 1h 30m." }) : null
|
|
298
|
+
] });
|
|
299
|
+
}
|
|
300
|
+
var ADDRESS_LABELS = {
|
|
301
|
+
street: "Street",
|
|
302
|
+
city: "City",
|
|
303
|
+
region: "State or region",
|
|
304
|
+
postal_code: "Postal code",
|
|
305
|
+
country: "Country"
|
|
306
|
+
};
|
|
307
|
+
var ADDRESS_EXAMPLES = {
|
|
308
|
+
street: "1420 Cedar Ridge Pkwy",
|
|
309
|
+
city: "Boulder",
|
|
310
|
+
region: "CO",
|
|
311
|
+
postal_code: "80301",
|
|
312
|
+
country: "US"
|
|
313
|
+
};
|
|
314
|
+
function AddressControl({ value, onChange, disabled, id }) {
|
|
315
|
+
const held = isAddressValue(value) ? value : typeof value === "string" && value.trim() !== "" ? { street: value } : {};
|
|
316
|
+
const set = (part, words) => {
|
|
317
|
+
const next = { ...held, [part]: words };
|
|
318
|
+
const kept = Object.fromEntries(Object.entries(next).filter(([, v]) => typeof v === "string" && v.trim() !== ""));
|
|
319
|
+
onChange(Object.keys(kept).length === 0 ? null : kept);
|
|
320
|
+
};
|
|
321
|
+
return /* @__PURE__ */ jsx("span", { id, className: "grid grid-cols-2 gap-1.5", "data-records-address-control": "", children: ADDRESS_PARTS.map((part) => /* @__PURE__ */ jsx(
|
|
322
|
+
BasicInput,
|
|
323
|
+
{
|
|
324
|
+
"aria-label": ADDRESS_LABELS[part],
|
|
325
|
+
placeholder: ADDRESS_EXAMPLES[part],
|
|
326
|
+
className: part === "street" ? "col-span-2" : "",
|
|
327
|
+
value: typeof held[part] === "string" ? held[part] : "",
|
|
328
|
+
disabled,
|
|
329
|
+
...part === "street" ? { autoComplete: "street-address" } : {},
|
|
330
|
+
onChange: (e) => set(part, e.target.value)
|
|
331
|
+
},
|
|
332
|
+
part
|
|
333
|
+
)) });
|
|
334
|
+
}
|
|
335
|
+
function barcodeDetector() {
|
|
336
|
+
const ctor = globalThis.BarcodeDetector;
|
|
337
|
+
return typeof ctor === "function" ? ctor : null;
|
|
338
|
+
}
|
|
339
|
+
function BarcodeControl({ value, onChange, disabled, id }) {
|
|
340
|
+
const file = useRef(null);
|
|
341
|
+
const [said, setSaid] = useState(null);
|
|
342
|
+
const Detect = barcodeDetector();
|
|
343
|
+
return /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-1", children: [
|
|
344
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
345
|
+
/* @__PURE__ */ jsx(
|
|
346
|
+
BasicInput,
|
|
347
|
+
{
|
|
348
|
+
id,
|
|
349
|
+
className: "font-mono",
|
|
350
|
+
value: typeof value === "string" ? value : "",
|
|
351
|
+
disabled,
|
|
352
|
+
placeholder: "4006381333931",
|
|
353
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
|
|
354
|
+
}
|
|
355
|
+
),
|
|
356
|
+
Detect && !disabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
357
|
+
/* @__PURE__ */ jsx(
|
|
358
|
+
"button",
|
|
359
|
+
{
|
|
360
|
+
type: "button",
|
|
361
|
+
className: "shrink-0 rounded-md border px-2 py-1 text-xs hover:bg-accent",
|
|
362
|
+
onClick: () => file.current?.click(),
|
|
363
|
+
children: "Scan"
|
|
364
|
+
}
|
|
365
|
+
),
|
|
366
|
+
/* @__PURE__ */ jsx(
|
|
367
|
+
"input",
|
|
368
|
+
{
|
|
369
|
+
ref: file,
|
|
370
|
+
type: "file",
|
|
371
|
+
accept: "image/*",
|
|
372
|
+
capture: "environment",
|
|
373
|
+
className: "hidden",
|
|
374
|
+
onChange: async (e) => {
|
|
375
|
+
const photo = e.target.files?.[0];
|
|
376
|
+
e.target.value = "";
|
|
377
|
+
if (!photo) return;
|
|
378
|
+
try {
|
|
379
|
+
const found = await new Detect().detect(await createImageBitmap(photo));
|
|
380
|
+
if (found[0]?.rawValue) {
|
|
381
|
+
onChange(found[0].rawValue);
|
|
382
|
+
setSaid(null);
|
|
383
|
+
} else setSaid("No barcode in that photo. Try closer, in good light.");
|
|
384
|
+
} catch {
|
|
385
|
+
setSaid("This phone could not read that photo. Type the code instead.");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
] }) : null
|
|
391
|
+
] }),
|
|
392
|
+
said ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: said }) : null
|
|
393
|
+
] });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/parity.ts
|
|
397
|
+
import {
|
|
398
|
+
KERNEL_TABLES,
|
|
399
|
+
PARITY_FIELD_TYPES,
|
|
400
|
+
fieldKindFor,
|
|
401
|
+
newerKindOf as newerKindOf2
|
|
402
|
+
} from "@ai-matrx/records";
|
|
403
|
+
import { humanizeIdentifier } from "@ai-matrx/kit/text-case";
|
|
404
|
+
|
|
405
|
+
// src/docs.ts
|
|
406
|
+
function isSignatureField(field) {
|
|
407
|
+
return field.type === "text" && (field.format ?? "").toLowerCase() === "signature";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/parity.ts
|
|
411
|
+
var kernelId = (name) => KERNEL_TABLES.find((t) => t.name === name)?.id ?? "";
|
|
412
|
+
var KERNEL_FILE_TABLE = kernelId("File");
|
|
413
|
+
var KERNEL_PERSON_TABLE = kernelId("Person");
|
|
414
|
+
var PARITY_MADE_OF = Object.fromEntries(
|
|
415
|
+
PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of])
|
|
416
|
+
);
|
|
417
|
+
var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
|
|
418
|
+
function editorKindFor(field) {
|
|
419
|
+
return fieldKindFor(field, KERNEL);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/names.ts
|
|
423
|
+
import { isUuidShape } from "@ai-matrx/kit/uuid";
|
|
424
|
+
import { humanizeIdentifier as humanizeIdentifier2 } from "@ai-matrx/kit/text-case";
|
|
425
|
+
function fieldName(field) {
|
|
426
|
+
const label = (field.label ?? "").trim();
|
|
427
|
+
if (label !== "" && label === field.key && /^[a-z0-9]+(?:[_.-][a-z0-9]+)+$/.test(label)) return humanizeIdentifier2(label);
|
|
428
|
+
if (label !== "") return label;
|
|
429
|
+
const name = (field.name ?? "").trim();
|
|
430
|
+
if (name !== "") return name;
|
|
431
|
+
return humanizeIdentifier2(field.key);
|
|
432
|
+
}
|
|
433
|
+
function recordName(document2, titleKey, fallback = "Untitled") {
|
|
434
|
+
const data = document2 ?? {};
|
|
435
|
+
const keys = [titleKey, "title", "name", "label", "full_name", "subject", "summary"].filter(
|
|
436
|
+
(key) => typeof key === "string" && key !== ""
|
|
437
|
+
);
|
|
438
|
+
for (const key of keys) {
|
|
439
|
+
const value = data[key];
|
|
440
|
+
if (typeof value === "string" && value.trim() !== "" && !looksLikeId(value.trim())) {
|
|
441
|
+
return value.trim();
|
|
442
|
+
}
|
|
443
|
+
if (typeof value === "number") return String(value);
|
|
444
|
+
}
|
|
445
|
+
return fallback;
|
|
446
|
+
}
|
|
447
|
+
function looksLikeId(value) {
|
|
448
|
+
return typeof value === "string" && isUuidShape(value.trim());
|
|
449
|
+
}
|
|
450
|
+
function rowNameIn(table, row, fallback = "Untitled") {
|
|
451
|
+
return recordName(row?.document, table?.title_field ?? null, fallback);
|
|
452
|
+
}
|
|
453
|
+
var WITHHELD_RECORD_LABEL = "A record you have not been given access to";
|
|
454
|
+
|
|
455
|
+
// src/RelationPicker.tsx
|
|
456
|
+
import { useEffect as useEffect3, useMemo, useState as useState3 } from "react";
|
|
457
|
+
import { rankTypedMatches } from "@ai-matrx/records";
|
|
458
|
+
import { useOptionalRecordsClient } from "@ai-matrx/records/react";
|
|
459
|
+
import { BasicInput as BasicInput3, Button as Button2, cn as cn4 } from "@ai-matrx/design-system";
|
|
460
|
+
|
|
461
|
+
// src/PickOrAdd.tsx
|
|
462
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
463
|
+
import { BasicInput as BasicInput2, Button, Popover, PopoverContent, PopoverTrigger, ScrollArea, cn as cn2 } from "@ai-matrx/design-system";
|
|
464
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
465
|
+
function useListKeys(args) {
|
|
466
|
+
const { count, resetOn, onPick, onNothing, onClose } = args;
|
|
467
|
+
const [highlight, setHighlight] = useState2(0);
|
|
468
|
+
useEffect2(() => setHighlight(0), [resetOn]);
|
|
469
|
+
const at = count === 0 ? -1 : Math.min(highlight, count - 1);
|
|
470
|
+
const onKeyDown = (event) => {
|
|
471
|
+
event.stopPropagation();
|
|
472
|
+
if (event.key === "ArrowDown") {
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
if (count > 0) setHighlight((h) => (Math.min(h, count - 1) + 1) % count);
|
|
475
|
+
} else if (event.key === "ArrowUp") {
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
if (count > 0) setHighlight((h) => (Math.min(h, count - 1) - 1 + count) % count);
|
|
478
|
+
} else if (event.key === "Enter") {
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
if (at >= 0) onPick(at);
|
|
481
|
+
else onNothing?.();
|
|
482
|
+
} else if (event.key === "Escape") {
|
|
483
|
+
event.preventDefault();
|
|
484
|
+
onClose();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
return { highlight: at, onKeyDown };
|
|
488
|
+
}
|
|
489
|
+
function PickOrAdd({
|
|
490
|
+
triggerLabel,
|
|
491
|
+
triggerText,
|
|
492
|
+
triggerId,
|
|
493
|
+
open,
|
|
494
|
+
onOpenChange,
|
|
495
|
+
search,
|
|
496
|
+
onSearch,
|
|
497
|
+
items,
|
|
498
|
+
onPick,
|
|
499
|
+
notice,
|
|
500
|
+
empty,
|
|
501
|
+
add,
|
|
502
|
+
foot,
|
|
503
|
+
footNote,
|
|
504
|
+
onEnterWithNoMatch
|
|
505
|
+
}) {
|
|
506
|
+
const hasFoot = Boolean(add) || Boolean(foot) || Boolean(footNote);
|
|
507
|
+
const keys = useListKeys({
|
|
508
|
+
count: items.length,
|
|
509
|
+
resetOn: search,
|
|
510
|
+
onPick: (index) => {
|
|
511
|
+
const item = items[index];
|
|
512
|
+
if (item) onPick(item.id);
|
|
513
|
+
},
|
|
514
|
+
onNothing: onEnterWithNoMatch ?? (add && !add.disabled ? add.onAdd : void 0),
|
|
515
|
+
onClose: () => onOpenChange(false)
|
|
516
|
+
});
|
|
517
|
+
return /* @__PURE__ */ jsxs2(Popover, { open, onOpenChange, children: [
|
|
518
|
+
/* @__PURE__ */ jsx2(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx2(Button, { id: triggerId, type: "button", size: "sm", variant: "outline", "aria-label": triggerLabel, children: triggerText }) }),
|
|
519
|
+
/* @__PURE__ */ jsxs2(
|
|
520
|
+
PopoverContent,
|
|
521
|
+
{
|
|
522
|
+
sizing: "content",
|
|
523
|
+
className: "p-1",
|
|
524
|
+
align: "start",
|
|
525
|
+
"data-matrx-cell-editor": "",
|
|
526
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
527
|
+
children: [
|
|
528
|
+
/* @__PURE__ */ jsx2(
|
|
529
|
+
BasicInput2,
|
|
530
|
+
{
|
|
531
|
+
value: search,
|
|
532
|
+
onChange: (e) => onSearch(e.target.value),
|
|
533
|
+
onKeyDown: keys.onKeyDown,
|
|
534
|
+
placeholder: "Search",
|
|
535
|
+
"aria-label": `Search \u2014 ${triggerLabel}`,
|
|
536
|
+
autoFocus: true,
|
|
537
|
+
autoComplete: "off",
|
|
538
|
+
className: "mb-1 h-7 text-xs"
|
|
539
|
+
}
|
|
540
|
+
),
|
|
541
|
+
notice,
|
|
542
|
+
/* @__PURE__ */ jsxs2(ScrollArea, { className: "max-h-56", children: [
|
|
543
|
+
/* @__PURE__ */ jsx2("ul", { role: "listbox", "aria-label": triggerLabel, children: items.map((item, index) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2(
|
|
544
|
+
"button",
|
|
545
|
+
{
|
|
546
|
+
type: "button",
|
|
547
|
+
onClick: () => onPick(item.id),
|
|
548
|
+
title: item.title ?? item.label,
|
|
549
|
+
...item.attrs ?? {},
|
|
550
|
+
"data-pick-highlighted": index === keys.highlight ? "" : void 0,
|
|
551
|
+
className: cn2(
|
|
552
|
+
"w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
|
|
553
|
+
item.picked ? "bg-accent text-accent-foreground" : "",
|
|
554
|
+
index === keys.highlight ? "ring-1 ring-ring" : ""
|
|
555
|
+
),
|
|
556
|
+
children: [
|
|
557
|
+
item.chip ? /* @__PURE__ */ jsx2(
|
|
558
|
+
"span",
|
|
559
|
+
{
|
|
560
|
+
className: cn2("inline-flex max-w-full truncate rounded-md border px-1.5 py-0.5 text-[11px] font-medium", item.chip),
|
|
561
|
+
"data-records-choice-chip": "",
|
|
562
|
+
children: item.label
|
|
563
|
+
}
|
|
564
|
+
) : item.label,
|
|
565
|
+
item.detail ? /* @__PURE__ */ jsx2("span", { className: "block truncate text-[11px] text-muted-foreground", "data-tell-apart": "", children: item.detail }) : null
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
) }, item.id)) }),
|
|
569
|
+
items.length === 0 && empty ? empty : null
|
|
570
|
+
] }),
|
|
571
|
+
hasFoot ? /* @__PURE__ */ jsxs2("div", { className: "mt-1 border-t pt-1", children: [
|
|
572
|
+
foot ?? (add ? /* @__PURE__ */ jsx2(
|
|
573
|
+
Button,
|
|
574
|
+
{
|
|
575
|
+
type: "button",
|
|
576
|
+
size: "sm",
|
|
577
|
+
variant: "ghost",
|
|
578
|
+
title: add.label,
|
|
579
|
+
className: "w-full justify-start text-xs",
|
|
580
|
+
disabled: add.disabled ?? false,
|
|
581
|
+
onClick: add.onAdd,
|
|
582
|
+
children: /* @__PURE__ */ jsx2("span", { className: "block w-full truncate text-left", children: add.label })
|
|
583
|
+
}
|
|
584
|
+
) : null),
|
|
585
|
+
footNote
|
|
586
|
+
] }) : null
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
] });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/Refusal.tsx
|
|
594
|
+
import { Alert, AlertDescription, AlertTitle, ErrorBox, cn as cn3 } from "@ai-matrx/design-system";
|
|
595
|
+
|
|
596
|
+
// src/plainWords.ts
|
|
597
|
+
var MACHINE_IDENTITY = [
|
|
598
|
+
/\b[A-Z]{2,4}-\d{1,3}\b/,
|
|
599
|
+
// A build lane's tag: capital words joined by hyphens, then a colon ("STORE-TAILS-3:",
|
|
600
|
+
// "MOVE-AND-OUTSIDER:"). A person never reads which lane wrote a sentence.
|
|
601
|
+
/\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+:/,
|
|
602
|
+
/\b(?:custom|platform|iam|history|public|auth|campaign_watch)\.[a-z_]+/,
|
|
603
|
+
/\b(?:config|data|document|spec|patch)\.[a-z_]+/,
|
|
604
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
|
|
605
|
+
/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/,
|
|
606
|
+
// Five characters of digits and capitals, at least one a digit — so `23514`
|
|
607
|
+
// and `PT409` match and an emphatic `NEVER` does not.
|
|
608
|
+
/\b(?=[0-9A-Z]{5}\b)(?=[0-9A-Z]*\d)[0-9A-Z]{5}\b/
|
|
609
|
+
];
|
|
610
|
+
function isMachineIdentity(text) {
|
|
611
|
+
return MACHINE_IDENTITY.some((shape) => shape.test(text));
|
|
612
|
+
}
|
|
613
|
+
var TITLE = {
|
|
614
|
+
stale_write: "Someone else wrote first",
|
|
615
|
+
door: "You do not have access to this",
|
|
616
|
+
door_absent: "That is not built yet",
|
|
617
|
+
refused_by_rule: "That value was not accepted",
|
|
618
|
+
invalid_argument: "That was not accepted",
|
|
619
|
+
not_found: "Not there any more",
|
|
620
|
+
missing_reference: "That points at something that is not there",
|
|
621
|
+
already_exists: "That already exists",
|
|
622
|
+
not_supported: "That cannot be done here",
|
|
623
|
+
store_limit: "This has got as big as it can",
|
|
624
|
+
store_not_served: "This organization does not keep its data here",
|
|
625
|
+
unreachable: "We could not reach your data",
|
|
626
|
+
needs_approval: "This is waiting on someone's approval",
|
|
627
|
+
stale_read: "Your view of this is a moment behind",
|
|
628
|
+
timed_out: "That took too long to answer",
|
|
629
|
+
internal: "Something went wrong at our end"
|
|
630
|
+
};
|
|
631
|
+
var REMEDY = {
|
|
632
|
+
stale_write: "Look at what they wrote. Keep theirs, or try again and yours goes on top.",
|
|
633
|
+
door: "Ask somebody who administers this to share it with you at the level you need.",
|
|
634
|
+
door_absent: "There is nothing to do here yet. It is on our list, not yours.",
|
|
635
|
+
refused_by_rule: "Change the value so it passes, then try again.",
|
|
636
|
+
invalid_argument: "Change what you entered, then try again.",
|
|
637
|
+
not_found: "It may have been deleted. Go back to the list and open it again from there.",
|
|
638
|
+
missing_reference: "Pick something that is still there.",
|
|
639
|
+
already_exists: "Use a different name.",
|
|
640
|
+
not_supported: "There is another way to do this \u2014 ask, and we will point you at it.",
|
|
641
|
+
store_limit: "Split it up, or ask us to raise the ceiling for your organization.",
|
|
642
|
+
store_not_served: "An administrator of this organization turns the record store on from the data settings screen.",
|
|
643
|
+
// 🚨 NOTHING WAS WRITTEN AND NOTHING WAS REFUSED (lane TAILS-6). The call
|
|
644
|
+
// never arrived, so "try again" is the honest instruction rather than a
|
|
645
|
+
// consolation — and saying nothing changed is the half a person actually
|
|
646
|
+
// worries about.
|
|
647
|
+
unreachable: "Nothing was changed. Check your connection and open this again.",
|
|
648
|
+
needs_approval: "Nothing was changed yet. The people who can approve it have been asked.",
|
|
649
|
+
stale_read: "Open this again in a moment and it will be up to date.",
|
|
650
|
+
timed_out: "Nothing was changed. Try a smaller page, or try again in a moment.",
|
|
651
|
+
internal: "Nothing you did caused this. Try again, and tell us if it keeps happening."
|
|
652
|
+
};
|
|
653
|
+
var ENGINEER_TEXT = [
|
|
654
|
+
/@ai-matrx\//,
|
|
655
|
+
/\bSQLSTATE\b/i,
|
|
656
|
+
/\bPostgREST\b/i,
|
|
657
|
+
/\bIndexedDB\b/i,
|
|
658
|
+
// `<RecordsUiProvider>` — a component, on a screen.
|
|
659
|
+
/<\/?[A-Z][A-Za-z0-9]*\s*\/?>/,
|
|
660
|
+
// `savedViews`, `openRecords`, `onReask` — an identifier, however it is
|
|
661
|
+
// quoted. English does not write camelCase; code does.
|
|
662
|
+
/\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/,
|
|
663
|
+
// A backticked token is a thing you type into an editor, not a thing you do.
|
|
664
|
+
/`[A-Za-z_][A-Za-z0-9_.]*`/,
|
|
665
|
+
// "bind X on Y", "pass X", "import X" — an instruction to whoever maintains
|
|
666
|
+
// the app, arriving on the screen of somebody who cannot act on it.
|
|
667
|
+
/\bbind\s+[`'"A-Za-z]/i
|
|
668
|
+
];
|
|
669
|
+
function isDeveloperHint(hint) {
|
|
670
|
+
const text = hint.trim();
|
|
671
|
+
if (text === "") return false;
|
|
672
|
+
if (text.includes("```")) return true;
|
|
673
|
+
if (/\n\s+at\s+\S+/.test(text)) return true;
|
|
674
|
+
if (text.length > 600) return true;
|
|
675
|
+
return isEngineerText(text);
|
|
676
|
+
}
|
|
677
|
+
function isEngineerText(text) {
|
|
678
|
+
return ENGINEER_TEXT.some((shape) => shape.test(text));
|
|
679
|
+
}
|
|
680
|
+
function withoutMachineClauses(sentence) {
|
|
681
|
+
const dropped = [];
|
|
682
|
+
const footnotes = [];
|
|
683
|
+
const withoutFootnotes = sentence.replace(/\s*\((?:see\s+)?[A-Z]{2,5}-[A-Z0-9-]{1,8}\)/g, (hit) => {
|
|
684
|
+
footnotes.push(hit.trim());
|
|
685
|
+
return "";
|
|
686
|
+
});
|
|
687
|
+
dropped.push(...footnotes);
|
|
688
|
+
const clauses = withoutFootnotes.split(/(?:,\s+so\s+|\s+—\s+|\s+--\s+|\s*\|\s*)/).map((clause) => clause.trim()).filter((clause) => clause !== "");
|
|
689
|
+
const kept = clauses.filter((clause) => {
|
|
690
|
+
if (!isMachineIdentity(clause)) return true;
|
|
691
|
+
dropped.push(clause);
|
|
692
|
+
return false;
|
|
693
|
+
});
|
|
694
|
+
if (kept.length === 0) return { kept: null, dropped };
|
|
695
|
+
if (dropped.length === 0) return { kept: sentence.trim(), dropped };
|
|
696
|
+
let joined = kept.join(". ").replace(/\.\.+/g, ".").trim();
|
|
697
|
+
if (!/[.!?]$/.test(joined)) joined = `${joined}.`;
|
|
698
|
+
return { kept: joined, dropped };
|
|
699
|
+
}
|
|
700
|
+
function peopleSentence(message) {
|
|
701
|
+
const [lead = "", ...tail] = message.split(/\s+—\s+|\s+--\s+/);
|
|
702
|
+
const sentences = lead.trim().split(/(?<=(?<!\b(?:Dr|Mr|Mrs|Ms|St|vs|No|e\.g|i\.e|etc|[A-Z]))[.!?])\s+/);
|
|
703
|
+
const first = sentences[0] ?? "";
|
|
704
|
+
const rest = [...sentences.slice(1), ...tail].map((part) => part.trim()).filter((part) => part !== "");
|
|
705
|
+
const cleaned = plainSentence(first);
|
|
706
|
+
return { kept: cleaned.kept, dropped: [...cleaned.dropped, ...rest] };
|
|
707
|
+
}
|
|
708
|
+
function refusalSentence(error) {
|
|
709
|
+
return refusalForAPerson(error).sentence;
|
|
710
|
+
}
|
|
711
|
+
function plainSentence(message) {
|
|
712
|
+
const dropped = [];
|
|
713
|
+
const kept = [];
|
|
714
|
+
for (const raw of message.split(/(?<=[.!?])\s+/)) {
|
|
715
|
+
const sentence = raw.trim();
|
|
716
|
+
if (sentence === "") continue;
|
|
717
|
+
const cleaned = withoutMachineClauses(sentence);
|
|
718
|
+
dropped.push(...cleaned.dropped);
|
|
719
|
+
if (cleaned.kept) kept.push(cleaned.kept);
|
|
720
|
+
}
|
|
721
|
+
return { kept: kept.length === 0 ? null : kept.join(" "), dropped };
|
|
722
|
+
}
|
|
723
|
+
function exampleOf(error) {
|
|
724
|
+
const detail = error.detail;
|
|
725
|
+
if (!detail || typeof detail !== "object") return null;
|
|
726
|
+
const example = detail.example;
|
|
727
|
+
return typeof example === "string" && example.trim() !== "" ? example.trim() : null;
|
|
728
|
+
}
|
|
729
|
+
function refusalForAPerson(error) {
|
|
730
|
+
const code = error.code ?? "internal";
|
|
731
|
+
const ours = error.ours === true;
|
|
732
|
+
const hint = plainSentence(String(error.hint ?? ""));
|
|
733
|
+
const message = ours ? plainSentence(String(error.message ?? "")) : peopleSentence(String(error.message ?? ""));
|
|
734
|
+
const title = TITLE[code] ?? "The store refused this";
|
|
735
|
+
const hintIsForEngineers = isDeveloperHint(String(error.hint ?? ""));
|
|
736
|
+
const example = exampleOf(error);
|
|
737
|
+
const remedy = (example ? `Enter it like ${example}.` : null) ?? (hintIsForEngineers ? null : hint.kept) ?? REMEDY[code] ?? REMEDY["internal"];
|
|
738
|
+
const messageIsForEngineers = isEngineerText(String(error.message ?? ""));
|
|
739
|
+
const said = (messageIsForEngineers ? "" : message.kept ?? "").trim();
|
|
740
|
+
const forEngineers = [
|
|
741
|
+
error.sqlstate ? `SQLSTATE ${error.sqlstate}.` : null,
|
|
742
|
+
// The client's own note about a class it did not recognise, or a call that
|
|
743
|
+
// never reached the store. It is rendered out of sight, never as a sentence.
|
|
744
|
+
error.diagnostic ? error.diagnostic : null,
|
|
745
|
+
messageIsForEngineers ? `Message: ${error.message}` : null,
|
|
746
|
+
error.hint && (hintIsForEngineers || hint.kept !== error.hint) ? `Hint: ${error.hint}` : null,
|
|
747
|
+
message.dropped.length > 0 || hint.dropped.length > 0 ? `Dropped from the person's view: ${[...message.dropped, ...hint.dropped].join(" | ")}` : null
|
|
748
|
+
].filter(Boolean).join(" ");
|
|
749
|
+
return {
|
|
750
|
+
title,
|
|
751
|
+
// Nothing invented. When every clause was machine identity the title says
|
|
752
|
+
// what happened and the remedy says what to do; a made-up diagnosis would
|
|
753
|
+
// be worse than the identifier it replaced.
|
|
754
|
+
sentence: said === "" ? `${title}.` : said,
|
|
755
|
+
remedy,
|
|
756
|
+
storeSaid: said !== "",
|
|
757
|
+
forEngineers
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function refusalLineForAPerson(error) {
|
|
761
|
+
const plain = refusalForAPerson(error);
|
|
762
|
+
const sentence = plain.sentence.trim();
|
|
763
|
+
const ended = sentence === "" || /[.!?…:]$/.test(sentence) ? sentence : `${sentence}.`;
|
|
764
|
+
return `${ended} ${plain.remedy}`.trim();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/Refusal.tsx
|
|
768
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
769
|
+
function RefusalNotice({
|
|
770
|
+
error,
|
|
771
|
+
className,
|
|
772
|
+
actions,
|
|
773
|
+
onKeepEditing,
|
|
774
|
+
onDiscard
|
|
775
|
+
}) {
|
|
776
|
+
const plain = refusalForAPerson(error);
|
|
777
|
+
const doors = onKeepEditing || onDiscard ? /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1 pt-0.5", "data-refusal-doors": "", children: [
|
|
778
|
+
onKeepEditing ? /* @__PURE__ */ jsx3(
|
|
779
|
+
"button",
|
|
780
|
+
{
|
|
781
|
+
type: "button",
|
|
782
|
+
"data-refusal-keep-editing": "",
|
|
783
|
+
className: "rounded border px-2 py-0.5 text-xs hover:bg-muted",
|
|
784
|
+
onClick: onKeepEditing,
|
|
785
|
+
children: "Keep editing"
|
|
786
|
+
}
|
|
787
|
+
) : null,
|
|
788
|
+
onDiscard ? /* @__PURE__ */ jsx3(
|
|
789
|
+
"button",
|
|
790
|
+
{
|
|
791
|
+
type: "button",
|
|
792
|
+
"data-refusal-discard": "",
|
|
793
|
+
className: "rounded border px-2 py-0.5 text-xs hover:bg-muted",
|
|
794
|
+
onClick: onDiscard,
|
|
795
|
+
children: "Discard"
|
|
796
|
+
}
|
|
797
|
+
) : null
|
|
798
|
+
] }) : null;
|
|
799
|
+
return /* @__PURE__ */ jsxs3(
|
|
800
|
+
Alert,
|
|
801
|
+
{
|
|
802
|
+
variant: "destructive",
|
|
803
|
+
role: "alert",
|
|
804
|
+
"data-refusal-notice": "",
|
|
805
|
+
className: cn3("text-xs", className),
|
|
806
|
+
errorFacts: { message: plain.sentence, title: plain.title, error, origin: "records-ui/RefusalNotice" },
|
|
807
|
+
...plain.forEngineers ? { "data-for-engineers": plain.forEngineers } : {},
|
|
808
|
+
...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {},
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ jsx3(AlertTitle, { className: "text-xs font-medium", children: plain.storeSaid ? plain.sentence : plain.title }),
|
|
811
|
+
/* @__PURE__ */ jsxs3(AlertDescription, { className: "space-y-1 text-xs", children: [
|
|
812
|
+
plain.storeSaid || plain.sentence.replace(/\.$/, "") === plain.title ? null : /* @__PURE__ */ jsx3("p", { children: plain.sentence }),
|
|
813
|
+
/* @__PURE__ */ jsx3("p", { className: "opacity-80", children: plain.remedy }),
|
|
814
|
+
actions,
|
|
815
|
+
doors
|
|
816
|
+
] })
|
|
817
|
+
]
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
function RefusalLine({ error, className }) {
|
|
822
|
+
const plain = refusalForAPerson(error);
|
|
823
|
+
return /* @__PURE__ */ jsx3(
|
|
824
|
+
ErrorBox,
|
|
825
|
+
{
|
|
826
|
+
"data-refusal-line": "",
|
|
827
|
+
className: cn3("text-xs text-destructive", className),
|
|
828
|
+
message: refusalLineForAPerson(error),
|
|
829
|
+
error,
|
|
830
|
+
origin: "records-ui/RefusalLine",
|
|
831
|
+
...plain.forEngineers ? { "data-for-engineers": plain.forEngineers } : {}
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/refusals.ts
|
|
837
|
+
function refusal(code, sentence, remedy, diagnostic) {
|
|
838
|
+
return {
|
|
839
|
+
code,
|
|
840
|
+
message: sentence,
|
|
841
|
+
ours: true,
|
|
842
|
+
...remedy ? { hint: remedy } : {},
|
|
843
|
+
...diagnostic ? { diagnostic } : {}
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function refusalFromThrown(thrown, sentence, remedy, code = "internal") {
|
|
847
|
+
const said = thrown instanceof Error ? thrown.message : String(thrown ?? "");
|
|
848
|
+
return refusal(code, sentence, remedy, said ? `Thrown: ${said}` : null);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/tellApart.ts
|
|
852
|
+
function shown(value) {
|
|
853
|
+
if (value === null || value === void 0) return null;
|
|
854
|
+
if (typeof value === "string") return value.trim() === "" ? null : value.trim();
|
|
855
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
function tellApart(rows, nameOf) {
|
|
859
|
+
const byName = /* @__PURE__ */ new Map();
|
|
860
|
+
for (const row of rows) {
|
|
861
|
+
const name = nameOf(row.id).trim().toLowerCase();
|
|
862
|
+
byName.set(name, [...byName.get(name) ?? [], row]);
|
|
863
|
+
}
|
|
864
|
+
const out = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const [name, group] of byName) {
|
|
866
|
+
if (group.length < 2) continue;
|
|
867
|
+
const keys = [
|
|
868
|
+
...new Set(
|
|
869
|
+
group.flatMap(
|
|
870
|
+
(row) => Object.keys(row.document ?? {}).filter((k) => !k.startsWith("_") && shown(row.document?.[k]) !== null)
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
].filter((k) => group.some((row) => (shown(row.document?.[k]) ?? "").toLowerCase() !== name));
|
|
874
|
+
const valuesOf = (k) => group.map((row) => shown(row.document?.[k]) ?? "");
|
|
875
|
+
const differs = keys.filter((k) => new Set(valuesOf(k)).size > 1);
|
|
876
|
+
const whole = differs.find((k) => new Set(valuesOf(k)).size === group.length);
|
|
877
|
+
const chosen = whole ? [whole] : differs.slice(0, 2);
|
|
878
|
+
if (chosen.length === 0) continue;
|
|
879
|
+
for (const row of group) {
|
|
880
|
+
const words = chosen.map((k) => shown(row.document?.[k])).filter((w) => w !== null);
|
|
881
|
+
if (words.length > 0) out.set(row.id, words.join(" \xB7 "));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/RelationPicker.tsx
|
|
888
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
889
|
+
var NO_STORE_ON_THIS_SCREEN_REASON = "This question points at a record in somebody's own data, and a page answered without an account is never allowed to search that. Leave it blank \u2014 whoever receives your answer will fill it in.";
|
|
890
|
+
function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true }) {
|
|
891
|
+
const client = useOptionalRecordsClient();
|
|
892
|
+
const [rows, setRows] = useState3(null);
|
|
893
|
+
const [words, setWords] = useState3({});
|
|
894
|
+
const [refusal2, setRefusal] = useState3(null);
|
|
895
|
+
const [search, setSearch] = useState3("");
|
|
896
|
+
const [open, setOpen] = useState3(false);
|
|
897
|
+
const [target, setTarget] = useState3(null);
|
|
898
|
+
const [targetRefusal, setTargetRefusal] = useState3(null);
|
|
899
|
+
const [creating, setCreating] = useState3(false);
|
|
900
|
+
const [newTitle, setNewTitle] = useState3("");
|
|
901
|
+
const [busy, setBusy] = useState3(false);
|
|
902
|
+
const [createRefusal, setCreateRefusal] = useState3(null);
|
|
903
|
+
const picked = useMemo(
|
|
904
|
+
() => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
|
|
905
|
+
[value]
|
|
906
|
+
);
|
|
907
|
+
useEffect3(() => {
|
|
908
|
+
const target2 = field.relation_target;
|
|
909
|
+
if (!target2 || !client) return;
|
|
910
|
+
let cancelled = false;
|
|
911
|
+
void client.list({ table_id: target2, limit: 200 }).then((result) => {
|
|
912
|
+
if (cancelled) return;
|
|
913
|
+
if (result.ok) setRows(result.data.rows);
|
|
914
|
+
else setRefusal(result.error);
|
|
915
|
+
});
|
|
916
|
+
return () => {
|
|
917
|
+
cancelled = true;
|
|
918
|
+
};
|
|
919
|
+
}, [client, field.relation_target]);
|
|
920
|
+
useEffect3(() => {
|
|
921
|
+
if (!rows || !client) return;
|
|
922
|
+
const wanted = [.../* @__PURE__ */ new Set([...rows.map((row) => row.id), ...picked])].filter(
|
|
923
|
+
(id2) => words[id2] === void 0
|
|
924
|
+
);
|
|
925
|
+
if (wanted.length === 0) return;
|
|
926
|
+
let cancelled = false;
|
|
927
|
+
void client.relationWordsMany({ field_id: field.id, record_ids: wanted }).then((result) => {
|
|
928
|
+
if (cancelled) return;
|
|
929
|
+
const learned = {};
|
|
930
|
+
for (const id2 of wanted) {
|
|
931
|
+
const said = result.ok ? result.data[id2] : void 0;
|
|
932
|
+
learned[id2] = typeof said === "string" && said !== "" ? said : WITHHELD_RECORD_LABEL;
|
|
933
|
+
}
|
|
934
|
+
setWords((current) => ({ ...current, ...learned }));
|
|
935
|
+
});
|
|
936
|
+
return () => {
|
|
937
|
+
cancelled = true;
|
|
938
|
+
};
|
|
939
|
+
}, [client, field.id, rows, picked, words]);
|
|
940
|
+
useEffect3(() => {
|
|
941
|
+
if (!open || !allowCreate || target || targetRefusal || !client) return;
|
|
942
|
+
const id2 = field.relation_target;
|
|
943
|
+
if (!id2) return;
|
|
944
|
+
let cancelled = false;
|
|
945
|
+
void client.tableList().then((result) => {
|
|
946
|
+
if (cancelled) return;
|
|
947
|
+
if (!result.ok) {
|
|
948
|
+
setTargetRefusal(result.error);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const found = result.data.find((t) => t.id === id2) ?? null;
|
|
952
|
+
if (found) setTarget(found);
|
|
953
|
+
else
|
|
954
|
+
setTargetRefusal(
|
|
955
|
+
refusal(
|
|
956
|
+
"door",
|
|
957
|
+
"That Table is not one you can open, so a new record cannot be added to it from here."
|
|
958
|
+
)
|
|
959
|
+
);
|
|
960
|
+
});
|
|
961
|
+
return () => {
|
|
962
|
+
cancelled = true;
|
|
963
|
+
};
|
|
964
|
+
}, [client, open, allowCreate, target, targetRefusal, field.relation_target]);
|
|
965
|
+
if (!client) {
|
|
966
|
+
return (
|
|
967
|
+
// The Field's own name is already printed above this control by
|
|
968
|
+
// `FieldLabel`, so the sentence does not say it a second time.
|
|
969
|
+
/* @__PURE__ */ jsx4("p", { id, className: cn4("text-xs text-muted-foreground", className), children: NO_STORE_ON_THIS_SCREEN_REASON })
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
if (!field.relation_target) {
|
|
973
|
+
return /* @__PURE__ */ jsxs4("p", { className: "text-xs text-muted-foreground", children: [
|
|
974
|
+
fieldName(field),
|
|
975
|
+
" points at records, but it does not say which Table yet, so there is nothing to pick from. A table admin sets its target in the field editor."
|
|
976
|
+
] });
|
|
977
|
+
}
|
|
978
|
+
const titleOf = (recordId) => words[recordId] ?? "Loading\u2026";
|
|
979
|
+
const full = field.multi && field.relation_max !== null && picked.length >= field.relation_max;
|
|
980
|
+
const toggle = (recordId) => {
|
|
981
|
+
if (!field.multi) {
|
|
982
|
+
onChange(picked[0] === recordId ? null : recordId);
|
|
983
|
+
setOpen(false);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const next = new Set(picked);
|
|
987
|
+
if (next.has(recordId)) next.delete(recordId);
|
|
988
|
+
else if (!full) next.add(recordId);
|
|
989
|
+
onChange(next.size === 0 ? null : [...next]);
|
|
990
|
+
};
|
|
991
|
+
const targetWord = (target?.label_singular ?? "").trim() || (target?.name ?? "").trim() || "record";
|
|
992
|
+
const create = async (rawTitle) => {
|
|
993
|
+
const title = rawTitle.trim();
|
|
994
|
+
const tableId = field.relation_target;
|
|
995
|
+
if (!tableId || title === "" || busy) return;
|
|
996
|
+
if (!target) {
|
|
997
|
+
setCreateRefusal(refusal("unreachable", "Still reading that Table.", "Try again in a moment."));
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const titleField = target.title_field;
|
|
1001
|
+
if (!titleField) {
|
|
1002
|
+
setCreateRefusal(
|
|
1003
|
+
refusal(
|
|
1004
|
+
"not_supported",
|
|
1005
|
+
`${target.name} has no column that names a record yet, so a new one would have nothing to show here.`,
|
|
1006
|
+
"A table admin picks its title column in that table's settings."
|
|
1007
|
+
)
|
|
1008
|
+
);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
setBusy(true);
|
|
1012
|
+
setCreateRefusal(null);
|
|
1013
|
+
const written = await client.recordWrite({ table_id: tableId, data: { [titleField]: title } });
|
|
1014
|
+
if (!written.ok) {
|
|
1015
|
+
setBusy(false);
|
|
1016
|
+
setCreateRefusal(written.error);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const createdId = String(written.data);
|
|
1020
|
+
const refreshed = await client.list({ table_id: tableId, limit: 200 });
|
|
1021
|
+
if (refreshed.ok) setRows(refreshed.data.rows);
|
|
1022
|
+
setBusy(false);
|
|
1023
|
+
setCreating(false);
|
|
1024
|
+
setNewTitle("");
|
|
1025
|
+
setSearch("");
|
|
1026
|
+
toggle(createdId);
|
|
1027
|
+
};
|
|
1028
|
+
const apart = tellApart(rows ?? [], titleOf);
|
|
1029
|
+
const visible = rankTypedMatches(rows ?? [], search, (row) => [titleOf(row.id), apart.get(row.id) ?? ""]);
|
|
1030
|
+
return (
|
|
1031
|
+
// Never wider than where it is drawn (a grid cell beside a sticky column: B3-07) — chips truncate, the button wraps.
|
|
1032
|
+
/* @__PURE__ */ jsxs4("div", { className: cn4("flex min-w-0 max-w-full flex-wrap items-center gap-1", className), children: [
|
|
1033
|
+
picked.map((recordId) => /* @__PURE__ */ jsx4(
|
|
1034
|
+
RecordChip,
|
|
1035
|
+
{
|
|
1036
|
+
title: titleOf(recordId),
|
|
1037
|
+
...disabled ? {} : { onRemove: () => toggle(recordId) }
|
|
1038
|
+
},
|
|
1039
|
+
recordId
|
|
1040
|
+
)),
|
|
1041
|
+
disabled ? null : (
|
|
1042
|
+
// THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the choice editor draws the same one.
|
|
1043
|
+
/* @__PURE__ */ jsx4(
|
|
1044
|
+
PickOrAdd,
|
|
1045
|
+
{
|
|
1046
|
+
triggerId: id,
|
|
1047
|
+
triggerLabel: `Pick for ${fieldName(field)}`,
|
|
1048
|
+
triggerText: picked.length === 0 ? "Pick" : "Change",
|
|
1049
|
+
open,
|
|
1050
|
+
onOpenChange: setOpen,
|
|
1051
|
+
search,
|
|
1052
|
+
onSearch: setSearch,
|
|
1053
|
+
items: visible.map((row) => ({
|
|
1054
|
+
id: row.id,
|
|
1055
|
+
label: titleOf(row.id),
|
|
1056
|
+
detail: apart.get(row.id) ?? null,
|
|
1057
|
+
picked: picked.includes(row.id),
|
|
1058
|
+
title: apart.has(row.id) ? `${titleOf(row.id)} \u2014 ${apart.get(row.id)}` : titleOf(row.id),
|
|
1059
|
+
attrs: { "data-relation-candidate": row.id }
|
|
1060
|
+
})),
|
|
1061
|
+
onPick: toggle,
|
|
1062
|
+
notice: /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1063
|
+
refusal2 ? /* @__PURE__ */ jsx4(RefusalLine, { error: refusal2, className: "p-2" }) : null,
|
|
1064
|
+
!rows && !refusal2 ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading\u2026" }) : null,
|
|
1065
|
+
full ? /* @__PURE__ */ jsxs4("p", { className: "p-2 text-xs text-muted-foreground", children: [
|
|
1066
|
+
fieldName(field),
|
|
1067
|
+
" can point at ",
|
|
1068
|
+
field.relation_max,
|
|
1069
|
+
" at most. Remove one to add another."
|
|
1070
|
+
] }) : null
|
|
1071
|
+
] }),
|
|
1072
|
+
empty: rows ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Nothing in that table matches." }) : null,
|
|
1073
|
+
add: allowCreate && !full && !targetRefusal && target && !creating ? {
|
|
1074
|
+
label: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`,
|
|
1075
|
+
disabled: busy,
|
|
1076
|
+
// TYPED SOMETHING THAT IS NOT THERE? That IS the name. The champions
|
|
1077
|
+
// (Airtable, Notion) make the search text the new record's title in one
|
|
1078
|
+
// press; an empty box asks for one.
|
|
1079
|
+
onAdd: () => {
|
|
1080
|
+
if (search.trim() !== "") void create(search);
|
|
1081
|
+
else {
|
|
1082
|
+
setNewTitle("");
|
|
1083
|
+
setCreating(true);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
} : null,
|
|
1087
|
+
foot: !allowCreate || full ? void 0 : targetRefusal ? /* @__PURE__ */ jsx4(RefusalLine, { error: targetRefusal, className: "p-2" }) : !target ? /* @__PURE__ */ jsx4("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading that table\u2026" }) : creating ? /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1 p-1", children: [
|
|
1088
|
+
/* @__PURE__ */ jsx4(
|
|
1089
|
+
BasicInput3,
|
|
1090
|
+
{
|
|
1091
|
+
autoFocus: true,
|
|
1092
|
+
value: newTitle,
|
|
1093
|
+
onChange: (e) => setNewTitle(e.target.value),
|
|
1094
|
+
onKeyDown: (e) => {
|
|
1095
|
+
if (e.key === "Enter") {
|
|
1096
|
+
e.preventDefault();
|
|
1097
|
+
void create(newTitle);
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
placeholder: `New ${targetWord}`,
|
|
1101
|
+
className: "h-7 flex-1 text-xs",
|
|
1102
|
+
"aria-label": `Name the new ${targetWord}`
|
|
1103
|
+
}
|
|
1104
|
+
),
|
|
1105
|
+
/* @__PURE__ */ jsx4(
|
|
1106
|
+
Button2,
|
|
1107
|
+
{
|
|
1108
|
+
type: "button",
|
|
1109
|
+
size: "sm",
|
|
1110
|
+
variant: "default",
|
|
1111
|
+
disabled: busy || newTitle.trim() === "",
|
|
1112
|
+
onClick: () => void create(newTitle),
|
|
1113
|
+
children: busy ? "Creating\u2026" : "Create"
|
|
1114
|
+
}
|
|
1115
|
+
)
|
|
1116
|
+
] }) : void 0,
|
|
1117
|
+
footNote: allowCreate && !full && createRefusal ? (
|
|
1118
|
+
// The typed title is still in the search box: the notice owns the
|
|
1119
|
+
// two doors out (lane REFUSAL-SWEEP).
|
|
1120
|
+
/* @__PURE__ */ jsx4(
|
|
1121
|
+
RefusalNotice,
|
|
1122
|
+
{
|
|
1123
|
+
error: createRefusal,
|
|
1124
|
+
className: "mt-1 max-w-72",
|
|
1125
|
+
onKeepEditing: () => setCreateRefusal(null),
|
|
1126
|
+
onDiscard: () => {
|
|
1127
|
+
setCreateRefusal(null);
|
|
1128
|
+
setSearch("");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
) : null
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
)
|
|
1136
|
+
] })
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/gridAttachments.tsx
|
|
1141
|
+
import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1142
|
+
import { useOptionalRecordsClient as useOptionalRecordsClient2 } from "@ai-matrx/records/react";
|
|
1143
|
+
import { Badge, Button as Button5 } from "@ai-matrx/design-system";
|
|
1144
|
+
|
|
1145
|
+
// src/host.tsx
|
|
1146
|
+
import { createContext, useContext, useMemo as useMemo2 } from "react";
|
|
1147
|
+
import { useMyLevel, useMyLevels } from "@ai-matrx/records/react";
|
|
1148
|
+
|
|
1149
|
+
// src/rights.ts
|
|
1150
|
+
import { atLeast } from "@ai-matrx/records";
|
|
1151
|
+
var LEVEL_WORD = {
|
|
1152
|
+
viewer: "Viewer",
|
|
1153
|
+
commenter: "Commenter",
|
|
1154
|
+
editor: "Editor",
|
|
1155
|
+
admin: "Admin"
|
|
1156
|
+
};
|
|
1157
|
+
var NEEDS = {
|
|
1158
|
+
read: "viewer",
|
|
1159
|
+
// custom.io_comment_write
|
|
1160
|
+
comment: "commenter",
|
|
1161
|
+
// custom.record_update / custom.record_write
|
|
1162
|
+
write: "editor",
|
|
1163
|
+
// custom.record_delete
|
|
1164
|
+
remove: "editor",
|
|
1165
|
+
// custom.share_grant — passing your access on is an administrator's right
|
|
1166
|
+
share: "admin",
|
|
1167
|
+
// custom.field_declare / field_update / field_retire / table settings
|
|
1168
|
+
structure: "admin"
|
|
1169
|
+
};
|
|
1170
|
+
var NOT_YET = "Still asking the store what you may do here. Nothing is offered until it has answered.";
|
|
1171
|
+
var NOTHING_AT_ALL = "You have not been given this. Ask somebody who administers it to share it with you.";
|
|
1172
|
+
function sentenceFor(what, level) {
|
|
1173
|
+
if (level === null) return NOTHING_AT_ALL;
|
|
1174
|
+
const need = LEVEL_WORD[NEEDS[what]];
|
|
1175
|
+
const have = LEVEL_WORD[level];
|
|
1176
|
+
const doing = {
|
|
1177
|
+
read: "open this",
|
|
1178
|
+
comment: "leave a comment",
|
|
1179
|
+
write: "change anything here",
|
|
1180
|
+
remove: "delete this",
|
|
1181
|
+
share: "pass this on to somebody else",
|
|
1182
|
+
structure: "change the columns of this table"
|
|
1183
|
+
};
|
|
1184
|
+
return `You are ${have} here, and it takes ${need} to ${doing[what]}. Ask somebody who is Admin on it to move you up.`;
|
|
1185
|
+
}
|
|
1186
|
+
function whatYouMayDo(level, known = true) {
|
|
1187
|
+
const may = (what) => known && atLeast(level, NEEDS[what]);
|
|
1188
|
+
return {
|
|
1189
|
+
level: known ? level : null,
|
|
1190
|
+
known,
|
|
1191
|
+
read: may("read"),
|
|
1192
|
+
comment: may("comment"),
|
|
1193
|
+
write: may("write"),
|
|
1194
|
+
remove: may("remove"),
|
|
1195
|
+
share: may("share"),
|
|
1196
|
+
structure: may("structure"),
|
|
1197
|
+
why: (what) => known ? sentenceFor(what, level) : NOT_YET
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
var NOT_ANSWERED_YET = whatYouMayDo(null, false);
|
|
1201
|
+
|
|
1202
|
+
// src/host.tsx
|
|
1203
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1204
|
+
function tableRights(may, overallReason) {
|
|
1205
|
+
return {
|
|
1206
|
+
...may,
|
|
1207
|
+
admin: may.structure,
|
|
1208
|
+
reason: overallReason ?? (may.structure ? "" : may.why("structure"))
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
var NO_RIGHTS = tableRights(NOT_ANSWERED_YET);
|
|
1212
|
+
var NO_MEMBERS_REASON = "This app cannot offer the people in your organization on this screen yet, so there is nobody to pick. Type the person's name into a text field for now, and tell us \u2014 it is on our list, not yours.";
|
|
1213
|
+
var NO_UPLOAD_REASON = "This screen cannot take a file yet, so there is no chooser here rather than one that loses what you pick. Put a link to the file in a text field for now. It is on our list, not yours.";
|
|
1214
|
+
var HostContext = createContext(null);
|
|
1215
|
+
function useRecordsUi() {
|
|
1216
|
+
return useContext(HostContext) ?? {};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/labels.tsx
|
|
1220
|
+
import { createContext as createContext2, useCallback, useContext as useContext2, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef3, useState as useState5 } from "react";
|
|
1221
|
+
import { useRecordsClient } from "@ai-matrx/records/react";
|
|
1222
|
+
import { newerKindExportWords, optionKey } from "@ai-matrx/records";
|
|
1223
|
+
import { isUuidShape as isUuidShape2 } from "@ai-matrx/kit/uuid";
|
|
1224
|
+
|
|
1225
|
+
// src/signature.tsx
|
|
1226
|
+
import { useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
|
|
1227
|
+
import { Button as Button3, ErrorBox as ErrorBox2, cn as cn5 } from "@ai-matrx/design-system";
|
|
1228
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1229
|
+
var DRAWN = /^data:image\/(png|jpeg);base64,[A-Za-z0-9+/=]{64,}$/;
|
|
1230
|
+
function drawnSignatureOf(value) {
|
|
1231
|
+
return typeof value === "string" && DRAWN.test(value) ? value : null;
|
|
1232
|
+
}
|
|
1233
|
+
var SIGNED_WORD = "Signed";
|
|
1234
|
+
function signatureWords(value) {
|
|
1235
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
1236
|
+
return drawnSignatureOf(value) ? SIGNED_WORD : String(value);
|
|
1237
|
+
}
|
|
1238
|
+
function SignatureValue({ value, inCell }) {
|
|
1239
|
+
const drawn = drawnSignatureOf(value);
|
|
1240
|
+
if (drawn) {
|
|
1241
|
+
return /* @__PURE__ */ jsx6(
|
|
1242
|
+
"img",
|
|
1243
|
+
{
|
|
1244
|
+
src: drawn,
|
|
1245
|
+
alt: SIGNED_WORD,
|
|
1246
|
+
"data-records-signature": "",
|
|
1247
|
+
className: cn5("max-w-[12rem] object-contain object-left dark:invert", inCell ? "h-7" : "h-12")
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return /* @__PURE__ */ jsx6("span", { className: "font-serif italic", children: String(value) });
|
|
1252
|
+
}
|
|
1253
|
+
var INK = "#111827";
|
|
1254
|
+
var SIGNATURE_BUDGET_BYTES = 6e4;
|
|
1255
|
+
var SIGNATURE_TOO_DETAILED = "Too detailed to save \u2014 clear and sign again.";
|
|
1256
|
+
function SignaturePad({
|
|
1257
|
+
value,
|
|
1258
|
+
onChange,
|
|
1259
|
+
disabled,
|
|
1260
|
+
id,
|
|
1261
|
+
className
|
|
1262
|
+
}) {
|
|
1263
|
+
const kept = drawnSignatureOf(value);
|
|
1264
|
+
const named = !kept && typeof value === "string" && value !== "" ? value : null;
|
|
1265
|
+
const [showPad, setShowPad] = useState4(() => !kept && !named);
|
|
1266
|
+
const canvasRef = useRef2(null);
|
|
1267
|
+
const drawing = useRef2(false);
|
|
1268
|
+
const [dirty, setDirty] = useState4(false);
|
|
1269
|
+
const [tooBig, setTooBig] = useState4(false);
|
|
1270
|
+
useEffect4(() => {
|
|
1271
|
+
if (!showPad) return;
|
|
1272
|
+
const canvas = canvasRef.current;
|
|
1273
|
+
if (!canvas) return;
|
|
1274
|
+
const ratio = window.devicePixelRatio || 1;
|
|
1275
|
+
const rect = canvas.getBoundingClientRect();
|
|
1276
|
+
canvas.width = Math.round(rect.width * ratio);
|
|
1277
|
+
canvas.height = Math.round(rect.height * ratio);
|
|
1278
|
+
const context = canvas.getContext("2d");
|
|
1279
|
+
if (!context) return;
|
|
1280
|
+
context.scale(ratio, ratio);
|
|
1281
|
+
context.lineWidth = 2.2;
|
|
1282
|
+
context.lineCap = "round";
|
|
1283
|
+
context.lineJoin = "round";
|
|
1284
|
+
context.strokeStyle = INK;
|
|
1285
|
+
}, [showPad]);
|
|
1286
|
+
const point = (event) => {
|
|
1287
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1288
|
+
return { x: event.clientX - rect.left, y: event.clientY - rect.top };
|
|
1289
|
+
};
|
|
1290
|
+
const start = (event) => {
|
|
1291
|
+
if (disabled) return;
|
|
1292
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1293
|
+
const context = canvasRef.current?.getContext("2d");
|
|
1294
|
+
if (!context) return;
|
|
1295
|
+
const { x, y } = point(event);
|
|
1296
|
+
context.beginPath();
|
|
1297
|
+
context.moveTo(x, y);
|
|
1298
|
+
drawing.current = true;
|
|
1299
|
+
};
|
|
1300
|
+
const move = (event) => {
|
|
1301
|
+
if (!drawing.current) return;
|
|
1302
|
+
const context = canvasRef.current?.getContext("2d");
|
|
1303
|
+
if (!context) return;
|
|
1304
|
+
const { x, y } = point(event);
|
|
1305
|
+
context.lineTo(x, y);
|
|
1306
|
+
context.stroke();
|
|
1307
|
+
if (!dirty) setDirty(true);
|
|
1308
|
+
};
|
|
1309
|
+
const end = () => {
|
|
1310
|
+
if (!drawing.current) return;
|
|
1311
|
+
drawing.current = false;
|
|
1312
|
+
const canvas = canvasRef.current;
|
|
1313
|
+
if (!canvas) return;
|
|
1314
|
+
const fitted = fitDrawing(canvas);
|
|
1315
|
+
setTooBig(fitted === null);
|
|
1316
|
+
onChange(fitted);
|
|
1317
|
+
setDirty(true);
|
|
1318
|
+
};
|
|
1319
|
+
const clear = () => {
|
|
1320
|
+
const canvas = canvasRef.current;
|
|
1321
|
+
const context = canvas?.getContext("2d");
|
|
1322
|
+
if (canvas && context) {
|
|
1323
|
+
context.save();
|
|
1324
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
1325
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1326
|
+
context.restore();
|
|
1327
|
+
}
|
|
1328
|
+
setDirty(false);
|
|
1329
|
+
setTooBig(false);
|
|
1330
|
+
setShowPad(true);
|
|
1331
|
+
onChange(null);
|
|
1332
|
+
};
|
|
1333
|
+
if (!showPad) {
|
|
1334
|
+
return /* @__PURE__ */ jsxs5("div", { id, className: cn5("flex items-center justify-between gap-2 rounded-md border px-3 py-2", className), children: [
|
|
1335
|
+
/* @__PURE__ */ jsx6(SignatureValue, { value }),
|
|
1336
|
+
!disabled ? /* @__PURE__ */ jsx6(Button3, { type: "button", variant: "ghost", size: "sm", onClick: clear, children: "Sign again" }) : null
|
|
1337
|
+
] });
|
|
1338
|
+
}
|
|
1339
|
+
return /* @__PURE__ */ jsxs5("div", { className: cn5("flex w-full flex-col gap-1", className), children: [
|
|
1340
|
+
/* @__PURE__ */ jsxs5("div", { className: "relative", children: [
|
|
1341
|
+
/* @__PURE__ */ jsx6(
|
|
1342
|
+
"canvas",
|
|
1343
|
+
{
|
|
1344
|
+
id,
|
|
1345
|
+
ref: canvasRef,
|
|
1346
|
+
role: "img",
|
|
1347
|
+
"aria-label": "Signature pad",
|
|
1348
|
+
"data-records-signature-pad": "",
|
|
1349
|
+
onPointerDown: start,
|
|
1350
|
+
onPointerMove: move,
|
|
1351
|
+
onPointerUp: end,
|
|
1352
|
+
onPointerLeave: end,
|
|
1353
|
+
onPointerCancel: end,
|
|
1354
|
+
className: cn5(
|
|
1355
|
+
"block h-36 w-full touch-none rounded-md border border-border bg-white",
|
|
1356
|
+
disabled ? "cursor-not-allowed opacity-60" : "cursor-crosshair"
|
|
1357
|
+
)
|
|
1358
|
+
}
|
|
1359
|
+
),
|
|
1360
|
+
!dirty ? /* @__PURE__ */ jsx6("span", { className: "pointer-events-none absolute inset-x-0 bottom-3 text-center text-xs text-neutral-400", children: "Sign here" }) : null
|
|
1361
|
+
] }),
|
|
1362
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-2", children: [
|
|
1363
|
+
tooBig ? /* @__PURE__ */ jsx6(
|
|
1364
|
+
ErrorBox2,
|
|
1365
|
+
{
|
|
1366
|
+
as: "span",
|
|
1367
|
+
className: "mr-auto text-xs text-destructive",
|
|
1368
|
+
message: SIGNATURE_TOO_DETAILED,
|
|
1369
|
+
operation: "Save a signature",
|
|
1370
|
+
origin: "records-ui/signature"
|
|
1371
|
+
}
|
|
1372
|
+
) : null,
|
|
1373
|
+
/* @__PURE__ */ jsx6(Button3, { type: "button", variant: "ghost", size: "sm", onClick: clear, disabled: disabled || !dirty, children: "Clear" })
|
|
1374
|
+
] })
|
|
1375
|
+
] });
|
|
1376
|
+
}
|
|
1377
|
+
var LADDER = [
|
|
1378
|
+
["image/png", 600, 1],
|
|
1379
|
+
["image/png", 400, 1],
|
|
1380
|
+
["image/jpeg", 600, 0.7],
|
|
1381
|
+
["image/jpeg", 400, 0.6],
|
|
1382
|
+
["image/jpeg", 300, 0.5]
|
|
1383
|
+
];
|
|
1384
|
+
function fitDrawing(canvas) {
|
|
1385
|
+
for (const [type, maxWidth, quality] of LADDER) {
|
|
1386
|
+
const encoded = encodeAt(canvas, type, maxWidth, quality);
|
|
1387
|
+
if (encoded && encoded.length <= SIGNATURE_BUDGET_BYTES) return encoded;
|
|
1388
|
+
}
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
function encodeAt(canvas, type, maxWidth, quality) {
|
|
1392
|
+
const width = Math.min(maxWidth, canvas.width || maxWidth);
|
|
1393
|
+
const scale = canvas.width > 0 ? width / canvas.width : 1;
|
|
1394
|
+
const out = document.createElement("canvas");
|
|
1395
|
+
out.width = width;
|
|
1396
|
+
out.height = Math.max(1, Math.round((canvas.height || 1) * scale));
|
|
1397
|
+
const context = out.getContext("2d");
|
|
1398
|
+
if (!context) return null;
|
|
1399
|
+
if (type === "image/jpeg") {
|
|
1400
|
+
context.fillStyle = "#ffffff";
|
|
1401
|
+
context.fillRect(0, 0, out.width, out.height);
|
|
1402
|
+
}
|
|
1403
|
+
context.drawImage(canvas, 0, 0, out.width, out.height);
|
|
1404
|
+
return out.toDataURL(type, quality);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/labels.tsx
|
|
1408
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1409
|
+
var LabelContext = createContext2({
|
|
1410
|
+
// With no provider the resolver is ABSENT, not silently empty: a caller gets
|
|
1411
|
+
// null and says "Loading…" forever, which is visible. The mount always
|
|
1412
|
+
// provides one, so this is only ever a bare unit test.
|
|
1413
|
+
lookup: () => null,
|
|
1414
|
+
want: () => {
|
|
1415
|
+
},
|
|
1416
|
+
waiting: () => null
|
|
1417
|
+
});
|
|
1418
|
+
var SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS = 1e3;
|
|
1419
|
+
function waitingLineFor(field, target) {
|
|
1420
|
+
return field.type === "list" ? "Reading this column\u2019s list of choices\u2026" : `Reading the ${target ?? "records"} this column points at\u2026`;
|
|
1421
|
+
}
|
|
1422
|
+
function idsOf(value) {
|
|
1423
|
+
if (Array.isArray(value)) return value.map(String).filter(isId);
|
|
1424
|
+
if (typeof value === "string" && isId(value)) return [value];
|
|
1425
|
+
return [];
|
|
1426
|
+
}
|
|
1427
|
+
function choiceKeyName(fieldId, key) {
|
|
1428
|
+
return `${fieldId}\0key\0${key}`;
|
|
1429
|
+
}
|
|
1430
|
+
function isId(value) {
|
|
1431
|
+
return typeof value === "string" && isUuidShape2(value);
|
|
1432
|
+
}
|
|
1433
|
+
function pointsAtRecords(field) {
|
|
1434
|
+
return field.type === "list" || field.type === "relation";
|
|
1435
|
+
}
|
|
1436
|
+
function RecordLabelProvider({ children }) {
|
|
1437
|
+
const client = useRecordsClient();
|
|
1438
|
+
const [names, setNames] = useState5({});
|
|
1439
|
+
const [archivedRefs, setArchivedRefs] = useState5({});
|
|
1440
|
+
const [settled, setSettled] = useState5({});
|
|
1441
|
+
const [waitingLines, setWaitingLines] = useState5({});
|
|
1442
|
+
const asked2 = useRef3(/* @__PURE__ */ new Set());
|
|
1443
|
+
const chased = useRef3(/* @__PURE__ */ new Set());
|
|
1444
|
+
const timers = useRef3(/* @__PURE__ */ new Map());
|
|
1445
|
+
const startWaitClock = useCallback((field, target) => {
|
|
1446
|
+
if (timers.current.has(field.id)) return;
|
|
1447
|
+
timers.current.set(
|
|
1448
|
+
field.id,
|
|
1449
|
+
setTimeout(() => {
|
|
1450
|
+
timers.current.delete(field.id);
|
|
1451
|
+
setWaitingLines(
|
|
1452
|
+
(current) => current[field.id] ? current : { ...current, [field.id]: waitingLineFor(field, target) }
|
|
1453
|
+
);
|
|
1454
|
+
}, SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS)
|
|
1455
|
+
);
|
|
1456
|
+
}, []);
|
|
1457
|
+
const stopWaitClock = useCallback((fieldId) => {
|
|
1458
|
+
const timer = timers.current.get(fieldId);
|
|
1459
|
+
if (timer) {
|
|
1460
|
+
clearTimeout(timer);
|
|
1461
|
+
timers.current.delete(fieldId);
|
|
1462
|
+
}
|
|
1463
|
+
setWaitingLines((current) => {
|
|
1464
|
+
if (!current[fieldId]) return current;
|
|
1465
|
+
const next = { ...current };
|
|
1466
|
+
delete next[fieldId];
|
|
1467
|
+
return next;
|
|
1468
|
+
});
|
|
1469
|
+
}, []);
|
|
1470
|
+
useEffect5(
|
|
1471
|
+
() => () => {
|
|
1472
|
+
for (const timer of timers.current.values()) clearTimeout(timer);
|
|
1473
|
+
timers.current.clear();
|
|
1474
|
+
},
|
|
1475
|
+
[]
|
|
1476
|
+
);
|
|
1477
|
+
const want = useCallback(
|
|
1478
|
+
(field) => {
|
|
1479
|
+
if (!pointsAtRecords(field)) return;
|
|
1480
|
+
if (asked2.current.has(field.id)) return;
|
|
1481
|
+
asked2.current.add(field.id);
|
|
1482
|
+
startWaitClock(field, null);
|
|
1483
|
+
void (async () => {
|
|
1484
|
+
if (field.type === "list") {
|
|
1485
|
+
const result = await client.fieldOptions({ field_id: field.id });
|
|
1486
|
+
if (result.ok) {
|
|
1487
|
+
setNames((current) => {
|
|
1488
|
+
const next = { ...current };
|
|
1489
|
+
for (const option of result.data ?? []) {
|
|
1490
|
+
const words = recordName(option.data, null, "Unnamed choice");
|
|
1491
|
+
next[option.id] = words;
|
|
1492
|
+
const key = optionKey(option);
|
|
1493
|
+
if (key) next[choiceKeyName(field.id, key)] = words;
|
|
1494
|
+
}
|
|
1495
|
+
return next;
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
setSettled((current) => ({ ...current, [field.id]: true }));
|
|
1499
|
+
stopWaitClock(field.id);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (!field.relation_target) {
|
|
1503
|
+
setSettled((current) => ({ ...current, [field.id]: true }));
|
|
1504
|
+
stopWaitClock(field.id);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
setSettled((current) => ({ ...current, [field.id]: true }));
|
|
1508
|
+
})();
|
|
1509
|
+
},
|
|
1510
|
+
[client, startWaitClock, stopWaitClock]
|
|
1511
|
+
);
|
|
1512
|
+
const queued = useRef3(/* @__PURE__ */ new Map());
|
|
1513
|
+
const flushing = useRef3(null);
|
|
1514
|
+
const flush = useCallback(() => {
|
|
1515
|
+
flushing.current = null;
|
|
1516
|
+
const batch = [...queued.current.values()];
|
|
1517
|
+
queued.current.clear();
|
|
1518
|
+
if (batch.length === 0) return;
|
|
1519
|
+
void (async () => {
|
|
1520
|
+
const learned = {};
|
|
1521
|
+
const archivedNow = {};
|
|
1522
|
+
await Promise.all(
|
|
1523
|
+
batch.map(async ({ field, ids }) => {
|
|
1524
|
+
const wanted = [...ids];
|
|
1525
|
+
const result = await client.relationWordsMany({
|
|
1526
|
+
field_id: field.id,
|
|
1527
|
+
record_ids: wanted
|
|
1528
|
+
});
|
|
1529
|
+
for (const id of wanted) {
|
|
1530
|
+
const words = result.ok ? result.data[id] : void 0;
|
|
1531
|
+
learned[id] = typeof words === "string" && words !== "" ? words : WITHHELD_RECORD_LABEL;
|
|
1532
|
+
}
|
|
1533
|
+
if (result.ok) for (const id of result.archived ?? []) archivedNow[`${field.id}:${id}`] = true;
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
setNames((current) => ({ ...current, ...learned }));
|
|
1537
|
+
if (Object.keys(archivedNow).length > 0) setArchivedRefs((current) => ({ ...current, ...archivedNow }));
|
|
1538
|
+
for (const { field } of batch) stopWaitClock(field.id);
|
|
1539
|
+
})();
|
|
1540
|
+
}, [client, stopWaitClock]);
|
|
1541
|
+
const chase = useCallback(
|
|
1542
|
+
(field, id) => {
|
|
1543
|
+
const key = `${field.id}:${id}`;
|
|
1544
|
+
if (chased.current.has(key)) return;
|
|
1545
|
+
chased.current.add(key);
|
|
1546
|
+
const pending = queued.current.get(field.id);
|
|
1547
|
+
if (pending) pending.ids.add(id);
|
|
1548
|
+
else queued.current.set(field.id, { field, ids: /* @__PURE__ */ new Set([id]) });
|
|
1549
|
+
startWaitClock(field, null);
|
|
1550
|
+
if (flushing.current === null) flushing.current = setTimeout(flush, 0);
|
|
1551
|
+
},
|
|
1552
|
+
[flush, startWaitClock]
|
|
1553
|
+
);
|
|
1554
|
+
useEffect5(
|
|
1555
|
+
() => () => {
|
|
1556
|
+
if (flushing.current !== null) clearTimeout(flushing.current);
|
|
1557
|
+
},
|
|
1558
|
+
[]
|
|
1559
|
+
);
|
|
1560
|
+
const rereadFor = useRef3(/* @__PURE__ */ new Set());
|
|
1561
|
+
const lookupWords = useCallback(
|
|
1562
|
+
(field, id) => {
|
|
1563
|
+
if (field.type === "list" && !isId(id)) {
|
|
1564
|
+
const named = names[choiceKeyName(field.id, id)];
|
|
1565
|
+
if (named !== void 0) return named;
|
|
1566
|
+
const once = choiceKeyName(field.id, id);
|
|
1567
|
+
if (settled[field.id] && !rereadFor.current.has(once)) {
|
|
1568
|
+
rereadFor.current.add(once);
|
|
1569
|
+
asked2.current.delete(field.id);
|
|
1570
|
+
want(field);
|
|
1571
|
+
}
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
const known = names[id];
|
|
1575
|
+
if (known !== void 0) return known;
|
|
1576
|
+
if (settled[field.id]) chase(field, id);
|
|
1577
|
+
return null;
|
|
1578
|
+
},
|
|
1579
|
+
[names, settled, chase, want]
|
|
1580
|
+
);
|
|
1581
|
+
const lookup = useMemo3(
|
|
1582
|
+
() => {
|
|
1583
|
+
const answer = ((field, id) => lookupWords(field, id));
|
|
1584
|
+
answer.archived = (field, id) => archivedRefs[`${field.id}:${id}`] === true;
|
|
1585
|
+
return answer;
|
|
1586
|
+
},
|
|
1587
|
+
[lookupWords, archivedRefs]
|
|
1588
|
+
);
|
|
1589
|
+
const waiting = useCallback((field) => waitingLines[field.id] ?? null, [waitingLines]);
|
|
1590
|
+
const value = useMemo3(() => ({ lookup, want, waiting }), [lookup, want, waiting]);
|
|
1591
|
+
return /* @__PURE__ */ jsx7(LabelContext.Provider, { value, children });
|
|
1592
|
+
}
|
|
1593
|
+
function useRecordLabels(fields) {
|
|
1594
|
+
const { lookup, want } = useContext2(LabelContext);
|
|
1595
|
+
useEffect5(() => {
|
|
1596
|
+
for (const field of fields ?? []) want(field);
|
|
1597
|
+
}, [fields, want]);
|
|
1598
|
+
return lookup;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/FilePicker.tsx
|
|
1602
|
+
import { useCallback as useCallback2, useId, useRef as useRef4, useState as useState6 } from "react";
|
|
1603
|
+
import { Button as Button4, cn as cn6 } from "@ai-matrx/design-system";
|
|
1604
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1605
|
+
function FilePicker({
|
|
1606
|
+
label,
|
|
1607
|
+
accept,
|
|
1608
|
+
inputLabel,
|
|
1609
|
+
hint,
|
|
1610
|
+
disabled = false,
|
|
1611
|
+
onFile,
|
|
1612
|
+
onClear,
|
|
1613
|
+
chosenName,
|
|
1614
|
+
className
|
|
1615
|
+
}) {
|
|
1616
|
+
const inputId = useId();
|
|
1617
|
+
const input = useRef4(null);
|
|
1618
|
+
const [over, setOver] = useState6(false);
|
|
1619
|
+
const take = useCallback2(
|
|
1620
|
+
(file) => {
|
|
1621
|
+
if (!file || disabled) return;
|
|
1622
|
+
onFile(file);
|
|
1623
|
+
},
|
|
1624
|
+
[disabled, onFile]
|
|
1625
|
+
);
|
|
1626
|
+
const onDrop = useCallback2(
|
|
1627
|
+
(e) => {
|
|
1628
|
+
e.preventDefault();
|
|
1629
|
+
setOver(false);
|
|
1630
|
+
take(e.dataTransfer?.files?.[0]);
|
|
1631
|
+
},
|
|
1632
|
+
[take]
|
|
1633
|
+
);
|
|
1634
|
+
return /* @__PURE__ */ jsxs6(
|
|
1635
|
+
"div",
|
|
1636
|
+
{
|
|
1637
|
+
"data-matrx-file-picker": true,
|
|
1638
|
+
className: cn6("flex min-w-0 flex-col gap-1", className),
|
|
1639
|
+
onDragOver: (e) => {
|
|
1640
|
+
if (disabled) return;
|
|
1641
|
+
e.preventDefault();
|
|
1642
|
+
setOver(true);
|
|
1643
|
+
},
|
|
1644
|
+
onDragLeave: () => setOver(false),
|
|
1645
|
+
onDrop,
|
|
1646
|
+
children: [
|
|
1647
|
+
/* @__PURE__ */ jsxs6(
|
|
1648
|
+
"div",
|
|
1649
|
+
{
|
|
1650
|
+
className: cn6(
|
|
1651
|
+
"flex min-w-0 flex-wrap items-center gap-2 rounded-md border border-dashed px-2 py-1.5",
|
|
1652
|
+
over ? "border-primary bg-primary/5" : "border-border",
|
|
1653
|
+
disabled && "opacity-60"
|
|
1654
|
+
),
|
|
1655
|
+
children: [
|
|
1656
|
+
/* @__PURE__ */ jsx8(
|
|
1657
|
+
"input",
|
|
1658
|
+
{
|
|
1659
|
+
ref: input,
|
|
1660
|
+
id: inputId,
|
|
1661
|
+
type: "file",
|
|
1662
|
+
"aria-label": inputLabel ?? label,
|
|
1663
|
+
tabIndex: -1,
|
|
1664
|
+
...accept ? { accept } : {},
|
|
1665
|
+
disabled,
|
|
1666
|
+
className: "sr-only",
|
|
1667
|
+
onChange: (e) => {
|
|
1668
|
+
take(e.target.files?.[0]);
|
|
1669
|
+
e.target.value = "";
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
),
|
|
1673
|
+
/* @__PURE__ */ jsx8(
|
|
1674
|
+
Button4,
|
|
1675
|
+
{
|
|
1676
|
+
type: "button",
|
|
1677
|
+
size: "sm",
|
|
1678
|
+
variant: "outline",
|
|
1679
|
+
disabled,
|
|
1680
|
+
onClick: () => input.current?.click(),
|
|
1681
|
+
children: chosenName ? "Choose a different file" : label
|
|
1682
|
+
}
|
|
1683
|
+
),
|
|
1684
|
+
chosenName ? /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1 truncate text-xs", title: chosenName, children: chosenName }) : /* @__PURE__ */ jsx8("span", { className: "text-xs text-muted-foreground", children: "or drop it here" }),
|
|
1685
|
+
chosenName && onClear ? /* @__PURE__ */ jsx8(Button4, { type: "button", size: "sm", variant: "ghost", disabled, onClick: onClear, children: "Cancel" }) : null
|
|
1686
|
+
]
|
|
1687
|
+
}
|
|
1688
|
+
),
|
|
1689
|
+
hint && !chosenName ? /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: hint }) : null
|
|
1690
|
+
]
|
|
1691
|
+
}
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/gridAttachments.tsx
|
|
1696
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1697
|
+
function attachmentIdsOf(value) {
|
|
1698
|
+
return idsOf(value);
|
|
1699
|
+
}
|
|
1700
|
+
function withAttached(field, value, added) {
|
|
1701
|
+
const held = attachmentIdsOf(value);
|
|
1702
|
+
if (!field.multi) return added.length > 0 ? added[added.length - 1] : held[0] ?? null;
|
|
1703
|
+
const next = [...held];
|
|
1704
|
+
for (const id of added) if (!next.includes(id)) next.push(id);
|
|
1705
|
+
return next.length > 0 ? next : null;
|
|
1706
|
+
}
|
|
1707
|
+
function withoutAttached(field, value, removed) {
|
|
1708
|
+
const next = attachmentIdsOf(value).filter((id) => id !== removed);
|
|
1709
|
+
if (!field.multi) return next[0] ?? null;
|
|
1710
|
+
return next.length > 0 ? next : null;
|
|
1711
|
+
}
|
|
1712
|
+
var handedHostFileIds = /* @__PURE__ */ new Set();
|
|
1713
|
+
function rememberHostFileIds(ids) {
|
|
1714
|
+
for (const id of ids) handedHostFileIds.add(id);
|
|
1715
|
+
}
|
|
1716
|
+
var FILE_CHIP_UNREAD = "File";
|
|
1717
|
+
var FILE_CHIP_NO_PAGE = "This screen has no way to open files yet.";
|
|
1718
|
+
function AttachmentChip({ id, onRemove }) {
|
|
1719
|
+
const client = useOptionalRecordsClient2();
|
|
1720
|
+
const host = useRecordsUi();
|
|
1721
|
+
const [read, setRead] = useState7(null);
|
|
1722
|
+
useEffect6(() => {
|
|
1723
|
+
let cancelled = false;
|
|
1724
|
+
if (!client) {
|
|
1725
|
+
setRead({ name: null, fileId: null, why: null });
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (handedHostFileIds.has(id)) {
|
|
1729
|
+
setRead({ name: null, fileId: id, why: null });
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
void client.recordRead({ record_id: id }).then((answered) => {
|
|
1733
|
+
if (cancelled) return;
|
|
1734
|
+
if (!answered.ok) {
|
|
1735
|
+
setRead({ name: null, fileId: id, why: refusalSentence(answered.error) });
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const doc = answered.data.document ?? {};
|
|
1739
|
+
const fileId = typeof doc["file_id"] === "string" ? doc["file_id"] : null;
|
|
1740
|
+
const name = typeof doc["name"] === "string" && doc["name"] !== "" ? doc["name"] : null;
|
|
1741
|
+
setRead({ name, fileId, why: null });
|
|
1742
|
+
});
|
|
1743
|
+
return () => {
|
|
1744
|
+
cancelled = true;
|
|
1745
|
+
};
|
|
1746
|
+
}, [client, id]);
|
|
1747
|
+
const words = read?.name ?? (read ? FILE_CHIP_UNREAD : "\u2026");
|
|
1748
|
+
const href = read?.fileId ? host.hrefForFile?.({ fileId: read.fileId }) : void 0;
|
|
1749
|
+
const title = href ? `Open ${words}` : read?.why ?? FILE_CHIP_NO_PAGE;
|
|
1750
|
+
return /* @__PURE__ */ jsxs7(Badge, { variant: "secondary", className: "max-w-[14rem] gap-1 text-[11px]", "data-records-file-chip": id, title, children: [
|
|
1751
|
+
href ? /* @__PURE__ */ jsx9(
|
|
1752
|
+
"a",
|
|
1753
|
+
{
|
|
1754
|
+
href,
|
|
1755
|
+
target: "_blank",
|
|
1756
|
+
rel: "noreferrer",
|
|
1757
|
+
className: "truncate underline-offset-2 hover:underline",
|
|
1758
|
+
"data-records-file-link": read?.fileId ?? "",
|
|
1759
|
+
onClick: (event) => event.stopPropagation(),
|
|
1760
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
1761
|
+
children: words
|
|
1762
|
+
}
|
|
1763
|
+
) : /* @__PURE__ */ jsx9("span", { className: "truncate", children: words }),
|
|
1764
|
+
onRemove ? /* @__PURE__ */ jsx9(
|
|
1765
|
+
"button",
|
|
1766
|
+
{
|
|
1767
|
+
type: "button",
|
|
1768
|
+
"aria-label": `Remove ${words}`,
|
|
1769
|
+
className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
|
|
1770
|
+
onClick: (event) => {
|
|
1771
|
+
event.stopPropagation();
|
|
1772
|
+
onRemove();
|
|
1773
|
+
},
|
|
1774
|
+
children: "\xD7"
|
|
1775
|
+
}
|
|
1776
|
+
) : null
|
|
1777
|
+
] });
|
|
1778
|
+
}
|
|
1779
|
+
function AttachmentChips({ value, onRemove }) {
|
|
1780
|
+
const ids = attachmentIdsOf(value);
|
|
1781
|
+
if (ids.length === 0) return null;
|
|
1782
|
+
return /* @__PURE__ */ jsx9("span", { className: "flex flex-wrap gap-1", "data-records-file-chips": "", children: ids.map((id) => /* @__PURE__ */ jsx9(AttachmentChip, { id, ...onRemove ? { onRemove: () => onRemove(id) } : {} }, id)) });
|
|
1783
|
+
}
|
|
1784
|
+
function hostTakesFiles(host) {
|
|
1785
|
+
return Boolean(host.pickFiles || host.upload);
|
|
1786
|
+
}
|
|
1787
|
+
function AttachmentControl({
|
|
1788
|
+
field,
|
|
1789
|
+
value,
|
|
1790
|
+
onChange,
|
|
1791
|
+
disabled,
|
|
1792
|
+
id,
|
|
1793
|
+
autoOpen,
|
|
1794
|
+
onClose
|
|
1795
|
+
}) {
|
|
1796
|
+
const host = useRecordsUi();
|
|
1797
|
+
const [busy, setBusy] = useState7(false);
|
|
1798
|
+
const [why, setWhy] = useState7(null);
|
|
1799
|
+
const latest = useRef5(value);
|
|
1800
|
+
latest.current = value;
|
|
1801
|
+
const multiple = Boolean(field.multi);
|
|
1802
|
+
const pick = async () => {
|
|
1803
|
+
if (!host.pickFiles || disabled) return;
|
|
1804
|
+
setBusy(true);
|
|
1805
|
+
setWhy(null);
|
|
1806
|
+
try {
|
|
1807
|
+
const picked = await host.pickFiles({ multiple });
|
|
1808
|
+
if (picked && picked.length > 0) {
|
|
1809
|
+
rememberHostFileIds(picked);
|
|
1810
|
+
onChange(withAttached(field, latest.current, picked));
|
|
1811
|
+
}
|
|
1812
|
+
} catch (cause) {
|
|
1813
|
+
setWhy(cause instanceof Error ? cause.message : String(cause));
|
|
1814
|
+
setBusy(false);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
setBusy(false);
|
|
1818
|
+
onClose?.();
|
|
1819
|
+
};
|
|
1820
|
+
const opened = useRef5(false);
|
|
1821
|
+
useEffect6(() => {
|
|
1822
|
+
if (!autoOpen || opened.current || !host.pickFiles) return;
|
|
1823
|
+
opened.current = true;
|
|
1824
|
+
void pick();
|
|
1825
|
+
}, [autoOpen]);
|
|
1826
|
+
if (!hostTakesFiles(host)) {
|
|
1827
|
+
return /* @__PURE__ */ jsx9("p", { id, className: "rounded border border-dashed px-2 py-1 text-xs text-muted-foreground", "data-records-no-file-door": "", children: NO_UPLOAD_REASON });
|
|
1828
|
+
}
|
|
1829
|
+
return /* @__PURE__ */ jsxs7("span", { id, className: "flex flex-col gap-1", "data-records-attachment-editor": "", children: [
|
|
1830
|
+
/* @__PURE__ */ jsx9(
|
|
1831
|
+
AttachmentChips,
|
|
1832
|
+
{
|
|
1833
|
+
value,
|
|
1834
|
+
...disabled ? {} : { onRemove: (removed) => onChange(withoutAttached(field, latest.current, removed)) }
|
|
1835
|
+
}
|
|
1836
|
+
),
|
|
1837
|
+
/* @__PURE__ */ jsxs7("span", { className: "flex flex-wrap items-center gap-1", children: [
|
|
1838
|
+
host.pickFiles ? /* @__PURE__ */ jsx9(Button5, { size: "sm", variant: "outline", disabled: disabled || busy, onClick: () => void pick(), "data-records-attach-files": "", children: busy ? "Choosing\u2026" : attachmentIdsOf(value).length > 0 ? "Add files\u2026" : "Attach files\u2026" }) : null,
|
|
1839
|
+
host.upload ? /* @__PURE__ */ jsx9(
|
|
1840
|
+
FilePicker,
|
|
1841
|
+
{
|
|
1842
|
+
label: "Upload a file",
|
|
1843
|
+
inputLabel: `Upload a file to ${fieldName(field)}`,
|
|
1844
|
+
disabled: disabled || busy,
|
|
1845
|
+
onFile: (file) => {
|
|
1846
|
+
setBusy(true);
|
|
1847
|
+
setWhy(null);
|
|
1848
|
+
void host.upload(file).then((result) => {
|
|
1849
|
+
setBusy(false);
|
|
1850
|
+
if (result.ok) {
|
|
1851
|
+
rememberHostFileIds([result.fileId]);
|
|
1852
|
+
onChange(withAttached(field, latest.current, [result.fileId]));
|
|
1853
|
+
} else setWhy(result.reason);
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
) : null
|
|
1858
|
+
] }),
|
|
1859
|
+
why ? /* @__PURE__ */ jsx9(RefusalLine, { error: refusal("invalid_argument", why, "Pick another file, or try this one again.") }) : null
|
|
1860
|
+
] });
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// src/EntityReferenceControl.tsx
|
|
1864
|
+
import { useMemo as useMemo4, useState as useState8 } from "react";
|
|
1865
|
+
import { entityRefsOf, entityRefsToWrite } from "@ai-matrx/records";
|
|
1866
|
+
import { useEntityReferenceKinds, useEntityReferenceSearch, useEntityReferenceWords, useOptionalRecordsClient as useOptionalRecordsClient3 } from "@ai-matrx/records/react";
|
|
1867
|
+
import { Badge as Badge2, Button as Button6, cn as cn7 } from "@ai-matrx/design-system";
|
|
1868
|
+
import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1869
|
+
var WITHHELD_ENTITY_LABEL = "Something you have not been given access to";
|
|
1870
|
+
var ENTITY_REFERENCE_EMPTY = "Nothing is pointed at yet.";
|
|
1871
|
+
function entityReferenceText(value) {
|
|
1872
|
+
const refs = entityRefsOf(value);
|
|
1873
|
+
if (refs.length === 0) return "\u2014";
|
|
1874
|
+
return refs.map((r) => r.label ?? WITHHELD_ENTITY_LABEL).join(", ");
|
|
1875
|
+
}
|
|
1876
|
+
function allowedKindsOf(field) {
|
|
1877
|
+
const config = field.config ?? {};
|
|
1878
|
+
const kinds = Array.isArray(config.allowed_types) ? config.allowed_types : [];
|
|
1879
|
+
return kinds.map((k) => String(k).trim().toLowerCase()).filter(Boolean);
|
|
1880
|
+
}
|
|
1881
|
+
function EntityReferenceControl(props) {
|
|
1882
|
+
const client = useOptionalRecordsClient3();
|
|
1883
|
+
return client ? /* @__PURE__ */ jsx10(WithWords, { ...props }) : /* @__PURE__ */ jsx10(Chips, { ...props, words: null, picker: null });
|
|
1884
|
+
}
|
|
1885
|
+
function WithWords(props) {
|
|
1886
|
+
const refs = useMemo4(() => entityRefsOf(props.value), [props.value]);
|
|
1887
|
+
const [known, setKnown] = useState8([]);
|
|
1888
|
+
const withKnown = refs.map((r) => r.label === null ? known.find((k) => k.token === r.token && k.id === r.id) ?? r : r);
|
|
1889
|
+
const unlabelled = useMemo4(
|
|
1890
|
+
() => withKnown.filter((r) => r.label === null).map((r) => ({ token: r.token, id: r.id })),
|
|
1891
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1892
|
+
[JSON.stringify(withKnown)]
|
|
1893
|
+
);
|
|
1894
|
+
const words = useEntityReferenceWords(unlabelled);
|
|
1895
|
+
const picker = props.disabled ? null : /* @__PURE__ */ jsx10(
|
|
1896
|
+
EntityReferencePicker,
|
|
1897
|
+
{
|
|
1898
|
+
field: props.field,
|
|
1899
|
+
refs: withKnown,
|
|
1900
|
+
onPick: (picked) => {
|
|
1901
|
+
setKnown((k) => [...k.filter((x) => !(x.token === picked.token && x.id === picked.id)), picked]);
|
|
1902
|
+
const multi = Boolean(props.field.multi);
|
|
1903
|
+
const next = multi ? [...withKnown.filter((x) => !(x.token === picked.token && x.id === picked.id)), picked] : [picked];
|
|
1904
|
+
props.onChange(entityRefsToWrite(next, multi));
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
);
|
|
1908
|
+
return /* @__PURE__ */ jsx10(Chips, { ...props, value: withKnown, words, picker });
|
|
1909
|
+
}
|
|
1910
|
+
function EntityReferencePicker({
|
|
1911
|
+
field,
|
|
1912
|
+
refs,
|
|
1913
|
+
onPick
|
|
1914
|
+
}) {
|
|
1915
|
+
const allowed = allowedKindsOf(field);
|
|
1916
|
+
const kinds = useEntityReferenceKinds();
|
|
1917
|
+
const labelOfKind = (token) => kinds.data?.find((k) => k.token === token)?.label ?? token.replace(/_/g, " ");
|
|
1918
|
+
const [open, setOpen] = useState8(false);
|
|
1919
|
+
const [search, setSearch] = useState8("");
|
|
1920
|
+
const [only, setOnly] = useState8(null);
|
|
1921
|
+
const tokens = only ? [only] : allowed;
|
|
1922
|
+
const found = useEntityReferenceSearch(tokens, search, open);
|
|
1923
|
+
const multi = Boolean(field.multi);
|
|
1924
|
+
const items = (found.data ?? []).map((r) => ({
|
|
1925
|
+
id: `${r.token}:${r.id}`,
|
|
1926
|
+
label: r.label ?? WITHHELD_ENTITY_LABEL,
|
|
1927
|
+
detail: allowed.length > 1 ? labelOfKind(r.token) : null,
|
|
1928
|
+
picked: refs.some((x) => x.token === r.token && x.id === r.id),
|
|
1929
|
+
attrs: { "data-entity-reference-candidate": `${r.token}:${r.id}` }
|
|
1930
|
+
}));
|
|
1931
|
+
const kindChoice = allowed.length > 1 ? allowed.length <= 4 ? /* @__PURE__ */ jsx10("div", { className: "mb-1 flex flex-wrap gap-1", role: "group", "aria-label": `Which kind \u2014 ${field.label}`, children: [null, ...allowed].map((token) => /* @__PURE__ */ jsx10(
|
|
1932
|
+
Button6,
|
|
1933
|
+
{
|
|
1934
|
+
type: "button",
|
|
1935
|
+
size: "sm",
|
|
1936
|
+
variant: only === token ? "secondary" : "ghost",
|
|
1937
|
+
className: "h-6 px-2 text-[11px]",
|
|
1938
|
+
"aria-pressed": only === token,
|
|
1939
|
+
onClick: () => setOnly(token),
|
|
1940
|
+
children: token ? labelOfKind(token) : "Any"
|
|
1941
|
+
},
|
|
1942
|
+
token ?? "any"
|
|
1943
|
+
)) }) : /* @__PURE__ */ jsxs8(
|
|
1944
|
+
"select",
|
|
1945
|
+
{
|
|
1946
|
+
className: "mb-1 h-7 w-full rounded border border-border bg-background px-1 text-xs",
|
|
1947
|
+
"aria-label": `Which kind \u2014 ${field.label}`,
|
|
1948
|
+
value: only ?? "",
|
|
1949
|
+
onChange: (e) => setOnly(e.target.value || null),
|
|
1950
|
+
children: [
|
|
1951
|
+
/* @__PURE__ */ jsx10("option", { value: "", children: "Any kind" }),
|
|
1952
|
+
allowed.map((token) => /* @__PURE__ */ jsx10("option", { value: token, children: labelOfKind(token) }, token))
|
|
1953
|
+
]
|
|
1954
|
+
}
|
|
1955
|
+
) : null;
|
|
1956
|
+
const notice = /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1957
|
+
kindChoice,
|
|
1958
|
+
found.error ? /* @__PURE__ */ jsx10(RefusalLine, { error: found.error, className: "px-2 py-1 text-[11px]" }) : null,
|
|
1959
|
+
found.loading && !found.data ? /* @__PURE__ */ jsx10("p", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: "Searching\u2026" }) : null
|
|
1960
|
+
] });
|
|
1961
|
+
const kindWords = allowed.length === 1 ? labelOfKind(allowed[0]).toLowerCase() : "thing";
|
|
1962
|
+
return /* @__PURE__ */ jsx10(
|
|
1963
|
+
PickOrAdd,
|
|
1964
|
+
{
|
|
1965
|
+
triggerLabel: `Pick for ${field.label}`,
|
|
1966
|
+
triggerText: refs.length > 0 && !multi ? "Change" : "Pick",
|
|
1967
|
+
open,
|
|
1968
|
+
onOpenChange: (next) => {
|
|
1969
|
+
setOpen(next);
|
|
1970
|
+
if (!next) setSearch("");
|
|
1971
|
+
},
|
|
1972
|
+
search,
|
|
1973
|
+
onSearch: setSearch,
|
|
1974
|
+
items,
|
|
1975
|
+
notice,
|
|
1976
|
+
empty: found.loading ? null : /* @__PURE__ */ jsx10("p", { className: "px-2 py-1 text-[11px] text-muted-foreground", children: search.trim() ? `No ${kindWords} you can open is called \u201C${search.trim()}\u201D.` : `There is no ${kindWords} you can open yet.` }),
|
|
1977
|
+
onPick: (key) => {
|
|
1978
|
+
const r = (found.data ?? []).find((x) => `${x.token}:${x.id}` === key);
|
|
1979
|
+
if (!r) return;
|
|
1980
|
+
onPick(r);
|
|
1981
|
+
if (!multi) setOpen(false);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
function Chips({
|
|
1987
|
+
field,
|
|
1988
|
+
value,
|
|
1989
|
+
onChange,
|
|
1990
|
+
disabled,
|
|
1991
|
+
id,
|
|
1992
|
+
className,
|
|
1993
|
+
words,
|
|
1994
|
+
picker
|
|
1995
|
+
}) {
|
|
1996
|
+
const refs = useMemo4(() => entityRefsOf(value), [value]);
|
|
1997
|
+
const labelOf = (token, refId, own3) => {
|
|
1998
|
+
if (own3) return own3;
|
|
1999
|
+
const read = words?.data?.find((w) => w.token === token && w.id === refId)?.label;
|
|
2000
|
+
if (read) return read;
|
|
2001
|
+
if (words === null) return "A linked item (its name is read when you are signed in)";
|
|
2002
|
+
if (words.loading) return "Reading\u2026";
|
|
2003
|
+
return WITHHELD_ENTITY_LABEL;
|
|
2004
|
+
};
|
|
2005
|
+
const multi = Boolean(field.multi);
|
|
2006
|
+
return /* @__PURE__ */ jsxs8("div", { id, className: cn7("flex flex-wrap items-center gap-1", className), "aria-label": `What ${field.label} points at`, children: [
|
|
2007
|
+
refs.length === 0 && !picker ? /* @__PURE__ */ jsx10("span", { className: "text-[11px] text-muted-foreground", children: ENTITY_REFERENCE_EMPTY }) : null,
|
|
2008
|
+
refs.map((r) => {
|
|
2009
|
+
const label = labelOf(r.token, r.id, r.label);
|
|
2010
|
+
return /* @__PURE__ */ jsxs8(Badge2, { variant: "secondary", className: "gap-1 text-[11px]", "data-entity-reference-chip": `${r.token}:${r.id}`, children: [
|
|
2011
|
+
label,
|
|
2012
|
+
disabled ? null : /* @__PURE__ */ jsx10(
|
|
2013
|
+
"button",
|
|
2014
|
+
{
|
|
2015
|
+
type: "button",
|
|
2016
|
+
className: "ml-0.5 rounded-sm px-0.5 opacity-70 hover:opacity-100",
|
|
2017
|
+
"aria-label": `Take ${label} off`,
|
|
2018
|
+
onClick: () => {
|
|
2019
|
+
const rest = refs.filter((x) => !(x.token === r.token && x.id === r.id));
|
|
2020
|
+
onChange(entityRefsToWrite(rest, multi));
|
|
2021
|
+
},
|
|
2022
|
+
children: "\xD7"
|
|
2023
|
+
}
|
|
2024
|
+
)
|
|
2025
|
+
] }, `${r.token}:${r.id}`);
|
|
2026
|
+
}),
|
|
2027
|
+
picker
|
|
2028
|
+
] });
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/PersonPicker.tsx
|
|
2032
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState9 } from "react";
|
|
2033
|
+
import { rankTypedMatches as rankTypedMatches2 } from "@ai-matrx/records";
|
|
2034
|
+
import { useOptionalRecordsClient as useOptionalRecordsClient4 } from "@ai-matrx/records/react";
|
|
2035
|
+
import {
|
|
2036
|
+
BasicInput as BasicInput4,
|
|
2037
|
+
Button as Button7,
|
|
2038
|
+
Popover as Popover2,
|
|
2039
|
+
PopoverContent as PopoverContent2,
|
|
2040
|
+
PopoverTrigger as PopoverTrigger2,
|
|
2041
|
+
ScrollArea as ScrollArea2,
|
|
2042
|
+
cn as cn8
|
|
2043
|
+
} from "@ai-matrx/design-system";
|
|
2044
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2045
|
+
function personFromRow(row) {
|
|
2046
|
+
const data = row.document ?? {};
|
|
2047
|
+
const userId = data["user_id"];
|
|
2048
|
+
return {
|
|
2049
|
+
id: row.id,
|
|
2050
|
+
userId: typeof userId === "string" && userId !== "" ? userId : null,
|
|
2051
|
+
name: String(data["full_name"] ?? data["name"] ?? data["email"] ?? "").trim()
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function memberName(member) {
|
|
2055
|
+
const display = (member.name ?? "").trim();
|
|
2056
|
+
if (display !== "") return display;
|
|
2057
|
+
const email = (member.email ?? "").trim();
|
|
2058
|
+
if (email !== "") return email;
|
|
2059
|
+
return "A member with no name set";
|
|
2060
|
+
}
|
|
2061
|
+
async function personRecordForMember(client, personTableId, people, member) {
|
|
2062
|
+
const held = people.find((p) => p.userId === member.userId);
|
|
2063
|
+
if (held) return { ok: true, data: held.id };
|
|
2064
|
+
const written = await client.recordWrite({
|
|
2065
|
+
table_id: personTableId,
|
|
2066
|
+
data: {
|
|
2067
|
+
user_id: member.userId,
|
|
2068
|
+
full_name: memberName(member),
|
|
2069
|
+
...member.email ? { email: member.email } : {}
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
return written;
|
|
2073
|
+
}
|
|
2074
|
+
function PersonPicker({ field, value, onChange, disabled, id, className, autoOpen, onClose, initialSearch }) {
|
|
2075
|
+
const client = useOptionalRecordsClient4();
|
|
2076
|
+
const host = useRecordsUi();
|
|
2077
|
+
const [members, setMembers] = useState9(null);
|
|
2078
|
+
const [people, setPeople] = useState9([]);
|
|
2079
|
+
const [error, setError] = useState9(null);
|
|
2080
|
+
const [search, setSearch] = useState9(initialSearch ?? "");
|
|
2081
|
+
const [open, setOpenState] = useState9(autoOpen === true);
|
|
2082
|
+
const onCloseRef = useRef6(onClose);
|
|
2083
|
+
onCloseRef.current = onClose;
|
|
2084
|
+
const setOpen = useCallback3((next) => {
|
|
2085
|
+
setOpenState(next);
|
|
2086
|
+
if (!next) onCloseRef.current?.();
|
|
2087
|
+
}, []);
|
|
2088
|
+
const [busy, setBusy] = useState9(false);
|
|
2089
|
+
const membersPort = host.members;
|
|
2090
|
+
const personTable = field.relation_target;
|
|
2091
|
+
const picked = useMemo5(
|
|
2092
|
+
() => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
|
|
2093
|
+
[value]
|
|
2094
|
+
);
|
|
2095
|
+
const load = useCallback3(async () => {
|
|
2096
|
+
if (!membersPort || !client) return;
|
|
2097
|
+
const [roster, records] = await Promise.all([
|
|
2098
|
+
membersPort(),
|
|
2099
|
+
personTable ? client.list({ table_id: personTable, limit: 500 }) : Promise.resolve(null)
|
|
2100
|
+
]);
|
|
2101
|
+
setMembers(roster);
|
|
2102
|
+
if (records && !records.ok) {
|
|
2103
|
+
setError(records.error);
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
setPeople(records ? records.data.rows.map(personFromRow).filter((p) => p.userId !== null) : []);
|
|
2107
|
+
}, [membersPort, client, personTable]);
|
|
2108
|
+
useEffect7(() => {
|
|
2109
|
+
if (!open) return;
|
|
2110
|
+
void load();
|
|
2111
|
+
}, [open, load]);
|
|
2112
|
+
useEffect7(() => {
|
|
2113
|
+
if (picked.length === 0 || !personTable || people.length > 0 || !client) return;
|
|
2114
|
+
let cancelled = false;
|
|
2115
|
+
void client.list({ table_id: personTable, limit: 500 }).then((result) => {
|
|
2116
|
+
if (cancelled || !result.ok) return;
|
|
2117
|
+
setPeople(result.data.rows.map(personFromRow).filter((p) => p.userId !== null));
|
|
2118
|
+
});
|
|
2119
|
+
return () => {
|
|
2120
|
+
cancelled = true;
|
|
2121
|
+
};
|
|
2122
|
+
}, [client, personTable, picked.length, people.length]);
|
|
2123
|
+
const listed = rankTypedMatches2(members ?? [], search, memberName);
|
|
2124
|
+
const keys = useListKeys({
|
|
2125
|
+
count: listed.length,
|
|
2126
|
+
resetOn: search,
|
|
2127
|
+
onPick: (index) => {
|
|
2128
|
+
const member = listed[index];
|
|
2129
|
+
if (member && !busy) void pickRef.current?.(member);
|
|
2130
|
+
},
|
|
2131
|
+
onClose: () => setOpen(false)
|
|
2132
|
+
});
|
|
2133
|
+
const pickRef = useRef6(null);
|
|
2134
|
+
if (!membersPort || !client) {
|
|
2135
|
+
return /* @__PURE__ */ jsx11("p", { className: cn8("text-xs text-muted-foreground", className), children: NO_MEMBERS_REASON });
|
|
2136
|
+
}
|
|
2137
|
+
if (!personTable) {
|
|
2138
|
+
return /* @__PURE__ */ jsxs9("p", { className: cn8("text-xs text-muted-foreground", className), children: [
|
|
2139
|
+
fieldName(field),
|
|
2140
|
+
" is a person field, but it does not say which Table the people are records of yet, so there is nothing to pick from. A table admin sets its target in the field editor."
|
|
2141
|
+
] });
|
|
2142
|
+
}
|
|
2143
|
+
const nameOfRecord = (recordId) => {
|
|
2144
|
+
const person = people.find((p) => p.id === recordId);
|
|
2145
|
+
if (!person) return "Loading\u2026";
|
|
2146
|
+
if (person.name !== "") return person.name;
|
|
2147
|
+
const member = (members ?? []).find((m) => m.userId === person.userId);
|
|
2148
|
+
return member ? memberName(member) : "A member with no name set";
|
|
2149
|
+
};
|
|
2150
|
+
const pick = async (member) => {
|
|
2151
|
+
setBusy(true);
|
|
2152
|
+
setError(null);
|
|
2153
|
+
const resolved = await personRecordForMember(client, personTable, people, member);
|
|
2154
|
+
setBusy(false);
|
|
2155
|
+
if (!resolved.ok) {
|
|
2156
|
+
setError(resolved.error);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
if (!people.some((p) => p.id === resolved.data)) {
|
|
2160
|
+
setPeople((held) => [...held, { id: resolved.data, userId: member.userId, name: memberName(member) }]);
|
|
2161
|
+
}
|
|
2162
|
+
if (!field.multi) {
|
|
2163
|
+
onChange(picked[0] === resolved.data ? null : resolved.data);
|
|
2164
|
+
setOpen(false);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const next = new Set(picked);
|
|
2168
|
+
if (next.has(resolved.data)) next.delete(resolved.data);
|
|
2169
|
+
else next.add(resolved.data);
|
|
2170
|
+
onChange(next.size === 0 ? null : [...next]);
|
|
2171
|
+
};
|
|
2172
|
+
pickRef.current = pick;
|
|
2173
|
+
const drop = (recordId) => {
|
|
2174
|
+
const next = picked.filter((id2) => id2 !== recordId);
|
|
2175
|
+
onChange(next.length === 0 ? null : field.multi ? next : next[0] ?? null);
|
|
2176
|
+
};
|
|
2177
|
+
const visible = listed;
|
|
2178
|
+
const pickedUserIds = new Set(
|
|
2179
|
+
picked.map((recordId) => people.find((p) => p.id === recordId)?.userId).filter(Boolean)
|
|
2180
|
+
);
|
|
2181
|
+
return /* @__PURE__ */ jsxs9("div", { className: cn8("flex flex-wrap items-center gap-1", className), children: [
|
|
2182
|
+
picked.map((recordId) => /* @__PURE__ */ jsx11(
|
|
2183
|
+
RecordChip,
|
|
2184
|
+
{
|
|
2185
|
+
title: nameOfRecord(recordId),
|
|
2186
|
+
...disabled ? {} : { onRemove: () => drop(recordId) }
|
|
2187
|
+
},
|
|
2188
|
+
recordId
|
|
2189
|
+
)),
|
|
2190
|
+
disabled ? null : /* @__PURE__ */ jsxs9(Popover2, { open, onOpenChange: setOpen, children: [
|
|
2191
|
+
/* @__PURE__ */ jsx11(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsx11(
|
|
2192
|
+
Button7,
|
|
2193
|
+
{
|
|
2194
|
+
id,
|
|
2195
|
+
type: "button",
|
|
2196
|
+
size: "sm",
|
|
2197
|
+
variant: "outline",
|
|
2198
|
+
"aria-label": `Pick a person for ${fieldName(field)}`,
|
|
2199
|
+
children: picked.length === 0 ? "Pick a person" : "Change"
|
|
2200
|
+
}
|
|
2201
|
+
) }),
|
|
2202
|
+
/* @__PURE__ */ jsxs9(
|
|
2203
|
+
PopoverContent2,
|
|
2204
|
+
{
|
|
2205
|
+
sizing: "content",
|
|
2206
|
+
className: "p-1",
|
|
2207
|
+
align: "start",
|
|
2208
|
+
"data-matrx-cell-editor": "",
|
|
2209
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
2210
|
+
children: [
|
|
2211
|
+
/* @__PURE__ */ jsx11(
|
|
2212
|
+
BasicInput4,
|
|
2213
|
+
{
|
|
2214
|
+
value: search,
|
|
2215
|
+
onChange: (e) => setSearch(e.target.value),
|
|
2216
|
+
onKeyDown: keys.onKeyDown,
|
|
2217
|
+
autoFocus: true,
|
|
2218
|
+
autoComplete: "off",
|
|
2219
|
+
"aria-label": `Search people for ${fieldName(field)}`,
|
|
2220
|
+
placeholder: "Search people",
|
|
2221
|
+
className: "mb-1 h-7 text-xs"
|
|
2222
|
+
}
|
|
2223
|
+
),
|
|
2224
|
+
error ? /* @__PURE__ */ jsx11(RefusalLine, { error, className: "p-2" }) : null,
|
|
2225
|
+
members === null ? /* @__PURE__ */ jsx11("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading the members\u2026" }) : null,
|
|
2226
|
+
/* @__PURE__ */ jsxs9(ScrollArea2, { className: "max-h-56", children: [
|
|
2227
|
+
/* @__PURE__ */ jsx11("ul", { role: "listbox", "aria-label": `People for ${fieldName(field)}`, children: visible.map((member, index) => /* @__PURE__ */ jsx11("li", { children: /* @__PURE__ */ jsxs9(
|
|
2228
|
+
"button",
|
|
2229
|
+
{
|
|
2230
|
+
type: "button",
|
|
2231
|
+
disabled: busy,
|
|
2232
|
+
"data-pick-highlighted": index === keys.highlight ? "" : void 0,
|
|
2233
|
+
onClick: () => void pick(member),
|
|
2234
|
+
className: cn8(
|
|
2235
|
+
"w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
|
|
2236
|
+
pickedUserIds.has(member.userId) ? "bg-accent text-accent-foreground" : "",
|
|
2237
|
+
index === keys.highlight ? "ring-1 ring-ring" : ""
|
|
2238
|
+
),
|
|
2239
|
+
children: [
|
|
2240
|
+
/* @__PURE__ */ jsx11("span", { children: memberName(member) }),
|
|
2241
|
+
member.email && member.email !== memberName(member) ? /* @__PURE__ */ jsxs9("span", { className: "ml-1.5 opacity-60", children: [
|
|
2242
|
+
"\xB7 ",
|
|
2243
|
+
member.email
|
|
2244
|
+
] }) : null
|
|
2245
|
+
]
|
|
2246
|
+
}
|
|
2247
|
+
) }, member.userId)) }),
|
|
2248
|
+
members !== null && visible.length === 0 ? /* @__PURE__ */ jsx11("p", { className: "p-2 text-xs text-muted-foreground", children: members.length === 0 ? "This organization has no other members yet, so there is nobody to assign." : "Nobody in this organization matches." }) : null
|
|
2249
|
+
] })
|
|
2250
|
+
]
|
|
2251
|
+
}
|
|
2252
|
+
)
|
|
2253
|
+
] })
|
|
2254
|
+
] });
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// src/presentation.ts
|
|
2258
|
+
import {
|
|
2259
|
+
STYLE_COLORS,
|
|
2260
|
+
parseTableStyle,
|
|
2261
|
+
tableStyleIsEmpty
|
|
2262
|
+
} from "@ai-matrx/design-system/data-table";
|
|
2263
|
+
import { getFieldFormat } from "@ai-matrx/design-system/field-formats";
|
|
2264
|
+
import { newerKindOf as newerKindOf3, ratingMaxOf } from "@ai-matrx/records";
|
|
2265
|
+
var DRAWN_KINDS = /* @__PURE__ */ new Set(["select", "multi_select", "relation", "member", "person", "attachment", "file", "entity_reference", "checkbox", "boolean", "signature", "json"]);
|
|
2266
|
+
function shownAsOf(field) {
|
|
2267
|
+
if (DRAWN_KINDS.has(String(editorKindFor(field)))) return void 0;
|
|
2268
|
+
const raw = field.display_format;
|
|
2269
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2270
|
+
const id = raw.id;
|
|
2271
|
+
if (typeof id !== "string" || id === "formula" || !getFieldFormat(id)) return void 0;
|
|
2272
|
+
const options = raw.options;
|
|
2273
|
+
return options && typeof options === "object" && Object.keys(options).length > 0 ? { id, options } : { id };
|
|
2274
|
+
}
|
|
2275
|
+
function formatForField(field) {
|
|
2276
|
+
const shownAs = shownAsOf(field);
|
|
2277
|
+
if (newerKindOf3(field) === "rating" && (!shownAs || shownAs.id === "rating")) {
|
|
2278
|
+
return { id: "rating", options: { ...shownAs?.options ?? {}, ratingMax: ratingMaxOf(field) } };
|
|
2279
|
+
}
|
|
2280
|
+
if (shownAs) return shownAs;
|
|
2281
|
+
const stamp = dateFormatFor(field);
|
|
2282
|
+
if (stamp && editorKindFor(field) !== "datetime") return stamp;
|
|
2283
|
+
switch (editorKindFor(field)) {
|
|
2284
|
+
case "currency":
|
|
2285
|
+
return {
|
|
2286
|
+
id: "currency",
|
|
2287
|
+
options: { currency: currencyCodeFor(field.unit) }
|
|
2288
|
+
};
|
|
2289
|
+
case "percent":
|
|
2290
|
+
return { id: "percent" };
|
|
2291
|
+
case "datetime":
|
|
2292
|
+
return String(field.config?.["kind"] ?? "") === "date" ? { id: "date" } : { id: "datetime" };
|
|
2293
|
+
case "email":
|
|
2294
|
+
return { id: "email" };
|
|
2295
|
+
case "url":
|
|
2296
|
+
return { id: "url" };
|
|
2297
|
+
case "phone":
|
|
2298
|
+
return { id: "phone" };
|
|
2299
|
+
case "number":
|
|
2300
|
+
return { id: "number" };
|
|
2301
|
+
default:
|
|
2302
|
+
return void 0;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
var SYMBOL_TO_CODE = {
|
|
2306
|
+
$: "USD",
|
|
2307
|
+
"\xA3": "GBP",
|
|
2308
|
+
"\u20AC": "EUR",
|
|
2309
|
+
"\xA5": "JPY",
|
|
2310
|
+
"\u20B9": "INR",
|
|
2311
|
+
"\u20BD": "RUB",
|
|
2312
|
+
"\u20A9": "KRW",
|
|
2313
|
+
"R$": "BRL",
|
|
2314
|
+
"C$": "CAD",
|
|
2315
|
+
"A$": "AUD"
|
|
2316
|
+
};
|
|
2317
|
+
function currencyCodeFor(unit) {
|
|
2318
|
+
const raw = (unit ?? "").trim();
|
|
2319
|
+
if (raw === "") return "USD";
|
|
2320
|
+
return SYMBOL_TO_CODE[raw] ?? raw.toUpperCase();
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// src/formatControls.tsx
|
|
2324
|
+
import { useEffect as useEffect8, useRef as useRef8, useState as useState10 } from "react";
|
|
2325
|
+
import { BasicInput as BasicInput5, BasicTextarea } from "@ai-matrx/design-system";
|
|
2326
|
+
import { getFieldFormat as getFieldFormat3, parseFieldInput } from "@ai-matrx/design-system/field-formats";
|
|
2327
|
+
|
|
2328
|
+
// src/formattedValue.tsx
|
|
2329
|
+
import { useRef as useRef7 } from "react";
|
|
2330
|
+
import { FormattedCell } from "@ai-matrx/design-system/data-table";
|
|
2331
|
+
import { formatFieldValue as formatFieldValue2, getFieldFormat as getFieldFormat2 } from "@ai-matrx/design-system/field-formats";
|
|
2332
|
+
import { cn as cn9 } from "@ai-matrx/design-system";
|
|
2333
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2334
|
+
var LINK = "inline-flex min-w-0 items-center gap-1 text-primary hover:underline";
|
|
2335
|
+
var own2 = (event) => event.stopPropagation();
|
|
2336
|
+
function numberOf(value) {
|
|
2337
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
2338
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2339
|
+
const n = Number(value.replace(/[,\s%]/g, ""));
|
|
2340
|
+
return Number.isFinite(n) ? n : null;
|
|
2341
|
+
}
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
function RatingStars({
|
|
2345
|
+
value,
|
|
2346
|
+
max = 5,
|
|
2347
|
+
onRate,
|
|
2348
|
+
label,
|
|
2349
|
+
selectFirst
|
|
2350
|
+
}) {
|
|
2351
|
+
const armed = useRef7(true);
|
|
2352
|
+
const score = numberOf(value) ?? 0;
|
|
2353
|
+
const shown2 = Math.round(score);
|
|
2354
|
+
const said = `${shown2} out of ${max}`;
|
|
2355
|
+
return /* @__PURE__ */ jsx12(
|
|
2356
|
+
"span",
|
|
2357
|
+
{
|
|
2358
|
+
"data-records-rating": "",
|
|
2359
|
+
role: onRate ? "radiogroup" : "img",
|
|
2360
|
+
"aria-label": label ? `${label}: ${said}` : said,
|
|
2361
|
+
title: said,
|
|
2362
|
+
className: "inline-flex items-center gap-0.5",
|
|
2363
|
+
...onRate ? { "data-matrx-cell-control": "" } : {},
|
|
2364
|
+
children: Array.from({ length: max }, (_, i) => {
|
|
2365
|
+
const n = i + 1;
|
|
2366
|
+
const lit = n <= shown2;
|
|
2367
|
+
const star = /* @__PURE__ */ jsx12("span", { "aria-hidden": true, className: cn9("text-sm leading-none", lit ? "text-amber-400" : "text-muted-foreground/40"), children: "\u2605" });
|
|
2368
|
+
return onRate ? /* @__PURE__ */ jsx12(
|
|
2369
|
+
"button",
|
|
2370
|
+
{
|
|
2371
|
+
type: "button",
|
|
2372
|
+
role: "radio",
|
|
2373
|
+
"aria-checked": n === shown2,
|
|
2374
|
+
"aria-label": `${n} out of ${max}`,
|
|
2375
|
+
"data-records-rating-star": n,
|
|
2376
|
+
className: "rounded px-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2377
|
+
onMouseDown: (event) => {
|
|
2378
|
+
if (!selectFirst) {
|
|
2379
|
+
own2(event);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
armed.current = event.currentTarget.closest("td")?.hasAttribute("data-matrx-cell-active") === true;
|
|
2383
|
+
},
|
|
2384
|
+
onDoubleClick: own2,
|
|
2385
|
+
onClick: (event) => {
|
|
2386
|
+
event.stopPropagation();
|
|
2387
|
+
if (selectFirst && event.detail !== 0 && !armed.current) return;
|
|
2388
|
+
onRate(n === shown2 ? null : n);
|
|
2389
|
+
},
|
|
2390
|
+
children: star
|
|
2391
|
+
},
|
|
2392
|
+
n
|
|
2393
|
+
) : /* @__PURE__ */ jsx12("span", { children: star }, n);
|
|
2394
|
+
})
|
|
2395
|
+
}
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
function rich(id, text, raw, format) {
|
|
2399
|
+
switch (id) {
|
|
2400
|
+
case "email":
|
|
2401
|
+
return /* @__PURE__ */ jsx12("a", { href: `mailto:${text}`, className: LINK, onClick: own2, children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text }) });
|
|
2402
|
+
case "url":
|
|
2403
|
+
return /* @__PURE__ */ jsx12("a", { href: text.includes("://") ? text : `https://${text}`, target: "_blank", rel: "noopener noreferrer", className: LINK, onClick: own2, children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text }) });
|
|
2404
|
+
case "phone":
|
|
2405
|
+
return /* @__PURE__ */ jsx12("a", { href: `tel:${text.replace(/[^\d+]/g, "")}`, className: LINK, onClick: own2, title: text, children: /* @__PURE__ */ jsx12("span", { className: "truncate tabular-nums", children: formatPhone(text) }) });
|
|
2406
|
+
case "address":
|
|
2407
|
+
return /* @__PURE__ */ jsx12(
|
|
2408
|
+
"a",
|
|
2409
|
+
{
|
|
2410
|
+
href: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(text)}`,
|
|
2411
|
+
target: "_blank",
|
|
2412
|
+
rel: "noopener noreferrer",
|
|
2413
|
+
className: LINK,
|
|
2414
|
+
title: "Open in maps",
|
|
2415
|
+
onClick: own2,
|
|
2416
|
+
children: /* @__PURE__ */ jsx12("span", { className: "truncate", children: text })
|
|
2417
|
+
}
|
|
2418
|
+
);
|
|
2419
|
+
case "progress": {
|
|
2420
|
+
const n = numberOf(raw);
|
|
2421
|
+
if (n === null) return void 0;
|
|
2422
|
+
const pct = format.options?.percentScale === "fraction" ? n * 100 : n;
|
|
2423
|
+
const clamped = Math.max(0, Math.min(100, pct));
|
|
2424
|
+
return /* @__PURE__ */ jsxs10("span", { className: "inline-flex w-full min-w-0 items-center gap-2", title: text, children: [
|
|
2425
|
+
/* @__PURE__ */ jsx12(
|
|
2426
|
+
"span",
|
|
2427
|
+
{
|
|
2428
|
+
role: "progressbar",
|
|
2429
|
+
"aria-valuemin": 0,
|
|
2430
|
+
"aria-valuemax": 100,
|
|
2431
|
+
"aria-valuenow": Math.round(clamped),
|
|
2432
|
+
className: "h-1.5 min-w-8 flex-1 overflow-hidden rounded-full bg-muted",
|
|
2433
|
+
children: /* @__PURE__ */ jsx12("span", { className: cn9("block h-full rounded-full", pct >= 100 ? "bg-emerald-500" : "bg-primary"), style: { width: `${clamped}%` } })
|
|
2434
|
+
}
|
|
2435
|
+
),
|
|
2436
|
+
/* @__PURE__ */ jsx12("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: text })
|
|
2437
|
+
] });
|
|
2438
|
+
}
|
|
2439
|
+
case "color":
|
|
2440
|
+
return /* @__PURE__ */ jsxs10("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
2441
|
+
/* @__PURE__ */ jsx12("span", { className: "h-4 w-4 shrink-0 rounded border border-border", style: { backgroundColor: text } }),
|
|
2442
|
+
/* @__PURE__ */ jsx12("code", { className: "font-mono text-xs", children: text })
|
|
2443
|
+
] });
|
|
2444
|
+
case "tags": {
|
|
2445
|
+
const items = Array.isArray(raw) ? raw.map(String) : text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2446
|
+
if (items.length === 0) return void 0;
|
|
2447
|
+
return /* @__PURE__ */ jsx12("span", { className: "flex flex-wrap items-center gap-1", children: items.map((item, i) => /* @__PURE__ */ jsx12("span", { className: "inline-flex items-center rounded-md border border-border bg-muted px-1.5 py-0.5 text-xs", children: item }, `${item}-${i}`)) });
|
|
2448
|
+
}
|
|
2449
|
+
case "rating":
|
|
2450
|
+
return numberOf(raw) === null ? void 0 : /* @__PURE__ */ jsx12(RatingStars, { value: raw, max: format.options?.ratingMax ?? 5 });
|
|
2451
|
+
default:
|
|
2452
|
+
return void 0;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function FormattedValue({
|
|
2456
|
+
value,
|
|
2457
|
+
format,
|
|
2458
|
+
onRate,
|
|
2459
|
+
selectFirst
|
|
2460
|
+
}) {
|
|
2461
|
+
const def = getFieldFormat2(format.id);
|
|
2462
|
+
const score = numberOf(value);
|
|
2463
|
+
const max = format.options?.ratingMax ?? 5;
|
|
2464
|
+
if (def?.id === "rating" && onRate && (value === null || value === void 0 || value === "" || score !== null && score >= 0 && score <= max)) {
|
|
2465
|
+
return /* @__PURE__ */ jsx12(RatingStars, { value, max: format.options?.ratingMax ?? 5, onRate, selectFirst });
|
|
2466
|
+
}
|
|
2467
|
+
const result = formatFieldValue2(value, format);
|
|
2468
|
+
if (!result.ok || result.empty || !def?.rich) return /* @__PURE__ */ jsx12(FormattedCell, { result });
|
|
2469
|
+
return rich(def.id, result.text, value, format) ?? /* @__PURE__ */ jsx12(FormattedCell, { result });
|
|
2470
|
+
}
|
|
2471
|
+
function formatPhone(raw) {
|
|
2472
|
+
const text = raw.trim();
|
|
2473
|
+
const digits = text.replace(/\D/g, "");
|
|
2474
|
+
if (/[a-z]/i.test(text.replace(/ext\.?|x/gi, ""))) return text;
|
|
2475
|
+
if (digits.length === 10 && !text.startsWith("+")) return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
2476
|
+
if (digits.length === 11 && digits.startsWith("1") && (text.startsWith("+1") || text.startsWith("1") || !text.startsWith("+"))) {
|
|
2477
|
+
return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
|
|
2478
|
+
}
|
|
2479
|
+
return text;
|
|
2480
|
+
}
|
|
2481
|
+
function linkChipText(raw) {
|
|
2482
|
+
try {
|
|
2483
|
+
const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
|
2484
|
+
const host = url.hostname.replace(/^www\./, "");
|
|
2485
|
+
const last = url.pathname.split("/").filter(Boolean).pop();
|
|
2486
|
+
return last ? `${host} \xB7 ${decodeURIComponent(last)}` : host;
|
|
2487
|
+
} catch {
|
|
2488
|
+
return raw;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/formatControls.tsx
|
|
2493
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2494
|
+
var TYPED_NUMBERS = /* @__PURE__ */ new Set(["currency", "percent", "progress", "decimal", "duration", "file_size", "integer"]);
|
|
2495
|
+
function scaled(format) {
|
|
2496
|
+
return (format.id === "percent" || format.id === "progress") && format.options?.percentScale === "fraction";
|
|
2497
|
+
}
|
|
2498
|
+
function typedText(format, value) {
|
|
2499
|
+
if (value === null || value === void 0) return "";
|
|
2500
|
+
if (typeof value === "number" && scaled(format)) return String(Math.round(value * 100 * 1e6) / 1e6);
|
|
2501
|
+
return String(value);
|
|
2502
|
+
}
|
|
2503
|
+
function keptValue(format, raw) {
|
|
2504
|
+
if (raw.trim() === "") return null;
|
|
2505
|
+
const parsed = parseFieldInput(raw, format);
|
|
2506
|
+
if (typeof parsed === "number" && scaled(format)) return Math.round(parsed / 100 * 1e9) / 1e9;
|
|
2507
|
+
return parsed;
|
|
2508
|
+
}
|
|
2509
|
+
function NumberInFormat({ format, value, onChange, disabled, id }) {
|
|
2510
|
+
const [text, setText] = useState10(() => typedText(format, value));
|
|
2511
|
+
const lastSent = useRef8(void 0);
|
|
2512
|
+
useEffect8(() => {
|
|
2513
|
+
if (value === lastSent.current) return;
|
|
2514
|
+
setText(typedText(format, value));
|
|
2515
|
+
}, [value, format]);
|
|
2516
|
+
return /* @__PURE__ */ jsx13(
|
|
2517
|
+
BasicInput5,
|
|
2518
|
+
{
|
|
2519
|
+
id,
|
|
2520
|
+
inputMode: "decimal",
|
|
2521
|
+
className: "tabular-nums",
|
|
2522
|
+
value: text,
|
|
2523
|
+
disabled,
|
|
2524
|
+
placeholder: format.id === "currency" ? "$0.00" : format.id === "percent" || format.id === "progress" ? "45%" : void 0,
|
|
2525
|
+
onChange: (e) => {
|
|
2526
|
+
setText(e.target.value);
|
|
2527
|
+
const next = keptValue(format, e.target.value);
|
|
2528
|
+
lastSent.current = next;
|
|
2529
|
+
onChange(next);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
function formatControl(args) {
|
|
2535
|
+
const def = getFieldFormat3(args.format.id);
|
|
2536
|
+
if (!def) return null;
|
|
2537
|
+
const { format, value, onChange, disabled, id, label, ownDetails } = args;
|
|
2538
|
+
const text = value === null || value === void 0 ? "" : String(value);
|
|
2539
|
+
const commit = (raw) => onChange(raw === "" ? null : parseFieldInput(raw, format));
|
|
2540
|
+
if (def.id === "rating") {
|
|
2541
|
+
return disabled ? /* @__PURE__ */ jsx13(RatingStars, { value, max: format.options?.ratingMax ?? 5, label }) : /* @__PURE__ */ jsx13("span", { id, className: "inline-flex", children: /* @__PURE__ */ jsx13(RatingStars, { value, max: format.options?.ratingMax ?? 5, label, onRate: (n) => onChange(n) }) });
|
|
2542
|
+
}
|
|
2543
|
+
if (TYPED_NUMBERS.has(def.id)) return /* @__PURE__ */ jsx13(NumberInFormat, { ...args });
|
|
2544
|
+
switch (def.editor) {
|
|
2545
|
+
// A TIME OF DAY is not this file's: `FieldControl` draws the typed box and its helper
|
|
2546
|
+
// (`TypedWordsControl`), never the browser's time control (grids review 3, 2026-09-30).
|
|
2547
|
+
case "email":
|
|
2548
|
+
case "url":
|
|
2549
|
+
case "tel":
|
|
2550
|
+
return /* @__PURE__ */ jsx13(
|
|
2551
|
+
BasicInput5,
|
|
2552
|
+
{
|
|
2553
|
+
id,
|
|
2554
|
+
type: def.editor,
|
|
2555
|
+
...ownDetails && def.editor !== "url" ? { autoComplete: def.editor === "email" ? "email" : "tel" } : {},
|
|
2556
|
+
value: text,
|
|
2557
|
+
disabled,
|
|
2558
|
+
onChange: (e) => commit(e.target.value)
|
|
2559
|
+
}
|
|
2560
|
+
);
|
|
2561
|
+
case "color":
|
|
2562
|
+
return /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-2", children: [
|
|
2563
|
+
/* @__PURE__ */ jsx13(
|
|
2564
|
+
BasicInput5,
|
|
2565
|
+
{
|
|
2566
|
+
id: `${id}-swatch`,
|
|
2567
|
+
type: "color",
|
|
2568
|
+
"aria-label": `${label} \u2014 pick a colour`,
|
|
2569
|
+
className: "h-8 w-12 p-1",
|
|
2570
|
+
value: /^#[0-9a-fA-F]{6}$/.test(text) ? text : "#000000",
|
|
2571
|
+
disabled,
|
|
2572
|
+
onChange: (e) => commit(e.target.value)
|
|
2573
|
+
}
|
|
2574
|
+
),
|
|
2575
|
+
/* @__PURE__ */ jsx13(BasicInput5, { id, value: text, placeholder: "#3b82f6", disabled, onChange: (e) => commit(e.target.value) })
|
|
2576
|
+
] });
|
|
2577
|
+
case "textarea":
|
|
2578
|
+
return /* @__PURE__ */ jsx13(BasicTextarea, { id, rows: 4, value: text, disabled, onChange: (e) => commit(e.target.value) });
|
|
2579
|
+
default:
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/typedDateHelper.tsx
|
|
2585
|
+
import { useState as useState11 } from "react";
|
|
2586
|
+
import { Popover as Popover3, PopoverContent as PopoverContent3, PopoverTrigger as PopoverTrigger3, cn as cn10 } from "@ai-matrx/design-system";
|
|
2587
|
+
import { readTypedDate, readTypedTime, typedDateText, typedTimeText } from "@ai-matrx/records";
|
|
2588
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2589
|
+
var WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
2590
|
+
var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
2591
|
+
function pad(n) {
|
|
2592
|
+
return String(n).padStart(2, "0");
|
|
2593
|
+
}
|
|
2594
|
+
function readBox(text, kind) {
|
|
2595
|
+
if (kind === "time") {
|
|
2596
|
+
const t = readTypedTime(text);
|
|
2597
|
+
if (!t.ok) return { day: null, time: null };
|
|
2598
|
+
const [h, m] = t.value.split(":").map(Number);
|
|
2599
|
+
return { day: null, time: { h, m } };
|
|
2600
|
+
}
|
|
2601
|
+
const read = readTypedDate(text, { withTime: kind === "datetime" });
|
|
2602
|
+
if (!read.ok) return { day: null, time: null };
|
|
2603
|
+
if (kind === "datetime" && read.instant) {
|
|
2604
|
+
const at = new Date(read.instant);
|
|
2605
|
+
return { day: read.day, time: { h: at.getHours(), m: at.getMinutes() } };
|
|
2606
|
+
}
|
|
2607
|
+
return { day: read.day, time: null };
|
|
2608
|
+
}
|
|
2609
|
+
function wordsFor(kind, day, time) {
|
|
2610
|
+
if (kind === "time") return time ? typedTimeText(time.h, time.m) : "";
|
|
2611
|
+
if (!day) return "";
|
|
2612
|
+
const dayText = typedDateText(day);
|
|
2613
|
+
return kind === "datetime" && time ? `${dayText} ${typedTimeText(time.h, time.m)}` : dayText;
|
|
2614
|
+
}
|
|
2615
|
+
var keepFocus = { onMouseDown: (e) => e.preventDefault(), tabIndex: -1 };
|
|
2616
|
+
function CalendarGlyph() {
|
|
2617
|
+
return /* @__PURE__ */ jsxs12("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2618
|
+
/* @__PURE__ */ jsx14("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
|
|
2619
|
+
/* @__PURE__ */ jsx14("path", { d: "M16 2v4M8 2v4M3 10h18" })
|
|
2620
|
+
] });
|
|
2621
|
+
}
|
|
2622
|
+
function ClockGlyph() {
|
|
2623
|
+
return /* @__PURE__ */ jsxs12("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2624
|
+
/* @__PURE__ */ jsx14("circle", { cx: "12", cy: "12", r: "10" }),
|
|
2625
|
+
/* @__PURE__ */ jsx14("path", { d: "M12 6v6l4 2" })
|
|
2626
|
+
] });
|
|
2627
|
+
}
|
|
2628
|
+
function TypedDateHelper({ kind, text, onText, label }) {
|
|
2629
|
+
const [open, setOpen] = useState11(false);
|
|
2630
|
+
const held = readBox(text, kind);
|
|
2631
|
+
const today = /* @__PURE__ */ new Date();
|
|
2632
|
+
const start = held.day ? new Date(Number(held.day.slice(0, 4)), Number(held.day.slice(5, 7)) - 1, 1) : new Date(today.getFullYear(), today.getMonth(), 1);
|
|
2633
|
+
const [month, setMonth] = useState11(null);
|
|
2634
|
+
const shown2 = month ?? { y: start.getFullYear(), m: start.getMonth() };
|
|
2635
|
+
const first = new Date(shown2.y, shown2.m, 1).getDay();
|
|
2636
|
+
const days = new Date(shown2.y, shown2.m + 1, 0).getDate();
|
|
2637
|
+
const cells = [...Array.from({ length: first }, () => null), ...Array.from({ length: days }, (_, i) => i + 1)];
|
|
2638
|
+
const todayKey = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
|
2639
|
+
const times = Array.from({ length: 48 }, (_, i) => ({ h: Math.floor(i / 2), m: i % 2 * 30 }));
|
|
2640
|
+
const pickDay = (d) => {
|
|
2641
|
+
const day = `${shown2.y}-${pad(shown2.m + 1)}-${pad(d)}`;
|
|
2642
|
+
onText(wordsFor(kind, day, held.time));
|
|
2643
|
+
if (kind === "date") setOpen(false);
|
|
2644
|
+
};
|
|
2645
|
+
const pickTime = (t) => {
|
|
2646
|
+
const day = kind === "datetime" ? held.day ?? todayKey : null;
|
|
2647
|
+
onText(wordsFor(kind, day, t));
|
|
2648
|
+
setOpen(false);
|
|
2649
|
+
};
|
|
2650
|
+
const turn = (by) => {
|
|
2651
|
+
const at = new Date(shown2.y, shown2.m + by, 1);
|
|
2652
|
+
setMonth({ y: at.getFullYear(), m: at.getMonth() });
|
|
2653
|
+
};
|
|
2654
|
+
return /* @__PURE__ */ jsxs12(Popover3, { open, onOpenChange: setOpen, children: [
|
|
2655
|
+
/* @__PURE__ */ jsx14(PopoverTrigger3, { asChild: true, children: /* @__PURE__ */ jsx14(
|
|
2656
|
+
"button",
|
|
2657
|
+
{
|
|
2658
|
+
type: "button",
|
|
2659
|
+
...keepFocus,
|
|
2660
|
+
"data-records-date-helper": "",
|
|
2661
|
+
"aria-label": kind === "time" ? `Pick a time for ${label}` : `Pick a date for ${label}`,
|
|
2662
|
+
title: kind === "time" ? "Pick a time" : "Pick a date",
|
|
2663
|
+
className: "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
2664
|
+
children: kind === "time" ? /* @__PURE__ */ jsx14(ClockGlyph, {}) : /* @__PURE__ */ jsx14(CalendarGlyph, {})
|
|
2665
|
+
}
|
|
2666
|
+
) }),
|
|
2667
|
+
/* @__PURE__ */ jsx14(
|
|
2668
|
+
PopoverContent3,
|
|
2669
|
+
{
|
|
2670
|
+
sizing: "content",
|
|
2671
|
+
align: "start",
|
|
2672
|
+
className: "p-2",
|
|
2673
|
+
"data-matrx-cell-editor": "",
|
|
2674
|
+
"data-records-date-helper-panel": "",
|
|
2675
|
+
onOpenAutoFocus: (e) => e.preventDefault(),
|
|
2676
|
+
onCloseAutoFocus: (e) => e.preventDefault(),
|
|
2677
|
+
children: /* @__PURE__ */ jsxs12("div", { className: "flex gap-2", children: [
|
|
2678
|
+
kind === "time" ? null : /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-1", role: "grid", "aria-label": `${MONTHS[shown2.m]} ${shown2.y}`, children: [
|
|
2679
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-xs font-medium", children: [
|
|
2680
|
+
/* @__PURE__ */ jsx14("button", { type: "button", ...keepFocus, "aria-label": "Previous month", className: "rounded px-1.5 hover:bg-muted", onClick: () => turn(-1), children: "\u2039" }),
|
|
2681
|
+
/* @__PURE__ */ jsxs12("span", { children: [
|
|
2682
|
+
MONTHS[shown2.m],
|
|
2683
|
+
" ",
|
|
2684
|
+
shown2.y
|
|
2685
|
+
] }),
|
|
2686
|
+
/* @__PURE__ */ jsx14("button", { type: "button", ...keepFocus, "aria-label": "Next month", className: "rounded px-1.5 hover:bg-muted", onClick: () => turn(1), children: "\u203A" })
|
|
2687
|
+
] }),
|
|
2688
|
+
/* @__PURE__ */ jsxs12("div", { className: "grid grid-cols-7 gap-0.5 text-center text-[11px]", children: [
|
|
2689
|
+
WEEK.map((w) => /* @__PURE__ */ jsx14("span", { className: "text-muted-foreground", children: w }, w)),
|
|
2690
|
+
cells.map((d, i) => {
|
|
2691
|
+
if (d === null) return /* @__PURE__ */ jsx14("span", {}, `blank-${i}`);
|
|
2692
|
+
const key = `${shown2.y}-${pad(shown2.m + 1)}-${pad(d)}`;
|
|
2693
|
+
return /* @__PURE__ */ jsx14(
|
|
2694
|
+
"button",
|
|
2695
|
+
{
|
|
2696
|
+
type: "button",
|
|
2697
|
+
...keepFocus,
|
|
2698
|
+
"data-records-date-helper-day": key,
|
|
2699
|
+
className: cn10(
|
|
2700
|
+
"h-6 w-6 rounded tabular-nums hover:bg-muted",
|
|
2701
|
+
key === held.day ? "bg-primary text-primary-foreground hover:bg-primary" : "",
|
|
2702
|
+
key === todayKey && key !== held.day ? "ring-1 ring-ring" : ""
|
|
2703
|
+
),
|
|
2704
|
+
onClick: () => pickDay(d),
|
|
2705
|
+
children: d
|
|
2706
|
+
},
|
|
2707
|
+
key
|
|
2708
|
+
);
|
|
2709
|
+
})
|
|
2710
|
+
] })
|
|
2711
|
+
] }),
|
|
2712
|
+
kind === "date" ? null : /* @__PURE__ */ jsx14("div", { className: "flex max-h-48 flex-col overflow-y-auto pr-1 text-[11px]", "aria-label": "Times", children: times.map((t) => /* @__PURE__ */ jsx14(
|
|
2713
|
+
"button",
|
|
2714
|
+
{
|
|
2715
|
+
type: "button",
|
|
2716
|
+
...keepFocus,
|
|
2717
|
+
"data-records-date-helper-time": `${pad(t.h)}:${pad(t.m)}`,
|
|
2718
|
+
className: cn10(
|
|
2719
|
+
"whitespace-nowrap rounded px-2 py-0.5 text-left tabular-nums hover:bg-muted",
|
|
2720
|
+
held.time && held.time.h === t.h && held.time.m === t.m ? "bg-primary text-primary-foreground" : ""
|
|
2721
|
+
),
|
|
2722
|
+
onClick: () => pickTime(t),
|
|
2723
|
+
children: typedTimeText(t.h, t.m)
|
|
2724
|
+
},
|
|
2725
|
+
`${t.h}:${t.m}`
|
|
2726
|
+
)) })
|
|
2727
|
+
] })
|
|
2728
|
+
}
|
|
2729
|
+
)
|
|
2730
|
+
] });
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// src/choiceNudge.ts
|
|
2734
|
+
import { useEffect as useEffect9, useState as useState12 } from "react";
|
|
2735
|
+
import { useOptionalRecordsClient as useOptionalRecordsClient5 } from "@ai-matrx/records/react";
|
|
2736
|
+
var asked = /* @__PURE__ */ new WeakMap();
|
|
2737
|
+
function choiceNudgeOf(client) {
|
|
2738
|
+
let answer = asked.get(client);
|
|
2739
|
+
if (!answer) {
|
|
2740
|
+
answer = Promise.resolve().then(() => client.choiceNudge()).then(
|
|
2741
|
+
(result) => result.ok ? result.data : "ask",
|
|
2742
|
+
() => "ask"
|
|
2743
|
+
);
|
|
2744
|
+
asked.set(client, answer);
|
|
2745
|
+
}
|
|
2746
|
+
return answer;
|
|
2747
|
+
}
|
|
2748
|
+
function useChoiceNudge(enabled = true) {
|
|
2749
|
+
const client = useOptionalRecordsClient5();
|
|
2750
|
+
const [nudge, setNudge] = useState12(null);
|
|
2751
|
+
useEffect9(() => {
|
|
2752
|
+
if (!enabled || !client) return;
|
|
2753
|
+
let live = true;
|
|
2754
|
+
void choiceNudgeOf(client).then((answer) => {
|
|
2755
|
+
if (live) setNudge(answer);
|
|
2756
|
+
});
|
|
2757
|
+
return () => {
|
|
2758
|
+
live = false;
|
|
2759
|
+
};
|
|
2760
|
+
}, [client, enabled]);
|
|
2761
|
+
if (!client) return "ask";
|
|
2762
|
+
return nudge;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/choiceLooks.ts
|
|
2766
|
+
import {
|
|
2767
|
+
CHOICE_CHIP_CLASS,
|
|
2768
|
+
NEUTRAL_CHIP_CLASS,
|
|
2769
|
+
STYLE_COLORS as STYLE_COLORS2
|
|
2770
|
+
} from "@ai-matrx/design-system/data-table";
|
|
2771
|
+
function displayFormatOf(field) {
|
|
2772
|
+
const raw = field.display_format;
|
|
2773
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2774
|
+
const id = raw.id;
|
|
2775
|
+
if (typeof id !== "string" || id === "") return null;
|
|
2776
|
+
const options = raw.options;
|
|
2777
|
+
return { id, options: options && typeof options === "object" ? options : {} };
|
|
2778
|
+
}
|
|
2779
|
+
function isStyleColor(v) {
|
|
2780
|
+
return typeof v === "string" && STYLE_COLORS2.includes(v);
|
|
2781
|
+
}
|
|
2782
|
+
function choiceLooksOf(field) {
|
|
2783
|
+
const display = displayFormatOf(field);
|
|
2784
|
+
const choices = display?.options["choices"];
|
|
2785
|
+
if (!Array.isArray(choices)) return [];
|
|
2786
|
+
const out = [];
|
|
2787
|
+
for (const c of choices) {
|
|
2788
|
+
if (!c || typeof c !== "object") continue;
|
|
2789
|
+
const value = c.value;
|
|
2790
|
+
if (typeof value !== "string" || value.trim() === "") continue;
|
|
2791
|
+
const color = c.color;
|
|
2792
|
+
const label = c.label;
|
|
2793
|
+
out.push({
|
|
2794
|
+
value: value.trim(),
|
|
2795
|
+
...typeof label === "string" && label.trim() !== "" ? { label: label.trim() } : {},
|
|
2796
|
+
...isStyleColor(color) ? { color } : {}
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
return out;
|
|
2800
|
+
}
|
|
2801
|
+
function choiceWord(value) {
|
|
2802
|
+
return String(value ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2803
|
+
}
|
|
2804
|
+
function storedChoiceColor(field, value) {
|
|
2805
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
2806
|
+
const word = choiceWord(value);
|
|
2807
|
+
if (word === "") return void 0;
|
|
2808
|
+
for (const look of choiceLooksOf(field)) {
|
|
2809
|
+
if (!look.color) continue;
|
|
2810
|
+
if (choiceWord(look.value) === word || look.label && choiceWord(look.label) === word) return look.color;
|
|
2811
|
+
}
|
|
2812
|
+
return void 0;
|
|
2813
|
+
}
|
|
2814
|
+
function choiceChipLookOf(field, ...words) {
|
|
2815
|
+
for (const word of words) {
|
|
2816
|
+
const color = storedChoiceColor(field, word);
|
|
2817
|
+
if (color) return { className: CHOICE_CHIP_CLASS[color], color };
|
|
2818
|
+
}
|
|
2819
|
+
return { className: NEUTRAL_CHIP_CLASS, color: null };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// src/editors.tsx
|
|
2823
|
+
import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2824
|
+
var CHOICE_EDITOR_ADDS_WHAT_IS_TYPED = true;
|
|
2825
|
+
function isChoiceAddition(value) {
|
|
2826
|
+
return typeof value === "object" && value !== null && value.addingChoices === true;
|
|
2827
|
+
}
|
|
2828
|
+
function choiceAddition(value, add) {
|
|
2829
|
+
return { addingChoices: true, value, add: [...add] };
|
|
2830
|
+
}
|
|
2831
|
+
function withoutAddition(value) {
|
|
2832
|
+
return isChoiceAddition(value) ? { value: value.value, add: value.add } : { value, add: [] };
|
|
2833
|
+
}
|
|
2834
|
+
function quoted(words) {
|
|
2835
|
+
const q = words.map((w) => `\u201C${w}\u201D`);
|
|
2836
|
+
return q.length <= 1 ? q[0] ?? "" : `${q.slice(0, -1).join(", ")} and ${q[q.length - 1]}`;
|
|
2837
|
+
}
|
|
2838
|
+
function choiceAskSentence(field, words) {
|
|
2839
|
+
return `Add ${quoted(words)} to the choices for ${fieldName(field)}?`;
|
|
2840
|
+
}
|
|
2841
|
+
function notAChoiceSentence(field, words) {
|
|
2842
|
+
return `${quoted(words)} ${words.length === 1 ? "is" : "are"} not one of the choices for ${fieldName(field)}.`;
|
|
2843
|
+
}
|
|
2844
|
+
function ChoiceAskButtons({
|
|
2845
|
+
field,
|
|
2846
|
+
words,
|
|
2847
|
+
keepOffered,
|
|
2848
|
+
onAdd,
|
|
2849
|
+
onKeep,
|
|
2850
|
+
onCancel,
|
|
2851
|
+
busy
|
|
2852
|
+
}) {
|
|
2853
|
+
return /* @__PURE__ */ jsxs13("span", { "data-records-choice-ask": "", role: "group", "aria-label": choiceAskSentence(field, words), className: "flex flex-col gap-1", children: [
|
|
2854
|
+
/* @__PURE__ */ jsx15("span", { className: "text-[11px] leading-snug", children: choiceAskSentence(field, words) }),
|
|
2855
|
+
/* @__PURE__ */ jsxs13("span", { className: "flex flex-wrap items-center gap-1", children: [
|
|
2856
|
+
/* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onAdd, children: "Add" }),
|
|
2857
|
+
keepOffered ? /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "outline", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onKeep, children: "Keep as typed" }) : null,
|
|
2858
|
+
/* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", disabled: busy ?? false, onClick: onCancel, children: "Cancel" })
|
|
2859
|
+
] })
|
|
2860
|
+
] });
|
|
2861
|
+
}
|
|
2862
|
+
var FORMAT_BLIND_KINDS = /* @__PURE__ */ new Set(["formula", "lookup", "rollup", "json", "checkbox", "select", "multi_select", "member", "entity_reference", "relation", "attachment", "datetime"]);
|
|
2863
|
+
function isTypedDraft(value) {
|
|
2864
|
+
return typeof value === "object" && value !== null && value.typedDraft === true;
|
|
2865
|
+
}
|
|
2866
|
+
function judgeTyped(field, text, choices, nudge) {
|
|
2867
|
+
const trimmed = text.trim();
|
|
2868
|
+
const kind = String(editorKindFor(field));
|
|
2869
|
+
const format = formatForField(field);
|
|
2870
|
+
if (trimmed === "") return { value: null };
|
|
2871
|
+
if (format && format.id === "time") {
|
|
2872
|
+
const read2 = readTypedTime2(trimmed);
|
|
2873
|
+
return read2.ok ? { value: read2.value } : { refusal: `${fieldName(field)} holds a time of day, and ${read2.why}` };
|
|
2874
|
+
}
|
|
2875
|
+
if (kind === "select" || kind === "multi_select") {
|
|
2876
|
+
if (!choices || nudge === null) return null;
|
|
2877
|
+
const several = field.multi || kind === "multi_select";
|
|
2878
|
+
const words = several ? trimmed.split(/\s*[,;\n]\s*/).filter(Boolean) : [trimmed];
|
|
2879
|
+
const allowOther = allowsOtherValues(field);
|
|
2880
|
+
const parts = [];
|
|
2881
|
+
const add = [];
|
|
2882
|
+
const ask = [];
|
|
2883
|
+
const notChoices = [];
|
|
2884
|
+
let keepOffered = allowOther;
|
|
2885
|
+
for (const word of words) {
|
|
2886
|
+
const decided = decideTypedChoice({ words: word, options: choices, allowOther, nudge });
|
|
2887
|
+
if (decided.decision === "empty") continue;
|
|
2888
|
+
if (decided.decision === "chosen") parts.push(decided.key);
|
|
2889
|
+
else {
|
|
2890
|
+
parts.push(decided.words);
|
|
2891
|
+
if (decided.decision === "add") add.push(decided.words);
|
|
2892
|
+
else if (decided.decision === "ask") {
|
|
2893
|
+
ask.push(decided.words);
|
|
2894
|
+
keepOffered = keepOffered && decided.keepOffered;
|
|
2895
|
+
} else if (decided.decision === "not_a_choice") notChoices.push(decided.words);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
const valueOf = (list) => several ? list : list[0] ?? null;
|
|
2899
|
+
if (notChoices.length > 0) return { refusal: notAChoiceSentence(field, notChoices), cancelOnly: true };
|
|
2900
|
+
if (ask.length > 0) {
|
|
2901
|
+
return {
|
|
2902
|
+
ask: { words: ask, keepOffered, onAdd: choiceAddition(valueOf(parts), [...add, ...ask]), asTyped: valueOf(parts) }
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
if (add.length > 0) return { value: choiceAddition(valueOf(parts), add) };
|
|
2906
|
+
return { value: valueOf(parts) };
|
|
2907
|
+
}
|
|
2908
|
+
if (format && format.id === "rating") {
|
|
2909
|
+
const n = Number(trimmed);
|
|
2910
|
+
const max = format.options?.ratingMax ?? 5;
|
|
2911
|
+
return Number.isInteger(n) && n >= 0 && n <= max ? { value: n === 0 ? null : n } : { refusal: `${fieldName(field)} is a rating from 1 to ${max}, and \u201C${text}\u201D is not one.` };
|
|
2912
|
+
}
|
|
2913
|
+
const showsAsPercent = Boolean(format && (format.id === "percent" || format.id === "progress"));
|
|
2914
|
+
const read = coerceTypedAnswer(field, text, { label: fieldName(field), showsAsPercent });
|
|
2915
|
+
if ("refusal" in read) return { refusal: read.refusal };
|
|
2916
|
+
if (typeof read.value === "number" && format && (format.id === "percent" || format.id === "progress") && format.options?.percentScale === "fraction") {
|
|
2917
|
+
return { value: Math.round(read.value / 100 * 1e9) / 1e9 };
|
|
2918
|
+
}
|
|
2919
|
+
return { value: read.value };
|
|
2920
|
+
}
|
|
2921
|
+
function dateHelperKind(field, value) {
|
|
2922
|
+
const format = formatForField(field);
|
|
2923
|
+
if (format && format.id === "time") return "time";
|
|
2924
|
+
if (String(editorKindFor(field)) !== "datetime") return null;
|
|
2925
|
+
return String(field.config?.["kind"] ?? "") === "datetime" || carriesTimeOfDay(field, value) ? "datetime" : "date";
|
|
2926
|
+
}
|
|
2927
|
+
function openingText(field, value) {
|
|
2928
|
+
if (value === null || value === void 0 || value === "") return "";
|
|
2929
|
+
const helper = dateHelperKind(field, value);
|
|
2930
|
+
if (helper === "time") {
|
|
2931
|
+
const read = readTypedTime2(String(value));
|
|
2932
|
+
if (!read.ok) return String(value);
|
|
2933
|
+
const [h, m] = read.value.split(":").map(Number);
|
|
2934
|
+
return typedTimeText2(h, m);
|
|
2935
|
+
}
|
|
2936
|
+
if (helper) return typedDateText2(value, { withTime: helper === "datetime" });
|
|
2937
|
+
const format = formatForField(field);
|
|
2938
|
+
return format ? typedText(format, value) : String(value);
|
|
2939
|
+
}
|
|
2940
|
+
function TypedTextControl({ field, value, onChange, disabled, id, onCommit, onCancel }) {
|
|
2941
|
+
const client = useOptionalRecordsClient6();
|
|
2942
|
+
const kind = String(editorKindFor(field));
|
|
2943
|
+
const needsChoices = kind === "select" || kind === "multi_select";
|
|
2944
|
+
const nudge = useChoiceNudge(needsChoices);
|
|
2945
|
+
const [choices, setChoices] = useState13(null);
|
|
2946
|
+
useEffect10(() => {
|
|
2947
|
+
if (!needsChoices || !client) return;
|
|
2948
|
+
let cancelled = false;
|
|
2949
|
+
void client.fieldOptions({ field_id: field.id }).then((result) => {
|
|
2950
|
+
if (!cancelled) setChoices(result.ok ? inTheStoresOrder(result.data ?? []) : []);
|
|
2951
|
+
});
|
|
2952
|
+
return () => {
|
|
2953
|
+
cancelled = true;
|
|
2954
|
+
};
|
|
2955
|
+
}, [client, field.id, needsChoices]);
|
|
2956
|
+
useEffect10(() => {
|
|
2957
|
+
if (choices && nudge && value.judged === null) onChange({ ...value, judged: judgeTyped(field, value.text, choices, nudge) });
|
|
2958
|
+
}, [choices, nudge, value, field, onChange]);
|
|
2959
|
+
const judged = value.judged;
|
|
2960
|
+
const format = formatForField(field);
|
|
2961
|
+
const helper = dateHelperKind(field);
|
|
2962
|
+
const retype = (text) => onChange({
|
|
2963
|
+
typedDraft: true,
|
|
2964
|
+
text,
|
|
2965
|
+
judged: judgeTyped(field, text, needsChoices ? choices : void 0, needsChoices ? nudge : void 0),
|
|
2966
|
+
...value.openedAs !== void 0 ? { openedAs: value.openedAs } : {}
|
|
2967
|
+
});
|
|
2968
|
+
const box = /* @__PURE__ */ jsx15(
|
|
2969
|
+
BasicInput6,
|
|
2970
|
+
{
|
|
2971
|
+
id,
|
|
2972
|
+
value: value.text,
|
|
2973
|
+
disabled,
|
|
2974
|
+
autoComplete: "off",
|
|
2975
|
+
"aria-label": fieldName(field),
|
|
2976
|
+
"aria-invalid": judged !== null && "refusal" in judged ? true : void 0,
|
|
2977
|
+
onChange: (e) => retype(e.target.value)
|
|
2978
|
+
}
|
|
2979
|
+
);
|
|
2980
|
+
return /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-0.5", children: [
|
|
2981
|
+
helper && !disabled ? /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-0.5", children: [
|
|
2982
|
+
box,
|
|
2983
|
+
/* @__PURE__ */ jsx15(TypedDateHelper, { kind: helper, text: value.text, onText: retype, label: fieldName(field) })
|
|
2984
|
+
] }) : box,
|
|
2985
|
+
judged === null ? /* @__PURE__ */ jsxs13("span", { className: "text-[10px] text-muted-foreground", children: [
|
|
2986
|
+
"Reading ",
|
|
2987
|
+
fieldName(field),
|
|
2988
|
+
"'s choices\u2026"
|
|
2989
|
+
] }) : "ask" in judged ? /* @__PURE__ */ jsx15(
|
|
2990
|
+
ChoiceAskButtons,
|
|
2991
|
+
{
|
|
2992
|
+
field,
|
|
2993
|
+
words: judged.ask.words,
|
|
2994
|
+
keepOffered: judged.ask.keepOffered,
|
|
2995
|
+
onAdd: () => {
|
|
2996
|
+
onChange({ ...value, judged: { value: judged.ask.onAdd } });
|
|
2997
|
+
onCommit?.();
|
|
2998
|
+
},
|
|
2999
|
+
onKeep: () => {
|
|
3000
|
+
onChange({ ...value, judged: { value: judged.ask.asTyped } });
|
|
3001
|
+
onCommit?.();
|
|
3002
|
+
},
|
|
3003
|
+
onCancel: () => onCancel?.()
|
|
3004
|
+
}
|
|
3005
|
+
) : "refusal" in judged ? /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-1", children: [
|
|
3006
|
+
/* @__PURE__ */ jsx15("span", { "data-records-typed-refusal": "", role: "status", className: "text-[11px] leading-snug text-amber-700 dark:text-amber-400", children: judged.refusal }),
|
|
3007
|
+
judged.cancelOnly && onCancel ? /* @__PURE__ */ jsx15("span", { "data-records-choice-ask": "", children: /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => onCancel(), children: "Cancel" }) }) : null
|
|
3008
|
+
] }) : judged.value === null || judged.value === void 0 ? null : /* @__PURE__ */ jsxs13("span", { "data-records-typed-reads-as": "", className: "text-[10px] text-muted-foreground", children: [
|
|
3009
|
+
isChoiceAddition(judged.value) ? `Adds ${quoted(judged.value.add)} to the choices for ${fieldName(field)}. ` : null,
|
|
3010
|
+
"Reads as",
|
|
3011
|
+
" ",
|
|
3012
|
+
needsChoices ? String(
|
|
3013
|
+
(Array.isArray(withoutAddition(judged.value).value) ? withoutAddition(judged.value).value : [withoutAddition(judged.value).value]).map((k) => {
|
|
3014
|
+
const option = choices?.find((o) => optionKey2(o) === k);
|
|
3015
|
+
return option ? recordName(option.data, null, String(k)) : String(k);
|
|
3016
|
+
}).join(", ")
|
|
3017
|
+
) : (() => {
|
|
3018
|
+
const shown2 = format ?? (kind === "datetime" ? { id: carriesTimeOfDay(field, judged.value) ? "datetime" : "date" } : void 0);
|
|
3019
|
+
return shown2 ? /* @__PURE__ */ jsx15(FormattedValue, { value: judged.value, format: shown2 }) : String(judged.value);
|
|
3020
|
+
})()
|
|
3021
|
+
] })
|
|
3022
|
+
] });
|
|
3023
|
+
}
|
|
3024
|
+
function isPersonsName(field) {
|
|
3025
|
+
const words = `${field.key} ${field.label ?? ""}`.toLowerCase();
|
|
3026
|
+
return /(^|[\s_])(full[\s_]?)?name(\s|$)/.test(words) && !/(therapist|doctor|physician|company|business|pet|room)/.test(words);
|
|
3027
|
+
}
|
|
3028
|
+
function FieldControl({ field, value, onChange, disabled, id, autoOpen, onClose, onCommit, onCancel, addsChoices, initialSearch, asForm }) {
|
|
3029
|
+
const host = useRecordsUi();
|
|
3030
|
+
const kind = editorKindFor(field);
|
|
3031
|
+
const inputId = id ?? `field-${field.key}`;
|
|
3032
|
+
const ownDetails = useOptionalRecordsClient6() === null;
|
|
3033
|
+
if (isTypedDraft(value)) return /* @__PURE__ */ jsx15(TypedTextControl, { field, value, onChange, disabled, id: inputId, onCommit, onCancel });
|
|
3034
|
+
if (isSignatureField(field)) return /* @__PURE__ */ jsx15(SignaturePad, { id: inputId, value, onChange, disabled });
|
|
3035
|
+
const newerControl = newerKindControl({ field, value, onChange, disabled, id: inputId });
|
|
3036
|
+
if (newerControl !== void 0) return newerControl;
|
|
3037
|
+
const format = formatForField(field);
|
|
3038
|
+
if (format && format.id === "time") return /* @__PURE__ */ jsx15(TypedWordsControl, { field, value, onChange, disabled, id: inputId });
|
|
3039
|
+
if (format && !FORMAT_BLIND_KINDS.has(String(kind))) {
|
|
3040
|
+
const own3 = formatControl({ format, value, onChange, disabled, id: inputId, label: fieldName(field), ownDetails });
|
|
3041
|
+
if (own3) return own3;
|
|
3042
|
+
}
|
|
3043
|
+
switch (kind) {
|
|
3044
|
+
case "formula":
|
|
3045
|
+
case "lookup":
|
|
3046
|
+
case "rollup":
|
|
3047
|
+
return /* @__PURE__ */ jsxs13("div", { id: inputId, className: "rounded border border-dashed px-2 py-1.5 text-xs text-muted-foreground", children: [
|
|
3048
|
+
value === null || value === void 0 ? "\u2014" : String(value),
|
|
3049
|
+
/* @__PURE__ */ jsxs13("span", { className: "ml-2 italic", children: [
|
|
3050
|
+
fieldName(field),
|
|
3051
|
+
" is worked out by the system, so it cannot be typed in"
|
|
3052
|
+
] })
|
|
3053
|
+
] });
|
|
3054
|
+
case "json":
|
|
3055
|
+
return /* @__PURE__ */ jsx15(JsonControl, { field, value, onChange, disabled, id: inputId });
|
|
3056
|
+
case "long_text":
|
|
3057
|
+
return /* @__PURE__ */ jsx15(
|
|
3058
|
+
BasicTextarea2,
|
|
3059
|
+
{
|
|
3060
|
+
id: inputId,
|
|
3061
|
+
rows: 3,
|
|
3062
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
3063
|
+
disabled,
|
|
3064
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
case "number":
|
|
3068
|
+
case "currency":
|
|
3069
|
+
case "percent":
|
|
3070
|
+
return /* @__PURE__ */ jsx15(
|
|
3071
|
+
BasicInput6,
|
|
3072
|
+
{
|
|
3073
|
+
id: inputId,
|
|
3074
|
+
type: "number",
|
|
3075
|
+
inputMode: "decimal",
|
|
3076
|
+
className: "tabular-nums",
|
|
3077
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
3078
|
+
disabled,
|
|
3079
|
+
onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value))
|
|
3080
|
+
}
|
|
3081
|
+
);
|
|
3082
|
+
case "datetime":
|
|
3083
|
+
return /* @__PURE__ */ jsx15(TypedWordsControl, { field, value, onChange, disabled, id: inputId });
|
|
3084
|
+
case "email":
|
|
3085
|
+
case "phone":
|
|
3086
|
+
case "url":
|
|
3087
|
+
return /* @__PURE__ */ jsx15(
|
|
3088
|
+
BasicInput6,
|
|
3089
|
+
{
|
|
3090
|
+
id: inputId,
|
|
3091
|
+
type: kind === "email" ? "email" : kind === "phone" ? "tel" : "url",
|
|
3092
|
+
...ownDetails && kind !== "url" ? { autoComplete: kind === "email" ? "email" : "tel" } : {},
|
|
3093
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
3094
|
+
disabled,
|
|
3095
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
|
|
3096
|
+
}
|
|
3097
|
+
);
|
|
3098
|
+
case "checkbox":
|
|
3099
|
+
return /* @__PURE__ */ jsx15(CheckboxControl, { field, value, onChange, disabled, id: inputId });
|
|
3100
|
+
case "select":
|
|
3101
|
+
case "multi_select":
|
|
3102
|
+
return /* @__PURE__ */ jsx15(OptionControl, { field, value, onChange, disabled, id: inputId, autoOpen, onClose, addsChoices, initialSearch, asForm });
|
|
3103
|
+
case "member":
|
|
3104
|
+
return /* @__PURE__ */ jsx15(PersonPicker, { field, value, onChange, disabled, id: inputId, autoOpen, onClose, initialSearch });
|
|
3105
|
+
case "entity_reference":
|
|
3106
|
+
return /* @__PURE__ */ jsx15(EntityReferenceControl, { field, value, onChange, disabled, id: inputId });
|
|
3107
|
+
case "attachment":
|
|
3108
|
+
if (hostTakesFiles(host)) {
|
|
3109
|
+
return /* @__PURE__ */ jsx15(AttachmentControl, { field, value, onChange, disabled, id: inputId, autoOpen, onClose });
|
|
3110
|
+
}
|
|
3111
|
+
return /* @__PURE__ */ jsx15(RelationPicker, { field, value, onChange, disabled, id: inputId });
|
|
3112
|
+
case "relation":
|
|
3113
|
+
return /* @__PURE__ */ jsx15(RelationPicker, { field, value, onChange, disabled, id: inputId });
|
|
3114
|
+
default:
|
|
3115
|
+
if (typeof value === "boolean" || field.config?.["kind"] === "boolean") {
|
|
3116
|
+
return /* @__PURE__ */ jsx15(
|
|
3117
|
+
Checkbox,
|
|
3118
|
+
{
|
|
3119
|
+
id: inputId,
|
|
3120
|
+
checked: value === true,
|
|
3121
|
+
disabled,
|
|
3122
|
+
onCheckedChange: (next) => onChange(next === true)
|
|
3123
|
+
}
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
return /* @__PURE__ */ jsx15(
|
|
3127
|
+
BasicInput6,
|
|
3128
|
+
{
|
|
3129
|
+
id: inputId,
|
|
3130
|
+
...ownDetails && isPersonsName(field) ? { autoComplete: "name" } : {},
|
|
3131
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
3132
|
+
disabled,
|
|
3133
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
|
|
3134
|
+
}
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function TypedWordsControl({ field, value, onChange, disabled, id }) {
|
|
3139
|
+
const [text, setText] = useState13(() => openingText(field, value));
|
|
3140
|
+
const [refusal2, setRefusal] = useState13(null);
|
|
3141
|
+
const sent = useRef9(value);
|
|
3142
|
+
useEffect10(() => {
|
|
3143
|
+
if (value === sent.current) return;
|
|
3144
|
+
sent.current = value;
|
|
3145
|
+
setText(openingText(field, value));
|
|
3146
|
+
setRefusal(null);
|
|
3147
|
+
}, [value, field]);
|
|
3148
|
+
const helper = dateHelperKind(field, value);
|
|
3149
|
+
const retype = (next) => {
|
|
3150
|
+
setText(next);
|
|
3151
|
+
const judged = judgeTyped(field, next);
|
|
3152
|
+
if (judged && "value" in judged) {
|
|
3153
|
+
setRefusal(null);
|
|
3154
|
+
sent.current = judged.value;
|
|
3155
|
+
onChange(judged.value);
|
|
3156
|
+
} else if (judged && "refusal" in judged) {
|
|
3157
|
+
setRefusal(judged.refusal);
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
return /* @__PURE__ */ jsxs13("span", { className: "flex flex-col gap-0.5", children: [
|
|
3161
|
+
/* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-0.5", children: [
|
|
3162
|
+
/* @__PURE__ */ jsx15(
|
|
3163
|
+
BasicInput6,
|
|
3164
|
+
{
|
|
3165
|
+
id,
|
|
3166
|
+
value: text,
|
|
3167
|
+
disabled,
|
|
3168
|
+
autoComplete: "off",
|
|
3169
|
+
"aria-invalid": refusal2 ? true : void 0,
|
|
3170
|
+
placeholder: helper === "time" ? "2:30 PM" : helper === "datetime" ? `${typedDateText2("2026-10-03")} 2:30 PM` : helper ? typedDateText2("2026-10-03") : void 0,
|
|
3171
|
+
onChange: (e) => retype(e.target.value),
|
|
3172
|
+
onBlur: () => {
|
|
3173
|
+
if (!refusal2) return;
|
|
3174
|
+
setText(openingText(field, sent.current));
|
|
3175
|
+
setRefusal(null);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
),
|
|
3179
|
+
helper && !disabled ? /* @__PURE__ */ jsx15(TypedDateHelper, { kind: helper, text, onText: retype, label: fieldName(field) }) : null
|
|
3180
|
+
] }),
|
|
3181
|
+
refusal2 ? /* @__PURE__ */ jsx15("span", { "data-records-typed-refusal": "", role: "status", className: "text-[11px] leading-snug text-amber-700 dark:text-amber-400", children: refusal2 }) : null
|
|
3182
|
+
] });
|
|
3183
|
+
}
|
|
3184
|
+
function CheckboxControl({ field, value, onChange, disabled, id }) {
|
|
3185
|
+
const unanswered = value === null || value === void 0;
|
|
3186
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2", children: [
|
|
3187
|
+
/* @__PURE__ */ jsx15(
|
|
3188
|
+
Checkbox,
|
|
3189
|
+
{
|
|
3190
|
+
id,
|
|
3191
|
+
checked: value === true,
|
|
3192
|
+
disabled,
|
|
3193
|
+
"aria-label": fieldName(field),
|
|
3194
|
+
onCheckedChange: (next) => onChange(next === true)
|
|
3195
|
+
}
|
|
3196
|
+
),
|
|
3197
|
+
unanswered ? /* @__PURE__ */ jsx15("span", { className: "text-xs italic text-muted-foreground", children: "not answered yet" }) : /* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
3198
|
+
/* @__PURE__ */ jsx15("span", { className: "text-xs text-muted-foreground", children: value === true ? "Yes" : "No" }),
|
|
3199
|
+
field.required ? null : /* @__PURE__ */ jsx15(
|
|
3200
|
+
Button8,
|
|
3201
|
+
{
|
|
3202
|
+
size: "sm",
|
|
3203
|
+
variant: "ghost",
|
|
3204
|
+
className: "h-5 px-1 text-[10px]",
|
|
3205
|
+
disabled,
|
|
3206
|
+
onClick: () => onChange(null),
|
|
3207
|
+
children: "Clear"
|
|
3208
|
+
}
|
|
3209
|
+
)
|
|
3210
|
+
] })
|
|
3211
|
+
] });
|
|
3212
|
+
}
|
|
3213
|
+
function JsonControl({ field, value, onChange, disabled, id }) {
|
|
3214
|
+
const initial = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
3215
|
+
const [text, setText] = useState13(initial);
|
|
3216
|
+
const [broken, setBroken] = useState13(null);
|
|
3217
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-1", children: [
|
|
3218
|
+
/* @__PURE__ */ jsx15(
|
|
3219
|
+
BasicTextarea2,
|
|
3220
|
+
{
|
|
3221
|
+
id,
|
|
3222
|
+
rows: 4,
|
|
3223
|
+
className: "font-mono text-[11px]",
|
|
3224
|
+
value: text,
|
|
3225
|
+
disabled,
|
|
3226
|
+
onChange: (e) => {
|
|
3227
|
+
const next = e.target.value;
|
|
3228
|
+
setText(next);
|
|
3229
|
+
if (next.trim() === "") {
|
|
3230
|
+
setBroken(null);
|
|
3231
|
+
onChange(null);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
try {
|
|
3235
|
+
onChange(JSON.parse(next));
|
|
3236
|
+
setBroken(null);
|
|
3237
|
+
} catch (thrown) {
|
|
3238
|
+
setBroken(
|
|
3239
|
+
refusalFromThrown(
|
|
3240
|
+
thrown,
|
|
3241
|
+
`${fieldName(field)} is not readable as structured data yet.`,
|
|
3242
|
+
"Fix the text so it reads as structured data, or discard it to put back what was there.",
|
|
3243
|
+
"invalid_argument"
|
|
3244
|
+
)
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
),
|
|
3250
|
+
broken ? (
|
|
3251
|
+
// The typed text is still in the box: the notice owns the two doors out.
|
|
3252
|
+
/* @__PURE__ */ jsx15(
|
|
3253
|
+
RefusalNotice,
|
|
3254
|
+
{
|
|
3255
|
+
error: broken,
|
|
3256
|
+
onKeepEditing: () => id ? document.getElementById(id)?.focus() : void 0,
|
|
3257
|
+
onDiscard: () => {
|
|
3258
|
+
setText(initial);
|
|
3259
|
+
setBroken(null);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
)
|
|
3263
|
+
) : null
|
|
3264
|
+
] });
|
|
3265
|
+
}
|
|
3266
|
+
function inTheStoresOrder(options) {
|
|
3267
|
+
const position = (o) => {
|
|
3268
|
+
const raw = o.metadata?.["option_position"];
|
|
3269
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : Number.NaN;
|
|
3270
|
+
return Number.isFinite(n) ? n : Number.POSITIVE_INFINITY;
|
|
3271
|
+
};
|
|
3272
|
+
const made = (o) => String(o.created_at ?? "");
|
|
3273
|
+
return [...options].sort((a, b) => position(a) - position(b) || made(a).localeCompare(made(b)));
|
|
3274
|
+
}
|
|
3275
|
+
function OptionControl({ field, value, onChange, disabled, id, autoOpen, onClose, addsChoices, initialSearch, asForm }) {
|
|
3276
|
+
const client = useOptionalRecordsClient6();
|
|
3277
|
+
const [options, setOptions] = useState13(null);
|
|
3278
|
+
const [refusal2, setRefusal] = useState13(null);
|
|
3279
|
+
const [reread, setReread] = useState13(0);
|
|
3280
|
+
useEffect10(() => {
|
|
3281
|
+
if (!client) return;
|
|
3282
|
+
let cancelled = false;
|
|
3283
|
+
void client.fieldOptions({ field_id: field.id }).then((result) => {
|
|
3284
|
+
if (cancelled) return;
|
|
3285
|
+
if (result.ok) setOptions(inTheStoresOrder(result.data ?? []));
|
|
3286
|
+
else setRefusal(result.error);
|
|
3287
|
+
});
|
|
3288
|
+
return () => {
|
|
3289
|
+
cancelled = true;
|
|
3290
|
+
};
|
|
3291
|
+
}, [client, field.id, reread]);
|
|
3292
|
+
const handed = publicChoicesOf(field);
|
|
3293
|
+
if (!client && handed.length > 0) {
|
|
3294
|
+
return /* @__PURE__ */ jsx15(ChoiceAnswer, { field, choices: handed, value, onChange, disabled, id });
|
|
3295
|
+
}
|
|
3296
|
+
if (!client) {
|
|
3297
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col gap-1", children: [
|
|
3298
|
+
/* @__PURE__ */ jsx15(
|
|
3299
|
+
BasicInput6,
|
|
3300
|
+
{
|
|
3301
|
+
id,
|
|
3302
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
3303
|
+
disabled,
|
|
3304
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
|
|
3305
|
+
}
|
|
3306
|
+
),
|
|
3307
|
+
/* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "This is normally picked from a list. A page answered without an account cannot read that list, so type your answer and whoever receives it will match it up." })
|
|
3308
|
+
] });
|
|
3309
|
+
}
|
|
3310
|
+
if (refusal2)
|
|
3311
|
+
return /* @__PURE__ */ jsx15("div", { id, children: /* @__PURE__ */ jsx15(RefusalLine, { error: refusal2 }) });
|
|
3312
|
+
if (!options) return /* @__PURE__ */ jsx15("p", { id, className: "text-xs text-muted-foreground", children: "Reading this field's choices\u2026" });
|
|
3313
|
+
if (asForm && options.length > 0) {
|
|
3314
|
+
const titled = (option) => recordName(option.data, null, "Unnamed choice");
|
|
3315
|
+
return /* @__PURE__ */ jsx15(
|
|
3316
|
+
ChoiceAnswer,
|
|
3317
|
+
{
|
|
3318
|
+
field,
|
|
3319
|
+
choices: options.map((option) => ({ key: optionKey2(option), label: titled(option) })),
|
|
3320
|
+
value,
|
|
3321
|
+
onChange,
|
|
3322
|
+
disabled,
|
|
3323
|
+
id,
|
|
3324
|
+
isChosen: (choice, v) => {
|
|
3325
|
+
const option = options.find((o) => optionKey2(o) === choice.key);
|
|
3326
|
+
return option ? isTheChosen(option, v) : false;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
if (CHOICE_EDITOR_ADDS_WHAT_IS_TYPED) {
|
|
3332
|
+
return /* @__PURE__ */ jsx15(
|
|
3333
|
+
OpenChoicePicker,
|
|
3334
|
+
{
|
|
3335
|
+
field,
|
|
3336
|
+
options,
|
|
3337
|
+
value,
|
|
3338
|
+
onChange,
|
|
3339
|
+
disabled,
|
|
3340
|
+
id,
|
|
3341
|
+
autoOpen,
|
|
3342
|
+
onClose,
|
|
3343
|
+
initialSearch,
|
|
3344
|
+
addsChoices,
|
|
3345
|
+
allowOther: allowsOtherValues(field),
|
|
3346
|
+
onChoicesChanged: () => setReread((n) => n + 1)
|
|
3347
|
+
}
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
if (options.length === 0) {
|
|
3351
|
+
return /* @__PURE__ */ jsx15("p", { id, className: "text-xs text-muted-foreground", children: field.options_table_id ? `${fieldName(field)} takes its choices from a Table that has no records yet \u2014 add the choices there and they appear here.` : `${fieldName(field)} does not name the Table its choices come from yet, so there is nothing to choose. A table admin sets it in the field editor.` });
|
|
3352
|
+
}
|
|
3353
|
+
const titleOf = (option) => recordName(option.data, null, "Unnamed choice");
|
|
3354
|
+
if (field.multi) {
|
|
3355
|
+
const chosen = options.filter((option) => choiceValuesOf(value).some((v) => isTheChosen(option, v)));
|
|
3356
|
+
const picked = new Set(chosen.map((option) => optionKey2(option)));
|
|
3357
|
+
const toggles = /* @__PURE__ */ jsx15("div", { id: autoOpen ? void 0 : id, className: "flex flex-wrap gap-1", children: options.map((option) => /* @__PURE__ */ jsx15(
|
|
3358
|
+
Button8,
|
|
3359
|
+
{
|
|
3360
|
+
type: "button",
|
|
3361
|
+
size: "sm",
|
|
3362
|
+
variant: picked.has(optionKey2(option)) ? "default" : "outline",
|
|
3363
|
+
disabled,
|
|
3364
|
+
onClick: () => {
|
|
3365
|
+
const next = new Set(picked);
|
|
3366
|
+
if (next.has(optionKey2(option))) next.delete(optionKey2(option));
|
|
3367
|
+
else next.add(optionKey2(option));
|
|
3368
|
+
onChange(next.size === 0 ? null : [...next]);
|
|
3369
|
+
},
|
|
3370
|
+
children: titleOf(option)
|
|
3371
|
+
},
|
|
3372
|
+
option.id
|
|
3373
|
+
)) });
|
|
3374
|
+
if (!autoOpen) return toggles;
|
|
3375
|
+
return /* @__PURE__ */ jsxs13(
|
|
3376
|
+
Popover4,
|
|
3377
|
+
{
|
|
3378
|
+
defaultOpen: true,
|
|
3379
|
+
onOpenChange: (open) => {
|
|
3380
|
+
if (!open) onClose?.();
|
|
3381
|
+
},
|
|
3382
|
+
children: [
|
|
3383
|
+
/* @__PURE__ */ jsx15(PopoverTrigger4, { asChild: true, children: /* @__PURE__ */ jsx15(Button8, { id, type: "button", size: "sm", variant: "outline", className: "h-6 max-w-full truncate", children: chosen.length === 0 ? "Choose" : chosen.map(titleOf).join(", ") }) }),
|
|
3384
|
+
/* @__PURE__ */ jsx15(PopoverContent4, { sizing: "content", className: "max-w-[20rem] p-2", align: "start", children: toggles })
|
|
3385
|
+
]
|
|
3386
|
+
}
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
const current = options.find((option) => isTheChosen(option, value));
|
|
3390
|
+
return /* @__PURE__ */ jsxs13(
|
|
3391
|
+
Select,
|
|
3392
|
+
{
|
|
3393
|
+
value: current ? optionKey2(current) : "",
|
|
3394
|
+
disabled: disabled ?? false,
|
|
3395
|
+
onValueChange: (next) => onChange(next === "" ? null : next),
|
|
3396
|
+
...autoOpen ? {
|
|
3397
|
+
// IN A GRID CELL the list opens at once and a pick is the commit (Airtable).
|
|
3398
|
+
defaultOpen: true,
|
|
3399
|
+
onOpenChange: (open) => {
|
|
3400
|
+
if (!open) onClose?.();
|
|
3401
|
+
}
|
|
3402
|
+
} : {},
|
|
3403
|
+
children: [
|
|
3404
|
+
/* @__PURE__ */ jsx15(SelectTrigger, { id, size: "sm", children: /* @__PURE__ */ jsx15(SelectValue, { placeholder: "Choose" }) }),
|
|
3405
|
+
/* @__PURE__ */ jsx15(SelectContent, { children: options.map((option) => /* @__PURE__ */ jsx15(SelectItem, { value: optionKey2(option), children: titleOf(option) }, option.id)) })
|
|
3406
|
+
]
|
|
3407
|
+
}
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
function OpenChoicePicker({
|
|
3411
|
+
field,
|
|
3412
|
+
options,
|
|
3413
|
+
value,
|
|
3414
|
+
onChange,
|
|
3415
|
+
disabled,
|
|
3416
|
+
id,
|
|
3417
|
+
addsChoices,
|
|
3418
|
+
autoOpen,
|
|
3419
|
+
onClose,
|
|
3420
|
+
initialSearch,
|
|
3421
|
+
allowOther = true,
|
|
3422
|
+
onChoicesChanged
|
|
3423
|
+
}) {
|
|
3424
|
+
const client = useOptionalRecordsClient6();
|
|
3425
|
+
const nudge = useChoiceNudge();
|
|
3426
|
+
const [open, setOpenState] = useState13(autoOpen === true);
|
|
3427
|
+
const onCloseRef = useRef9(onClose);
|
|
3428
|
+
onCloseRef.current = onClose;
|
|
3429
|
+
const setOpen = useCallback4((next) => {
|
|
3430
|
+
setOpenState(next);
|
|
3431
|
+
if (!next) onCloseRef.current?.();
|
|
3432
|
+
}, []);
|
|
3433
|
+
const [search, setSearch] = useState13(initialSearch ?? "");
|
|
3434
|
+
const [adding, setAdding] = useState13(false);
|
|
3435
|
+
const [addRefused, setAddRefused] = useState13(null);
|
|
3436
|
+
const titleOf = (option) => recordName(option.data, null, "Unnamed choice");
|
|
3437
|
+
const held = withoutAddition(value);
|
|
3438
|
+
const current = choiceValuesOf(held.value);
|
|
3439
|
+
const pickedWords = current.map((v) => {
|
|
3440
|
+
const option = options.find((o) => isTheChosen(o, v));
|
|
3441
|
+
return option ? titleOf(option) : String(v);
|
|
3442
|
+
});
|
|
3443
|
+
const chipOf = (option, word) => option ? choiceChipLookOf(field, optionKey2(option), titleOf(option)) : choiceChipLookOf(field, word);
|
|
3444
|
+
const q = search.trim();
|
|
3445
|
+
const visible = rankTypedMatches3(options, q, titleOf);
|
|
3446
|
+
const decided = decideTypedChoice({ words: q, options, allowOther, nudge: nudge ?? "ask" });
|
|
3447
|
+
const choose = (word, addingIt = false) => {
|
|
3448
|
+
const pending = held.add.filter((w) => w.toLowerCase() !== word.toLowerCase());
|
|
3449
|
+
let next;
|
|
3450
|
+
let add2 = [...pending];
|
|
3451
|
+
if (field.multi) {
|
|
3452
|
+
const existing = current.map((v) => {
|
|
3453
|
+
const option = options.find((o) => isTheChosen(o, v));
|
|
3454
|
+
return option ? optionKey2(option) : String(v);
|
|
3455
|
+
});
|
|
3456
|
+
const has = existing.some((v) => v.toLowerCase() === word.toLowerCase());
|
|
3457
|
+
const list = has ? existing.filter((v) => v.toLowerCase() !== word.toLowerCase()) : [...existing, word];
|
|
3458
|
+
if (addingIt && !has) add2.push(word);
|
|
3459
|
+
add2 = add2.filter((w) => list.some((v) => v.toLowerCase() === w.toLowerCase()));
|
|
3460
|
+
next = list.length === 0 ? null : list;
|
|
3461
|
+
} else {
|
|
3462
|
+
add2 = addingIt ? [word] : [];
|
|
3463
|
+
next = word;
|
|
3464
|
+
}
|
|
3465
|
+
onChange(add2.length > 0 ? choiceAddition(next, add2) : next);
|
|
3466
|
+
setSearch("");
|
|
3467
|
+
if (!field.multi) setOpen(false);
|
|
3468
|
+
};
|
|
3469
|
+
const addTyped = () => {
|
|
3470
|
+
setAddRefused(null);
|
|
3471
|
+
if (addsChoices || !client) {
|
|
3472
|
+
choose(q, Boolean(addsChoices));
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
setAdding(true);
|
|
3476
|
+
void client.fieldUpdate({ field_id: field.id, patch: { options_add: [q] } }).then((result) => {
|
|
3477
|
+
setAdding(false);
|
|
3478
|
+
if (!result.ok) {
|
|
3479
|
+
setAddRefused(result.error);
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
onChoicesChanged();
|
|
3483
|
+
choose(q);
|
|
3484
|
+
});
|
|
3485
|
+
};
|
|
3486
|
+
const offList = q !== "" && decided.decision !== "chosen" && decided.decision !== "empty";
|
|
3487
|
+
const foot = offList && decided.decision === "ask" ? /* @__PURE__ */ jsx15(
|
|
3488
|
+
ChoiceAskButtons,
|
|
3489
|
+
{
|
|
3490
|
+
field,
|
|
3491
|
+
words: [q],
|
|
3492
|
+
keepOffered: decided.keepOffered,
|
|
3493
|
+
busy: adding,
|
|
3494
|
+
onAdd: addTyped,
|
|
3495
|
+
onKeep: () => choose(q),
|
|
3496
|
+
onCancel: () => {
|
|
3497
|
+
setSearch("");
|
|
3498
|
+
setAddRefused(null);
|
|
3499
|
+
if (autoOpen) setOpen(false);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
) : null;
|
|
3503
|
+
const add = offList && decided.decision === "add" ? { label: `Add \u201C${q}\u201D to the choices`, onAdd: addTyped, disabled: adding } : offList && decided.decision === "keep_as_typed" ? { label: `Keep \u201C${q}\u201D as typed`, onAdd: () => choose(q) } : null;
|
|
3504
|
+
return /* @__PURE__ */ jsxs13("div", { id, className: "flex flex-wrap items-center gap-1", children: [
|
|
3505
|
+
pickedWords.map((word, i) => {
|
|
3506
|
+
const chip = chipOf(options.find((o) => isTheChosen(o, current[i])), word);
|
|
3507
|
+
return /* @__PURE__ */ jsx15(Badge3, { variant: "outline", className: cn11("text-[11px]", chip.className), "data-records-choice-chip": "", children: word }, word);
|
|
3508
|
+
}),
|
|
3509
|
+
disabled ? null : (
|
|
3510
|
+
// THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the relation picker draws the same one.
|
|
3511
|
+
/* @__PURE__ */ jsx15(
|
|
3512
|
+
PickOrAdd,
|
|
3513
|
+
{
|
|
3514
|
+
triggerLabel: `Pick for ${fieldName(field)}`,
|
|
3515
|
+
triggerText: pickedWords.length === 0 ? "Choose" : "Change",
|
|
3516
|
+
open,
|
|
3517
|
+
onOpenChange: setOpen,
|
|
3518
|
+
search,
|
|
3519
|
+
onSearch: (next) => {
|
|
3520
|
+
setSearch(next);
|
|
3521
|
+
setAddRefused(null);
|
|
3522
|
+
},
|
|
3523
|
+
items: visible.map((option) => ({
|
|
3524
|
+
id: option.id,
|
|
3525
|
+
label: titleOf(option),
|
|
3526
|
+
chip: chipOf(option, titleOf(option)).className,
|
|
3527
|
+
picked: current.some((v) => isTheChosen(option, v))
|
|
3528
|
+
})),
|
|
3529
|
+
onPick: (optionId) => {
|
|
3530
|
+
const option = options.find((o) => o.id === optionId);
|
|
3531
|
+
if (option) choose(optionKey2(option));
|
|
3532
|
+
},
|
|
3533
|
+
empty: q === "" ? /* @__PURE__ */ jsx15("p", { className: "p-2 text-xs text-muted-foreground", children: "No choices yet \u2014 type one to add it." }) : null,
|
|
3534
|
+
add,
|
|
3535
|
+
foot,
|
|
3536
|
+
...offList && decided.decision === "ask" && !adding ? { onEnterWithNoMatch: addTyped } : {},
|
|
3537
|
+
footNote: addRefused ? /* @__PURE__ */ jsx15(RefusalLine, { error: addRefused }) : offList && decided.decision === "not_a_choice" ? (
|
|
3538
|
+
// never_add on a column that takes only its choices: said, never a silent nothing.
|
|
3539
|
+
/* @__PURE__ */ jsxs13(Fragment4, { children: [
|
|
3540
|
+
/* @__PURE__ */ jsx15("p", { "data-records-typed-refusal": "", className: "p-2 text-xs text-muted-foreground", children: notAChoiceSentence(field, [q]) }),
|
|
3541
|
+
autoOpen ? /* @__PURE__ */ jsx15("span", { "data-records-choice-ask": "", className: "px-2 pb-1", children: /* @__PURE__ */ jsx15(Button8, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => setOpen(false), children: "Cancel" }) }) : null
|
|
3542
|
+
] })
|
|
3543
|
+
) : null
|
|
3544
|
+
}
|
|
3545
|
+
)
|
|
3546
|
+
)
|
|
3547
|
+
] });
|
|
3548
|
+
}
|
|
3549
|
+
function RecordChip({
|
|
3550
|
+
title,
|
|
3551
|
+
onRemove,
|
|
3552
|
+
className
|
|
3553
|
+
}) {
|
|
3554
|
+
return (
|
|
3555
|
+
// A chip carries a RECORD'S OWN TITLE, which nobody here chose the length of —
|
|
3556
|
+
// "Marisol Okonkwo — 418 Calle Puerto Vallarta, Camarillo" is an ordinary customer
|
|
3557
|
+
// name. So it caps at its container and ends in an ellipsis with the whole name in
|
|
3558
|
+
// its tooltip, rather than being cut mid-word by whatever box it landed in
|
|
3559
|
+
// (lane FIX-14, 2026-09-22).
|
|
3560
|
+
/* @__PURE__ */ jsxs13(Badge3, { variant: "secondary", className: cn11("max-w-full gap-1 text-[11px]", className), title, children: [
|
|
3561
|
+
/* @__PURE__ */ jsx15("span", { className: "min-w-0 truncate", children: title }),
|
|
3562
|
+
onRemove ? /* @__PURE__ */ jsx15("button", { type: "button", onClick: onRemove, "aria-label": `Remove ${title}`, className: "shrink-0 opacity-60 hover:opacity-100", children: "\xD7" }) : null
|
|
3563
|
+
] })
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
function publicChoicesOf(field) {
|
|
3567
|
+
const raw = field.public_choices;
|
|
3568
|
+
if (!Array.isArray(raw)) return [];
|
|
3569
|
+
return raw.flatMap((c) => {
|
|
3570
|
+
const o = c;
|
|
3571
|
+
return typeof o.key === "string" && typeof o.label === "string" ? [{ key: o.key, label: o.label }] : [];
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
var FORM_PILL_LIMIT = 4;
|
|
3575
|
+
function ChoiceAnswer({
|
|
3576
|
+
field,
|
|
3577
|
+
choices,
|
|
3578
|
+
value,
|
|
3579
|
+
onChange,
|
|
3580
|
+
disabled,
|
|
3581
|
+
id,
|
|
3582
|
+
isChosen
|
|
3583
|
+
}) {
|
|
3584
|
+
const [search, setSearch] = useState13("");
|
|
3585
|
+
const held = field.multi ? choiceValuesOf(value) : value === null || value === void 0 || value === "" ? [] : [value];
|
|
3586
|
+
const on = (choice) => held.some((v) => isChosen ? isChosen(choice, v) : String(v) === choice.key || String(v) === choice.label);
|
|
3587
|
+
const press = (choice) => {
|
|
3588
|
+
if (!field.multi) {
|
|
3589
|
+
onChange(on(choice) ? null : choice.key);
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
const keys = choices.filter((c) => c.key === choice.key ? !on(c) : on(c)).map((c) => c.key);
|
|
3593
|
+
onChange(keys.length === 0 ? null : keys);
|
|
3594
|
+
};
|
|
3595
|
+
const pill = (choice) => {
|
|
3596
|
+
const chosen = on(choice);
|
|
3597
|
+
const look = choiceChipLookOf(field, choice.key, choice.label);
|
|
3598
|
+
return /* @__PURE__ */ jsxs13(
|
|
3599
|
+
"button",
|
|
3600
|
+
{
|
|
3601
|
+
type: "button",
|
|
3602
|
+
role: field.multi ? "checkbox" : "radio",
|
|
3603
|
+
"aria-checked": chosen,
|
|
3604
|
+
disabled,
|
|
3605
|
+
onClick: () => press(choice),
|
|
3606
|
+
"data-records-choice-chip": "",
|
|
3607
|
+
"data-records-choice-answer": choice.key,
|
|
3608
|
+
className: cn11(
|
|
3609
|
+
"inline-flex min-h-11 max-w-full items-center gap-1 rounded-md border px-3 text-sm font-medium sm:min-h-8 sm:px-2.5 sm:text-xs",
|
|
3610
|
+
look.className,
|
|
3611
|
+
chosen ? "ring-2 ring-ring ring-offset-1" : "opacity-80 hover:opacity-100"
|
|
3612
|
+
),
|
|
3613
|
+
children: [
|
|
3614
|
+
chosen ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", children: "\u2713" }) : null,
|
|
3615
|
+
/* @__PURE__ */ jsx15("span", { className: "truncate", children: choice.label })
|
|
3616
|
+
]
|
|
3617
|
+
},
|
|
3618
|
+
choice.key
|
|
3619
|
+
);
|
|
3620
|
+
};
|
|
3621
|
+
const group = { role: field.multi ? "group" : "radiogroup", "aria-label": fieldName(field) };
|
|
3622
|
+
if (choices.length <= FORM_PILL_LIMIT) {
|
|
3623
|
+
return /* @__PURE__ */ jsx15("div", { id, className: "flex flex-wrap gap-1.5", ...group, children: choices.map(pill) });
|
|
3624
|
+
}
|
|
3625
|
+
const q = search.trim().toLowerCase();
|
|
3626
|
+
const shown2 = q === "" ? choices : choices.filter((c) => c.label.toLowerCase().includes(q));
|
|
3627
|
+
return /* @__PURE__ */ jsxs13("div", { id, className: "flex flex-col gap-1.5", children: [
|
|
3628
|
+
/* @__PURE__ */ jsx15(
|
|
3629
|
+
BasicInput6,
|
|
3630
|
+
{
|
|
3631
|
+
value: search,
|
|
3632
|
+
onChange: (e) => setSearch(e.target.value),
|
|
3633
|
+
placeholder: "Search",
|
|
3634
|
+
"aria-label": `Search \u2014 ${fieldName(field)}`,
|
|
3635
|
+
autoComplete: "off",
|
|
3636
|
+
className: "h-11 text-base sm:h-8 sm:text-xs"
|
|
3637
|
+
}
|
|
3638
|
+
),
|
|
3639
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex max-h-56 flex-wrap content-start gap-1.5 overflow-y-auto", ...group, children: [
|
|
3640
|
+
shown2.map(pill),
|
|
3641
|
+
shown2.length === 0 ? /* @__PURE__ */ jsx15("p", { className: "text-xs text-muted-foreground", children: "No match" }) : null
|
|
3642
|
+
] })
|
|
3643
|
+
] });
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
// src/RecordsMount.tsx
|
|
3647
|
+
import { useRef as useRef10 } from "react";
|
|
3648
|
+
import { RecordsProvider } from "@ai-matrx/records/react";
|
|
3649
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
3650
|
+
function personActor(userId) {
|
|
3651
|
+
return userId ? { actor: "user", user_id: userId } : { actor: "user" };
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// src/tablePicking.tsx
|
|
3655
|
+
import { useEffect as useEffect11, useMemo as useMemo6, useState as useState14 } from "react";
|
|
3656
|
+
import { withTablePlacement } from "@ai-matrx/records";
|
|
3657
|
+
import { useRecordsClient as useRecordsClient2, useTables } from "@ai-matrx/records/react";
|
|
3658
|
+
import { Checkbox as Checkbox2, cn as cn12 } from "@ai-matrx/design-system";
|
|
3659
|
+
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3660
|
+
var APP_TABLE_PREFIX = "records_ui_";
|
|
3661
|
+
function isValueSet(table) {
|
|
3662
|
+
if (table.kept_for === "choices") return true;
|
|
3663
|
+
return table.kept_for == null && table.kind === "list" && table.kept_by_the_app === true;
|
|
3664
|
+
}
|
|
3665
|
+
function isKeptTable(table) {
|
|
3666
|
+
return table.kept_by_the_app === true || isValueSet(table) || Boolean(table.work_kind) || (table.slug ?? "").startsWith(APP_TABLE_PREFIX);
|
|
3667
|
+
}
|
|
3668
|
+
var CHOICES_SUFFIX = / choices$/i;
|
|
3669
|
+
function tablePickerEntries(tables, options = {}) {
|
|
3670
|
+
const byId = new Map(tables.map((t) => [t.id, t]));
|
|
3671
|
+
const lists = tables.filter(isValueSet);
|
|
3672
|
+
const listLabel = (t) => {
|
|
3673
|
+
const column = t.name.replace(CHOICES_SUFFIX, "");
|
|
3674
|
+
if (!t.used_in_table_id || t.used_in_table_id === t.id) {
|
|
3675
|
+
const made = madeOn(t.created_at);
|
|
3676
|
+
return made ? `${column} \u2014 no column \xB7 ${made}` : `${column} \u2014 no column`;
|
|
3677
|
+
}
|
|
3678
|
+
const owner = byId.get(t.used_in_table_id);
|
|
3679
|
+
if (!owner) return t.name;
|
|
3680
|
+
return `${column} \u2014 ${owner.name}`;
|
|
3681
|
+
};
|
|
3682
|
+
const plain = tables.filter((t) => !isKeptTable(t) || t.id === options.keep && !isValueSet(t)).map((t) => ({ table: t, id: t.id, label: t.name, isList: false })).sort((a, b) => a.label.localeCompare(b.label));
|
|
3683
|
+
const shownLists = lists.filter((t) => options.showLists || t.id === options.keep).map((t) => ({ table: t, id: t.id, label: listLabel(t), isList: true })).sort((a, b) => a.label.localeCompare(b.label));
|
|
3684
|
+
const entries = [...plain, ...shownLists];
|
|
3685
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3686
|
+
for (const entry of entries) {
|
|
3687
|
+
const key = entry.label.toLowerCase();
|
|
3688
|
+
const n = (seen.get(key) ?? 0) + 1;
|
|
3689
|
+
seen.set(key, n);
|
|
3690
|
+
if (n > 1) entry.label = `${entry.label} (${n})`;
|
|
3691
|
+
}
|
|
3692
|
+
return { entries, listCount: lists.length };
|
|
3693
|
+
}
|
|
3694
|
+
function madeOn(stamp) {
|
|
3695
|
+
if (!stamp) return null;
|
|
3696
|
+
const at = new Date(stamp);
|
|
3697
|
+
if (Number.isNaN(at.getTime())) return null;
|
|
3698
|
+
return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric", year: "numeric" }).format(at);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
// src/TablePickers.tsx
|
|
3702
|
+
import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3703
|
+
function useTablesAnywhere(dataSource) {
|
|
3704
|
+
const [state, setState] = useState15({ loading: true, error: null, rows: [] });
|
|
3705
|
+
useEffect12(() => {
|
|
3706
|
+
let live = true;
|
|
3707
|
+
void Promise.resolve(dataSource.rpc("data_home_tables", {}, { schema: "custom" })).then(
|
|
3708
|
+
({ data, error }) => {
|
|
3709
|
+
if (!live) return;
|
|
3710
|
+
if (error) {
|
|
3711
|
+
setState({ loading: false, error: refusalSentence({ message: String(error["message"] ?? "") }) || "Tables could not be read.", rows: [] });
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
const rows = (Array.isArray(data) ? data : []).slice();
|
|
3715
|
+
rows.sort((a, b) => a.table_name.localeCompare(b.table_name));
|
|
3716
|
+
setState({ loading: false, error: null, rows });
|
|
3717
|
+
},
|
|
3718
|
+
(thrown) => {
|
|
3719
|
+
if (live) setState({ loading: false, error: String(thrown?.message ?? thrown), rows: [] });
|
|
3720
|
+
}
|
|
3721
|
+
);
|
|
3722
|
+
return () => {
|
|
3723
|
+
live = false;
|
|
3724
|
+
};
|
|
3725
|
+
}, [dataSource]);
|
|
3726
|
+
return state;
|
|
3727
|
+
}
|
|
3728
|
+
function TablePicker({ dataSource, value, onChange, disabled, tables }) {
|
|
3729
|
+
const own3 = useTablesAnywhere(dataSource);
|
|
3730
|
+
const list = tables ?? own3;
|
|
3731
|
+
const options = tablePickerEntries(
|
|
3732
|
+
list.rows.map((t) => ({ ...t, id: t.table_id, name: t.table_name })),
|
|
3733
|
+
{ keep: value }
|
|
3734
|
+
).entries.map(({ table: t }) => ({
|
|
3735
|
+
value: t.table_id,
|
|
3736
|
+
// The organization hint already tells two same-named tables apart across organizations.
|
|
3737
|
+
label: t.table_name,
|
|
3738
|
+
hint: t.organization_name,
|
|
3739
|
+
keywords: t.organization_name
|
|
3740
|
+
}));
|
|
3741
|
+
const missing = Boolean(value) && !list.loading && !list.error && !list.rows.some((t) => t.table_id === value);
|
|
3742
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1", children: [
|
|
3743
|
+
/* @__PURE__ */ jsx18(
|
|
3744
|
+
CreatablePicker,
|
|
3745
|
+
{
|
|
3746
|
+
value: value || null,
|
|
3747
|
+
options,
|
|
3748
|
+
onSelect: onChange,
|
|
3749
|
+
placeholder: missing ? "Table not available" : "Choose a table",
|
|
3750
|
+
searchPlaceholder: "Search tables",
|
|
3751
|
+
noun: "table",
|
|
3752
|
+
loading: list.loading,
|
|
3753
|
+
...disabled ? { disabled: true } : {},
|
|
3754
|
+
ariaLabel: "Table"
|
|
3755
|
+
}
|
|
3756
|
+
),
|
|
3757
|
+
list.error && /* @__PURE__ */ jsx18(RefusalLine, { error: refusal("door", list.error, "Try again in a moment.") }),
|
|
3758
|
+
missing && /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-destructive", children: "This table is archived or not shared with you." })
|
|
3759
|
+
] });
|
|
3760
|
+
}
|
|
3761
|
+
function TableScope({ dataSource, userId, tableId, children, fallback = null, tables }) {
|
|
3762
|
+
const own3 = useTablesAnywhere(dataSource);
|
|
3763
|
+
const list = tables ?? own3;
|
|
3764
|
+
const organizationId = list.rows.find((t) => t.table_id === tableId)?.organization_id ?? null;
|
|
3765
|
+
if (!tableId || !organizationId) return /* @__PURE__ */ jsx18(Fragment5, { children: fallback });
|
|
3766
|
+
return /* @__PURE__ */ jsx18(RecordsProvider2, { config: { dataSource, actor: personActor(userId), organizationId }, children });
|
|
3767
|
+
}
|
|
3768
|
+
function ColumnValuesEditor({ tableId, value, onChange, disabled, addLabel }) {
|
|
3769
|
+
const fields = useFields(tableId);
|
|
3770
|
+
const all = useMemo7(() => fields.data ?? [], [fields.data]);
|
|
3771
|
+
const byKey = useMemo7(() => new Map(all.map((f) => [f.key, f])), [all]);
|
|
3772
|
+
const keys = Object.keys(value);
|
|
3773
|
+
const [adding, setAdding] = useState15(false);
|
|
3774
|
+
const optionsFor = (current) => all.filter((f) => f.key === current || !(f.key in value)).map((f) => ({ value: f.key, label: fieldName(f) }));
|
|
3775
|
+
const rename = (from, to) => {
|
|
3776
|
+
if (from === to) return;
|
|
3777
|
+
const next = {};
|
|
3778
|
+
for (const k of keys) next[k === from ? to : k] = k === from ? null : value[k];
|
|
3779
|
+
onChange(next);
|
|
3780
|
+
};
|
|
3781
|
+
const remove = (key) => {
|
|
3782
|
+
const { [key]: _gone, ...rest } = value;
|
|
3783
|
+
void _gone;
|
|
3784
|
+
onChange(rest);
|
|
3785
|
+
};
|
|
3786
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-2", children: [
|
|
3787
|
+
fields.error && /* @__PURE__ */ jsx18(RefusalLine, { error: fields.error }),
|
|
3788
|
+
keys.map((key) => {
|
|
3789
|
+
const field = byKey.get(key);
|
|
3790
|
+
return /* @__PURE__ */ jsx18(
|
|
3791
|
+
ColumnValueRow,
|
|
3792
|
+
{
|
|
3793
|
+
columnKey: key,
|
|
3794
|
+
field,
|
|
3795
|
+
loading: fields.loading,
|
|
3796
|
+
options: optionsFor(key),
|
|
3797
|
+
value: value[key],
|
|
3798
|
+
disabled,
|
|
3799
|
+
onColumn: (to) => rename(key, to),
|
|
3800
|
+
onValue: (v) => onChange({ ...value, [key]: v }),
|
|
3801
|
+
onRemove: () => remove(key)
|
|
3802
|
+
},
|
|
3803
|
+
key
|
|
3804
|
+
);
|
|
3805
|
+
}),
|
|
3806
|
+
adding && /* @__PURE__ */ jsx18(
|
|
3807
|
+
ColumnValueRow,
|
|
3808
|
+
{
|
|
3809
|
+
columnKey: null,
|
|
3810
|
+
field: void 0,
|
|
3811
|
+
loading: fields.loading,
|
|
3812
|
+
options: optionsFor(null),
|
|
3813
|
+
value: null,
|
|
3814
|
+
disabled,
|
|
3815
|
+
onColumn: (to) => {
|
|
3816
|
+
setAdding(false);
|
|
3817
|
+
onChange({ ...value, [to]: null });
|
|
3818
|
+
},
|
|
3819
|
+
onValue: () => void 0,
|
|
3820
|
+
onRemove: () => setAdding(false)
|
|
3821
|
+
}
|
|
3822
|
+
),
|
|
3823
|
+
!disabled && !adding && keys.length < all.length && /* @__PURE__ */ jsx18(Button9, { type: "button", variant: "outline", size: "sm", className: "self-start", onClick: () => setAdding(true), children: addLabel })
|
|
3824
|
+
] });
|
|
3825
|
+
}
|
|
3826
|
+
function ColumnValueRow({
|
|
3827
|
+
columnKey,
|
|
3828
|
+
field,
|
|
3829
|
+
loading,
|
|
3830
|
+
options,
|
|
3831
|
+
value,
|
|
3832
|
+
disabled,
|
|
3833
|
+
onColumn,
|
|
3834
|
+
onValue,
|
|
3835
|
+
onRemove
|
|
3836
|
+
}) {
|
|
3837
|
+
const orphan = columnKey !== null && !loading && field === void 0;
|
|
3838
|
+
const shown2 = orphan ? [{ value: columnKey, label: columnKey, hint: "Not a column" }, ...options] : options;
|
|
3839
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1 rounded-md border border-border/60 p-2", children: [
|
|
3840
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
|
|
3841
|
+
/* @__PURE__ */ jsx18("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx18(
|
|
3842
|
+
CreatablePicker,
|
|
3843
|
+
{
|
|
3844
|
+
value: columnKey,
|
|
3845
|
+
options: shown2,
|
|
3846
|
+
onSelect: onColumn,
|
|
3847
|
+
placeholder: "Choose a column",
|
|
3848
|
+
searchPlaceholder: "Search columns",
|
|
3849
|
+
noun: "column",
|
|
3850
|
+
loading,
|
|
3851
|
+
...disabled ? { disabled: true } : {},
|
|
3852
|
+
ariaLabel: "Column"
|
|
3853
|
+
}
|
|
3854
|
+
) }),
|
|
3855
|
+
!disabled && /* @__PURE__ */ jsx18(Button9, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove", onClick: onRemove, children: "\u2715" })
|
|
3856
|
+
] }),
|
|
3857
|
+
field ? /* @__PURE__ */ jsx18(FieldControl, { field, value, onChange: onValue, disabled }) : orphan ? /* @__PURE__ */ jsx18("p", { className: "text-[11px] text-muted-foreground", children: value === null || value === void 0 ? "\u2014" : String(value) }) : null
|
|
3858
|
+
] });
|
|
3859
|
+
}
|
|
3860
|
+
function RecordPicker({ tableId, value, onChange, disabled }) {
|
|
3861
|
+
const table = useTable(tableId);
|
|
3862
|
+
const records = useRecords(tableId, { pageSize: 200 });
|
|
3863
|
+
const rows = records.data?.rows ?? [];
|
|
3864
|
+
const options = rows.map((row) => ({
|
|
3865
|
+
value: row.id,
|
|
3866
|
+
label: rowNameIn(table.data ?? null, row)
|
|
3867
|
+
}));
|
|
3868
|
+
const missing = Boolean(value) && !records.loading && !records.error && !rows.some((r) => r.id === value);
|
|
3869
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col gap-1", children: [
|
|
3870
|
+
/* @__PURE__ */ jsx18(
|
|
3871
|
+
CreatablePicker,
|
|
3872
|
+
{
|
|
3873
|
+
value: value || null,
|
|
3874
|
+
options,
|
|
3875
|
+
onSelect: onChange,
|
|
3876
|
+
placeholder: missing ? "Record not in this table" : "Choose a record",
|
|
3877
|
+
searchPlaceholder: "Search records",
|
|
3878
|
+
noun: "record",
|
|
3879
|
+
loading: records.loading,
|
|
3880
|
+
...disabled ? { disabled: true } : {},
|
|
3881
|
+
ariaLabel: "Record"
|
|
3882
|
+
}
|
|
3883
|
+
),
|
|
3884
|
+
records.error && /* @__PURE__ */ jsx18(RefusalLine, { error: records.error })
|
|
3885
|
+
] });
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// src/DataBindingPicker.tsx
|
|
3889
|
+
import { useEffect as useEffect13, useMemo as useMemo8 } from "react";
|
|
3890
|
+
import { isEntityReferenceConfig } from "@ai-matrx/records";
|
|
3891
|
+
import { useFields as useFields2, useTable as useTable2 } from "@ai-matrx/records/react";
|
|
3892
|
+
import { BasicInput as BasicInput7, Button as Button10, CreatablePicker as CreatablePicker2, cn as cn13 } from "@ai-matrx/design-system";
|
|
3893
|
+
import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3894
|
+
var DATA_BINDING_SHAPES = [
|
|
3895
|
+
{ value: "collection", label: "The whole table" },
|
|
3896
|
+
{ value: "reference", label: "One record" },
|
|
3897
|
+
{ value: "value", label: "One field of a record" }
|
|
3898
|
+
];
|
|
3899
|
+
var DATA_BINDING_MISSING = [
|
|
3900
|
+
{ value: "absent", label: "Say it's missing" },
|
|
3901
|
+
{ value: "block", label: "Stop the run" }
|
|
3902
|
+
];
|
|
3903
|
+
function emptyDataBinding() {
|
|
3904
|
+
return {
|
|
3905
|
+
kind: "merge_field",
|
|
3906
|
+
source: "record",
|
|
3907
|
+
semantic_type: "collection",
|
|
3908
|
+
table_id: "",
|
|
3909
|
+
missing: "absent",
|
|
3910
|
+
override_policy: "shown_locked"
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
function isDataBinding(value) {
|
|
3914
|
+
return Boolean(value) && typeof value === "object" && value.kind === "merge_field";
|
|
3915
|
+
}
|
|
3916
|
+
function isCompleteDataBinding(binding) {
|
|
3917
|
+
if (!binding.table_id) return false;
|
|
3918
|
+
if (binding.semantic_type === "collection") return true;
|
|
3919
|
+
if (!binding.record_id) return false;
|
|
3920
|
+
return binding.semantic_type === "value" ? Boolean(binding.field_key) : true;
|
|
3921
|
+
}
|
|
3922
|
+
function bindingPlaceholdersFor(field) {
|
|
3923
|
+
const label = fieldName(field);
|
|
3924
|
+
if (isEntityReferenceConfig(field.config)) {
|
|
3925
|
+
return [
|
|
3926
|
+
{ token: `{${field.key}.name}`, label: `${label} \xB7 name` },
|
|
3927
|
+
{ token: `{${field.key}.id}`, label: `${label} \xB7 id` }
|
|
3928
|
+
];
|
|
3929
|
+
}
|
|
3930
|
+
return [{ token: `{${field.key}}`, label }];
|
|
3931
|
+
}
|
|
3932
|
+
var NAME_LIKE = /(^|_)(name|title|label)$/;
|
|
3933
|
+
var DESCRIPTIVE = /(desc|description|summary|notes?|details?|about|why|purpose|role|category|type|group|status)/;
|
|
3934
|
+
function defaultBindingTemplate(table, fields) {
|
|
3935
|
+
const first = fields[0];
|
|
3936
|
+
if (!first) return "- {name}";
|
|
3937
|
+
const declared = table?.title_field;
|
|
3938
|
+
const title = (declared ? fields.find((f) => f.key === declared || f.id === declared) : void 0) ?? fields.find((f) => NAME_LIKE.test(f.key)) ?? first;
|
|
3939
|
+
const token = (f) => bindingPlaceholdersFor(f)[0]?.token ?? `{${f.key}}`;
|
|
3940
|
+
const rest = fields.filter((f) => f.id !== title.id);
|
|
3941
|
+
const [a, b] = [...rest.filter((f) => DESCRIPTIVE.test(f.key)), ...rest.filter((f) => !DESCRIPTIVE.test(f.key))];
|
|
3942
|
+
if (!a) return `- ${token(title)}`;
|
|
3943
|
+
return b ? `- ${token(title)}: ${token(a)} (${token(b)})` : `- ${token(title)}: ${token(a)}`;
|
|
3944
|
+
}
|
|
3945
|
+
function DataBindingPicker({ dataSource, userId, binding, onChange, disabled, defaultRowLimit = null }) {
|
|
3946
|
+
const tables = useTablesAnywhere(dataSource);
|
|
3947
|
+
const tableId = binding.table_id || null;
|
|
3948
|
+
const selectTable = (id) => {
|
|
3949
|
+
if (id === binding.table_id) return;
|
|
3950
|
+
onChange({
|
|
3951
|
+
kind: "merge_field",
|
|
3952
|
+
source: "record",
|
|
3953
|
+
semantic_type: binding.semantic_type,
|
|
3954
|
+
table_id: id,
|
|
3955
|
+
missing: binding.missing,
|
|
3956
|
+
override_policy: "shown_locked",
|
|
3957
|
+
...binding.semantic_type === "collection" && defaultRowLimit ? { limit: defaultRowLimit } : {}
|
|
3958
|
+
});
|
|
3959
|
+
};
|
|
3960
|
+
return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-2", children: [
|
|
3961
|
+
/* @__PURE__ */ jsx19(TablePicker, { dataSource, value: tableId, onChange: selectTable, disabled: Boolean(disabled), tables }),
|
|
3962
|
+
/* @__PURE__ */ jsx19(TableScope, { dataSource, userId, tableId, tables, children: tableId && /* @__PURE__ */ jsx19(
|
|
3963
|
+
BindingDetails,
|
|
3964
|
+
{
|
|
3965
|
+
tableId,
|
|
3966
|
+
binding,
|
|
3967
|
+
onChange,
|
|
3968
|
+
disabled,
|
|
3969
|
+
defaultRowLimit
|
|
3970
|
+
}
|
|
3971
|
+
) })
|
|
3972
|
+
] });
|
|
3973
|
+
}
|
|
3974
|
+
function BindingDetails({
|
|
3975
|
+
tableId,
|
|
3976
|
+
binding,
|
|
3977
|
+
onChange,
|
|
3978
|
+
disabled,
|
|
3979
|
+
defaultRowLimit
|
|
3980
|
+
}) {
|
|
3981
|
+
const table = useTable2(tableId);
|
|
3982
|
+
const fields = useFields2(tableId);
|
|
3983
|
+
const fieldList = useMemo8(() => fields.data ?? [], [fields.data]);
|
|
3984
|
+
const shape = binding.semantic_type;
|
|
3985
|
+
useEffect13(() => {
|
|
3986
|
+
if (disabled || shape === "value" || binding.transform || fields.loading || fieldList.length === 0) return;
|
|
3987
|
+
onChange({
|
|
3988
|
+
...binding,
|
|
3989
|
+
transform: {
|
|
3990
|
+
name: "list",
|
|
3991
|
+
template: defaultBindingTemplate(table.data ?? null, fieldList),
|
|
3992
|
+
join: "\n",
|
|
3993
|
+
...shape === "collection" && binding.limit !== void 0 ? { max: binding.limit } : {}
|
|
3994
|
+
}
|
|
3995
|
+
});
|
|
3996
|
+
}, [disabled, shape, binding, fields.loading, fieldList, table.data, onChange]);
|
|
3997
|
+
const selectShape = (next) => {
|
|
3998
|
+
if (next === shape) return;
|
|
3999
|
+
const base = {
|
|
4000
|
+
kind: "merge_field",
|
|
4001
|
+
source: "record",
|
|
4002
|
+
semantic_type: next,
|
|
4003
|
+
table_id: binding.table_id,
|
|
4004
|
+
missing: binding.missing,
|
|
4005
|
+
override_policy: "shown_locked"
|
|
4006
|
+
};
|
|
4007
|
+
if (next !== "collection" && binding.record_id) base.record_id = binding.record_id;
|
|
4008
|
+
if (next === "value" && binding.field_key) base.field_key = binding.field_key;
|
|
4009
|
+
if (next === "collection") {
|
|
4010
|
+
const limit = binding.limit ?? defaultRowLimit;
|
|
4011
|
+
if (limit) base.limit = limit;
|
|
4012
|
+
}
|
|
4013
|
+
if (next !== "value" && binding.transform) {
|
|
4014
|
+
base.transform = {
|
|
4015
|
+
name: "list",
|
|
4016
|
+
template: binding.transform.template,
|
|
4017
|
+
...binding.transform.join !== void 0 ? { join: binding.transform.join } : {},
|
|
4018
|
+
...next === "collection" && base.limit !== void 0 ? { max: base.limit } : {}
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
onChange(base);
|
|
4022
|
+
};
|
|
4023
|
+
const fieldOptions = fieldList.map((f) => ({ value: f.key, label: fieldName(f) }));
|
|
4024
|
+
const template = binding.transform?.template ?? "";
|
|
4025
|
+
const setTemplate = (next) => onChange({ ...binding, transform: { join: "\n", ...binding.transform, name: "list", template: next } });
|
|
4026
|
+
return /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-2", children: [
|
|
4027
|
+
/* @__PURE__ */ jsx19("div", { className: "flex flex-wrap gap-1", role: "radiogroup", "aria-label": "What to read", children: DATA_BINDING_SHAPES.map((s) => /* @__PURE__ */ jsx19(
|
|
4028
|
+
Button10,
|
|
4029
|
+
{
|
|
4030
|
+
type: "button",
|
|
4031
|
+
size: "sm",
|
|
4032
|
+
variant: s.value === shape ? "default" : "outline",
|
|
4033
|
+
role: "radio",
|
|
4034
|
+
"aria-checked": s.value === shape,
|
|
4035
|
+
disabled,
|
|
4036
|
+
onClick: () => selectShape(s.value),
|
|
4037
|
+
children: s.label
|
|
4038
|
+
},
|
|
4039
|
+
s.value
|
|
4040
|
+
)) }),
|
|
4041
|
+
shape !== "collection" && /* @__PURE__ */ jsx19(
|
|
4042
|
+
RecordPicker,
|
|
4043
|
+
{
|
|
4044
|
+
tableId,
|
|
4045
|
+
value: binding.record_id ?? null,
|
|
4046
|
+
onChange: (id) => onChange({ ...binding, record_id: id }),
|
|
4047
|
+
disabled: Boolean(disabled)
|
|
4048
|
+
}
|
|
4049
|
+
),
|
|
4050
|
+
shape === "value" && /* @__PURE__ */ jsx19(
|
|
4051
|
+
CreatablePicker2,
|
|
4052
|
+
{
|
|
4053
|
+
value: binding.field_key ?? null,
|
|
4054
|
+
options: fieldOptions,
|
|
4055
|
+
onSelect: (key) => onChange({ ...binding, field_key: key }),
|
|
4056
|
+
placeholder: "Choose a column",
|
|
4057
|
+
searchPlaceholder: "Search columns",
|
|
4058
|
+
noun: "column",
|
|
4059
|
+
loading: fields.loading,
|
|
4060
|
+
...disabled ? { disabled: true } : {},
|
|
4061
|
+
ariaLabel: "Column"
|
|
4062
|
+
}
|
|
4063
|
+
),
|
|
4064
|
+
shape !== "value" && /* @__PURE__ */ jsxs16("div", { className: "flex flex-col gap-1", children: [
|
|
4065
|
+
/* @__PURE__ */ jsx19(
|
|
4066
|
+
"textarea",
|
|
4067
|
+
{
|
|
4068
|
+
className: cn13(
|
|
4069
|
+
"min-h-[3.5rem] w-full rounded-md border border-border bg-background px-2 py-1.5 font-mono text-xs"
|
|
4070
|
+
),
|
|
4071
|
+
"aria-label": "Each row reads as",
|
|
4072
|
+
value: template,
|
|
4073
|
+
disabled,
|
|
4074
|
+
onChange: (e) => setTemplate(e.target.value)
|
|
4075
|
+
}
|
|
4076
|
+
),
|
|
4077
|
+
!disabled && /* @__PURE__ */ jsx19("div", { className: "flex flex-wrap gap-1", children: fieldList.flatMap(bindingPlaceholdersFor).map((p) => /* @__PURE__ */ jsx19(
|
|
4078
|
+
Button10,
|
|
4079
|
+
{
|
|
4080
|
+
type: "button",
|
|
4081
|
+
size: "sm",
|
|
4082
|
+
variant: "ghost",
|
|
4083
|
+
className: "h-6 px-1.5 text-[11px]",
|
|
4084
|
+
onClick: () => setTemplate(template + p.token),
|
|
4085
|
+
children: p.label
|
|
4086
|
+
},
|
|
4087
|
+
p.token
|
|
4088
|
+
)) })
|
|
4089
|
+
] }),
|
|
4090
|
+
shape === "collection" && /* @__PURE__ */ jsxs16("label", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
4091
|
+
"Up to",
|
|
4092
|
+
/* @__PURE__ */ jsx19(
|
|
4093
|
+
BasicInput7,
|
|
4094
|
+
{
|
|
4095
|
+
type: "number",
|
|
4096
|
+
min: 1,
|
|
4097
|
+
className: "h-7 w-20 text-xs",
|
|
4098
|
+
value: binding.limit ?? "",
|
|
4099
|
+
disabled,
|
|
4100
|
+
onChange: (e) => {
|
|
4101
|
+
const n = Number.parseInt(e.target.value, 10);
|
|
4102
|
+
if (!Number.isFinite(n) || n < 1) return;
|
|
4103
|
+
onChange({ ...binding, limit: n, ...binding.transform ? { transform: { ...binding.transform, max: n } } : {} });
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
),
|
|
4107
|
+
"rows"
|
|
4108
|
+
] }),
|
|
4109
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex flex-wrap items-center gap-1", role: "radiogroup", "aria-label": "If nothing is found", children: [
|
|
4110
|
+
/* @__PURE__ */ jsx19("span", { className: "text-xs text-muted-foreground", children: "If nothing is found" }),
|
|
4111
|
+
DATA_BINDING_MISSING.map((m) => /* @__PURE__ */ jsx19(
|
|
4112
|
+
Button10,
|
|
4113
|
+
{
|
|
4114
|
+
type: "button",
|
|
4115
|
+
size: "sm",
|
|
4116
|
+
variant: m.value === binding.missing ? "default" : "outline",
|
|
4117
|
+
role: "radio",
|
|
4118
|
+
"aria-checked": m.value === binding.missing,
|
|
4119
|
+
disabled,
|
|
4120
|
+
onClick: () => onChange({ ...binding, missing: m.value }),
|
|
4121
|
+
children: m.label
|
|
4122
|
+
},
|
|
4123
|
+
m.value
|
|
4124
|
+
))
|
|
4125
|
+
] })
|
|
4126
|
+
] });
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// src/PickedSummaries.tsx
|
|
4130
|
+
import { useFields as useFields3, useRecord, useTable as useTable3 } from "@ai-matrx/records/react";
|
|
4131
|
+
|
|
4132
|
+
// src/values.tsx
|
|
4133
|
+
import { formatColumnValue } from "@ai-matrx/design-system/data-table";
|
|
4134
|
+
import { formatFieldValue as formatFieldValue3 } from "@ai-matrx/design-system/field-formats";
|
|
4135
|
+
import { Badge as Badge5, cn as cn15 } from "@ai-matrx/design-system";
|
|
4136
|
+
|
|
4137
|
+
// src/enumLabels.ts
|
|
4138
|
+
function exhaustiveValuesOf() {
|
|
4139
|
+
return function(values) {
|
|
4140
|
+
return values;
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
var WORK_DUE_STATES = exhaustiveValuesOf()([
|
|
4144
|
+
"overdue",
|
|
4145
|
+
"due_today",
|
|
4146
|
+
"scheduled",
|
|
4147
|
+
"undated",
|
|
4148
|
+
"finished"
|
|
4149
|
+
]);
|
|
4150
|
+
var WORK_INBOX_KINDS = exhaustiveValuesOf()(["assignment", "approval", "proposal"]);
|
|
4151
|
+
var WORK_APPROVAL_STATES = exhaustiveValuesOf()(["pending", "approved", "declined", "withdrawn"]);
|
|
4152
|
+
var CHECKLIST_REQUIREMENT_KINDS = exhaustiveValuesOf()([
|
|
4153
|
+
"none",
|
|
4154
|
+
"note",
|
|
4155
|
+
"answer",
|
|
4156
|
+
"record_field",
|
|
4157
|
+
"form",
|
|
4158
|
+
"document"
|
|
4159
|
+
]);
|
|
4160
|
+
var SUBSCRIPTION_CADENCES = exhaustiveValuesOf()(["instant", "hourly", "daily", "weekly"]);
|
|
4161
|
+
var ABSENCE_WORD_LABEL = {
|
|
4162
|
+
"never asked": "never asked",
|
|
4163
|
+
none: "none",
|
|
4164
|
+
refused: "refused",
|
|
4165
|
+
conflicting: "conflicting"
|
|
4166
|
+
};
|
|
4167
|
+
|
|
4168
|
+
// src/values.tsx
|
|
4169
|
+
import { wholeValueOf } from "@ai-matrx/records/core";
|
|
4170
|
+
|
|
4171
|
+
// src/WholeValue.tsx
|
|
4172
|
+
import { useEffect as useEffect14, useState as useState16 } from "react";
|
|
4173
|
+
import { wholeValueSize } from "@ai-matrx/records/core";
|
|
4174
|
+
import { BasicTextarea as BasicTextarea3 } from "@ai-matrx/design-system";
|
|
4175
|
+
import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
4176
|
+
var WHOLE_VALUE_UNBOUND = "The whole text is kept as a file, and this screen has no way to open files yet.";
|
|
4177
|
+
function wholeValueLine(pointer) {
|
|
4178
|
+
return `Open the whole text (${wholeValueSize(pointer)})`;
|
|
4179
|
+
}
|
|
4180
|
+
function WholeValueCell({ words, pointer }) {
|
|
4181
|
+
const host = useRecordsUi();
|
|
4182
|
+
const href = host.hrefForFile?.({ fileId: pointer.file_id });
|
|
4183
|
+
const line = wholeValueLine(pointer);
|
|
4184
|
+
return /* @__PURE__ */ jsxs17("span", { className: "flex min-w-0 items-baseline gap-2", "data-whole-value-file": pointer.file_id, children: [
|
|
4185
|
+
/* @__PURE__ */ jsxs17("span", { className: "truncate", title: `The first ${pointer.shown_chars} of ${pointer.chars} characters`, children: [
|
|
4186
|
+
words,
|
|
4187
|
+
"\u2026"
|
|
4188
|
+
] }),
|
|
4189
|
+
href ? host.Link ? host.Link({ href, children: line, className: "shrink-0 text-xs text-primary underline-offset-2 hover:underline" }) : /* @__PURE__ */ jsx20("a", { className: "shrink-0 text-xs text-primary underline-offset-2 hover:underline", href, target: "_blank", rel: "noreferrer", children: line }) : /* @__PURE__ */ jsx20("span", { className: "shrink-0 text-xs italic text-muted-foreground", title: WHOLE_VALUE_UNBOUND, children: `${wholeValueSize(pointer)} in a file` })
|
|
4190
|
+
] });
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// src/ReferenceChips.tsx
|
|
4194
|
+
import { entityRefsOf as entityRefsOf2 } from "@ai-matrx/records";
|
|
4195
|
+
import { Badge as Badge4, cn as cn14 } from "@ai-matrx/design-system";
|
|
4196
|
+
|
|
4197
|
+
// src/kindValues.ts
|
|
4198
|
+
import { tryDecodeDirective } from "@ai-matrx/content-ir/directives";
|
|
4199
|
+
import { humanizeIdentifier as humanizeIdentifier3 } from "@ai-matrx/kit/text-case";
|
|
4200
|
+
var KIND_KEY = "__kind";
|
|
4201
|
+
var WHOLE_FENCE = /^\s*```(?:matrx|json)?[ \t]*\r?\n?([\s\S]*?)\r?\n?[ \t]*```\s*$/;
|
|
4202
|
+
function asObject(value) {
|
|
4203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
4204
|
+
}
|
|
4205
|
+
function parseJsonObject(text) {
|
|
4206
|
+
const trimmed = text.trim();
|
|
4207
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
4208
|
+
try {
|
|
4209
|
+
return asObject(JSON.parse(trimmed));
|
|
4210
|
+
} catch {
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function fromObject(obj) {
|
|
4215
|
+
const directive = tryDecodeDirective(obj);
|
|
4216
|
+
if (directive) return { value: obj, kind: directive.slug, directive: true, items: [...directive.items] };
|
|
4217
|
+
const named = obj[KIND_KEY];
|
|
4218
|
+
if (typeof named === "string" && named.trim() !== "") {
|
|
4219
|
+
const items = Array.isArray(obj.items) ? obj.items.filter((i) => asObject(i) !== null) : [];
|
|
4220
|
+
return { value: obj, kind: named.trim(), directive: false, items };
|
|
4221
|
+
}
|
|
4222
|
+
return null;
|
|
4223
|
+
}
|
|
4224
|
+
function kindValueOf(value) {
|
|
4225
|
+
const obj = asObject(value);
|
|
4226
|
+
if (obj) return fromObject(obj);
|
|
4227
|
+
if (typeof value !== "string" || value.length > 2e5) return null;
|
|
4228
|
+
const fenced = WHOLE_FENCE.exec(value);
|
|
4229
|
+
const parsed = parseJsonObject(fenced ? fenced[1] : value);
|
|
4230
|
+
return parsed ? fromObject(parsed) : null;
|
|
4231
|
+
}
|
|
4232
|
+
function kindItemWords(item) {
|
|
4233
|
+
for (const key of ["label", "title", "name", "display_name"]) {
|
|
4234
|
+
const v = item[key];
|
|
4235
|
+
if (typeof v === "string" && v.trim() !== "") return v.trim();
|
|
4236
|
+
}
|
|
4237
|
+
return null;
|
|
4238
|
+
}
|
|
4239
|
+
function kindValueWords(kv) {
|
|
4240
|
+
const named = kv.items.map(kindItemWords).filter((w) => w !== null);
|
|
4241
|
+
if (named.length > 0) return named.join(", ");
|
|
4242
|
+
return kindLabel(kv.kind);
|
|
4243
|
+
}
|
|
4244
|
+
function kindLabel(kind) {
|
|
4245
|
+
return humanizeIdentifier3(kind.replace(/^directive_v\d+_/, "").replace(/_v\d+$/, "")) || "Structured value";
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
// src/ReferenceChips.tsx
|
|
4249
|
+
import { jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
4250
|
+
function referencesOf(field, value, kind, labels) {
|
|
4251
|
+
if (kind === "entity_reference") {
|
|
4252
|
+
return entityRefsOf2(value).map((r) => ({ token: r.token, id: r.id, label: r.label }));
|
|
4253
|
+
}
|
|
4254
|
+
const ids = (Array.isArray(value) ? value : [value]).filter(isId).map(String);
|
|
4255
|
+
const tableId = typeof field.relation_target === "string" ? field.relation_target : void 0;
|
|
4256
|
+
return ids.map((id) => ({
|
|
4257
|
+
token: "record",
|
|
4258
|
+
id,
|
|
4259
|
+
label: labels?.(field, id) ?? null,
|
|
4260
|
+
tableId,
|
|
4261
|
+
...labels?.archived?.(field, id) ? { archived: true } : {}
|
|
4262
|
+
}));
|
|
4263
|
+
}
|
|
4264
|
+
var ARCHIVED_REFERENCE_WORD = "Archived";
|
|
4265
|
+
var ARCHIVED_REFERENCE_TIP = "In Trash; restore it to use it again";
|
|
4266
|
+
function ReferenceChip({ reference, waiting }) {
|
|
4267
|
+
const host = useRecordsUi();
|
|
4268
|
+
const words = reference.label ?? (reference.token === "record" ? waiting ?? "Loading\u2026" : WITHHELD_ENTITY_LABEL);
|
|
4269
|
+
const drawn = host.renderReference ? host.renderReference({ ...reference, label: reference.label ?? words }) : null;
|
|
4270
|
+
return /* @__PURE__ */ jsxs18(
|
|
4271
|
+
Badge4,
|
|
4272
|
+
{
|
|
4273
|
+
variant: "secondary",
|
|
4274
|
+
className: cn14(
|
|
4275
|
+
"max-w-full min-w-0 gap-1 px-1.5 py-0 text-[11px] font-normal",
|
|
4276
|
+
reference.archived && "opacity-70"
|
|
4277
|
+
),
|
|
4278
|
+
"data-records-reference-chip": reference.token,
|
|
4279
|
+
"data-records-reference-archived": reference.archived ? "" : void 0,
|
|
4280
|
+
title: reference.archived ? ARCHIVED_REFERENCE_TIP : words,
|
|
4281
|
+
children: [
|
|
4282
|
+
drawn ?? /* @__PURE__ */ jsx21("span", { className: "truncate", children: words }),
|
|
4283
|
+
reference.archived ? /* @__PURE__ */ jsx21("span", { className: "shrink-0 text-muted-foreground", children: ARCHIVED_REFERENCE_WORD }) : null
|
|
4284
|
+
]
|
|
4285
|
+
}
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
function ReferenceChips({ refs, waiting }) {
|
|
4289
|
+
if (refs.length === 0) return /* @__PURE__ */ jsx21("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
4290
|
+
return /* @__PURE__ */ jsx21("span", { className: "flex min-w-0 flex-wrap gap-1", "data-records-reference-chips": "", children: refs.map((r) => /* @__PURE__ */ jsx21(ReferenceChip, { reference: r, waiting }, `${r.token}:${r.id}`)) });
|
|
4291
|
+
}
|
|
4292
|
+
function KindValueCell({
|
|
4293
|
+
kv,
|
|
4294
|
+
where,
|
|
4295
|
+
title
|
|
4296
|
+
}) {
|
|
4297
|
+
const host = useRecordsUi();
|
|
4298
|
+
if (host.renderKind) {
|
|
4299
|
+
return /* @__PURE__ */ jsx21("span", { className: "flex min-w-0 max-w-full items-center", "data-records-kind-value": kv.kind, children: host.renderKind({ value: kv.value, kind: kv.kind, where, title }) });
|
|
4300
|
+
}
|
|
4301
|
+
const named = kv.items.map(kindItemWords).filter((w) => w !== null);
|
|
4302
|
+
return /* @__PURE__ */ jsx21("span", { className: cn14("flex min-w-0 flex-wrap gap-1"), "data-records-kind-value": kv.kind, title: JSON.stringify(kv.value), children: named.length > 0 ? named.map((w, i) => /* @__PURE__ */ jsx21(Badge4, { variant: "secondary", className: "px-1.5 py-0 text-[11px] font-normal", children: w }, `${w}-${i}`)) : /* @__PURE__ */ jsx21(Badge4, { variant: "outline", className: "px-1.5 py-0 text-[11px] font-normal", children: kindLabel(kv.kind) }) });
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
// src/values.tsx
|
|
4306
|
+
import { newerKindWords } from "@ai-matrx/records";
|
|
4307
|
+
import { Fragment as Fragment6, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4308
|
+
function statusMark(field, item) {
|
|
4309
|
+
const mark = statusMarkOf(field, item);
|
|
4310
|
+
return mark ? /* @__PURE__ */ jsx22("span", { "aria-label": mark.words, title: mark.words, className: "mr-1", "data-records-status-group": mark.group, children: mark.mark }) : null;
|
|
4311
|
+
}
|
|
4312
|
+
function envelopeFor(document2, key) {
|
|
4313
|
+
const block = document2?._values;
|
|
4314
|
+
return block?.[key];
|
|
4315
|
+
}
|
|
4316
|
+
function unresolvedFor(document2, key) {
|
|
4317
|
+
const sources = document2?._sources;
|
|
4318
|
+
if (!sources || typeof sources !== "object") return void 0;
|
|
4319
|
+
for (const source of Object.values(sources)) {
|
|
4320
|
+
const held = source?.unresolved?.[key];
|
|
4321
|
+
if (typeof held === "string" && held) return held;
|
|
4322
|
+
if (Array.isArray(held) && held.length) return held.map(String).join(", ");
|
|
4323
|
+
}
|
|
4324
|
+
return void 0;
|
|
4325
|
+
}
|
|
4326
|
+
function hasFormula(field) {
|
|
4327
|
+
const config = field.config ?? {};
|
|
4328
|
+
return Boolean(config.expr) || Boolean(String(config.formula_text ?? "").trim());
|
|
4329
|
+
}
|
|
4330
|
+
function computedEmptyOf(field, document2) {
|
|
4331
|
+
const kind = editorKindFor(field);
|
|
4332
|
+
const config = field.config ?? {};
|
|
4333
|
+
if (kind === "lookup" || kind === "rollup") {
|
|
4334
|
+
const via = typeof config["via"] === "string" ? config["via"] : null;
|
|
4335
|
+
const held = document2 && via ? document2[via] : void 0;
|
|
4336
|
+
const known = Boolean(document2 && via);
|
|
4337
|
+
const linked = !(held === null || held === void 0 || held === "" || Array.isArray(held) && held.length === 0);
|
|
4338
|
+
const count = kind === "rollup" && config["agg"] === "count";
|
|
4339
|
+
const which = kind === "lookup" ? "lookup" : count ? "count" : "rollup";
|
|
4340
|
+
if (!known) return { kind: which };
|
|
4341
|
+
if (kind === "lookup") return { kind: which, why: linked ? "Empty on the linked record" : "Pick a linked record" };
|
|
4342
|
+
if (!linked) return { kind: which, why: "No linked records yet" };
|
|
4343
|
+
return count ? { kind: which, words: "0" } : { kind: which, why: "Nothing to add up" };
|
|
4344
|
+
}
|
|
4345
|
+
if (kind !== "formula" && field.type !== "formula") return null;
|
|
4346
|
+
if (typeof config["system"] === "string") return { kind: "stamp" };
|
|
4347
|
+
if (!hasFormula(field)) return { kind: "formula", sentence: "Worked out once its formula is in place" };
|
|
4348
|
+
return { kind: "formula" };
|
|
4349
|
+
}
|
|
4350
|
+
var ABSENCE_WORDS = ABSENCE_WORD_LABEL;
|
|
4351
|
+
function renderValue(field, value, document2, labels, stillWaitingFor) {
|
|
4352
|
+
const envelope = envelopeFor(document2, field.key);
|
|
4353
|
+
if (envelope?.absent) {
|
|
4354
|
+
return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", title: `VAL-2: ${envelope.absent}`, children: ABSENCE_WORDS[String(envelope.absent)] ?? String(envelope.absent) });
|
|
4355
|
+
}
|
|
4356
|
+
if (value === null || value === void 0 || value === "") {
|
|
4357
|
+
const gone = unresolvedFor(document2, field.key);
|
|
4358
|
+
if (gone) {
|
|
4359
|
+
return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", title: `It pointed at ${gone}`, children: "The record this pointed at no longer exists" });
|
|
4360
|
+
}
|
|
4361
|
+
const computed = computedEmptyOf(field, document2);
|
|
4362
|
+
if (computed?.sentence) return /* @__PURE__ */ jsx22("span", { className: "text-xs italic text-muted-foreground", children: computed.sentence });
|
|
4363
|
+
if (computed?.words) return /* @__PURE__ */ jsx22("span", { "data-records-empty-why": computed.kind, children: computed.words });
|
|
4364
|
+
if (computed?.why) {
|
|
4365
|
+
return /* @__PURE__ */ jsx22("span", { className: "text-muted-foreground", title: computed.why, "data-records-empty-why": computed.kind, children: "\u2014" });
|
|
4366
|
+
}
|
|
4367
|
+
return /* @__PURE__ */ jsx22("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
4368
|
+
}
|
|
4369
|
+
if (isSignatureField(field)) return /* @__PURE__ */ jsx22(SignatureValue, { value });
|
|
4370
|
+
const newer = newerKindValue(field, value);
|
|
4371
|
+
if (newer !== void 0) return newer;
|
|
4372
|
+
const whole = typeof value === "string" ? wholeValueOf(document2, field.key) : null;
|
|
4373
|
+
if (whole) return /* @__PURE__ */ jsx22(WholeValueCell, { words: value, pointer: whole });
|
|
4374
|
+
const kind = editorKindFor(field);
|
|
4375
|
+
if (kind === "relation" || kind === "entity_reference") {
|
|
4376
|
+
return /* @__PURE__ */ jsx22(ReferenceChips, { refs: referencesOf(field, value, kind, labels), waiting: stillWaitingFor });
|
|
4377
|
+
}
|
|
4378
|
+
const kv = kind === "attachment" || kind === "member" ? null : kindValueOf(value);
|
|
4379
|
+
if (kv) return /* @__PURE__ */ jsx22(KindValueCell, { kv, where: "cell", title: field.label });
|
|
4380
|
+
const chosen = kind === "select" || kind === "multi_select";
|
|
4381
|
+
const chipOf = (item) => {
|
|
4382
|
+
const look = choiceChipLookOf(field, item, scalarText(field, item, labels));
|
|
4383
|
+
return { className: look.className, tinted: look.color !== null };
|
|
4384
|
+
};
|
|
4385
|
+
if (chosen) {
|
|
4386
|
+
const items = Array.isArray(value) ? value : [value];
|
|
4387
|
+
return /* @__PURE__ */ jsx22("span", { className: "flex flex-wrap gap-1", "data-records-choice-chips": "", children: items.map((item, i) => {
|
|
4388
|
+
const chip = chipOf(item);
|
|
4389
|
+
return /* @__PURE__ */ jsxs19(
|
|
4390
|
+
Badge5,
|
|
4391
|
+
{
|
|
4392
|
+
variant: "outline",
|
|
4393
|
+
className: cn15("text-[11px]", chip.className),
|
|
4394
|
+
"data-records-choice-chip": "",
|
|
4395
|
+
...chip.tinted ? { "data-records-choice-tint": "" } : {},
|
|
4396
|
+
children: [
|
|
4397
|
+
statusMark(field, item),
|
|
4398
|
+
scalarText(field, item, labels, Array.isArray(value) ? void 0 : stillWaitingFor)
|
|
4399
|
+
]
|
|
4400
|
+
},
|
|
4401
|
+
`${String(item)}-${i}`
|
|
4402
|
+
);
|
|
4403
|
+
}) });
|
|
4404
|
+
}
|
|
4405
|
+
if (kind === "attachment") return /* @__PURE__ */ jsx22(AttachmentChips, { value });
|
|
4406
|
+
if (Array.isArray(value)) {
|
|
4407
|
+
return /* @__PURE__ */ jsx22("span", { className: "flex flex-wrap gap-1", children: value.map((item, i) => /* @__PURE__ */ jsx22(Badge5, { variant: "secondary", className: "text-[11px]", children: scalarText(field, item, labels) }, `${String(item)}-${i}`)) });
|
|
4408
|
+
}
|
|
4409
|
+
if (kind === "url" && typeof value === "string") {
|
|
4410
|
+
const href = value.includes("://") ? value : `https://${value}`;
|
|
4411
|
+
return /* @__PURE__ */ jsxs19(
|
|
4412
|
+
"a",
|
|
4413
|
+
{
|
|
4414
|
+
className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border border-border bg-muted/60 px-1.5 py-0.5 text-[11px] text-foreground hover:bg-accent",
|
|
4415
|
+
href,
|
|
4416
|
+
target: "_blank",
|
|
4417
|
+
rel: "noreferrer",
|
|
4418
|
+
title: value,
|
|
4419
|
+
"data-records-link-chip": "",
|
|
4420
|
+
onClick: (event) => event.stopPropagation(),
|
|
4421
|
+
children: [
|
|
4422
|
+
/* @__PURE__ */ jsx22("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2197" }),
|
|
4423
|
+
/* @__PURE__ */ jsx22("span", { className: "truncate", children: linkChipText(value) })
|
|
4424
|
+
]
|
|
4425
|
+
}
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
if (kind === "phone" && typeof value === "string" && value.trim() !== "") {
|
|
4429
|
+
return /* @__PURE__ */ jsx22(
|
|
4430
|
+
"a",
|
|
4431
|
+
{
|
|
4432
|
+
className: "truncate tabular-nums text-primary underline-offset-2 hover:underline",
|
|
4433
|
+
href: `tel:${value.replace(/[^\d+]/g, "")}`,
|
|
4434
|
+
title: value,
|
|
4435
|
+
"data-records-phone": "",
|
|
4436
|
+
onClick: (event) => event.stopPropagation(),
|
|
4437
|
+
children: formatPhone(value)
|
|
4438
|
+
}
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
if (kind === "email" && typeof value === "string") {
|
|
4442
|
+
return /* @__PURE__ */ jsx22("a", { className: "truncate text-primary underline-offset-2 hover:underline", href: `mailto:${value}`, children: value });
|
|
4443
|
+
}
|
|
4444
|
+
if (typeof value === "boolean") {
|
|
4445
|
+
return /* @__PURE__ */ jsx22("span", { className: kind === "checkbox" ? "text-sm" : "", title: value ? "Yes" : "No", children: value ? "\u2713" : "\u2014" });
|
|
4446
|
+
}
|
|
4447
|
+
return /* @__PURE__ */ jsx22("span", { className: cn15("truncate", kind === "number" || kind === "currency" || kind === "percent" ? "tabular-nums" : ""), children: scalarText(field, value, labels, stillWaitingFor) });
|
|
4448
|
+
}
|
|
4449
|
+
function scalarText(field, value, labels, stillWaitingFor) {
|
|
4450
|
+
if (value === null || value === void 0) return "\u2014";
|
|
4451
|
+
if (isSignatureField(field)) return signatureWords(value) ?? "\u2014";
|
|
4452
|
+
const newerWords = newerKindWords(field, value);
|
|
4453
|
+
if (newerWords !== null) return newerWords;
|
|
4454
|
+
const kind = editorKindFor(field);
|
|
4455
|
+
if (kind === "entity_reference") return entityReferenceText(value);
|
|
4456
|
+
if ((kind === "relation" || field.type === "list") && Array.isArray(value)) {
|
|
4457
|
+
return value.map((item) => scalarText(field, item, labels, stillWaitingFor)).join(", ");
|
|
4458
|
+
}
|
|
4459
|
+
const kv = kindValueOf(value);
|
|
4460
|
+
if (kv) return kindValueWords(kv);
|
|
4461
|
+
if (isId(value) && (field.type === "list" || field.type === "relation")) {
|
|
4462
|
+
return labels?.(field, String(value)) ?? stillWaitingFor ?? "Loading\u2026";
|
|
4463
|
+
}
|
|
4464
|
+
if (field.type === "list" && typeof value === "string" && labels) {
|
|
4465
|
+
const words = labels(field, value);
|
|
4466
|
+
if (words) return words;
|
|
4467
|
+
}
|
|
4468
|
+
const shownAs = shownAsOf(field);
|
|
4469
|
+
if (shownAs) {
|
|
4470
|
+
const said = formatFieldValue3(value, shownAs);
|
|
4471
|
+
if (said.ok && !said.empty) return said.text;
|
|
4472
|
+
}
|
|
4473
|
+
if (typeof value === "number") {
|
|
4474
|
+
if (kind === "percent") return `${value}%`;
|
|
4475
|
+
if (kind === "currency") return field.unit ? `${field.unit}${value.toLocaleString()}` : value.toLocaleString();
|
|
4476
|
+
return field.unit ? `${value.toLocaleString()} ${field.unit}` : value.toLocaleString();
|
|
4477
|
+
}
|
|
4478
|
+
if (kind === "datetime" || systemStampOf(field) === "created_time" || systemStampOf(field) === "modified_time") {
|
|
4479
|
+
const said = dateText(field, value);
|
|
4480
|
+
if (said !== null) return said;
|
|
4481
|
+
}
|
|
4482
|
+
if (Array.isArray(value)) return value.length === 0 ? "" : `${value.length} of them`;
|
|
4483
|
+
if (typeof value === "object") return "\u2026";
|
|
4484
|
+
return String(value);
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// src/PickedSummaries.tsx
|
|
4488
|
+
import { Fragment as Fragment7, jsx as jsx23, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
4489
|
+
function TableNameText({ dataSource, tableId }) {
|
|
4490
|
+
const tables = useTablesAnywhere(dataSource);
|
|
4491
|
+
if (!tableId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
|
|
4492
|
+
if (tables.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
|
|
4493
|
+
return /* @__PURE__ */ jsx23(Fragment7, { children: tables.rows.find((t) => t.table_id === tableId)?.table_name ?? "\u2014" });
|
|
4494
|
+
}
|
|
4495
|
+
function ColumnValuesText({
|
|
4496
|
+
dataSource,
|
|
4497
|
+
userId,
|
|
4498
|
+
tableId,
|
|
4499
|
+
value
|
|
4500
|
+
}) {
|
|
4501
|
+
if (!tableId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
|
|
4502
|
+
return /* @__PURE__ */ jsx23(TableScope, { dataSource, userId, tableId, fallback: /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" }), children: /* @__PURE__ */ jsx23(RecordLabelProvider, { children: /* @__PURE__ */ jsx23(ColumnValuesWords, { tableId, value }) }) });
|
|
4503
|
+
}
|
|
4504
|
+
function ColumnValuesWords({ tableId, value }) {
|
|
4505
|
+
const fields = useFields3(tableId);
|
|
4506
|
+
const labels = useRecordLabels(fields.data ?? void 0);
|
|
4507
|
+
if (fields.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
|
|
4508
|
+
const parts = [];
|
|
4509
|
+
for (const [key, stored] of Object.entries(value)) {
|
|
4510
|
+
const field = (fields.data ?? []).find((f) => f.key === key);
|
|
4511
|
+
if (parts.length) parts.push(/* @__PURE__ */ jsx23("span", { children: ", " }, `${key}-sep`));
|
|
4512
|
+
parts.push(
|
|
4513
|
+
/* @__PURE__ */ jsxs20("span", { children: [
|
|
4514
|
+
field ? fieldName(field) : "A removed column",
|
|
4515
|
+
" is ",
|
|
4516
|
+
field ? renderValue(field, stored, void 0, labels) : "\u2014"
|
|
4517
|
+
] }, key)
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
4520
|
+
return parts.length ? /* @__PURE__ */ jsx23(Fragment7, { children: parts }) : /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
|
|
4521
|
+
}
|
|
4522
|
+
function RecordNameText({
|
|
4523
|
+
dataSource,
|
|
4524
|
+
userId,
|
|
4525
|
+
tableId,
|
|
4526
|
+
recordId
|
|
4527
|
+
}) {
|
|
4528
|
+
if (!tableId || !recordId) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2014" });
|
|
4529
|
+
return /* @__PURE__ */ jsx23(TableScope, { dataSource, userId, tableId, fallback: /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" }), children: /* @__PURE__ */ jsx23(RecordNameWords, { tableId, recordId }) });
|
|
4530
|
+
}
|
|
4531
|
+
function RecordNameWords({ tableId, recordId }) {
|
|
4532
|
+
const table = useTable3(tableId);
|
|
4533
|
+
const record = useRecord(recordId);
|
|
4534
|
+
if (record.loading || table.loading) return /* @__PURE__ */ jsx23(Fragment7, { children: "\u2026" });
|
|
4535
|
+
if (!record.data) return /* @__PURE__ */ jsx23(Fragment7, { children: "A record you cannot open" });
|
|
4536
|
+
return /* @__PURE__ */ jsx23(Fragment7, { children: recordName(record.data.document, table.data?.title_field ?? null) });
|
|
4537
|
+
}
|
|
4538
|
+
export {
|
|
4539
|
+
ColumnValuesEditor,
|
|
4540
|
+
ColumnValuesText,
|
|
4541
|
+
DATA_BINDING_MISSING,
|
|
4542
|
+
DATA_BINDING_SHAPES,
|
|
4543
|
+
DataBindingPicker,
|
|
4544
|
+
RecordNameText,
|
|
4545
|
+
RecordPicker,
|
|
4546
|
+
TableNameText,
|
|
4547
|
+
TablePicker,
|
|
4548
|
+
TableScope,
|
|
4549
|
+
bindingPlaceholdersFor,
|
|
4550
|
+
defaultBindingTemplate,
|
|
4551
|
+
emptyDataBinding,
|
|
4552
|
+
isCompleteDataBinding,
|
|
4553
|
+
isDataBinding,
|
|
4554
|
+
useTablesAnywhere
|
|
4555
|
+
};
|
|
4556
|
+
//# sourceMappingURL=pickers.js.map
|