@ai-matrx/records-ui 0.60.0 → 0.61.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
@@ -1178,8 +1178,13 @@ var LabelContext = createContext3({
1178
1178
  // provides one, so this is only ever a bare unit test.
1179
1179
  lookup: () => null,
1180
1180
  want: () => {
1181
- }
1181
+ },
1182
+ waiting: () => null
1182
1183
  });
1184
+ var SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS = 1e3;
1185
+ function waitingLineFor(field, target) {
1186
+ return field.type === "list" ? "Reading this column\u2019s list of choices\u2026" : `Reading the ${target ?? "records"} this column points at\u2026`;
1187
+ }
1183
1188
  function idsOf(value) {
1184
1189
  if (Array.isArray(value)) return value.map(String).filter(isId);
1185
1190
  if (typeof value === "string" && isId(value)) return [value];
@@ -1196,13 +1201,48 @@ function RecordLabelProvider({ children }) {
1196
1201
  const client = useRecordsClient5();
1197
1202
  const [names, setNames] = useState5({});
1198
1203
  const [settled, setSettled] = useState5({});
1204
+ const [waitingLines, setWaitingLines] = useState5({});
1199
1205
  const asked = useRef2(/* @__PURE__ */ new Set());
1200
1206
  const chased = useRef2(/* @__PURE__ */ new Set());
1207
+ const timers = useRef2(/* @__PURE__ */ new Map());
1208
+ const startWaitClock = useCallback3((field, target) => {
1209
+ if (timers.current.has(field.id)) return;
1210
+ timers.current.set(
1211
+ field.id,
1212
+ setTimeout(() => {
1213
+ timers.current.delete(field.id);
1214
+ setWaitingLines(
1215
+ (current) => current[field.id] ? current : { ...current, [field.id]: waitingLineFor(field, target) }
1216
+ );
1217
+ }, SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS)
1218
+ );
1219
+ }, []);
1220
+ const stopWaitClock = useCallback3((fieldId) => {
1221
+ const timer = timers.current.get(fieldId);
1222
+ if (timer) {
1223
+ clearTimeout(timer);
1224
+ timers.current.delete(fieldId);
1225
+ }
1226
+ setWaitingLines((current) => {
1227
+ if (!current[fieldId]) return current;
1228
+ const next = { ...current };
1229
+ delete next[fieldId];
1230
+ return next;
1231
+ });
1232
+ }, []);
1233
+ useEffect5(
1234
+ () => () => {
1235
+ for (const timer of timers.current.values()) clearTimeout(timer);
1236
+ timers.current.clear();
1237
+ },
1238
+ []
1239
+ );
1201
1240
  const want = useCallback3(
1202
1241
  (field) => {
1203
1242
  if (!pointsAtRecords(field)) return;
1204
1243
  if (asked.current.has(field.id)) return;
1205
1244
  asked.current.add(field.id);
1245
+ startWaitClock(field, null);
1206
1246
  void (async () => {
1207
1247
  if (field.type === "list") {
1208
1248
  const result = await client.fieldOptions({ field_id: field.id });
@@ -1216,11 +1256,13 @@ function RecordLabelProvider({ children }) {
1216
1256
  });
1217
1257
  }
1218
1258
  setSettled((current) => ({ ...current, [field.id]: true }));
1259
+ stopWaitClock(field.id);
1219
1260
  return;
1220
1261
  }
1221
1262
  const target = field.relation_target;
1222
1263
  if (!target) {
1223
1264
  setSettled((current) => ({ ...current, [field.id]: true }));
1265
+ stopWaitClock(field.id);
1224
1266
  return;
1225
1267
  }
1226
1268
  const [rows, tables] = await Promise.all([
@@ -1238,24 +1280,54 @@ function RecordLabelProvider({ children }) {
1238
1280
  });
1239
1281
  }
1240
1282
  setSettled((current) => ({ ...current, [field.id]: true }));
1283
+ stopWaitClock(field.id);
1241
1284
  })();
1242
1285
  },
1243
- [client]
1286
+ [client, startWaitClock, stopWaitClock]
1244
1287
  );
