@ai-matrx/records-ui 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2114,7 +2114,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2114
2114
  startedFrom.current = null;
2115
2115
  }, []);
2116
2116
  const writeCellNow = useCallback4(
2117
- async (rowId, key, value) => {
2117
+ async (rowId, key, value, resent = false) => {
2118
2118
  const id = cellKey(rowId, key);
2119
2119
  setOptimistic((held) => ({ ...held, [id]: value }));
2120
2120
  setSaving((held) => ({ ...held, [id]: true }));
@@ -2157,16 +2157,25 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2157
2157
  delete next[id];
2158
2158
  return next;
2159
2159
  });
2160
- if (written.error.code === "stale_write") versions.current.delete(rowId);
2160
+ const conflict = asWriteConflict(written.error, rowId);
2161
+ if (written.error.code === "stale_write") {
2162
+ if (conflict) rememberVersion(versions.current, rowId, conflict.current_version);
2163
+ else versions.current.delete(rowId);
2164
+ if (conflict && !resent && !Object.prototype.hasOwnProperty.call(conflict.contested_fields, key)) {
2165
+ await writeCellNow(rowId, key, value, true);
2166
+ return;
2167
+ }
2168
+ }
2161
2169
  setRefusals((held) => ({
2162
2170
  ...held,
2163
2171
  [id]: {
2164
2172
  error: written.error,
2165
- conflict: asWriteConflict(written.error, rowId),
2173
+ conflict,
2166
2174
  attempted: value
2167
2175
  }
2168
2176
  }));
2169
2177
  },
2178
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2170
2179
  [client, reload]
2171
2180
  );
2172
2181
  const writeCell = useCallback4(
@@ -2241,6 +2250,33 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2241
2250
  },
2242
2251
  [reload]
2243
2252
  );
2253
+ const keepEditing = useCallback4(
2254
+ (rowId, key) => {
2255
+ const held = refusals[cellKey(rowId, key)];
2256
+ if (!held || !writable(rowId)) return;
2257
+ startedFrom.current = shownValue(rowId, key);
2258
+ typed.current = held.attempted;
2259
+ setDraft(held.attempted);
2260
+ setEditing({ rowId, key });
2261
+ },
2262
+ [refusals, writable, shownValue]
2263
+ );
2264
+ const discard = useCallback4((rowId, key) => {
2265
+ const id = cellKey(rowId, key);
2266
+ setRefusals((held) => {
2267
+ if (!held[id]) return held;
2268
+ const next = { ...held };
2269
+ delete next[id];
2270
+ return next;
2271
+ });
2272
+ setOptimistic((held) => {
2273
+ if (!(id in held)) return held;
2274
+ const next = { ...held };
2275
+ delete next[id];
2276
+ return next;
2277
+ });
2278
+ setEditing((open) => open && open.rowId === rowId && open.key === key ? null : open);
2279
+ }, []);
2244
2280
  const remove = useCallback4(
2245
2281
  async (rowId) => {
2246
2282
  setBusy(true);
@@ -2292,6 +2328,8 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2292
2328
  },
2293
2329
  retry,
2294
2330
  keepTheirs,
2331
+ keepEditing,
2332
+ discard,
2295
2333
  remove,
2296
2334
  addRecord,
2297
2335
  rowError,
@@ -2360,13 +2398,28 @@ function GridCell({
2360
2398
  draft: editing.draft,
2361
2399
  onType: editing.type,
2362
2400
  onCommit: (move) => editing.commit(move),
2363
- onCancel: editing.cancel
2401
+ onCancel: editing.cancel,
2402
+ below: (
2403
+ // KEPT EDITING: the refusal stays beside the value it is about, so the
2404
+ // person reads what to change while changing it. Keep editing is the
2405
+ // state they are in, so only Discard is offered. It is INSIDE the open
2406
+ // cell, so pressing Discard is not "focus left the cell" — which would
2407
+ // commit the refused value one more time on its way out.
2408
+ refusal2 && !refusal2.conflict ? /* @__PURE__ */ jsx10(
2409
+ RefusalNotice,
2410
+ {
2411
+ error: refusal2.error,
2412
+ className: "py-1.5",
2413
+ onDiscard: () => editing.discard(row.id, field.key)
2414
+ }
2415
+ ) : null
2416
+ )
2364
2417
  }
2365
2418
  );
2366
2419
  }
