@ai-matrx/records-ui 0.83.0 → 0.83.1

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,20 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.83.1
4
+
5
+ **A table shared by link is in the World lane, not Community.** (lane HUB-FIX, chair ruling
6
+ 2026-09-23.)
7
+
8
+ * `visibilityLaneFor` maps `link` → `world` (was `community`). A link anyone can open without
9
+ signing in reaches the world; Community is for things any SIGNED-IN account can reach, and
10
+ the store has no signed-in-only link yet, so no stored visibility lands in Community today.
11
+ `VISIBILITY_LANES` still carries all four — the chip stays.
12
+ * Test: `src/visibility-lanes.test.tsx` updated (link → world) plus a case pinning that no
13
+ stored visibility maps to Community yet — 5 cases.
14
+
15
+ **Consumer action:** none. The hub reads `visibilityLaneFor`, so link-shared tables move from
16
+ the Community chip to the World chip on install.
17
+
3
18
  ## 0.83.0
4
19
 
5
20
  **Four visibility lanes a filter can read, and the store's own choice lists stop sitting
package/dist/index.cjs CHANGED
@@ -2388,7 +2388,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2388
2388
  startedFrom.current = null;
2389
2389
  }, []);
2390
2390
  const writeCellNow = (0, import_react15.useCallback)(
2391
- async (rowId, key, value, resent = false) => {
2391
+ async (rowId, key, value, base, resent = false) => {
2392
2392
  const id = cellKey(rowId, key);
2393
2393
  setOptimistic((held) => ({ ...held, [id]: value }));
2394
2394
  setSaving((held) => ({ ...held, [id]: true }));
@@ -2435,8 +2435,8 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2435
2435
  if (written.error.code === "stale_write") {
2436
2436
  if (conflict) rememberVersion(versions.current, rowId, conflict.current_version);
2437
2437
  else versions.current.delete(rowId);
2438
- if (conflict && !resent && !Object.prototype.hasOwnProperty.call(conflict.contested_fields, key)) {
2439
- await writeCellNow(rowId, key, value, true);
2438
+ if (conflict && !resent && sameValue(conflict.contested_fields[key], base)) {
2439
+ await writeCellNow(rowId, key, value, base, true);
2440
2440
  return;
2441
2441
  }
2442
2442
  }
@@ -2453,9 +2453,9 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2453
2453
  [client, reload]
2454
2454
  );
2455
2455
  const writeCell = (0, import_react15.useCallback)(
2456
- (rowId, key, value) => {
2456
+ (rowId, key, value, base) => {
2457
2457
  const behind = queues.current.get(rowId) ?? Promise.resolve();
2458
- const next = behind.then(() => writeCellNow(rowId, key, value));
2458
+ const next = behind.then(() => writeCellNow(rowId, key, value, base));
2459
2459
  queues.current.set(
2460
2460
  rowId,
2461
2461
  next.then(
@@ -2473,7 +2473,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2473
2473
  const open = editing;
2474
2474
  if (!open) return;
2475
2475
  const value = typed.current;
2476
- if (!sameValue(startedFrom.current, value)) void writeCell(open.rowId, open.key, value);
2476
+ if (!sameValue(startedFrom.current, value)) void writeCell(open.rowId, open.key, value, startedFrom.current);
2477
2477
  if (!move) {
2478
2478
  setEditing(null);
2479
2479
  setDraft(null);
@@ -2502,7 +2502,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2502
2502
  (rowId, key) => {
2503
2503
  const held = refusals[cellKey(rowId, key)];
2504
2504
  if (!held) return;
2505
- void writeCell(rowId, key, held.attempted);
2505
+ void writeCell(rowId, key, held.attempted, held.conflict?.contested_fields[key]);
2506
2506
  },
2507
2507
  [refusals, writeCell]
2508
2508
  );
@@ -2598,7 +2598,7 @@ function useGridEditing({ tableId, fields, rows, reload, canWrite, mayWriteRow }
2598
2598
  commit,
2599
2599
  setCell: (rowId, key, value) => {
2600
2600
  if (!writable(rowId)) return;
2601
- void writeCell(rowId, key, value);
2601
+ void writeCell(rowId, key, value, shownValue(rowId, key));
2602
2602
  },
2603
2603
  retry,
2604
2604
  keepTheirs,
@@ -2683,7 +2683,7 @@ function GridCell({
2683
2683
  RefusalNotice,
2684
2684
  {
2685
2685
  error: refusal2.error,
2686
- className: "py-1.5",
2686
+ className: "py-1.5 text-left",
2687
2687
  onDiscard: () => editing.discard(row.id, field.key)
2688
2688
  }
2689
2689
  ) : null
@@ -2797,7 +2797,7 @@ function CellRefusalNotice({
2797
2797
  RefusalNotice,
2798
2798
  {
2799
2799
  error: refusal2.error,
2800
- className: "py-1.5",
2800
+ className: "py-1.5 text-left",
2801
2801
  actions: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "flex flex-col gap-1", "data-refusal-conflict": "", children: [
2802
2802
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-[11px]", children: [
2803
2803
  "Theirs: ",
@@ -2815,7 +2815,7 @@ function CellRefusalNotice({
2815
2815
  RefusalNotice,
2816
2816
  {
2817
2817
  error: refusal2.error,
2818
- className: "py-1.5",
2818
+ className: "py-1.5 text-left",
2819
2819
  ...typed ? { onKeepEditing: () => editing?.keepEditing(row.id, field.key) } : {},
2820
2820
  onDiscard: () => editing?.discard(row.id, field.key)
2821
2821
  }
@@ -7131,7 +7131,39 @@ var import_design_system22 = require("@ai-matrx/design-system");
7131
7131
  // src/RecordForm.tsx
7132
7132
  var import_react41 = require("react");
7133
7133
  var import_react42 = require("@ai-matrx/records/react");
7134
+ var import_core7 = require("@ai-matrx/records/core");
7135
+
7136
+ // src/writeOnTheWinningVersion.ts
7134
7137
  var import_core6 = require("@ai-matrx/records/core");
7138
+ function sameStoredValue(left, right) {
7139
+ if (left === right) return true;
7140
+ const empty = (v) => v === null || v === void 0 || v === "";
7141
+ if (empty(left) || empty(right)) return empty(left) && empty(right);
7142
+ if (typeof left === "object" || typeof right === "object") {
7143
+ try {
7144
+ return JSON.stringify(left) === JSON.stringify(right);
7145
+ } catch {
7146
+ return false;
7147
+ }
7148
+ }
7149
+ return false;
7150
+ }
7151
+ async function updateOnTheWinningVersion(client, recordId, patch, expectedVersion, base) {
7152
+ const send = (expected) => client.recordUpdate({
7153
+ record_id: recordId,
7154
+ patch,
7155
+ ...expected === null ? {} : { expectedVersion: expected }
7156
+ });
7157
+ const first = await send(expectedVersion);
7158
+ if (first.ok) return first;
7159
+ const conflict = (0, import_core6.asWriteConflict)(first.error, recordId);
7160
+ if (!conflict) return first;
7161
+ const untouched = base === "always" || Object.keys(patch).every((key) => sameStoredValue(conflict.contested_fields[key], base[key]));
7162
+ if (!untouched) return first;
7163
+ return send(conflict.current_version);
7164
+ }
7165
+
7166
+ // src/RecordForm.tsx
7135
7167
  var import_design_system20 = require("@ai-matrx/design-system");
7136
7168
 
7137
7169
  // src/systemTable.ts
@@ -7319,15 +7351,38 @@ function RecordForm({
7319
7351
  const fields = (0, import_react42.useFields)(tableId, recordType);
7320
7352
  const existing = (0, import_react42.useRecord)(recordId ?? null);
7321
7353
  const mutation = (0, import_react42.useRecordMutation)();
7354
+ const client = (0, import_react42.useRecordsClient)();
7355
+ const [edit, setEdit] = (0, import_react41.useState)({
7356
+ saving: false,
7357
+ error: null,
7358
+ conflict: null
7359
+ });
7360
+ const said = {
7361
+ saving: mutation.saving || edit.saving,
7362
+ error: recordId ? edit.error : mutation.error,
7363
+ conflict: recordId ? edit.conflict : mutation.conflict,
7364
+ clearConflict: () => {
7365
+ mutation.clearConflict();
7366
+ setEdit((e) => ({ ...e, conflict: null }));
7367
+ }
7368
+ };
7322
7369
  const [draft, setDraft] = (0, import_react41.useState)({});
7323
7370
  const [touched, setTouched] = (0, import_react41.useState)(false);
7324
7371
  const [changed, setChanged] = (0, import_react41.useState)({});
7325
7372
  const [answered, setAnswered] = (0, import_react41.useState)(null);
7326
7373
  const loaded = useRecordVersion(recordId ?? null);
7327
7374
  const loadedVersion = loaded.version;
7375
+ const changedRef = (0, import_react41.useRef)({});
7376
+ changedRef.current = changed;
7328
7377
  (0, import_react41.useEffect)(() => {
7329
7378
  const document3 = existing.data?.document;
7330
- if (document3) setDraft({ ...document3 });
7379
+ if (!document3) return;
7380
+ const mine = changedRef.current;
7381
+ setDraft((held) => {
7382
+ const next = { ...document3 };
7383
+ for (const key of Object.keys(mine)) next[key] = held[key];
7384
+ return next;
7385
+ });
7331
7386
  }, [existing.data?.record_id, loadedVersion]);
7332
7387
  const document2 = (0, import_react41.useMemo)(() => {
7333
7388
  const out = {};
@@ -7338,15 +7393,15 @@ function RecordForm({
7338
7393
  return out;
7339
7394
  }, [draft]);
7340
7395
  const predicted = (0, import_react41.useMemo)(
7341
- () => fields.data ? (0, import_core6.predictWriteRefusals)({ fields: fields.data, document: document2, ...recordType ? { recordType } : {} }) : [],
7396
+ () => fields.data ? (0, import_core7.predictWriteRefusals)({ fields: fields.data, document: document2, ...recordType ? { recordType } : {} }) : [],
7342
7397
  [fields.data, document2, recordType]
7343
7398
  );
7344
7399
  if (fields.error) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(RefusalNotice, { error: fields.error, className });
7345
7400
  if (existing.error) return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(RefusalNotice, { error: existing.error, className });
7346
- const storeSaid = mutation.error && mutation.error !== answered ? mutation.error : null;
7401
+ const storeSaid = said.error && said.error !== answered ? said.error : null;
7347
7402
  const fieldProblems = predicted.filter((p) => p.field_key);
7348
7403
  const problemsShown = (key) => touched || changed[key] ? fieldProblems.filter((p) => p.field_key === key) : [];
7349
- const storeRefusalIsOnAField = storeSaid !== null && !mutation.conflict && storeSaid.code === "refused_by_rule" && (fields.data ?? []).some((f) => problemsShown(f.key).length > 0);
7404
+ const storeRefusalIsOnAField = storeSaid !== null && !said.conflict && storeSaid.code === "refused_by_rule" && (fields.data ?? []).some((f) => problemsShown(f.key).length > 0);
7350
7405
  const stored = existing.data?.document;
7351
7406
  const focusField = (field) => {
7352
7407
  if (!field) return;
@@ -7377,13 +7432,22 @@ function RecordForm({
7377
7432
  setAnswered(null);
7378
7433
  if (recordId) {
7379
7434
  const version = loadedVersion;
7380
- const moved = await mutation.update({
7381
- record_id: recordId,
7382
- patch: document2,
7383
- ...version === null ? {} : { expectedVersion: version }
7435
+ if (Object.keys(changed).length === 0) {
7436
+ onSaved?.(recordId);
7437
+ return;
7438
+ }
7439
+ const patch = Object.fromEntries(Object.entries(document2).filter(([key]) => changed[key]));
7440
+ setEdit({ saving: true, error: null, conflict: null });
7441
+ const answered2 = await updateOnTheWinningVersion(client, recordId, patch, version, stored ?? {});
7442
+ setEdit({
7443
+ saving: false,
7444
+ error: answered2.ok ? null : answered2.error,
7445
+ conflict: answered2.ok ? null : (0, import_core7.asWriteConflict)(answered2.error, recordId)
7384
7446
  });
7447
+ const moved = answered2.ok ? answered2.data : null;
7385
7448
  if (moved !== null) {
7386
7449
  loaded.note(moved);
7450
+ setChanged({});
7387
7451
  host.notify?.success("Saved");
7388
7452
  existing.reload();
7389
7453
  onSaved?.(recordId);
@@ -7432,9 +7496,9 @@ function RecordForm({
7432
7496
  RefusalNotice,
7433
7497
  {
7434
7498
  error: storeSaid,
7435
- ...mutation.conflict ? {
7499
+ ...said.conflict ? {
7436
7500
  actions: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "mt-1 space-y-1", children: [
7437
- Object.entries(mutation.conflict.contested_fields).map(([key, theirs]) => {
7501
+ Object.entries(said.conflict.contested_fields).map(([key, theirs]) => {
7438
7502
  const column = (fields.data ?? []).find((f) => f.key === key);
7439
7503
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("p", { className: "text-[11px]", children: [
7440
7504
  column ? fieldName(column) : humanize(key),
@@ -7452,9 +7516,9 @@ function RecordForm({
7452
7516
  size: "sm",
7453
7517
  variant: "outline",
7454
7518
  onClick: () => {
7455
- if (mutation.conflict) loaded.note(mutation.conflict.current_version);
7519
+ if (said.conflict) loaded.note(said.conflict.current_version);
7456
7520
  existing.reload();
7457
- mutation.clearConflict();
7521
+ said.clearConflict();
7458
7522
  setAnswered(storeSaid);
7459
7523
  },
7460
7524
  children: "Take theirs and start again"
@@ -7466,7 +7530,7 @@ function RecordForm({
7466
7530
  ) : null,
7467
7531
  /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_design_system20.Separator, {}),
7468
7532
  /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex items-center gap-2", children: [
7469
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_design_system20.Button, { type: "submit", size: "sm", disabled: mutation.saving, children: mutation.saving ? "Saving\u2026" : recordId ? "Save" : "Create" }),
7533
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_design_system20.Button, { type: "submit", size: "sm", disabled: said.saving, children: said.saving ? "Saving\u2026" : recordId ? "Save" : "Create" }),
7470
7534
  onCancel ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_design_system20.Button, { type: "button", size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" }) : null
7471
7535
  ] })
7472
7536
  ]
@@ -7661,7 +7725,6 @@ function computedInPlainWords(row, field) {
7661
7725
  }
7662
7726
 
7663
7727
  // src/views.ts
7664
- var import_core7 = require("@ai-matrx/records/core");
7665
7728
  var VIEW_LAYOUTS = ["grid", "kanban", "calendar", "gallery"];
7666
7729
  var LAYOUT_LABEL = {
7667
7730
  grid: "Grid",
@@ -7786,16 +7849,7 @@ function viewFromRecord(row) {
7786
7849
  };
7787
7850
  }
7788
7851
  async function saveViewPatch(client, viewId, document2, expectedVersion) {
7789
- const send = (expected) => client.recordUpdate({
7790
- record_id: viewId,
7791
- patch: document2,
7792
- ...expected === null ? {} : { expectedVersion: expected }
7793
- });
7794
- const first = await send(expectedVersion);
7795
- if (first.ok) return first;
7796
- const conflict = (0, import_core7.asWriteConflict)(first.error, viewId);
7797
- if (!conflict) return first;
7798
- return send(conflict.current_version);
7852
+ return updateOnTheWinningVersion(client, viewId, document2, expectedVersion, "always");
7799
7853
  }
7800
7854
  function viewPatchDocument(patch) {
7801
7855
  const out = {};
@@ -16505,8 +16559,7 @@ function visibilityLaneFor(table) {
16505
16559
  if (table.is_kernel) return null;
16506
16560
  if ((table.slug ?? "").startsWith(APP_TABLE_PREFIX) || keptByTheApp(table)) return null;
16507
16561
  if (table.visibility === "personal") return "mine";
16508
- if (table.visibility === "link") return "community";
16509
- if (table.visibility === "public") return "world";
16562
+ if (table.visibility === "link" || table.visibility === "public") return "world";
16510
16563
  return "organization";
16511
16564
  }
16512
16565
  function TablesHome({ onOpenTable, className }) {