1288
+ const queued = useRef2(/* @__PURE__ */ new Map());
1289
+ const flushing = useRef2(null);
1290
+ const flush = useCallback3(() => {
1291
+ flushing.current = null;
1292
+ const batch = [...queued.current.entries()];
1293
+ queued.current.clear();
1294
+ if (batch.length === 0) return;
1295
+ void (async () => {
1296
+ const learned = {};
1297
+ const LANES = 6;
1298
+ let at = 0;
1299
+ await Promise.all(
1300
+ Array.from({ length: Math.min(LANES, batch.length) }, async () => {
1301
+ for (; ; ) {
1302
+ const next = batch[at];
1303
+ at += 1;
1304
+ if (!next) return;
1305
+ const [id] = next;
1306
+ const result = await client.recordRead({ record_id: id });
1307
+ learned[id] = result.ok ? recordName(result.data.document, null, "Untitled") : WITHHELD_RECORD_LABEL;
1308
+ }
1309
+ })
1310
+ );
1311
+ setNames((current) => ({ ...current, ...learned }));
1312
+ for (const [, field] of batch) stopWaitClock(field.id);
1313
+ })();
1314
+ }, [client, stopWaitClock]);
1245
1315
  const chase = useCallback3(
1246
1316
  (field, id) => {
1247
1317
  const key = `${field.id}:${id}`;
1248
1318
  if (chased.current.has(key)) return;
1249
1319
  chased.current.add(key);
1250
- void (async () => {
1251
- const result = await client.recordRead({ record_id: id });
1252
- setNames((current) => ({
1253
- ...current,
1254
- [id]: result.ok ? recordName(result.data.document, null, "Untitled") : WITHHELD_RECORD_LABEL
1255
- }));
1256
- })();
1320
+ queued.current.set(id, field);
1321
+ startWaitClock(field, null);
1322
+ if (flushing.current === null) flushing.current = setTimeout(flush, 0);
1257
1323
  },
1258
- [client]
1324
+ [flush, startWaitClock]
1325
+ );
1326
+ useEffect5(
1327
+ () => () => {
1328
+ if (flushing.current !== null) clearTimeout(flushing.current);
1329
+ },
1330
+ []
1259
1331
  );
1260
1332
  const lookup = useCallback3(
1261
1333
  (field, id) => {
@@ -1266,7 +1338,8 @@ function RecordLabelProvider({ children }) {
1266
1338
  },
1267
1339
  [names, settled, chase]
1268
1340
  );
1269
- const value = useMemo5(() => ({ lookup, want }), [lookup, want]);
1341
+ const waiting = useCallback3((field) => waitingLines[field.id] ?? null, [waitingLines]);
1342
+ const value = useMemo5(() => ({ lookup, want, waiting }), [lookup, want, waiting]);
1270
1343
  return /* @__PURE__ */ jsx7(LabelContext.Provider, { value, children });
1271
1344
  }
1272
1345
  function useRecordLabels(fields) {
@@ -1276,6 +1349,10 @@ function useRecordLabels(fields) {
1276
1349
  }, [fields, want]);
1277
1350
  return lookup;
1278
1351
  }
1352
+ function useLabelWait(field) {
1353
+ const { waiting } = useContext3(LabelContext);
1354
+ return field ? waiting(field) : null;
1355
+ }
1279
1356
 
1280
1357
  // src/values.tsx
1281
1358
  import { Fragment as Fragment2, jsx as jsx8 } from "react/jsx-runtime";
@@ -1289,7 +1366,7 @@ var ABSENCE_WORDS = {
1289
1366
  refused: "refused",
1290
1367
  conflicting: "conflicting"
1291
1368
  };
