@ai-matrx/records-ui 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/dist/index.cjs +248 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +256 -62
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records-ui
|
|
2
2
|
|
|
3
|
+
## 0.59.0
|
|
4
|
+
|
|
5
|
+
**Three things the browser walks found by using the builders on real businesses.**
|
|
6
|
+
|
|
7
|
+
* **A booking page's day rows lied about what it would offer.** Every day started
|
|
8
|
+
unticked and read "not taking bookings", while `custom._booking_availability`
|
|
9
|
+
substitutes Monday-to-Friday nine-to-five for an empty window list — so a
|
|
10
|
+
person who saved without touching the days was told the page offered nothing
|
|
11
|
+
and got a page offering the whole working week. `BookingBuilder` now opens on
|
|
12
|
+
the working week, and once the store has answered, the rows are ITS answer.
|
|
13
|
+
* **Both builders offered questions a stranger could never answer.** Ironclad
|
|
14
|
+
Mobile Mechanic ticked "Customer" on his booking page — a relation Field — a
|
|
15
|
+
visitor typed her name, and the store refused at the last step with *"Customer
|
|
16
|
+
points at something that is not there"*, after her slot was already held. A
|
|
17
|
+
relation holds the identity of a record in a Table she cannot open. The new
|
|
18
|
+
`publiclyAnswerable` / `askableFields` / `whyNotAskable` decide what a public
|
|
19
|
+
page may ask for (out: relation, member, attachment, formula, lookup, rollup)
|
|
20
|
+
and both `FormBuilder` and `BookingBuilder` use them — the Fields left out are
|
|
21
|
+
NAMED with the reason, in the place somebody would have ticked them.
|
|
22
|
+
* The Booking and Portal builders and the Digest scheduler carry the container
|
|
23
|
+
queries 0.57.0 gave the form builder, so all four are one column in a rail.
|
|
24
|
+
|
|
25
|
+
**Consumer action:** none.
|
|
26
|
+
|
|
27
|
+
## 0.58.0
|
|
28
|
+
|
|
29
|
+
**A word pasted into a choice column becomes that option — and a paste is no
|
|
30
|
+
longer capped by the page it is looking at.**
|
|
31
|
+
|
|
32
|
+
**Pasting "Gold Annual" into a choice column.** Until now every word pasted into
|
|
33
|
+
a `select` or `multi_select` was refused outright: correct about a raw string,
|
|
34
|
+
and useless to Rincon Plumbing's office manager whose Service plan column says
|
|
35
|
+
"Gold Annual" forty times. Airtable, the champion here, matches the word to the
|
|
36
|
+
option labelled that word, and so does this.
|
|
37
|
+
|
|
38
|
+
* The grid now READS each choice column's options before the preview draws, so
|
|
39
|
+
the preview is an answer rather than a "loading" a person would have to
|
|
40
|
+
confirm blind.
|
|
41
|
+
* Matching ignores case and also accepts the option's key or id, so a round trip
|
|
42
|
+
through a spreadsheet comes back in. A multi-choice cell splits on commas and
|
|
43
|
+
semicolons and is written WHOLE or not at all — half a multi-choice answer is
|
|
44
|
+
a wrong answer, not a partial one.
|
|
45
|
+
* What is stored is the option RECORD'S ID (FLD-5/FLD-6), which is what the
|
|
46
|
+
store's own validator accepts — writing the slug would have made the preview
|
|
47
|
+
promise a write the door then refused.
|
|
48
|
+
* An unmatched word is REFUSED, naming the line, the column and the options that
|
|
49
|
+
ARE on offer, and **never silently created**: a typo promoted to a lifecycle
|
|
50
|
+
stage is a column nobody chose. The preview offers them back as ONE explicit
|
|
51
|
+
"Add these N options" action, pressed by a person; it sends the existing labels
|
|
52
|
+
back with the new ones (the door REPLACES the list) and re-plans, so the rows
|
|
53
|
+
land without pasting again.
|
|
54
|
+
|
|
55
|
+
**A 120-line paste against a 50-row page.** The page ceiling was never a paste
|
|
56
|
+
ceiling, and nothing exercised past it.
|
|
57
|
+
|
|
58
|
+
* The creates now chunk through `record_write_many` at the grid's own page size —
|
|
59
|
+
the number a person is looking at is the number that travels. A refused chunk is
|
|
60
|
+
refused whole, so every line in it is NAMED ("Rows 51-100: …"); updates stay one
|
|
61
|
+
at a time because they carry a per-row optimistic version and no bulk door can
|
|
62
|
+
honour it.
|
|
63
|
+
* The read-back PAGES. A single `limit: 500` read silently stopped counting at row
|
|
64
|
+
500, so a table that had grown past it reported values "missing" that were
|
|
65
|
+
sitting in it — a read-back that lies is worse than none.
|
|
66
|
+
|
|
67
|
+
**Consumer action:** none. `PastePreview` gains an optional `onAddOptions`; a host
|
|
68
|
+
that does not pass it simply does not offer the action.
|
|
69
|
+
|
|
3
70
|
## 0.57.0
|
|
4
71
|
|
|
5
72
|
**The builders are usable where a person actually reaches them: a 384px rail.**
|
package/dist/index.cjs
CHANGED
|
@@ -156,6 +156,7 @@ __export(src_exports, {
|
|
|
156
156
|
actorBadge: () => actorBadge,
|
|
157
157
|
actorWords: () => actorWords,
|
|
158
158
|
addFields: () => addFields,
|
|
159
|
+
askableFields: () => askableFields,
|
|
159
160
|
blockFromSpec: () => blockFromSpec,
|
|
160
161
|
bodyForReading: () => bodyForReading,
|
|
161
162
|
bodyFromKeys: () => bodyFromKeys,
|
|
@@ -208,6 +209,7 @@ __export(src_exports, {
|
|
|
208
209
|
presentationDocument: () => presentationDocument,
|
|
209
210
|
presentationIsEmpty: () => presentationIsEmpty,
|
|
210
211
|
previewLine: () => previewLine,
|
|
212
|
+
publiclyAnswerable: () => publiclyAnswerable,
|
|
211
213
|
recordName: () => recordName,
|
|
212
214
|
recordsDataSource: () => recordsDataSource,
|
|
213
215
|
refusalForAPerson: () => refusalForAPerson,
|
|
@@ -244,7 +246,8 @@ __export(src_exports, {
|
|
|
244
246
|
whatIsMissing: () => whatIsMissing,
|
|
245
247
|
whatYouMayDo: () => whatYouMayDo,
|
|
246
248
|
whatYouMayDoWithTable: () => whatYouMayDoWithTable,
|
|
247
|
-
whenWords: () => whenWords
|
|
249
|
+
whenWords: () => whenWords,
|
|
250
|
+
whyNotAskable: () => whyNotAskable
|
|
248
251
|
});
|
|
249
252
|
module.exports = __toCommonJS(src_exports);
|
|
250
253
|
|
|
@@ -2731,9 +2734,18 @@ function EnrichPanel({ tableId, fieldId, className }) {
|
|
|
2731
2734
|
var import_react17 = require("react");
|
|
2732
2735
|
var import_design_system8 = require("@ai-matrx/design-system");
|
|
2733
2736
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
2734
|
-
function PastePreview({
|
|
2737
|
+
function PastePreview({
|
|
2738
|
+
plan,
|
|
2739
|
+
noun,
|
|
2740
|
+
nounPlural,
|
|
2741
|
+
onConfirm,
|
|
2742
|
+
onCancel,
|
|
2743
|
+
onAddOptions
|
|
2744
|
+
}) {
|
|
2735
2745
|
const [busy, setBusy] = (0, import_react17.useState)(false);
|
|
2736
2746
|
const [outcome, setOutcome] = (0, import_react17.useState)(null);
|
|
2747
|
+
const [adding, setAdding] = (0, import_react17.useState)(null);
|
|
2748
|
+
const [addFailed, setAddFailed] = (0, import_react17.useState)(null);
|
|
2737
2749
|
const nothingToDo = plan.cells === 0;
|
|
2738
2750
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2739
2751
|
"div",
|
|
@@ -2772,6 +2784,37 @@ function PastePreview({ plan, noun, nounPlural, onConfirm, onCancel }) {
|
|
|
2772
2784
|
" more of the same kinds."
|
|
2773
2785
|
] }) : null
|
|
2774
2786
|
] }) : null,
|
|
2787
|
+
onAddOptions && plan.unmatchedOptions.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-matrx-paste-add-options": true, className: "flex flex-col gap-2", children: [
|
|
2788
|
+
plan.unmatchedOptions.map((group) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2789
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-xs", children: [
|
|
2790
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "font-medium", children: group.column }),
|
|
2791
|
+
" does not offer",
|
|
2792
|
+
" ",
|
|
2793
|
+
group.words.map((w) => `\u201C${w}\u201D`).join(", "),
|
|
2794
|
+
"."
|
|
2795
|
+
] }),
|
|
2796
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2797
|
+
import_design_system8.Button,
|
|
2798
|
+
{
|
|
2799
|
+
type: "button",
|
|
2800
|
+
size: "sm",
|
|
2801
|
+
variant: "outline",
|
|
2802
|
+
disabled: adding !== null || busy,
|
|
2803
|
+
onClick: () => {
|
|
2804
|
+
setAddFailed(null);
|
|
2805
|
+
setAdding(group.fieldKey);
|
|
2806
|
+
void onAddOptions(group.fieldKey, group.words).catch(
|
|
2807
|
+
(err) => setAddFailed(
|
|
2808
|
+
`Those options were not added: ${err instanceof Error ? err.message : String(err)}`
|
|
2809
|
+
)
|
|
2810
|
+
).finally(() => setAdding(null));
|
|
2811
|
+
},
|
|
2812
|
+
children: adding === group.fieldKey ? "Adding\u2026" : group.words.length === 1 ? `Add \u201C${group.words[0]}\u201D as an option` : `Add these ${group.words.length} options`
|
|
2813
|
+
}
|
|
2814
|
+
)
|
|
2815
|
+
] }, group.fieldKey)),
|
|
2816
|
+
addFailed ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-destructive", children: addFailed }) : null
|
|
2817
|
+
] }) : null,
|
|
2775
2818
|
plan.pastRightEdge > 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
|
|
2776
2819
|
plan.pastRightEdge,
|
|
2777
2820
|
" ",
|
|
@@ -2863,9 +2906,26 @@ function confirmLabel(plan, noun, nounPlural) {
|
|
|
2863
2906
|
|
|
2864
2907
|
// src/pasteValues.ts
|
|
2865
2908
|
var import_core2 = require("@ai-matrx/records/core");
|
|
2909
|
+
var import_records5 = require("@ai-matrx/records");
|
|
2910
|
+
function choiceOptionsOf(options) {
|
|
2911
|
+
const byWord = /* @__PURE__ */ new Map();
|
|
2912
|
+
const labels = [];
|
|
2913
|
+
for (const option of options) {
|
|
2914
|
+
const id = option.id;
|
|
2915
|
+
const key = (0, import_records5.optionKey)(option);
|
|
2916
|
+
const label = recordName(option.data, null, "");
|
|
2917
|
+
if (label) {
|
|
2918
|
+
labels.push(label);
|
|
2919
|
+
if (!byWord.has(label.toLowerCase())) byWord.set(label.toLowerCase(), id);
|
|
2920
|
+
}
|
|
2921
|
+
if (key && !byWord.has(String(key).toLowerCase())) byWord.set(String(key).toLowerCase(), id);
|
|
2922
|
+
if (id && !byWord.has(id.toLowerCase())) byWord.set(id.toLowerCase(), id);
|
|
2923
|
+
}
|
|
2924
|
+
return { byWord, labels };
|
|
2925
|
+
}
|
|
2866
2926
|
var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
|
|
2867
2927
|
var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
|
|
2868
|
-
function valueFromPastedText(field, raw) {
|
|
2928
|
+
function valueFromPastedText(field, raw, options) {
|
|
2869
2929
|
const label = fieldName(field);
|
|
2870
2930
|
const text = raw.trim();
|
|
2871
2931
|
const kind = editorKindFor(field);
|
|
@@ -2925,10 +2985,30 @@ function valueFromPastedText(field, raw) {
|
|
|
2925
2985
|
// stop agreeing with it. So the cell is left alone and the person is told
|
|
2926
2986
|
// which control does hold it.
|
|
2927
2987
|
case "select":
|
|
2928
|
-
case "multi_select":
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2988
|
+
case "multi_select": {
|
|
2989
|
+
if (!options) {
|
|
2990
|
+
return {
|
|
2991
|
+
refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to \u201C${raw}\u201D. Reopen the paste once the column's choices have loaded.`
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
|
|
2995
|
+
if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
|
|
2996
|
+
const matched = [];
|
|
2997
|
+
const unmatched = [];
|
|
2998
|
+
for (const word of wanted) {
|
|
2999
|
+
const hit = options.byWord.get(word.toLowerCase());
|
|
3000
|
+
if (hit === void 0) unmatched.push(word);
|
|
3001
|
+
else matched.push(hit);
|
|
3002
|
+
}
|
|
3003
|
+
if (unmatched.length > 0) {
|
|
3004
|
+
const offered = options.labels.length > 0 ? options.labels.join(", ") : "it has none yet";
|
|
3005
|
+
return {
|
|
3006
|
+
refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
|
|
3007
|
+
unmatched
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
return { value: kind === "multi_select" ? matched : matched[0] };
|
|
3011
|
+
}
|
|
2932
3012
|
case "member":
|
|
2933
3013
|
return {
|
|
2934
3014
|
refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open the cell and pick them.`
|
|
@@ -2955,6 +3035,7 @@ function planPastedBlock(args) {
|
|
|
2955
3035
|
const byKey = new Map(args.fields.map((f) => [f.key, f]));
|
|
2956
3036
|
const rows = [];
|
|
2957
3037
|
const refusals = [];
|
|
3038
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
2958
3039
|
let cells = 0;
|
|
2959
3040
|
const lines = [
|
|
2960
3041
|
...args.onExisting.map((r) => ({ recordId: r.recordId, raw: r.raw })),
|
|
@@ -2970,8 +3051,15 @@ function planPastedBlock(args) {
|
|
|
2970
3051
|
const field = byKey.get(key);
|
|
2971
3052
|
const raw = line.raw[c];
|
|
2972
3053
|
if (field === void 0 || raw === void 0) continue;
|
|
2973
|
-
const judged = valueFromPastedText(field, raw);
|
|
3054
|
+
const judged = valueFromPastedText(field, raw, args.options?.get(key));
|
|
2974
3055
|
if ("refusal" in judged) {
|
|
3056
|
+
if ("unmatched" in judged) {
|
|
3057
|
+
const seen = unmatched.get(key) ?? { column: fieldName(field), words: [] };
|
|
3058
|
+
for (const word of judged.unmatched) {
|
|
3059
|
+
if (!seen.words.some((w) => w.toLowerCase() === word.toLowerCase())) seen.words.push(word);
|
|
3060
|
+
}
|
|
3061
|
+
unmatched.set(key, seen);
|
|
3062
|
+
}
|
|
2975
3063
|
built.push({ key, raw, value: null, refusal: judged.refusal });
|
|
2976
3064
|
refusals.push({ fromLine, column: fieldName(field), raw, why: judged.refusal });
|
|
2977
3065
|
continue;
|
|
@@ -3007,7 +3095,12 @@ function planPastedBlock(args) {
|
|
|
3007
3095
|
creates: keepers.filter((r) => r.recordId === null).length,
|
|
3008
3096
|
cells,
|
|
3009
3097
|
refusals,
|
|
3010
|
-
pastRightEdge: args.pastRightEdge ?? 0
|
|
3098
|
+
pastRightEdge: args.pastRightEdge ?? 0,
|
|
3099
|
+
unmatchedOptions: [...unmatched.entries()].map(([fieldKey, seen]) => ({
|
|
3100
|
+
fieldKey,
|
|
3101
|
+
column: seen.column,
|
|
3102
|
+
words: seen.words
|
|
3103
|
+
}))
|
|
3011
3104
|
};
|
|
3012
3105
|
}
|
|
3013
3106
|
function documentOf(row) {
|
|
@@ -3089,13 +3182,29 @@ function Grid({
|
|
|
3089
3182
|
[client, host, refreshEnrich]
|
|
3090
3183
|
);
|
|
3091
3184
|
const [pastePlan, setPastePlan] = (0, import_react18.useState)(null);
|
|
3185
|
+
const choiceOptions = (0, import_react18.useRef)(/* @__PURE__ */ new Map());
|
|
3186
|
+
const loadChoiceOptions = (0, import_react18.useCallback)(
|
|
3187
|
+
async (fieldList) => {
|
|
3188
|
+
const wanted = fieldList.filter(
|
|
3189
|
+
(f) => (editorKindFor(f) === "select" || editorKindFor(f) === "multi_select") && !choiceOptions.current.has(f.key)
|
|
3190
|
+
);
|
|
3191
|
+
await Promise.all(
|
|
3192
|
+
wanted.map(async (field) => {
|
|
3193
|
+
const got = await client.fieldOptions({ field_id: field.id });
|
|
3194
|
+
if (got.ok) choiceOptions.current.set(field.key, choiceOptionsOf(got.data ?? []));
|
|
3195
|
+
})
|
|
3196
|
+
);
|
|
3197
|
+
},
|
|
3198
|
+
[client]
|
|
3199
|
+
);
|
|
3092
3200
|
const pasteableKeys = (0, import_react18.useMemo)(
|
|
3093
3201
|
() => ordered.filter((f) => fieldIsEditable(f)).map((f) => f.key),
|
|
3094
3202
|
[ordered]
|
|
3095
3203
|
);
|
|
3096
3204
|
const receivePastePlan = (0, import_react18.useCallback)(
|
|
3097
|
-
(sheetPlan) => {
|
|
3205
|
+
async (sheetPlan) => {
|
|
3098
3206
|
const fieldList = fields.data ?? [];
|
|
3207
|
+
await loadChoiceOptions(fieldList);
|
|
3099
3208
|
const byRecord = /* @__PURE__ */ new Map();
|
|
3100
3209
|
const order = [];
|
|
3101
3210
|
const take = (rowId, columnId, value) => {
|
|
@@ -3123,6 +3232,7 @@ function Grid({
|
|
|
3123
3232
|
);
|
|
3124
3233
|
const built = planPastedBlock({
|
|
3125
3234
|
fields: fieldList,
|
|
3235
|
+
options: choiceOptions.current,
|
|
3126
3236
|
columnKeys,
|
|
3127
3237
|
onExisting: touched.map((recordId) => ({
|
|
3128
3238
|
recordId,
|
|
@@ -3146,7 +3256,37 @@ function Grid({
|
|
|
3146
3256
|
}
|
|
3147
3257
|
setPastePlan({ plan: built });
|
|
3148
3258
|
},
|
|
3149
|
-
[fields.data, rows]
|
|
3259
|
+
[fields.data, loadChoiceOptions, rows]
|
|
3260
|
+
);
|
|
3261
|
+
const addPastedOptions = (0, import_react18.useCallback)(
|
|
3262
|
+
async (fieldKey, words) => {
|
|
3263
|
+
const field = (fields.data ?? []).find((f) => f.key === fieldKey);
|
|
3264
|
+
if (!field) throw new Error(`This table has no column called ${fieldKey}.`);
|
|
3265
|
+
const existing = choiceOptions.current.get(fieldKey)?.labels ?? [];
|
|
3266
|
+
const written = await client.fieldUpdate({
|
|
3267
|
+
field_id: field.id,
|
|
3268
|
+
patch: { options: [...existing, ...words] }
|
|
3269
|
+
});
|
|
3270
|
+
if (!written.ok) throw new Error(written.error.message);
|
|
3271
|
+
choiceOptions.current.delete(fieldKey);
|
|
3272
|
+
await loadChoiceOptions(fields.data ?? []);
|
|
3273
|
+
setPastePlan(
|
|
3274
|
+
(held) => held ? {
|
|
3275
|
+
plan: planPastedBlock({
|
|
3276
|
+
fields: fields.data ?? [],
|
|
3277
|
+
options: choiceOptions.current,
|
|
3278
|
+
columnKeys: held.plan.rows[0]?.cells.map((c) => c.key) ?? [],
|
|
3279
|
+
onExisting: held.plan.rows.filter((r) => r.recordId !== null).map((r) => ({
|
|
3280
|
+
recordId: r.recordId,
|
|
3281
|
+
raw: r.cells.map((c) => c.raw)
|
|
3282
|
+
})),
|
|
3283
|
+
onNew: held.plan.rows.filter((r) => r.recordId === null).map((r) => r.cells.map((c) => c.raw)),
|
|
3284
|
+
pastRightEdge: held.plan.pastRightEdge
|
|
3285
|
+
})
|
|
3286
|
+
} : held
|
|
3287
|
+
);
|
|
3288
|
+
},
|
|
3289
|
+
[client, fields.data, loadChoiceOptions]
|
|
3150
3290
|
);
|
|
3151
3291
|
const applyPaste = (0, import_react18.useCallback)(async () => {
|
|
3152
3292
|
const held = pastePlan;
|
|
@@ -3154,39 +3294,56 @@ function Grid({
|
|
|
3154
3294
|
const refusals = [];
|
|
3155
3295
|
let written = 0;
|
|
3156
3296
|
const expected = [];
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3297
|
+
const creates = held.plan.rows.filter((row) => row.recordId === null).map((row) => ({ row, document: documentOf(row) })).filter((c) => Object.keys(c.document).length > 0);
|
|
3298
|
+
const updates = held.plan.rows.filter((row) => row.recordId !== null).map((row) => ({ row, document: documentOf(row) })).filter((c) => Object.keys(c.document).length > 0);
|
|
3299
|
+
for (let at = 0; at < creates.length; at += pageSize) {
|
|
3300
|
+
const chunk = creates.slice(at, at + pageSize);
|
|
3301
|
+
const made = await client.recordWriteMany({
|
|
3302
|
+
table_id: tableId,
|
|
3303
|
+
rows: chunk.map((c) => c.document)
|
|
3304
|
+
});
|
|
3305
|
+
if (!made.ok) {
|
|
3306
|
+
refusals.push(
|
|
3307
|
+
`Rows ${chunk[0].row.fromLine}-${chunk[chunk.length - 1].row.fromLine}: ${made.error.message}`
|
|
3308
|
+
);
|
|
3309
|
+
continue;
|
|
3310
|
+
}
|
|
3311
|
+
chunk.forEach((c, index) => {
|
|
3312
|
+
const id = made.data[index];
|
|
3313
|
+
if (id === void 0) {
|
|
3314
|
+
refusals.push(`Row ${c.row.fromLine}: the store wrote the batch and named no id for this line.`);
|
|
3315
|
+
return;
|
|
3173
3316
|
}
|
|
3174
3317
|
written += 1;
|
|
3175
|
-
expected.push({ recordId:
|
|
3318
|
+
expected.push({ recordId: id, document: c.document });
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
for (const { row, document: document2 } of updates) {
|
|
3322
|
+
const done = await client.recordUpdate({ record_id: row.recordId, patch: document2 });
|
|
3323
|
+
if (!done.ok) {
|
|
3324
|
+
refusals.push(`Row ${row.fromLine}: ${done.error.message}`);
|
|
3325
|
+
continue;
|
|
3176
3326
|
}
|
|
3327
|
+
written += 1;
|
|
3328
|
+
expected.push({ recordId: row.recordId, document: document2 });
|
|
3177
3329
|
}
|
|
3178
3330
|
let readBack = 0;
|
|
3179
3331
|
let missing = 0;
|
|
3180
3332
|
const seen = /* @__PURE__ */ new Map();
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3333
|
+
const READ_BACK_PAGE = 500;
|
|
3334
|
+
for (let offset = 0; ; offset += READ_BACK_PAGE) {
|
|
3335
|
+
const reread = await client.list({ table_id: tableId, limit: READ_BACK_PAGE, offset });
|
|
3336
|
+
if (!reread.ok) {
|
|
3337
|
+
refusals.push(
|
|
3338
|
+
`The rows were written and this could not read them back to check: ${reread.error.message}`
|
|
3339
|
+
);
|
|
3340
|
+
break;
|
|
3341
|
+
}
|
|
3183
3342
|
for (const row of reread.data.rows) {
|
|
3184
3343
|
seen.set(row.id, row.document ?? {});
|
|
3185
3344
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
`The rows were written and this could not read them back to check: ${reread.error.message}`
|
|
3189
|
-
);
|
|
3345
|
+
if (reread.data.rows.length < READ_BACK_PAGE) break;
|
|
3346
|
+
if (expected.every((e) => seen.has(e.recordId))) break;
|
|
3190
3347
|
}
|
|
3191
3348
|
for (const entry of expected) {
|
|
3192
3349
|
const stored = seen.get(entry.recordId);
|
|
@@ -3459,6 +3616,7 @@ function Grid({
|
|
|
3459
3616
|
noun: table.data?.label_singular ?? "record",
|
|
3460
3617
|
nounPlural: table.data?.label_plural ?? `${table.data?.label_singular ?? "record"}s`,
|
|
3461
3618
|
onConfirm: applyPaste,
|
|
3619
|
+
onAddOptions: addPastedOptions,
|
|
3462
3620
|
onCancel: () => setPastePlan(null)
|
|
3463
3621
|
}
|
|
3464
3622
|
) : null,
|
|
@@ -9133,7 +9291,7 @@ function PortalBuilder({ tableId, portalId, onSaved, onClose, className }) {
|
|
|
9133
9291
|
// src/PortalsPanel.tsx
|
|
9134
9292
|
var import_react65 = require("react");
|
|
9135
9293
|
var import_react66 = require("@ai-matrx/records/react");
|
|
9136
|
-
var
|
|
9294
|
+
var import_records6 = require("@ai-matrx/records");
|
|
9137
9295
|
var import_design_system32 = require("@ai-matrx/design-system");
|
|
9138
9296
|
|
|
9139
9297
|
// src/BuildOrAsk.tsx
|
|
@@ -9250,7 +9408,7 @@ function PortalsPanel({ tableId, className }) {
|
|
|
9250
9408
|
}
|
|
9251
9409
|
) : null,
|
|
9252
9410
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsx)("ul", { className: "flex flex-col gap-2", children: portals.map((portal) => {
|
|
9253
|
-
const url = `${origin}${(0,
|
|
9411
|
+
const url = `${origin}${(0, import_records6.portalPath)(portal.slug)}`;
|
|
9254
9412
|
const open = openId === portal.portal_id;
|
|
9255
9413
|
return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("li", { className: "rounded-md border border-border bg-card p-2.5", children: [
|
|
9256
9414
|
/* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
@@ -10154,7 +10312,7 @@ function SubscriptionsPanel({ tableId, className }) {
|
|
|
10154
10312
|
// src/FormBuilder.tsx
|
|
10155
10313
|
var import_react73 = require("react");
|
|
10156
10314
|
var import_react74 = require("@ai-matrx/records/react");
|
|
10157
|
-
var
|
|
10315
|
+
var import_records7 = require("@ai-matrx/records");
|
|
10158
10316
|
var import_design_system36 = require("@ai-matrx/design-system");
|
|
10159
10317
|
|
|
10160
10318
|
// src/FormRunner.tsx
|
|
@@ -10449,6 +10607,28 @@ function Question({
|
|
|
10449
10607
|
] });
|
|
10450
10608
|
}
|
|
10451
10609
|
|
|
10610
|
+
// src/publicQuestions.ts
|
|
10611
|
+
var CANNOT_ASK = {
|
|
10612
|
+
relation: "points at a record in another table, and somebody with no account cannot see that table to pick from it",
|
|
10613
|
+
member: "is a person in this organization, and somebody outside it has no way to choose one",
|
|
10614
|
+
attachment: "holds a file in your own store, and a public page has nowhere to put one yet",
|
|
10615
|
+
formula: "is worked out by the store, so there is nothing for anybody to answer",
|
|
10616
|
+
lookup: "is read through a relation, so the store fills it in",
|
|
10617
|
+
rollup: "is added up by the store, so there is nothing for anybody to answer"
|
|
10618
|
+
};
|
|
10619
|
+
function publiclyAnswerable(field) {
|
|
10620
|
+
return !(field.type in CANNOT_ASK);
|
|
10621
|
+
}
|
|
10622
|
+
function askableFields(fields, hidden = []) {
|
|
10623
|
+
return fields.filter((f) => publiclyAnswerable(f) && !hidden.includes(f.key));
|
|
10624
|
+
}
|
|
10625
|
+
function whyNotAskable(fields, hidden = []) {
|
|
10626
|
+
const left = fields.filter((f) => !publiclyAnswerable(f) && !hidden.includes(f.key));
|
|
10627
|
+
if (left.length === 0) return null;
|
|
10628
|
+
const said = left.map((f) => `${f.label || f.key} ${CANNOT_ASK[f.type]}`);
|
|
10629
|
+
return said.length === 1 ? `${said[0]}, so it is not offered here.` : `These are not offered here: ${said.join("; ")}.`;
|
|
10630
|
+
}
|
|
10631
|
+
|
|
10452
10632
|
// src/FormBuilder.tsx
|
|
10453
10633
|
var import_jsx_runtime39 = require("react/jsx-runtime");
|
|
10454
10634
|
function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
@@ -10607,7 +10787,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
|
10607
10787
|
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
10608
10788
|
PublishRow,
|
|
10609
10789
|
{
|
|
10610
|
-
url: `${publicOrigin}${(0,
|
|
10790
|
+
url: `${publicOrigin}${(0, import_records7.publicFormPath)(draft.id)}`,
|
|
10611
10791
|
state: draft.state,
|
|
10612
10792
|
mayPublish: rights.structure,
|
|
10613
10793
|
why: rights.why("structure"),
|
|
@@ -10681,7 +10861,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
|
10681
10861
|
" fields"
|
|
10682
10862
|
] })
|
|
10683
10863
|
] }),
|
|
10684
|
-
(fields.data ?? []).map((field) => {
|
|
10864
|
+
askableFields(fields.data ?? []).map((field) => {
|
|
10685
10865
|
const index = draft.questions.findIndex((q) => q.field === field.key);
|
|
10686
10866
|
const asked = index >= 0;
|
|
10687
10867
|
const question = asked ? draft.questions[index] : null;
|
|
@@ -10733,6 +10913,7 @@ function FormBuilder({ tableId, seed, activeFormId, onActiveForm, className }) {
|
|
|
10733
10913
|
] }) : null
|
|
10734
10914
|
] }, field.id);
|
|
10735
10915
|
}),
|
|
10916
|
+
whyNotAskable(fields.data ?? []) ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("p", { className: "text-xs text-muted-foreground", children: whyNotAskable(fields.data ?? []) }) : null,
|
|
10736
10917
|
draft.questions.filter((q) => !byKey.has(q.field)).map((q) => /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("p", { className: "text-xs text-destructive", children: [
|
|
10737
10918
|
'This form asks for "',
|
|
10738
10919
|
q.field,
|
|
@@ -11672,7 +11853,7 @@ function NotifyRuleEditor({ tableId, seed, className }) {
|
|
|
11672
11853
|
// src/ChartBlock.tsx
|
|
11673
11854
|
var import_react85 = require("react");
|
|
11674
11855
|
var import_recharts = require("recharts");
|
|
11675
|
-
var
|
|
11856
|
+
var import_records8 = require("@ai-matrx/records");
|
|
11676
11857
|
var import_core6 = require("@ai-matrx/records/core");
|
|
11677
11858
|
var import_design_system42 = require("@ai-matrx/design-system");
|
|
11678
11859
|
var import_jsx_runtime45 = require("react/jsx-runtime");
|
|
@@ -11693,7 +11874,7 @@ function ChartBlock({ block, subject, className }) {
|
|
|
11693
11874
|
const kind = block.kind;
|
|
11694
11875
|
const tableId = block.table_id ?? subject;
|
|
11695
11876
|
const measures = block.measures && block.measures.length > 0 ? block.measures : [{ op: "count" }];
|
|
11696
|
-
const primary = (0,
|
|
11877
|
+
const primary = (0, import_records8.measureKey)(measures[0]);
|
|
11697
11878
|
const series = [primary];
|
|
11698
11879
|
const points = (0, import_react85.useMemo)(() => {
|
|
11699
11880
|
if (kind === "stuck") return [];
|
|
@@ -12227,7 +12408,7 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
|
|
|
12227
12408
|
// src/FormsPanel.tsx
|
|
12228
12409
|
var import_react88 = require("react");
|
|
12229
12410
|
var import_react89 = require("@ai-matrx/records/react");
|
|
12230
|
-
var
|
|
12411
|
+
var import_records9 = require("@ai-matrx/records");
|
|
12231
12412
|
var import_design_system44 = require("@ai-matrx/design-system");
|
|
12232
12413
|
var import_jsx_runtime47 = require("react/jsx-runtime");
|
|
12233
12414
|
var WHAT_A_FORM_IS = "A form asks for this table's own fields, and its answers land here as ordinary records stamped with the form they came through.";
|
|
@@ -12322,7 +12503,7 @@ function FormsPanel({ tableId, className }) {
|
|
|
12322
12503
|
}
|
|
12323
12504
|
) : null,
|
|
12324
12505
|
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { className: "flex flex-col gap-2", children: forms.map((form) => {
|
|
12325
|
-
const url = `${origin}${(0,
|
|
12506
|
+
const url = `${origin}${(0, import_records9.publicFormPath)(form.form_id)}`;
|
|
12326
12507
|
return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("li", { className: "rounded-md border p-2.5", children: [
|
|
12327
12508
|
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
12328
12509
|
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { className: "min-w-0 flex-1 truncate text-sm font-medium", children: form.title ?? form.slug }),
|
|
@@ -12374,7 +12555,7 @@ function FormsPanel({ tableId, className }) {
|
|
|
12374
12555
|
// src/BookingBuilder.tsx
|
|
12375
12556
|
var import_react90 = require("react");
|
|
12376
12557
|
var import_react91 = require("@ai-matrx/records/react");
|
|
12377
|
-
var
|
|
12558
|
+
var import_records10 = require("@ai-matrx/records");
|
|
12378
12559
|
var import_design_system45 = require("@ai-matrx/design-system");
|
|
12379
12560
|
var import_jsx_runtime48 = require("react/jsx-runtime");
|
|
12380
12561
|
var STORE_ANSWERS_THESE = ["slot", "status", "booked_with"];
|
|
@@ -12388,6 +12569,7 @@ var DAYS = [
|
|
|
12388
12569
|
{ weekday: 0, label: "Sun" }
|
|
12389
12570
|
];
|
|
12390
12571
|
var LENGTHS = [15, 20, 30, 45, 60, 90, 120];
|
|
12572
|
+
var WORKING_WEEK = /* @__PURE__ */ new Set([1, 2, 3, 4, 5]);
|
|
12391
12573
|
function draftWindows(availability) {
|
|
12392
12574
|
return DAYS.map((day) => {
|
|
12393
12575
|
const found = availability?.windows.find((w) => w.weekday === day.weekday);
|
|
@@ -12395,7 +12577,7 @@ function draftWindows(availability) {
|
|
|
12395
12577
|
weekday: day.weekday,
|
|
12396
12578
|
from: found?.from ?? "09:00",
|
|
12397
12579
|
to: found?.to ?? "17:00",
|
|
12398
|
-
on: Boolean(found)
|
|
12580
|
+
on: availability ? Boolean(found) : WORKING_WEEK.has(day.weekday)
|
|
12399
12581
|
};
|
|
12400
12582
|
});
|
|
12401
12583
|
}
|
|
@@ -12424,9 +12606,11 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
|
|
|
12424
12606
|
const [formId, setFormId] = (0, import_react90.useState)(bookingId ?? null);
|
|
12425
12607
|
const publicOrigin = host.publicOrigin ?? (typeof window === "undefined" ? "" : window.location.origin);
|
|
12426
12608
|
const askable = (0, import_react90.useMemo)(
|
|
12427
|
-
() => (fields.data ?? [])
|
|
12428
|
-
|
|
12429
|
-
|
|
12609
|
+
() => askableFields(fields.data ?? [], STORE_ANSWERS_THESE),
|
|
12610
|
+
[fields.data]
|
|
12611
|
+
);
|
|
12612
|
+
const leftOut = (0, import_react90.useMemo)(
|
|
12613
|
+
() => whyNotAskable(fields.data ?? [], STORE_ANSWERS_THESE),
|
|
12430
12614
|
[fields.data]
|
|
12431
12615
|
);
|
|
12432
12616
|
const load = (0, import_react90.useCallback)(async () => {
|
|
@@ -12452,6 +12636,15 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
|
|
|
12452
12636
|
if (!existing) return;
|
|
12453
12637
|
setMinutes(existing.slot_minutes);
|
|
12454
12638
|
}, [existing]);
|
|
12639
|
+
(0, import_react90.useEffect)(() => {
|
|
12640
|
+
if (!offer) return;
|
|
12641
|
+
setWindows(draftWindows(offer));
|
|
12642
|
+
setMinutes(offer.slot_minutes);
|
|
12643
|
+
setBuffer(offer.buffer_minutes);
|
|
12644
|
+
setLead(offer.lead_minutes);
|
|
12645
|
+
setPerDay(offer.max_per_day);
|
|
12646
|
+
setDays(offer.days);
|
|
12647
|
+
}, [offer]);
|
|
12455
12648
|
const availability = (0, import_react90.useCallback)(
|
|
12456
12649
|
() => ({
|
|
12457
12650
|
slot_minutes: minutes,
|
|
@@ -12505,7 +12698,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
|
|
|
12505
12698
|
if (!rights.structure) {
|
|
12506
12699
|
return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: (0, import_design_system45.cn)("text-xs text-muted-foreground", className), children: rights.why("structure") ?? "Making a booking page needs the admin level on this table, because it lets people with no account take time in your calendar." });
|
|
12507
12700
|
}
|
|
12508
|
-
const url = formId ? `${publicOrigin}${(0,
|
|
12701
|
+
const url = formId ? `${publicOrigin}${(0, import_records10.bookingPath)(formId)}` : null;
|
|
12509
12702
|
const shown = offer ?? null;
|
|
12510
12703
|
return (
|
|
12511
12704
|
// THE RAIL IS 384px WIDE AND THE VIEWPORT IS NOT (walk 1, 2026-09-21):
|
|
@@ -12639,7 +12832,8 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
|
|
|
12639
12832
|
),
|
|
12640
12833
|
fieldName(f)
|
|
12641
12834
|
] }, f.key)) }),
|
|
12642
|
-
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-xs text-muted-foreground", children: "The time, whether it is booked or cancelled, and who it is with are filled in by the store \u2014 a booking page cannot ask a visitor for any of the three." })
|
|
12835
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-xs text-muted-foreground", children: "The time, whether it is booked or cancelled, and who it is with are filled in by the store \u2014 a booking page cannot ask a visitor for any of the three." }),
|
|
12836
|
+
leftOut ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { className: "text-xs text-muted-foreground", children: leftOut }) : null
|
|
12643
12837
|
] }),
|
|
12644
12838
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("label", { className: "flex flex-col gap-1", children: [
|
|
12645
12839
|
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_design_system45.Label, { className: "text-xs font-medium", children: "What they see after booking" }),
|
|
@@ -12706,7 +12900,7 @@ function BookingBuilder({ tableId, bookingId, onSaved, onClose, className }) {
|
|
|
12706
12900
|
// src/BookingSlots.tsx
|
|
12707
12901
|
var import_react92 = require("react");
|
|
12708
12902
|
var import_react93 = require("@ai-matrx/records/react");
|
|
12709
|
-
var
|
|
12903
|
+
var import_records11 = require("@ai-matrx/records");
|
|
12710
12904
|
var import_design_system46 = require("@ai-matrx/design-system");
|
|
12711
12905
|
var import_jsx_runtime49 = require("react/jsx-runtime");
|
|
12712
12906
|
var WHAT_A_BOOKING_PAGE_IS = "A booking page offers times you are free and writes each appointment into this table as an ordinary record.";
|
|
@@ -12814,7 +13008,7 @@ function BookingSlots({ tableId, className }) {
|
|
|
12814
13008
|
" Open the table the appointments should land in to make one."
|
|
12815
13009
|
] }) : null,
|
|
12816
13010
|
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("ul", { className: "flex flex-col gap-2", children: pages.map((page) => {
|
|
12817
|
-
const url = `${origin}${(0,
|
|
13011
|
+
const url = `${origin}${(0, import_records11.bookingPath)(page.form_id)}`;
|
|
12818
13012
|
const mayOpen = levels.data?.[page.table_id] === "admin";
|
|
12819
13013
|
const open = page.published_at !== null && page.closed_at === null;
|
|
12820
13014
|
return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { className: "rounded-md border p-2.5", "data-testid": "booking-page", children: [
|
|
@@ -12903,7 +13097,7 @@ var import_design_system48 = require("@ai-matrx/design-system");
|
|
|
12903
13097
|
|
|
12904
13098
|
// src/CaptureRun.tsx
|
|
12905
13099
|
var import_react94 = require("react");
|
|
12906
|
-
var
|
|
13100
|
+
var import_records12 = require("@ai-matrx/records");
|
|
12907
13101
|
var import_react95 = require("@ai-matrx/records/react");
|
|
12908
13102
|
var import_design_system47 = require("@ai-matrx/design-system");
|
|
12909
13103
|
var import_jsx_runtime50 = require("react/jsx-runtime");
|
|
@@ -12958,7 +13152,7 @@ function CaptureRun({ sheetId, face: given, className }) {
|
|
|
12958
13152
|
const [sending, setSending] = (0, import_react94.useState)(false);
|
|
12959
13153
|
const queueRef = (0, import_react94.useRef)(null);
|
|
12960
13154
|
(0, import_react94.useEffect)(() => {
|
|
12961
|
-
const q2 = (0,
|
|
13155
|
+
const q2 = (0, import_records12.openCaptureQueue)({
|
|
12962
13156
|
onChange: (c, all) => {
|
|
12963
13157
|
setCounts(c);
|
|
12964
13158
|
setItems(all);
|