2367
2420
  if (editorKindFor(field) === "checkbox") {
2368
2421
  const answered = value === true || value === false;
2369
- return /* @__PURE__ */ jsxs6("span", { "data-matrx-cell-control": "", className: "flex w-full items-center gap-1.5", children: [
2422
+ return /* @__PURE__ */ jsxs6("span", { "data-matrx-cell-control": "", className: "flex w-full flex-wrap items-center gap-1.5", children: [
2370
2423
  /* @__PURE__ */ jsx10(
2371
2424
  Checkbox3,
2372
2425
  {
@@ -2379,10 +2432,7 @@ function GridCell({
2379
2432
  }
2380
2433
  ),
2381
2434
  answered ? null : /* @__PURE__ */ jsx10("span", { className: "text-xs text-muted-foreground", title: "nobody has answered this yet", children: "\u2014" }),
2382
- refusal2 ? /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-1 rounded border border-destructive/40 px-1", children: [
2383
- /* @__PURE__ */ jsx10(RefusalLine, { error: refusal2.error }),
2384
- /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "outline", onClick: () => editing?.retry(row.id, field.key), children: "Try again" })
2385
- ] }) : null
2435
+ refusal2 ? /* @__PURE__ */ jsx10(CellRefusalNotice, { refusal: refusal2, field, row, editing, typed: false }) : null
2386
2436
  ] });
2387
2437
  }
2388
2438
  const openThisCell = (event) => {
@@ -2455,22 +2505,48 @@ function GridCell({
2455
2505
  ),
2456
2506
  onAskWhoChanged ? /* @__PURE__ */ jsx10(WhoBadge, { field, row, onAskWhoChanged }) : null,
2457
2507
  state === "saving" ? /* @__PURE__ */ jsx10("span", { className: "px-1 text-[10px] text-muted-foreground", children: "Saving\u2026" }) : null,
2458
- refusal2 ? /* @__PURE__ */ jsxs6("span", { className: "flex flex-col gap-0.5 rounded border border-destructive/40 p-1", children: [
2459
- /* @__PURE__ */ jsx10(RefusalLine, { error: refusal2.error }),
2460
- refusal2.conflict ? /* @__PURE__ */ jsxs6("span", { className: "text-[11px]", children: [
2461
- "Theirs: ",
2462
- /* @__PURE__ */ jsx10("strong", { children: formatTheirs(refusal2.conflict, field.key) })
2463
- ] }) : null,
2464
- /* @__PURE__ */ jsxs6("span", { className: "flex gap-1", children: [
2465
- /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "outline", onClick: () => editing?.retry(row.id, field.key), children: "Try again" }),
2466
- /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "ghost", onClick: () => editing?.keepTheirs(row.id, field.key), children: "Keep theirs" })
2467
- ] })
2468
- ] }) : null
2508
+ refusal2 ? /* @__PURE__ */ jsx10(CellRefusalNotice, { refusal: refusal2, field, row, editing, typed: true }) : null
2469
2509
  ]
2470
2510
  }
2471
2511
  )
2472
2512
  );
2473
2513
  }