1292
- function renderValue(field, value, document2, labels) {
1369
+ function renderValue(field, value, document2, labels, stillWaitingFor) {
1293
1370
  const envelope = envelopeFor(document2, field.key);
1294
1371
  if (envelope?.absent) {
1295
1372
  return /* @__PURE__ */ jsx8("span", { className: "text-xs italic text-muted-foreground", title: `VAL-2: ${envelope.absent}`, children: ABSENCE_WORDS[String(envelope.absent)] ?? String(envelope.absent) });
@@ -1310,13 +1387,13 @@ function renderValue(field, value, document2, labels) {
1310
1387
  if (typeof value === "boolean") {
1311
1388
  return /* @__PURE__ */ jsx8("span", { className: kind === "checkbox" ? "text-sm" : "", title: value ? "Yes" : "No", children: value ? "\u2713" : "\u2014" });
1312
1389
  }
1313
- return /* @__PURE__ */ jsx8("span", { className: cn5("truncate", kind === "number" || kind === "currency" || kind === "percent" ? "tabular-nums" : ""), children: scalarText(field, value, labels) });
1390
+ return /* @__PURE__ */ jsx8("span", { className: cn5("truncate", kind === "number" || kind === "currency" || kind === "percent" ? "tabular-nums" : ""), children: scalarText(field, value, labels, stillWaitingFor) });
1314
1391
  }
1315
- function scalarText(field, value, labels) {
1392
+ function scalarText(field, value, labels, stillWaitingFor) {
1316
1393
  if (value === null || value === void 0) return "\u2014";
1317
1394
  const kind = editorKindFor(field);
1318
1395
  if (isId(value) && (field.type === "list" || field.type === "relation")) {
1319
- return labels?.(field, String(value)) ?? "Loading\u2026";
1396
+ return labels?.(field, String(value)) ?? stillWaitingFor ?? "Loading\u2026";
1320
1397
  }
1321
1398
  if (typeof value === "number") {
1322
1399
  if (kind === "percent") return `${value}%`;
@@ -1348,9 +1425,10 @@ function RecordValue({
1348
1425
  format
1349
1426
  }) {
1350
1427
  const labels = useRecordLabels(ONE_FIELD_ARRAY_CACHE(field));
1428
+ const stillWaitingFor = useLabelWait(field);
1351
1429
  const formatted = format ? formatColumnValue({ format }, value) : null;
1352
1430
  if (formatted) return /* @__PURE__ */ jsx8(FormattedCell, { result: formatted });
1353
- return /* @__PURE__ */ jsx8(Fragment2, { children: renderValue(field, value, document2, labels) });
1431
+ return /* @__PURE__ */ jsx8(Fragment2, { children: renderValue(field, value, document2, labels, stillWaitingFor) });
1354
1432
  }
1355
1433
  var ONE_FIELD_ARRAYS = /* @__PURE__ */ new WeakMap();
1356
1434
  function ONE_FIELD_ARRAY_CACHE(field) {
@@ -6214,6 +6292,37 @@ var LAYOUT_FIELD_KIND = {
6214
6292
  calendar: "date",
6215
6293
  gallery: null
6216
6294
  };
6295
+ var PAGE_VIEWS = [...VIEW_LAYOUTS, "dashboards"];
6296
+ var PAGE_VIEW_LABEL = {
6297
+ ...LAYOUT_LABEL,
6298
+ dashboards: "Dashboards"
6299
+ };
6300
+ var PAGE_VIEW_SPELLINGS = {
6301
+ grid: "grid",
6302
+ table: "grid",
6303
+ rows: "grid",
6304
+ list: "grid",
6305
+ kanban: "kanban",
6306
+ board: "kanban",
6307
+ pipeline: "kanban",
6308
+ calendar: "calendar",
6309
+ month: "calendar",
6310
+ schedule: "calendar",
6311
+ gallery: "gallery",
6312
+ cards: "gallery",
6313
+ dashboards: "dashboards",
6314
+ dashboard: "dashboards",
6315
+ charts: "dashboards"
6316
+ };
6317
+ function pageViewFromParam(raw) {
6318
+ if (typeof raw !== "string") return null;
6319
+ const token = raw.trim().toLowerCase();
6320
+ if (token === "") return null;
6321
+ return PAGE_VIEW_SPELLINGS[token] ?? null;
6322
+ }
6323
+ function unknownViewLine(raw) {
6324
+ return `The link asked for a view called \u201C${raw}\u201D, which this table has no view for, so you are looking at ${PAGE_VIEW_LABEL.grid}. The views this table has are ${PAGE_VIEWS.map((v) => PAGE_VIEW_LABEL[v]).join(", ")} \u2014 pick one above and the address follows it.`;
6325
+ }
6217
6326
  var LAYOUT_NO_FIELD = {
6218
6327
  grid: "",
6219
6328
  kanban: "This table has no choice field yet, so there is nothing to make columns from. Add a field that holds one of a set of choices \u2014 Add field, then Select \u2014 and it appears here.",
@@ -14401,6 +14510,19 @@ function surfaceChosen(current, asking) {
14401
14510
  function openingRail(activeRecordId) {
14402
14511
  return activeRecordId ? { rail: "record", record: activeRecordId } : { rail: "none", record: null };
14403
14512
  }
14513
+ function openingView(activeView, activeDashboardId) {
14514
+ const named = pageViewFromParam(activeView);
14515
+ if (named === null) {
14516
+ const asked = typeof activeView === "string" ? activeView.trim() : "";
14517
+ return {
14518
+ main: activeDashboardId ? "dashboards" : "records",
14519
+ layout: null,
14520
+ unknown: asked === "" ? null : asked
14521
+ };
14522
+ }
14523
+ if (named === "dashboards") return { main: "dashboards", layout: null, unknown: null };
14524
+ return { main: "records", layout: named, unknown: null };
14525
+ }
14404
14526
  function TablePage({
14405
14527
  tableId,
14406
14528
  pageSize = 100,
@@ -14409,6 +14531,8 @@ function TablePage({
14409
14531
  leaveLabel = "Back to your tables",
14410
14532
  activeDashboardId,
14411
14533
  activeRecordId,
14534
+ activeView,
14535
+ onViewChanged,
14412
14536
  className
14413
14537
  }) {
14414
14538
  const client = useRecordsClient43();
@@ -14417,22 +14541,37 @@ function TablePage({
14417
14541
  const organizationId = useRecordsClient43().config.organizationId;
14418
14542
  const [view, setView] = useState52(null);
14419
14543
  const opening = openingRail(activeRecordId);
14544
+ const opened = openingView(activeView, activeDashboardId);
14420
14545
  const [asking, setAsking] = useState52(null);
14546
+ const [layoutFromLink, setLayoutFromLink] = useState52(opened.layout);
14421
14547
  const [surface, setSurface] = useState52({
14422
- main: activeDashboardId ? "dashboards" : "records",
14548
+ main: opened.main,
14423
14549
  rail: opening.rail
14424
14550
  });
14425
14551
  const { main, rail } = surface;
14426
14552
  const setRail = (next) => setSurface((now) => ({ ...now, rail: next }));
14427
14553
  const [openRecord, setOpenRecord] = useState52(opening.record);
14428
14554
  const viewVersion = useRecordVersion(view?.id ?? null);
14429
- const press = (pressed) => setSurface((now) => chooseSurface(now, pressed));
14555
+ const shownLayout = layoutFromLink ?? view?.layout ?? "grid";
14556
+ const press = (pressed) => {
14557
+ const next = chooseSurface(surface, pressed);
14558
+ setSurface(next);
14559
+ if (next.main !== surface.main) {
14560
+ onViewChanged?.(next.main === "dashboards" ? "dashboards" : shownLayout);
14561
+ }
14562
+ };
14430
14563
  const show = (next) => press({ rail: next });
14431
14564
  useEffect41(() => {
14432
14565
  if (!activeRecordId) return;
14433
14566
  setOpenRecord(activeRecordId);
14434
14567
  setSurface((now) => ({ ...now, rail: "record" }));
14435
14568
  }, [activeRecordId]);
14569
+ useEffect41(() => {
14570
+ const named = pageViewFromParam(activeView);
14571
+ if (named === null) return;
14572
+ setLayoutFromLink(named === "dashboards" ? null : named);
14573
+ setSurface((now) => ({ ...now, main: named === "dashboards" ? "dashboards" : "records" }));
14574
+ }, [activeView]);
14436
14575
  const patchView = useCallback37(
14437
14576
  async (patch) => {
14438
14577
  const current = view;
@@ -14586,11 +14725,16 @@ function TablePage({
14586
14725
  /* @__PURE__ */ jsx57(ExportMenu, { tableId })
14587
14726
  ] }),
14588
14727
  main === "dashboards" ? /* @__PURE__ */ jsx57(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : /* @__PURE__ */ jsxs52(Fragment27, { children: [
14728
+ opened.unknown ? /* @__PURE__ */ jsx57("p", { className: "rounded-md border border-amber-600/40 bg-amber-500/5 px-3 py-2 text-xs leading-relaxed text-amber-700 dark:border-amber-400/40 dark:text-amber-300", children: unknownViewLine(opened.unknown) }) : null,
14589
14729
  /* @__PURE__ */ jsx57(
14590
14730
  ViewSwitcher,
14591
14731
  {
14592
- view: view ?? defaultView(tableId),
14732
+ view: { ...view ?? defaultView(tableId), layout: shownLayout },
14593
14733
  pageSize,
14734
+ onLayoutChange: (layout) => {
14735
+ setLayoutFromLink(layout);
14736
+ onViewChanged?.(layout);
14737
+ },
14594
14738
  ...view ? { onViewChange: (patch) => void patchView(patch) } : {},
14595
14739
  ...rights.write ? { onNewRecordForm: () => setRail("new-record") } : {},
14596
14740
  onOpenRecord: (recordId) => {
@@ -14766,6 +14910,8 @@ export {
14766
14910
  NO_SHARE_REASON,
14767
14911
  NO_UPLOAD_REASON,
14768
14912
  NotifyRuleEditor,
14913
+ PAGE_VIEWS,
14914
+ PAGE_VIEW_LABEL,
14769
14915
  PARITY_LABEL,
14770
14916
  PARITY_MADE_OF,
14771
14917
  Peek,
@@ -14789,6 +14935,7 @@ export {
14789
14935
  RefusalNotice,
14790
14936
  RelationPicker,
14791
14937
  SAVED_VIEWS_UNAVAILABLE,
14938
+ SAY_WHAT_YOU_ARE_WAITING_FOR_AFTER_MS,
14792
14939
  SERIES_COLORS,
14793
14940
  SOMEBODY,
14794
14941
  STORE_ANSWERS_THESE,
@@ -14859,6 +15006,9 @@ export {
14859
15006
  machineIdentityIn,
14860
15007
  memberName,
14861
15008
  nextInWords,
15009
+ openingRail,
15010
+ openingView,
15011
+ pageViewFromParam,
14862
15012
  parityTypesWithNoExplanation,
14863
15013
  parseGridPresentation,
14864
15014
  personActor,
@@ -14884,10 +15034,12 @@ export {
14884
15034
  tableName,
14885
15035
  tableRightsAt,
14886
15036
  tokenFor,
15037
+ unknownViewLine,
14887
15038
  useCanShare,
14888
15039
  useEmbedHandshake,
14889
15040
  useEnrichCells,
14890
15041
  useGridEditing,
15042
+ useLabelWait,
14891
15043
  useMeasuredWidth,
14892
15044
  useMyChecklistSteps,
14893
15045
  useRecordLabels,