@ai-matrx/records-ui 0.85.15 → 0.85.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.85.16
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/records-ui/v0.85.15..npm/records-ui/v0.85.16 -- apps/shared/records-ui`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## Unreleased — lane VIEW-STATE-PERSONAL (2026-09-25)
10
+
11
+ Needs `@ai-matrx/records` with `viewLookRead` / `viewLookSet` (Unreleased) and the store doors
12
+ `custom.view_look_read` / `custom.view_look_set` (live on production, 2026-09-25 19:19Z).
13
+
14
+ ### A look at a view is yours until you save it (VERIFIER-23 item 1)
15
+
16
+ * **Every view-configuration change a person makes while looking is HER look, not the view's.** On
17
+ production the owner's *Group by → Status* on a board became the tenant liaison's board the next
18
+ time she opened it: `TablePage` merged every pick onto the shared view (`view_declare`). Now a
19
+ pick of the board's grouping, the calendar's date field, a sort, a filter, a hidden column, a
20
+ width, a colour or the layout is kept for that person through `custom.view_look_set` and laid
21
+ over the view (`applyLook`); a reload keeps it; nobody else sees it. The view row is never
22
+ written by a pick. (Airtable and Notion's rule; Notion's "Save for everyone" / "Reset".)
23
+ * **One line says so, with its doors** (`[data-view-look]`): *"You changed how this view looks. Only
24
+ you see it."* · **Save to view** (someone who may edit the table; the one `view_declare`, and the
25
+ default view's layout stays the person's look — "Make … the default" is that door) · **Save as
26
+ new view** (editor; the look becomes a view of its own) · **Reset to view** (everyone; the look is
27
+ archived). A viewer never sees Save to view or Save as new view — absent, not dead.
28
+ * **Reorder is offered only to someone who may edit the table**: a hand-set order is the view's
29
+ sort for everybody (`view_record_order_set` now needs editor; the store refuses it otherwise).
30
+ * New module `viewLook.ts` (exported): `ViewLook`, `lookPart`, `lookDocument`, `lookFromDocument`,
31
+ `applyLook`, `lookDiffers`, `saveToViewPlan`, `readLooks`, `keepLook`, `LOOK_WORDS`.
32
+ * Suite `a-look-at-a-view-is-yours-until-you-save-it.test.tsx`: RED 4/6 on the prior `TablePage` /
33
+ `ViewSwitcher` (the pick sent `view_declare`; no look, no Save / Reset) → GREEN 6/6.
34
+
3
35
  ## 0.85.15 (2026-09-25 — published by PUBLISH-SWEEP-7)
4
36
 
5
37
  The tagged 0.85.14 never reached npm: its deterministic gate failed on `NeverRecorded … custom.field_declare` (fixture recorded against records 0.58.11). The fixture is re-recorded (below); everything 0.85.14 carried ships here. Needs `@ai-matrx/records` >= 0.58.13 (`viewDesignate` / `custom.view_designate`).
package/dist/index.cjs CHANGED
@@ -116,6 +116,7 @@ __export(src_exports, {
116
116
  LAYOUT_NEEDS: () => LAYOUT_NEEDS,
117
117
  LAYOUT_NO_FIELD: () => LAYOUT_NO_FIELD,
118
118
  LEVEL_WORD: () => LEVEL_WORD,
119
+ LOOK_WORDS: () => LOOK_WORDS,
119
120
  MACHINE_IDENTITY: () => MACHINE_IDENTITY,
120
121
  MAX_ROW_HEIGHT: () => MAX_ROW_HEIGHT,
121
122
  MIN_ROW_HEIGHT: () => MIN_ROW_HEIGHT,
@@ -215,6 +216,7 @@ __export(src_exports, {
215
216
  actorBadge: () => actorBadge,
216
217
  actorWords: () => actorWords,
217
218
  addFields: () => import_core11.addFields,
219
+ applyLook: () => applyLook,
218
220
  archivedRowLine: () => archivedRowLine,
219
221
  asRefusal: () => asRefusal,
220
222
  askableFields: () => askableFields,
@@ -274,11 +276,16 @@ __export(src_exports, {
274
276
  isPlainFieldType: () => isPlainFieldType,
275
277
  isRelationFieldType: () => isRelationFieldType,
276
278
  isSignatureField: () => isSignatureField,
279
+ keepLook: () => keepLook,
277
280
  keptByTheApp: () => keptByTheApp,
278
281
  keyFor: () => keyFor,
279
282
  kindsWithNoChoice: () => kindsWithNoChoice,
280
283
  laneFor: () => laneFor,
281
284
  lastChangeSentence: () => lastChangeSentence,
285
+ lookDiffers: () => lookDiffers,
286
+ lookDocument: () => lookDocument,
287
+ lookFromDocument: () => lookFromDocument,
288
+ lookPart: () => lookPart,
282
289
  looksLikeId: () => looksLikeId,
283
290
  machineIdentityIn: () => machineIdentityIn,
284
291
  mayNotRestoreLine: () => mayNotRestoreLine,
@@ -299,6 +306,7 @@ __export(src_exports, {
299
306
  previewWords: () => previewWords,
300
307
  publiclyAnswerable: () => publiclyAnswerable,
301
308
  railFromParam: () => railFromParam,
309
+ readLooks: () => readLooks,
302
310
  recordName: () => recordName,
303
311
  recordNameIn: () => recordNameIn,
304
312
  recordsDataSource: () => recordsDataSource,
@@ -312,6 +320,7 @@ __export(src_exports, {
312
320
  revokeConsequence: () => revokeConsequence,
313
321
  rowName: () => rowName,
314
322
  rowNameIn: () => rowNameIn,
323
+ saveToViewPlan: () => saveToViewPlan,
315
324
  saveViewPatch: () => saveViewPatch,
316
325
  scalarText: () => scalarText,
317
326
  seedIsSatisfied: () => seedIsSatisfied,
@@ -10831,6 +10840,86 @@ function computedInPlainWords(row, field) {
10831
10840
  return `${name} was worked out as ${shown3}${at}.`;
10832
10841
  }
10833
10842
 
10843
+ // src/viewLook.ts
10844
+ var LOOK_KEYS = [
10845
+ "layout",
10846
+ "groupField",
10847
+ "swimlaneField",
10848
+ "collapsedColumns",
10849
+ "measure",
10850
+ "dateField",
10851
+ "imageField",
10852
+ "sorts",
10853
+ "where",
10854
+ "presentation"
10855
+ ];
10856
+ function lookPart(patch) {
10857
+ const out = {};
10858
+ for (const key of LOOK_KEYS) {
10859
+ if (patch[key] !== void 0) out[key] = patch[key];
10860
+ }
10861
+ return out;
10862
+ }
10863
+ function lookDocument(look) {
10864
+ return viewSettingsPatch(look);
10865
+ }
10866
+ function lookFromDocument(doc) {
10867
+ const data = doc ?? {};
10868
+ const read = viewSettings(data);
10869
+ const out = {};
10870
+ const has = (key) => Object.prototype.hasOwnProperty.call(data, key);
10871
+ if (has("layout") && VIEW_LAYOUTS.includes(String(data["layout"]))) {
10872
+ out.layout = data["layout"];
10873
+ }
10874
+ if (has("group_field")) out.groupField = read.groupField;
10875
+ if (has("swimlane_field")) out.swimlaneField = read.swimlaneField;
10876
+ if (has("collapsed_columns")) out.collapsedColumns = read.collapsedColumns;
10877
+ if (has("measure")) out.measure = read.measure;
10878
+ if (has("date_field")) out.dateField = read.dateField;
10879
+ if (has("image_field")) out.imageField = read.imageField;
10880
+ if (has("sorts")) out.sorts = read.sorts;
10881
+ if (has("where")) out.where = read.where;
10882
+ if (has("presentation")) out.presentation = parseGridPresentation(data["presentation"]);
10883
+ return out;
10884
+ }
10885
+ function applyLook(view, look) {
10886
+ if (!look) return view;
10887
+ return { ...view, ...look };
10888
+ }
10889
+ function lookDiffers(view, look) {
10890
+ if (!look) return false;
10891
+ const shown3 = applyLook(view, look);
10892
+ return LOOK_KEYS.some(
10893
+ (key) => look[key] !== void 0 && JSON.stringify(shown3[key] ?? null) !== JSON.stringify(view[key] ?? null)
10894
+ );
10895
+ }
10896
+ function saveToViewPlan(view, look) {
10897
+ const patch = viewPatchFor(view, look);
10898
+ const left = {};
10899
+ if (look.layout !== void 0 && patch.layout === void 0 && look.layout !== view.layout) left.layout = look.layout;
10900
+ return { patch, left: Object.keys(left).length > 0 ? left : null };
10901
+ }
10902
+ async function readLooks(client, tableId) {
10903
+ const answered = await client.viewLookRead({ table_id: tableId });
10904
+ if (!answered.ok) return answered;
10905
+ const out = {};
10906
+ for (const [viewId, row] of Object.entries(answered.data ?? {})) {
10907
+ out[viewId] = lookFromDocument(row?.look ?? null);
10908
+ }
10909
+ return { ok: true, data: out };
10910
+ }
10911
+ async function keepLook(client, tableId, viewId, look) {
10912
+ const doc = look ? lookDocument(look) : null;
10913
+ return client.viewLookSet({ table_id: tableId, view_id: viewId, look: doc && Object.keys(doc).length > 0 ? doc : null });
10914
+ }
10915
+ var LOOK_WORDS = {
10916
+ yours: "You changed how this view looks. Only you see it.",
10917
+ saveToView: "Save to view",
10918
+ saveAsNew: "Save as new view",
10919
+ reset: "Reset to view",
10920
+ newName: "Name for the new view"
10921
+ };
10922
+
10834
10923
  // src/ViewSwitcher.tsx
10835
10924
  var import_react67 = require("react");
10836
10925
  var import_react68 = require("@ai-matrx/records/react");
@@ -11726,10 +11815,14 @@ function ViewSwitcher({
11726
11815
  viewId,
11727
11816
  order: local.order !== void 0 ? local.order : view.order ?? null,
11728
11817
  sorts: local.sorts ?? view.sorts ?? [],
11729
- onPlaced: () => {
11730
- setLocal((held) => ({ ...held, order: "manual", sorts: [] }));
11731
- onOrderPlaced?.();
11732
- },
11818
+ /* Placing the rows saves the VIEW's order, so Reorder is offered only when the
11819
+ host bound it (TablePage: to someone who may edit the table). */
11820
+ ...onOrderPlaced ? {
11821
+ onPlaced: () => {
11822
+ setLocal((held) => ({ ...held, order: "manual", sorts: [] }));
11823
+ onOrderPlaced();
11824
+ }
11825
+ } : {},
11733
11826
  ...onViewChange ? {
11734
11827
  onUseSort: (sort) => change({ sorts: [sort], order: "sorted" })
11735
11828
  } : {}
@@ -22646,6 +22739,21 @@ function TablePage({
22646
22739
  const askedInWords = filter ? filterInWords(filter, pageFields.data ?? []) : null;
22647
22740
  const organizationId = (0, import_react147.useRecordsClient)().config.organizationId;
22648
22741
  const [view, setView] = (0, import_react146.useState)(null);
22742
+ const [looks, setLooks] = (0, import_react146.useState)(void 0);
22743
+ (0, import_react146.useEffect)(() => {
22744
+ let live = true;
22745
+ setLooks(void 0);
22746
+ void readLooks(client, tableId).then((answered) => {
22747
+ if (!live) return;
22748
+ setLooks(answered.ok ? answered.data : {});
22749
+ });
22750
+ return () => {
22751
+ live = false;
22752
+ };
22753
+ }, [client, tableId]);
22754
+ const look = view && looks ? looks[view.id] : void 0;
22755
+ const shown3 = view ? applyLook(view, look) : null;
22756
+ const [viewsEpoch, setViewsEpoch] = (0, import_react146.useState)(0);
22649
22757
  const opening = openingRail(activeRecordId, activeRail);
22650
22758
  const hostLayouts = useRecordsUi().layouts ?? [];
22651
22759
  const hostLayoutFor = (asked) => openingHostLayout(asked, hostLayouts);
@@ -22680,9 +22788,9 @@ function TablePage({
22680
22788
  if (hostFromLink || opened.layout || layoutChosen) return;
22681
22789
  setHostLayout(face.hostLayout);
22682
22790
  }, [face.kind, face.kind === "designated" ? face.hostLayout : null, layoutChosen]);
22683
- const faceUnknown = !hostFromLink && !opened.layout && (views === void 0 || Array.isArray(views) && views.length > 0 && view === null);
22684
- const shownLayout = layoutFromLink ?? view?.layout ?? "grid";
22685
- const shownFilter = filter ?? (view?.where ? view.where : null) ?? (view?.filters && Object.keys(view.filters).length > 0 ? view.filters : null);
22791
+ const faceUnknown = !hostFromLink && !opened.layout && (views === void 0 || Array.isArray(views) && views.length > 0 && (view === null || looks === void 0));
22792
+ const shownLayout = layoutFromLink ?? shown3?.layout ?? "grid";
22793
+ const shownFilter = filter ?? (shown3?.where ? shown3.where : null) ?? (shown3?.filters && Object.keys(shown3.filters).length > 0 ? shown3.filters : null);
22686
22794
  const press = (pressed) => {
22687
22795
  const next = chooseSurface(surface, pressed);
22688
22796
  setSurface(next);
@@ -22737,16 +22845,79 @@ function TablePage({
22737
22845
  async (asked) => {
22738
22846
  const current = view;
22739
22847
  if (!current) return;
22740
- const patch = viewPatchFor(current, asked);
22741
- setView({ ...current, ...patch });
22742
- if (Object.keys(viewPatchDocument(patch)).length === 0 && patch.name === void 0 && patch.filters === void 0) return;
22848
+ const part = lookPart(asked);
22849
+ if (Object.keys(part).length === 0) return;
22850
+ const before = looks?.[current.id];
22851
+ const next = { ...before ?? {}, ...part };
22852
+ setLooks((all) => ({ ...all ?? {}, [current.id]: next }));
22743
22853
  setViewSaveRefused(null);
22744
- const written = await saveViewPatch(client, tableId, current.id, patch);
22745
- if (written.ok) return;
22746
- setViewSaveRefused({ error: written.error, patch, before: current });
22854
+ const kept = await keepLook(client, tableId, current.id, next);
22855
+ if (kept.ok) return;
22856
+ setViewSaveRefused({ error: kept.error, patch: asked, lookBefore: before });
22747
22857
  },
22748
- [client, tableId, view]
22858
+ [client, tableId, view, looks]
22749
22859
  );
22860
+ const lookShowing = Boolean(view && looks && lookDiffers(view, looks[view.id]));
22861
+ const [lookRefused, setLookRefused] = (0, import_react146.useState)(null);
22862
+ const [namingLook, setNamingLook] = (0, import_react146.useState)(false);
22863
+ const [lookName, setLookName] = (0, import_react146.useState)("");
22864
+ const resetLook = async () => {
22865
+ const current = view;
22866
+ if (!current) return;
22867
+ setLookRefused(null);
22868
+ setViewSaveRefused(null);
22869
+ const before = looks?.[current.id];
22870
+ setLooks((all) => {
22871
+ const { [current.id]: _gone, ...rest } = all ?? {};
22872
+ return rest;
22873
+ });
22874
+ const kept = await keepLook(client, tableId, current.id, null);
22875
+ if (!kept.ok) {
22876
+ setLooks((all) => ({ ...all ?? {}, [current.id]: before ?? {} }));
22877
+ setLookRefused(kept.error);
22878
+ }
22879
+ };
22880
+ const saveLookToView = async () => {
22881
+ const current = view;
22882
+ const mine = current && looks ? looks[current.id] : void 0;
22883
+ if (!current || !mine) return;
22884
+ setLookRefused(null);
22885
+ const { patch, left } = saveToViewPlan(current, mine);
22886
+ if (Object.keys(viewPatchDocument(patch)).length > 0) {
22887
+ const written = await saveViewPatch(client, tableId, current.id, patch);
22888
+ if (!written.ok) {
22889
+ setLookRefused(written.error);
22890
+ return;
22891
+ }
22892
+ setView({ ...current, ...patch });
22893
+ }
22894
+ setLooks((all) => {
22895
+ const { [current.id]: _saved, ...rest } = all ?? {};
22896
+ return left ? { ...rest, [current.id]: left } : rest;
22897
+ });
22898
+ const kept = await keepLook(client, tableId, current.id, left);
22899
+ if (!kept.ok) setLookRefused(kept.error);
22900
+ };
22901
+ const saveLookAsNewView = async (name) => {
22902
+ const current = view;
22903
+ const seen = shown3;
22904
+ if (!current || !seen || name.trim() === "") return;
22905
+ setLookRefused(null);
22906
+ const written = await declareView(client, { ...seen, name: name.trim(), subject: tableId, isDefault: false });
22907
+ if (!written.ok) {
22908
+ setLookRefused(written.error);
22909
+ return;
22910
+ }
22911
+ setNamingLook(false);
22912
+ setLookName("");
22913
+ setLooks((all) => {
22914
+ const { [current.id]: _moved, ...rest } = all ?? {};
22915
+ return rest;
22916
+ });
22917
+ void keepLook(client, tableId, current.id, null);
22918
+ setPickedViewId(written.data);
22919
+ setViewsEpoch((n) => n + 1);
22920
+ };
22750
22921
  const [designateRefused, setDesignateRefused] = (0, import_react146.useState)(null);
22751
22922
  const opensAs = view?.isDefault ? openingLayoutOf(view) : null;
22752
22923
  const canDesignate = Boolean(rights.write && view && layoutShowing && layoutShowing !== opensAs);
@@ -22789,7 +22960,8 @@ function TablePage({
22789
22960
  quietWhenAlone: !showViews,
22790
22961
  seed: seedViews ?? [defaultView(tableId)],
22791
22962
  className: "min-w-0 flex-1"
22792
- }
22963
+ },
22964
+ viewsEpoch
22793
22965
  ),
22794
22966
  /* @__PURE__ */ (0, import_jsx_runtime79.jsxs)("div", { className: "ml-auto flex shrink-0 items-center gap-1", "data-table-cluster": "", children: [
22795
22967
  chooserInline ? /* @__PURE__ */ (0, import_jsx_runtime79.jsxs)("div", { className: "flex items-center gap-0.5", role: "group", "aria-label": "Layout", children: [
@@ -22816,7 +22988,7 @@ function TablePage({
22816
22988
  `host:${option.id}`
22817
22989
  ))
22818
22990
  ] }) : null,
22819
- main === "records" && !hostLayout ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(ViewFilter, { tableId, view, onApply: (where) => void patchView({ where }) }) : null,
22991
+ main === "records" && !hostLayout ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(ViewFilter, { tableId, view: shown3, onApply: (where) => void patchView({ where }) }) : null,
22820
22992
  rights.write && view && main === "records" && !hostLayout && shownLayout !== "grid" ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(import_design_system71.Button, { size: "sm", onClick: () => show("new-record"), children: "New record" }) : null,
22821
22993
  openedFromMenu ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(
22822
22994
  import_design_system71.Button,
@@ -22916,7 +23088,13 @@ function TablePage({
22916
23088
  size: "sm",
22917
23089
  variant: "ghost",
22918
23090
  onClick: () => {
22919
- setView(viewSaveRefused.before);
23091
+ const current = view;
23092
+ if (current) {
23093
+ setLooks((all) => {
23094
+ const { [current.id]: _dropped, ...rest } = all ?? {};
23095
+ return viewSaveRefused.lookBefore ? { ...rest, [current.id]: viewSaveRefused.lookBefore } : rest;
23096
+ });
23097
+ }
22920
23098
  setViewSaveRefused(null);
22921
23099
  },
22922
23100
  children: "Discard"
@@ -22925,6 +23103,36 @@ function TablePage({
22925
23103
  ] })
22926
23104
  }
22927
23105
  ) : null,
23106
+ lookShowing && main === "records" ? /* @__PURE__ */ (0, import_jsx_runtime79.jsxs)("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground", "data-view-look": "", children: [
23107
+ /* @__PURE__ */ (0, import_jsx_runtime79.jsx)("span", { className: "truncate", children: LOOK_WORDS.yours }),
23108
+ rights.write && view && Object.keys(viewPatchDocument(saveToViewPlan(view, look ?? {}).patch)).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(import_design_system71.Button, { size: "sm", variant: "outline", className: "h-7", onClick: () => void saveLookToView(), "data-view-look-save": "", children: LOOK_WORDS.saveToView }) : null,
23109
+ rights.write ? namingLook ? /* @__PURE__ */ (0, import_jsx_runtime79.jsxs)(
23110
+ "form",
23111
+ {
23112
+ className: "flex items-center gap-1",
23113
+ onSubmit: (e) => {
23114
+ e.preventDefault();
23115
+ void saveLookAsNewView(lookName);
23116
+ },
23117
+ children: [
23118
+ /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(
23119
+ import_design_system71.Input,
23120
+ {
23121
+ autoFocus: true,
23122
+ "aria-label": LOOK_WORDS.newName,
23123
+ value: lookName,
23124
+ onChange: (e) => setLookName(e.target.value),
23125
+ className: "h-7 w-44 text-xs",
23126
+ placeholder: "View name"
23127
+ }
23128
+ ),
23129
+ /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(import_design_system71.Button, { size: "sm", type: "submit", className: "h-7", disabled: lookName.trim() === "", children: "Save" })
23130
+ ]
23131
+ }
23132
+ ) : /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(import_design_system71.Button, { size: "sm", variant: "ghost", className: "h-7", onClick: () => setNamingLook(true), "data-view-look-save-new": "", children: LOOK_WORDS.saveAsNew }) : null,
23133
+ /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(import_design_system71.Button, { size: "sm", variant: "ghost", className: "h-7", onClick: () => void resetLook(), "data-view-look-reset": "", children: LOOK_WORDS.reset })
23134
+ ] }) : null,
23135
+ lookRefused ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(RefusalNotice, { error: lookRefused }) : null,
22928
23136
  main === "dashboards" ? /* @__PURE__ */ (0, import_jsx_runtime79.jsx)(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : main === "archived" ? (
22929
23137
  /* THE ROWS THAT LEFT. Restoring one puts it back in the grid, so the
22930
23138
  page returns to the records rather than leaving the person looking
@@ -22960,7 +23168,7 @@ function TablePage({
22960
23168
  chooser: false,
22961
23169
  openExport: () => setRail("export"),
22962
23170
  view: {
22963
- ...view ?? defaultView(tableId),
23171
+ ...shown3 ?? defaultView(tableId),
22964
23172
  layout: shownLayout,
22965
23173
  // The address's field wins for this visit; silence keeps the
22966
23174
  // saved view's own choice, exactly as it always did.
@@ -22981,7 +23189,12 @@ function TablePage({
22981
23189
  ...view ? { onViewChange: (patch) => void patchView(patch) } : {},
22982
23190
  ...view ? {
22983
23191
  viewId: view.id,
22984
- onOrderPlaced: () => setView((current) => current ? { ...current, order: "manual", sorts: [] } : current)
23192
+ /* A hand-set order is the VIEW's sort for everybody (VIEW-STATE-PERSONAL): placing
23193
+ the rows is saving to the view, so it is offered to someone who may edit the
23194
+ table and is absent otherwise (the store refuses it too). */
23195
+ ...rights.write ? {
23196
+ onOrderPlaced: () => setView((current) => current ? { ...current, order: "manual", sorts: [] } : current)
23197
+ } : {}
22985
23198
  } : {},
22986
23199
  ...rights.write ? { onNewRecordForm: () => setRail("new-record") } : {},
22987
23200
  onOpenRecord: (recordId) => {