2514
+ function CellRefusalNotice({
2515
+ refusal: refusal2,
2516
+ field,
2517
+ row,
2518
+ editing,
2519
+ typed
2520
+ }) {
2521
+ if (refusal2.conflict) {
2522
+ return /* @__PURE__ */ jsx10(
2523
+ RefusalNotice,
2524
+ {
2525
+ error: refusal2.error,
2526
+ className: "py-1.5",
2527
+ actions: /* @__PURE__ */ jsxs6("span", { className: "flex flex-col gap-1", "data-refusal-conflict": "", children: [
2528
+ /* @__PURE__ */ jsxs6("span", { className: "text-[11px]", children: [
2529
+ "Theirs: ",
2530
+ /* @__PURE__ */ jsx10("strong", { children: formatTheirs(refusal2.conflict, field.key) })
2531
+ ] }),
2532
+ /* @__PURE__ */ jsxs6("span", { className: "flex flex-wrap gap-1", children: [
2533
+ /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "outline", onClick: () => editing?.retry(row.id, field.key), children: "Try again" }),
2534
+ /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "ghost", onClick: () => editing?.keepTheirs(row.id, field.key), children: "Keep theirs" })
2535
+ ] })
2536
+ ] })
2537
+ }
2538
+ );
2539
+ }
2540
+ return /* @__PURE__ */ jsx10(
2541
+ RefusalNotice,
2542
+ {
2543
+ error: refusal2.error,
2544
+ className: "py-1.5",
2545
+ ...typed ? { onKeepEditing: () => editing?.keepEditing(row.id, field.key) } : {},
2546
+ onDiscard: () => editing?.discard(row.id, field.key)
2547
+ }
2548
+ );
2549
+ }
2474
2550
  function WhoBadge({
2475
2551
  field,
2476
2552
  row,
@@ -2523,7 +2599,8 @@ function OpenCell({
2523
2599
  draft,
2524
2600
  onType,
2525
2601
  onCommit,
2526
- onCancel
2602
+ onCancel,
2603
+ below
2527
2604
  }) {
2528
2605
  const multiline = takesANewline(field);
2529
2606
  const blurCommits = closesOnBlur(field);
@@ -2552,6 +2629,7 @@ function OpenCell({
2552
2629
  className: "flex w-full min-w-[10rem] flex-col gap-1",
2553
2630
  onDoubleClick: (event) => event.stopPropagation(),
2554
2631
  onKeyDown: (event) => {
2632
+ if (event.target?.closest?.("[data-refusal-notice]")) return;
2555
2633
  if (event.key === "Escape") {
2556
2634
  event.preventDefault();
2557
2635
  event.stopPropagation();
@@ -2577,7 +2655,8 @@ function OpenCell({
2577
2655
  blurCommits ? /* @__PURE__ */ jsx10("span", { className: "text-[10px] text-muted-foreground", children: multiline ? "\u23CE saves \xB7 \u21E7\u23CE new line \xB7 Esc cancels \xB7 Tab next" : "\u23CE saves \xB7 Esc cancels \xB7 Tab next" }) : /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-1", children: [
2578
2656
  /* @__PURE__ */ jsx10(Button5, { size: "sm", onClick: () => once(() => onCommit(null)), children: "Save" }),
2579
2657
  /* @__PURE__ */ jsx10(Button5, { size: "sm", variant: "ghost", onClick: () => once(onCancel), children: "Cancel" })
2580
- ] })
2658
+ ] }),
2659
+ below
2581
2660
  ]
2582
2661
  }
2583
2662
  );
@@ -6604,8 +6683,8 @@ function TableSettings({
6604
6683
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "ghost", onClick: () => setAskingToRemove(null), children: "Keep it" })
6605
6684
  ] }) : null,
6606
6685
  /* @__PURE__ */ jsxs17("ul", { className: "divide-y rounded border", children: [
6607
- rows.map((field, index) => /* @__PURE__ */ jsxs17("li", { className: "flex items-center gap-2 px-2 py-1.5", children: [
6608
- /* @__PURE__ */ jsx21("span", { className: "min-w-0 flex-1 truncate", children: fieldName(field) }),
6686
+ rows.map((field, index) => /* @__PURE__ */ jsxs17("li", { "data-settings-field": "", className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-2 py-1.5", children: [
6687
+ /* @__PURE__ */ jsx21("span", { "data-settings-field-name": "", className: "min-w-[8rem] flex-1 break-words", children: fieldName(field) }),
6609
6688
  /* @__PURE__ */ jsx21(Badge6, { variant: "outline", className: "text-[10px] font-normal", children: fieldTypeLabel(field) }),
6610
6689
  field.required ? /* @__PURE__ */ jsx21("span", { className: "text-[10px] text-destructive", children: "required" }) : null,
6611
6690
  /* @__PURE__ */ jsx21(
@@ -6662,9 +6741,9 @@ function TableSettings({
6662
6741
  /* @__PURE__ */ jsx21("span", { className: "font-medium", children: "Proposed" }),
6663
6742
  /* @__PURE__ */ jsx21("span", { className: "text-muted-foreground", children: proposals?.length ?? 0 })
6664
6743
  ] }),
6665
- proposals === void 0 ? /* @__PURE__ */ jsx21("p", { className: "text-muted-foreground", children: "Nothing is listing proposals here yet: the store has no `custom.field_propose` door, so this panel would be inventing an empty queue. When the door lands, its rows appear in this list with the same accept and reject." }) : proposals.length === 0 ? /* @__PURE__ */ jsx21("p", { className: "text-muted-foreground", children: "No field is waiting for a decision." }) : /* @__PURE__ */ jsx21("ul", { className: "divide-y rounded border", children: proposals.map((proposal) => /* @__PURE__ */ jsxs17("li", { className: "flex items-center gap-2 px-2 py-1.5", children: [
6666
- /* @__PURE__ */ jsx21("span", { className: "min-w-0 flex-1 truncate", children: proposal.field.label || proposal.field.key }),
6667
- /* @__PURE__ */ jsx21("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", children: proposal.why }),
6744
+ proposals === void 0 ? /* @__PURE__ */ jsx21("p", { className: "text-muted-foreground", children: "Nothing is listing proposals here yet: the store has no `custom.field_propose` door, so this panel would be inventing an empty queue. When the door lands, its rows appear in this list with the same accept and reject." }) : proposals.length === 0 ? /* @__PURE__ */ jsx21("p", { className: "text-muted-foreground", children: "No field is waiting for a decision." }) : /* @__PURE__ */ jsx21("ul", { className: "divide-y rounded border", children: proposals.map((proposal) => /* @__PURE__ */ jsxs17("li", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-2 py-1.5", children: [
6745
+ /* @__PURE__ */ jsx21("span", { className: "min-w-[8rem] flex-1 break-words", children: proposal.field.label || proposal.field.key }),
6746
+ /* @__PURE__ */ jsx21("span", { className: "min-w-[8rem] flex-1 break-words text-muted-foreground", children: proposal.why }),
6668
6747
  /* @__PURE__ */ jsx21(Badge6, { variant: "secondary", className: "text-[10px] font-normal", children: proposal.proposed_by }),
6669
6748
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "outline", onClick: () => onAcceptProposal?.(proposal), children: "Accept" }),
6670
6749
  /* @__PURE__ */ jsx21(Button16, { size: "sm", variant: "ghost", onClick: () => onRejectProposal?.(proposal), children: "Reject" })
@@ -7033,6 +7112,8 @@ function RecordForm({
7033
7112
  const mutation = useRecordMutation();
7034
7113
  const [draft, setDraft] = useState21({});
7035
7114
  const [touched, setTouched] = useState21(false);
7115
+ const [changed, setChanged] = useState21({});
7116
+ const [answered, setAnswered] = useState21(null);
7036
7117
  const loaded = useRecordVersion(recordId ?? null);
7037
7118
  const loadedVersion = loaded.version;
7038
7119
  useEffect12(() => {
@@ -7053,8 +7134,38 @@ function RecordForm({
7053
7134
  );
7054
7135
  if (fields.error) return /* @__PURE__ */ jsx23(RefusalNotice, { error: fields.error, className });
7055
7136
  if (existing.error) return /* @__PURE__ */ jsx23(RefusalNotice, { error: existing.error, className });
7137
+ const storeSaid = mutation.error && mutation.error !== answered ? mutation.error : null;
7138
+ const fieldProblems = predicted.filter((p) => p.field_key);
7139
+ const problemsShown = (key) => touched || changed[key] ? fieldProblems.filter((p) => p.field_key === key) : [];
7140
+ const storeRefusalIsOnAField = storeSaid !== null && !mutation.conflict && storeSaid.code === "refused_by_rule" && (fields.data ?? []).some((f) => problemsShown(f.key).length > 0);
7141
+ const stored = existing.data?.document;
7142
+ const focusField = (field) => {
7143
+ if (!field) return;
7144
+ globalThis.document?.getElementById(`field-${field.id}`)?.focus();
7145
+ };
7146
+ const keepEditing = (field) => {
7147
+ if (storeSaid) setAnswered(storeSaid);
7148
+ const first = field ?? (fields.data ?? []).find((f) => problemsShown(f.key).length > 0) ?? (fields.data ?? [])[0];
7149
+ focusField(first);
7150
+ };
7151
+ const discard = (field) => {
7152
+ if (storeSaid) setAnswered(storeSaid);
7153
+ if (field) {
7154
+ setDraft((d) => ({ ...d, [field.key]: stored ? stored[field.key] : void 0 }));
7155
+ setChanged((c) => {
7156
+ const next = { ...c };
7157
+ delete next[field.key];
7158
+ return next;
7159
+ });
7160
+ return;
7161
+ }
7162
+ setDraft(stored ? { ...stored } : {});
7163
+ setChanged({});
7164
+ setTouched(false);
7165
+ };
7056
7166
  const save = async () => {
7057
7167
  setTouched(true);
7168
+ setAnswered(null);
7058
7169
  if (recordId) {
7059
7170
  const version = loadedVersion;
7060
7171
  const moved = await mutation.update({
@@ -7091,8 +7202,13 @@ function RecordForm({
7091
7202
  field,
7092
7203
  value: draft[field.key],
7093
7204
  document: existing.data?.document,
7094
- problems: touched ? predicted.filter((p) => p.field_key === field.key) : [],
7095
- onChange: (value) => setDraft((d) => ({ ...d, [field.key]: value }))
7205
+ problems: problemsShown(field.key),
7206
+ onChange: (value) => {
7207
+ setDraft((d) => ({ ...d, [field.key]: value }));
7208
+ setChanged((c) => c[field.key] ? c : { ...c, [field.key]: true });
7209
+ },
7210
+ onKeepEditing: () => keepEditing(field),
7211
+ onDiscard: () => discard(field)
7096
7212
  },
7097
7213
  field.id
7098
7214
  )) }),
@@ -7103,10 +7219,10 @@ function RecordForm({
7103
7219
  },
7104
7220
  p.message
7105
7221
  )) : null,
7106
- mutation.error ? /* @__PURE__ */ jsx23(
7222
+ storeSaid && !storeRefusalIsOnAField ? /* @__PURE__ */ jsx23(
7107
7223
  RefusalNotice,
7108
7224
  {
7109
- error: mutation.error,
7225
+ error: storeSaid,
7110
7226
  ...mutation.conflict ? {
7111
7227
  actions: /* @__PURE__ */ jsxs19("div", { className: "mt-1 space-y-1", children: [
7112
7228
  Object.entries(mutation.conflict.contested_fields).map(([key, theirs]) => {
@@ -7127,14 +7243,16 @@ function RecordForm({
7127
7243
  size: "sm",
7128
7244
  variant: "outline",
7129
7245
  onClick: () => {
7246
+ if (mutation.conflict) loaded.note(mutation.conflict.current_version);
7130
7247
  existing.reload();
7131
7248
  mutation.clearConflict();
7249
+ setAnswered(storeSaid);
7132
7250
  },
7133
7251
  children: "Take theirs and start again"
7134
7252
  }
7135
7253
  )
7136
7254
  ] })
7137
- } : {}
7255
+ } : { onKeepEditing: () => keepEditing(), onDiscard: () => discard() }
7138
7256
  }
7139
7257
  ) : null,
7140
7258
  /* @__PURE__ */ jsx23(Separator7, {}),
@@ -7151,17 +7269,20 @@ function FieldRow({
7151
7269
  value,
7152
7270
  document: document2,
7153
7271
  problems,
7154
- onChange
7272
+ onChange,
7273
+ onKeepEditing,
7274
+ onDiscard
7155
7275
  }) {
7156
7276
  const id = `field-${field.id}`;
7157
7277
  return /* @__PURE__ */ jsxs19("div", { className: "flex min-w-0 flex-col gap-1", children: [
7158
7278
  /* @__PURE__ */ jsx23(FieldLabel, { field, htmlFor: id, children: /* @__PURE__ */ jsx23(ProvenanceBadge, { document: document2, fieldKey: field.key }) }),
7159
7279
  /* @__PURE__ */ jsx23(FieldControl, { field, value, onChange, id }),
7160
- problems.map((problem) => /* @__PURE__ */ jsx23(
7280
+ problems.map((problem, index) => /* @__PURE__ */ jsx23(
7161
7281
  RefusalNotice,
7162
7282
  {
7163
7283
  error: { code: "refused_by_rule", message: problem.message },
7164
- className: "border-0 p-0"
7284
+ className: "border-0 p-0",
7285
+ ...index === 0 ? { onKeepEditing, onDiscard } : {}
7165
7286
  },
7166
7287
  problem.message
7167
7288
  ))
@@ -7331,6 +7452,7 @@ function computedInPlainWords(row, field) {
7331
7452
  }
7332
7453
 
7333
7454
  // src/views.ts
7455
+ import { asWriteConflict as asWriteConflict2 } from "@ai-matrx/records/core";
7334
7456
  var VIEW_LAYOUTS = ["grid", "kanban", "calendar", "gallery"];
7335
7457
  var LAYOUT_LABEL = {
7336
7458
  grid: "Grid",
@@ -7454,6 +7576,18 @@ function viewFromRecord(row) {
7454
7576
  presentation: parseGridPresentation(data["presentation"])
7455
7577
  };
7456
7578
  }
7579
+ async function saveViewPatch(client, viewId, document2, expectedVersion) {
7580
+ const send = (expected) => client.recordUpdate({
7581
+ record_id: viewId,
7582
+ patch: document2,
7583
+ ...expected === null ? {} : { expectedVersion: expected }
7584
+ });
7585
+ const first = await send(expectedVersion);
7586
+ if (first.ok) return first;
7587
+ const conflict = asWriteConflict2(first.error, viewId);
7588
+ if (!conflict) return first;
7589
+ return send(conflict.current_version);
7590
+ }
7457
7591
  function viewPatchDocument(patch) {
7458
7592
  const out = {};
7459
7593
  if (patch.name !== void 0) out["name"] = patch.name;
@@ -11881,6 +12015,21 @@ function whenItFires(subscription) {
11881
12015
  const every = subscription.cadence === "hourly" ? "an hourly summary" : subscription.cadence === "weekly" ? "a weekly summary" : "a daily summary";
11882
12016
  return subscription.schedule ? `${every}, ${subscription.schedule}` : every;
11883
12017
  }
12018
+ var PERIOD_MS = {
12019
+ hourly: 60 * 60 * 1e3,
12020
+ daily: 24 * 60 * 60 * 1e3,
12021
+ weekly: 7 * 24 * 60 * 60 * 1e3
12022
+ };
12023
+ function nextSummaryAt(subscription, now) {
12024
+ if (!subscription.next_digest_at) return null;
12025
+ const due = new Date(subscription.next_digest_at);
12026
+ if (Number.isNaN(due.getTime())) return null;
12027
+ if (due.getTime() > now.getTime()) return due;
12028
+ const period = PERIOD_MS[subscription.cadence];
12029
+ if (!period) return due;
12030
+ const behind = now.getTime() - due.getTime();
12031
+ return new Date(due.getTime() + (Math.floor(behind / period) + 1) * period);
12032
+ }
11884
12033
  var CHANNEL_WORDS2 = SUBSCRIPTION_CHANNEL_LABEL;
11885
12034
  var DIGEST_SUGGESTION = "Email me a summary of this every Monday at 8 in the morning.";
11886
12035
  function SubscriptionsPanel({ tableId, activeRuleId, className }) {
@@ -12008,7 +12157,7 @@ function SubscriptionsPanel({ tableId, activeRuleId, className }) {
12008
12157
  /* @__PURE__ */ jsxs37("div", { className: "mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
12009
12158
  subscription.next_digest_at ? /* @__PURE__ */ jsxs37("span", { children: [
12010
12159
  "Next summary ",
12011
- new Date(subscription.next_digest_at).toLocaleString()
12160
+ nextSummaryAt(subscription, /* @__PURE__ */ new Date())?.toLocaleString()
12012
12161
  ] }) : subscription.cadence === "instant" ? null : subscription.muted ? /* @__PURE__ */ jsx41("span", { children: "No next summary while it is off." }) : null,
12013
12162
  subscription.last_sent_at ? /* @__PURE__ */ jsxs37("span", { children: [
12014
12163
  "Last told you ",
@@ -16202,13 +16351,31 @@ var LANE_EMPTY = {
16202
16351
  community: "Nothing shared beyond your organization. A table published by link or to the world lands here.",
16203
16352
  app: "The app has not needed a table of its own in this organization yet. Saving a view, writing a comment or building a form makes one, and it appears here rather than among your own data."
16204
16353
  };
16354
+ function keptByTheApp(table) {
16355
+ return table.kept_by_the_app === true;
16356
+ }
16205
16357
  function laneFor(table) {
16206
16358
  if (table.is_kernel) return "system";
16207
- if ((table.slug ?? "").startsWith(APP_TABLE_PREFIX)) return "app";
16359
+ if ((table.slug ?? "").startsWith(APP_TABLE_PREFIX) || keptByTheApp(table)) return "app";
16208
16360
  if (table.visibility === "public" || table.visibility === "link") return "community";
16209
16361
  if (table.visibility === "personal") return "mine";
16210
16362
  return "organization";
16211
16363
  }
16364
+ var VISIBILITY_LANES = ["mine", "organization", "community", "world"];
16365
+ var VISIBILITY_LANE_TITLE = {
16366
+ mine: "Mine",
16367
+ organization: "My organization",
16368
+ community: "Community",
16369
+ world: "World"
16370
+ };
16371
+ function visibilityLaneFor(table) {
16372
+ if (table.is_kernel) return null;
16373
+ if ((table.slug ?? "").startsWith(APP_TABLE_PREFIX) || keptByTheApp(table)) return null;
16374
+ if (table.visibility === "personal") return "mine";
16375
+ if (table.visibility === "link") return "community";
16376
+ if (table.visibility === "public") return "world";
16377
+ return "organization";
16378
+ }
16212
16379
  function TablesHome({ onOpenTable, className }) {
16213
16380
  const client = useRecordsClient42();
16214
16381
  const tables = useTables4();
@@ -16354,8 +16521,45 @@ function TablesHome({ onOpenTable, className }) {
16354
16521
  // src/TablePage.tsx
16355
16522
  import { useCallback as useCallback40, useEffect as useEffect46, useState as useState57 } from "react";
16356
16523
  import { useFields as useFields24, useRecordsClient as useRecordsClient43, useTable as useTable23 } from "@ai-matrx/records/react";
16357
- import { Button as Button51, Separator as Separator12, Skeleton as Skeleton36, cn as cn54 } from "@ai-matrx/design-system";
16524
+ import { BottomSheet, Button as Button51, Separator as Separator12, Skeleton as Skeleton36, cn as cn54, useIsMobile } from "@ai-matrx/design-system";
16358
16525
  import { Fragment as Fragment28, jsx as jsx61, jsxs as jsxs56 } from "react/jsx-runtime";
16526
+ var RAIL_TITLE = {
16527
+ settings: "Table settings",
16528
+ inbox: "Inbox",
16529
+ import: "Import",
16530
+ field: "Add a field",
16531
+ record: "Record",
16532
+ "new-record": "New record",
16533
+ forms: "Forms",
16534
+ bookings: "Bookings",
16535
+ checklists: "Checklists",
16536
+ notifications: "Notifications",
16537
+ portals: "Portals",
16538
+ "who-changed": "Who changed this"
16539
+ };
16540
+ function RailFrame({
16541
+ phone,
16542
+ title,
16543
+ onClose,
16544
+ children
16545
+ }) {
16546
+ if (phone) {
16547
+ return /* @__PURE__ */ jsx61(
16548
+ BottomSheet,
16549
+ {
16550
+ open: true,
16551
+ onOpenChange: (open) => {
16552
+ if (!open) onClose();
16553
+ },
16554
+ title,
16555
+ size: "full",
16556
+ surface: "solid",
16557
+ children: /* @__PURE__ */ jsx61("div", { "data-rail-sheet": "", className: "min-h-0 flex-1 overflow-y-auto px-3 pb-6", children })
16558
+ }
16559
+ );
16560
+ }
16561
+ return /* @__PURE__ */ jsx61("aside", { "data-rail-column": "", className: "w-[26rem] shrink-0 overflow-y-auto rounded-md border p-3", children });
16562
+ }
16359
16563
  function filterInWords(filter, fields) {
16360
16564
  const nameOf = (key) => {
16361
16565
  const field = fields.find((f) => f.key === key);
@@ -16495,6 +16699,7 @@ function TablePage({
16495
16699
  const setRail = (next) => setSurface((now) => ({ ...now, rail: next }));
16496
16700
  const [openRecord, setOpenRecord] = useState57(opening.record);
16497
16701
  const viewVersion = useRecordVersion(view?.id ?? null);
16702
+ const phone = useIsMobile();
16498
16703
  const shownLayout = layoutFromLink ?? view?.layout ?? "grid";
16499
16704
  const press = (pressed) => {
16500
16705
  const next = chooseSurface(surface, pressed);
@@ -16522,6 +16727,7 @@ function TablePage({
16522
16727
  setLayoutFromLink(isLayout ? named : null);
16523
16728
  setSurface((now) => ({ ...now, main: isLayout ? "records" : named }));
16524
16729
  }, [activeView]);
16730
+ const [viewSaveRefused, setViewSaveRefused] = useState57(null);
16525
16731
  const patchView = useCallback40(
16526
16732
  async (patch) => {
16527
16733
  const current = view;
@@ -16529,12 +16735,13 @@ function TablePage({
16529
16735
  setView({ ...current, ...patch });
16530
16736
  const document2 = viewPatchDocument(patch);
16531
16737
  if (Object.keys(document2).length === 0) return;
16532
- const written = await client.recordUpdate({
16533
- record_id: current.id,
16534
- patch: document2,
16535
- ...viewVersion.version === null ? {} : { expectedVersion: viewVersion.version }
16536
- });
16537
- if (written.ok) viewVersion.note(written.data);
16738
+ setViewSaveRefused(null);
16739
+ const written = await saveViewPatch(client, current.id, document2, viewVersion.version);
16740
+ if (written.ok) {
16741
+ viewVersion.note(written.data);
16742
+ return;
16743
+ }
16744
+ setViewSaveRefused({ error: written.error, patch, before: current });
16538
16745
  },
16539
16746
  [client, view, viewVersion]
16540
16747
  );
@@ -16685,6 +16892,27 @@ function TablePage({
16685
16892
  ),
16686
16893
  /* @__PURE__ */ jsx61(ExportMenu, { tableId })
16687
16894
  ] }),
16895
+ viewSaveRefused ? /* @__PURE__ */ jsx61(
16896
+ RefusalNotice,
16897
+ {
16898
+ error: viewSaveRefused.error,
16899
+ actions: /* @__PURE__ */ jsxs56("span", { className: "flex flex-wrap gap-1 pt-0.5", "data-view-save-refused": "", children: [
16900
+ /* @__PURE__ */ jsx61(Button51, { size: "sm", variant: "outline", onClick: () => void patchView(viewSaveRefused.patch), children: "Try again" }),
16901
+ /* @__PURE__ */ jsx61(
16902
+ Button51,
16903
+ {
16904
+ size: "sm",
16905
+ variant: "ghost",
16906
+ onClick: () => {
16907
+ setView(viewSaveRefused.before);
16908
+ setViewSaveRefused(null);
16909
+ },
16910
+ children: "Discard"
16911
+ }
16912
+ )
16913
+ ] })
16914
+ }
16915
+ ) : null,
16688
16916
  main === "dashboards" ? /* @__PURE__ */ jsx61(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : main === "archived" ? (
16689
16917
  /* THE ROWS THAT LEFT. Restoring one puts it back in the grid, so the
16690
16918
  page returns to the records rather than leaving the person looking
@@ -16745,7 +16973,7 @@ function TablePage({
16745
16973
  view ? null : /* @__PURE__ */ jsx61("p", { className: "text-xs text-muted-foreground", children: VIEW_NOT_SAVED_YET })
16746
16974
  ] })
16747
16975
  ] }),
16748
- rail === "none" ? null : /* @__PURE__ */ jsxs56("aside", { className: "w-[26rem] shrink-0 overflow-y-auto rounded-md border p-3", children: [
16976
+ rail === "none" ? null : /* @__PURE__ */ jsxs56(RailFrame, { phone, title: RAIL_TITLE[rail], onClose: () => setRail("none"), children: [
16749
16977
  rail === "settings" ? /* @__PURE__ */ jsx61(
16750
16978
  TableSettings,
16751
16979
  {
@@ -16984,6 +17212,8 @@ export {
16984
17212
  VIEW_LAYOUTS,
16985
17213
  VIEW_NOT_SAVED_YET,
16986
17214
  VIEW_TABLE,
17215
+ VISIBILITY_LANES,
17216
+ VISIBILITY_LANE_TITLE,
16987
17217
  ViewBar,
16988
17218
  ViewSwitcher,
16989
17219
  WITHHELD_RECORD_LABEL,
@@ -17039,6 +17269,7 @@ export {
17039
17269
  isPlainFieldType,
17040
17270
  isRelationFieldType,
17041
17271
  isSignatureField,
17272
+ keptByTheApp,
17042
17273
  keyFor,
17043
17274
  kindsWithNoChoice,
17044
17275
  laneFor,
@@ -17074,6 +17305,7 @@ export {
17074
17305
  revokeConsequence,
17075
17306
  rowName,
17076
17307
  rowNameIn,
17308
+ saveViewPatch,
17077
17309
  scalarText,
17078
17310
  shareUnavailableReason,
17079
17311
  specFromBlock,
@@ -17103,6 +17335,7 @@ export {
17103
17335
  viewDocument,
17104
17336
  viewFromRecord,
17105
17337
  viewPatchDocument,
17338
+ visibilityLaneFor,
17106
17339
  whatIsMissing,
17107
17340
  whatYouMayDo,
17108
17341
  whatYouMayDoWithTable,