@ai-matrx/records-ui 0.65.0 → 0.67.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
@@ -80,12 +80,12 @@ var NO_RIGHTS = tableRights(NOT_ANSWERED_YET);
80
80
  function tableRightsAt(level) {
81
81
  return tableRights(whatYouMayDo(level, true));
82
82
  }
83
- var NO_SAVED_VIEWS_REASON = "No saved-view port is bound, so there is nothing here to subscribe to. A subscription points at a `platform.saved_view`, which the record store has no door onto \u2014 bind `savedViews` on <RecordsUiProvider> with your host's own list and the picker appears.";
84
- var NO_MEMBERS_REASON = "No membership port is bound, so this question cannot offer anybody. A person field points at the kernel Person Table, and who is in this organization is the platform's answer rather than the record store's \u2014 bind `members` on <RecordsUiProvider> with your host's own roster and the picker appears.";
85
- var NO_SHARE_REASON = "No share dialog is bound, so this screen offers no Share button rather than one that opens nothing. Bind `share` on <RecordsUiProvider> with the app's own sharing dialog \u2014 AI Matrx has exactly one (features/sharing), and this package deliberately does not ship a second.";
86
- var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records, and where this app puts its grid is the host's routing rather than this package's \u2014 bind `openRecords` on <RecordsUiProvider> and a bar, a slice or a row opens the records it counted.";
87
- var NO_REASK_REASON = "Asking for a change in words needs an agent, and this package deliberately reaches one through the host rather than calling a model itself. Bind `onReask` on <RecordsUiProvider> \u2014 it drives `dashboard_propose` on the server's `records` tool with this dashboard's id \u2014 and this box works.";
88
- var NO_UPLOAD_REASON = "No file store is bound, so this question cannot take a file. Bind `upload` on <RecordsUiProvider> with your host's file service and the chooser appears.";
83
+ var NO_SAVED_VIEWS_REASON = "There are no saved views here yet, so there is nothing to be told about. Save a view of this table first and it will be offered here.";
84
+ var NO_MEMBERS_REASON = "This app cannot offer the people in your organization on this screen yet, so there is nobody to pick. Type the person's name into a text field for now, and tell us \u2014 it is on our list, not yours.";
85
+ var NO_SHARE_REASON = "Sharing is not available on this screen, so there is no Share button here rather than one that opens nothing. Share this table from the tables list instead.";
86
+ var NO_OPEN_RECORDS_REASON = "These numbers do not click through yet. Every one of them is a count of real records \u2014 open the records view and group it by the same field to see them. It is on our list, not yours.";
87
+ var NO_REASK_REASON = "Changing this dashboard by describing it is not switched on here yet, so this box is off rather than one that goes nowhere. Add and remove the blocks by hand above. It is on our list, not yours.";
88
+ var NO_UPLOAD_REASON = "This screen cannot take a file yet, so there is no chooser here rather than one that loses what you pick. Put a link to the file in a text field for now. It is on our list, not yours.";
89
89
  var HostContext = createContext(null);
90
90
  function RecordsUiProvider({ value, children }) {
91
91
  const bound = useMemo(() => value, [value]);
@@ -160,6 +160,10 @@ var TITLE = {
160
160
  not_supported: "That cannot be done here",
161
161
  store_limit: "This has got as big as it can",
162
162
  store_not_served: "This organization does not keep its data here",
163
+ unreachable: "We could not reach your data",
164
+ needs_approval: "This is waiting on someone's approval",
165
+ stale_read: "Your view of this is a moment behind",
166
+ timed_out: "That took too long to answer",
163
167
  internal: "Something went wrong at our end"
164
168
  };
165
169
  var REMEDY = {
@@ -174,8 +178,35 @@ var REMEDY = {
174
178
  not_supported: "There is another way to do this \u2014 ask, and we will point you at it.",
175
179
  store_limit: "Split it up, or ask us to raise the ceiling for your organization.",
176
180
  store_not_served: "An administrator of this organization turns the record store on from the data settings screen.",
181
+ // 🚨 NOTHING WAS WRITTEN AND NOTHING WAS REFUSED (lane TAILS-6). The call
182
+ // never arrived, so "try again" is the honest instruction rather than a
183
+ // consolation — and saying nothing changed is the half a person actually
184
+ // worries about.
185
+ unreachable: "Nothing was changed. Check your connection and open this again.",
186
+ needs_approval: "Nothing was changed yet. The people who can approve it have been asked.",
187
+ stale_read: "Open this again in a moment and it will be up to date.",
188
+ timed_out: "Nothing was changed. Try a smaller page, or try again in a moment.",
177
189
  internal: "Nothing you did caused this. Try again, and tell us if it keeps happening."
178
190
  };
191
+ var ENGINEER_TEXT = [
192
+ /@ai-matrx\//,
193
+ /\bSQLSTATE\b/i,
194
+ /\bPostgREST\b/i,
195
+ /\bIndexedDB\b/i,
196
+ // `<RecordsUiProvider>` — a component, on a screen.
197
+ /<\/?[A-Z][A-Za-z0-9]*\s*\/?>/,
198
+ // `savedViews`, `openRecords`, `onReask` — an identifier, however it is
199
+ // quoted. English does not write camelCase; code does.
200
+ /\b[a-z][a-z0-9]*[A-Z][A-Za-z0-9]*\b/,
201
+ // A backticked token is a thing you type into an editor, not a thing you do.
202
+ /`[A-Za-z_][A-Za-z0-9_.]*`/,
203
+ // "bind X on Y", "pass X", "import X" — an instruction to whoever maintains
204
+ // the app, arriving on the screen of somebody who cannot act on it.
205
+ /\bbind\s+[`'"A-Za-z]/i
206
+ ];
207
+ function isEngineerText(text) {
208
+ return ENGINEER_TEXT.some((shape) => shape.test(text));
209
+ }
179
210
  function withoutMachineClauses(sentence2) {
180
211
  const dropped = [];
181
212
  const footnotes = [];
@@ -213,11 +244,17 @@ function refusalForAPerson(error) {
213
244
  const message = plainSentence(String(error.message ?? ""));
214
245
  const hint = plainSentence(String(error.hint ?? ""));
215
246
  const title = TITLE[code] ?? "The store refused this";
216
- const remedy = hint.kept ?? REMEDY[code] ?? REMEDY["internal"];
217
- const said = (message.kept ?? "").trim();
247
+ const hintIsForEngineers = isEngineerText(String(error.hint ?? ""));
248
+ const remedy = (hintIsForEngineers ? null : hint.kept) ?? REMEDY[code] ?? REMEDY["internal"];
249
+ const messageIsForEngineers = isEngineerText(String(error.message ?? ""));
250
+ const said = (messageIsForEngineers ? "" : message.kept ?? "").trim();
218
251
  const forEngineers = [
219
252
  error.sqlstate ? `SQLSTATE ${error.sqlstate}.` : null,
220
- error.hint && hint.kept !== error.hint ? `Hint: ${error.hint}` : null,
253
+ // The client's own note about a class it did not recognise, or a call that
254
+ // never reached the store. It is rendered out of sight, never as a sentence.
255
+ error.diagnostic ? error.diagnostic : null,
256
+ messageIsForEngineers ? `Message: ${error.message}` : null,
257
+ error.hint && (hintIsForEngineers || hint.kept !== error.hint) ? `Hint: ${error.hint}` : null,
221
258
  message.dropped.length > 0 || hint.dropped.length > 0 ? `Dropped from the person's view: ${[...message.dropped, ...hint.dropped].join(" | ")}` : null
222
259
  ].filter(Boolean).join(" ");
223
260
  return {
@@ -571,6 +608,9 @@ function isRelationFieldType(id) {
571
608
  function parityTypesWithNoExplanation() {
572
609
  return PARITY_FIELD_TYPES.map((row) => row.parity_type).filter((type) => !BY_ID.has(type));
573
610
  }
611
+ var ADD_FIELD_OPEN_LABEL = "Add field";
612
+ var ADD_FIELD_SAVE_LABEL = "Create field";
613
+ var EDIT_FIELD_SAVE_LABEL = "Save field";
574
614
 
575
615
  // src/parity.ts
576
616
  var kernelId = (name) => KERNEL_TABLES.find((t) => t.name === name)?.id ?? "";
@@ -2482,7 +2522,7 @@ import {
2482
2522
  cn as cn8
2483
2523
  } from "@ai-matrx/design-system";
2484
2524
  import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2485
- var NO_ENRICH_REASON = "No enrich port is bound, so this panel does not offer to fill a column it cannot fill. The value has to be written by the SERVER with actor `agent` on behalf of you \u2014 a browser that stamped itself `agent` would be a browser claiming to be one \u2014 so bind `enrich` on <RecordsUiProvider> with a call to the server's own `records` tool (action `enrich_propose`). Everything else on this panel works without it.";
2525
+ var NO_ENRICH_REASON = "Having an agent fill this column is not switched on here yet, so this panel does not offer it rather than offering something that cannot happen. Everything else on this panel works. It is on our list, not yours.";
2486
2526
  function cellState(cell) {
2487
2527
  if (!cell || !cell.agent_owned) return "none";
2488
2528
  if (cell.pinned) return "yours";
@@ -4781,7 +4821,7 @@ function FieldEditor({ tableId, field, onSaved, onCancel, onRemoved, className }
4781
4821
  size: "sm",
4782
4822
  disabled: mutation.saving || missing !== null,
4783
4823
  onClick: () => void save(),
4784
- children: mutation.saving ? "Saving\u2026" : field ? "Save field" : "Add field"
4824
+ children: mutation.saving ? "Saving\u2026" : field ? EDIT_FIELD_SAVE_LABEL : ADD_FIELD_SAVE_LABEL
4785
4825
  }
4786
4826
  ),
4787
4827
  onCancel ? /* @__PURE__ */ jsx17(Button12, { size: "sm", variant: "ghost", onClick: onCancel, children: "Cancel" }) : null,
@@ -5816,7 +5856,7 @@ import { useEffect as useEffect9, useMemo as useMemo14, useState as useState18 }
5816
5856
  import { useRecordsClient as useRecordsClient12 } from "@ai-matrx/records/react";
5817
5857
  import { Skeleton as Skeleton4, cn as cn16 } from "@ai-matrx/design-system";
5818
5858
  import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
5819
- var NO_CHAT_REASON = "No chat port is bound, so this panel is absent rather than showing a composer that goes nowhere. Bind `chat` on <RecordsUiProvider> with your host's own chat surface \u2014 in AI Matrx that is `AgentConversationColumn`, launched through a Mandate \u2014 and this panel becomes that surface with this record as its context. This package deliberately ships no second chat UI.";
5859
+ var NO_CHAT_REASON = "Chatting about this record is not switched on here yet, so this panel is empty rather than showing a box that goes nowhere. It is on our list, not yours.";
5820
5860
  function RecordChat({ tableId, recordId, className }) {
5821
5861
  const client = useRecordsClient12();
5822
5862
  const host = useRecordsUi();
@@ -9760,6 +9800,7 @@ function PortalsPanel({ tableId, className }) {
9760
9800
  {
9761
9801
  portalId: portal.portal_id,
9762
9802
  ...tableId ? { tableId } : {},
9803
+ origin,
9763
9804
  onChanged: () => void load()
9764
9805
  }
9765
9806
  ) : null
@@ -9767,7 +9808,7 @@ function PortalsPanel({ tableId, className }) {
9767
9808
  }) })
9768
9809
  ] });
9769
9810
  }
9770
- function CopyLink({ url }) {
9811
+ function CopyLink({ url, what }) {
9771
9812
  const [copied, setCopied] = useState33(false);
9772
9813
  const [shown, setShown] = useState33(false);
9773
9814
  return /* @__PURE__ */ jsxs32(Fragment16, { children: [
@@ -9788,7 +9829,7 @@ function CopyLink({ url }) {
9788
9829
  }
9789
9830
  })();
9790
9831
  },
9791
- children: copied ? "Copied" : "Copy link"
9832
+ children: copied ? "Copied" : what ? `Copy ${what}` : "Copy link"
9792
9833
  }
9793
9834
  ),
9794
9835
  shown ? /* @__PURE__ */ jsxs32("p", { className: "mt-1.5 w-full break-all rounded border border-dashed border-border px-2 py-1 text-xs", children: [
@@ -9800,6 +9841,7 @@ function CopyLink({ url }) {
9800
9841
  function PortalDetail({
9801
9842
  portalId,
9802
9843
  tableId,
9844
+ origin,
9803
9845
  onChanged
9804
9846
  }) {
9805
9847
  const client = useRecordsClient25();
@@ -9849,6 +9891,7 @@ function PortalDetail({
9849
9891
  PortalCardView,
9850
9892
  {
9851
9893
  card,
9894
+ origin,
9852
9895
  ...tableId ? { tableId } : {},
9853
9896
  onRevoke: revoke,
9854
9897
  onPreview: preview,
@@ -9871,6 +9914,7 @@ function PortalCardView({
9871
9914
  onRevoke,
9872
9915
  onPreview,
9873
9916
  invite,
9917
+ origin,
9874
9918
  className
9875
9919
  }) {
9876
9920
  return /* @__PURE__ */ jsxs32("div", { className: cn30("mt-2.5 flex flex-col gap-2.5 border-t border-border pt-2.5", className), children: [
@@ -9901,7 +9945,7 @@ function PortalCardView({
9901
9945
  ] }, exposed.table_id))
9902
9946
  ] }),
9903
9947
  /* @__PURE__ */ jsx36(Separator10, {}),
9904
- /* @__PURE__ */ jsx36(People, { card, onRevoke, onPreview }),
9948
+ /* @__PURE__ */ jsx36(People, { card, onRevoke, onPreview, origin }),
9905
9949
  invite ? /* @__PURE__ */ jsxs32(Fragment16, { children: [
9906
9950
  /* @__PURE__ */ jsx36(Separator10, {}),
9907
9951
  invite
@@ -9911,8 +9955,10 @@ function PortalCardView({
9911
9955
  function People({
9912
9956
  card,
9913
9957
  onRevoke,
9914
- onPreview
9958
+ onPreview,
9959
+ origin
9915
9960
  }) {
9961
+ const here = origin ?? (typeof window === "undefined" ? "" : window.location.origin);
9916
9962
  const [askingToRevoke, setAskingToRevoke] = useState33(null);
9917
9963
  const [busy, setBusy] = useState33(false);
9918
9964
  const [said, setSaid] = useState33(null);
@@ -9954,6 +10000,7 @@ function People({
9954
10000
  /* @__PURE__ */ jsx36("p", { className: "mt-0.5 break-all text-muted-foreground", children: person.email }),
9955
10001
  /* @__PURE__ */ jsx36("p", { className: "mt-0.5 text-muted-foreground", children: state.why }),
9956
10002
  person.is_active ? /* @__PURE__ */ jsxs32("div", { className: "mt-1.5 flex flex-wrap gap-1.5", children: [
10003
+ person.accept_path ? /* @__PURE__ */ jsx36(CopyLink, { url: `${here}${person.accept_path}`, what: "their link" }) : null,
9957
10004
  /* @__PURE__ */ jsx36(
9958
10005
  Button30,
9959
10006
  {
@@ -9965,6 +10012,11 @@ function People({
9965
10012
  ),
9966
10013
  /* @__PURE__ */ jsx36(Button30, { size: "sm", variant: "ghost", onClick: () => setAskingToRevoke(person), children: "Remove their access" })
9967
10014
  ] }) : null,
10015
+ person.is_active && !person.signed_in && !person.accept_path ? /* @__PURE__ */ jsxs32("p", { className: "mt-1.5 text-muted-foreground", children: [
10016
+ "There is no link to copy for ",
10017
+ person.client || person.email,
10018
+ ": they were asked some other way, or the link they were sent has expired. Invite them again and a fresh one is made."
10019
+ ] }) : null,
9968
10020
  asking ? /* @__PURE__ */ jsxs32("div", { className: "mt-1.5 flex flex-col gap-1.5 rounded border border-destructive/40 px-2 py-1.5", children: [
9969
10021
  /* @__PURE__ */ jsx36("span", { className: "text-destructive", children: revokeConsequence(person, card.title) }),
9970
10022
  /* @__PURE__ */ jsxs32("div", { className: "flex flex-wrap gap-1.5", children: [
@@ -13810,7 +13862,7 @@ function CaptureRun({ sheetId, face: given, className }) {
13810
13862
  // src/CaptureSheet.tsx
13811
13863
  import { Fragment as Fragment25, jsx as jsx52, jsxs as jsxs48 } from "react/jsx-runtime";
13812
13864
  var CAPTURE_MODES = ["reading", "photo", "voice"];
13813
- var IN_MEMORY_QUEUE_REASON = "No capture queue is bound, so anything that cannot be sent right now is held in this page's memory and is lost if the page closes. Bind `captureQueue` on <RecordsUiProvider> with your host's own durable store (IndexedDB in a browser) and a queued capture survives the tab, the reload and the flight. It is said here rather than discovered later.";
13865
+ var IN_MEMORY_QUEUE_REASON = "Anything you capture while offline is held only in this page until it can be sent, and it is lost if you close the tab. Keep this page open until you are back on a connection. It is said here rather than discovered later.";
13814
13866
  function mintClientKey() {
13815
13867
  const c = globalThis.crypto;
13816
13868
  if (typeof c?.randomUUID === "function") return `capture:${c.randomUUID()}`;
@@ -14686,6 +14738,10 @@ import { useCallback as useCallback37, useEffect as useEffect41, useState as use
14686
14738
  import { useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14687
14739
  import { Button as Button48, Separator as Separator12, Skeleton as Skeleton34, cn as cn51 } from "@ai-matrx/design-system";
14688
14740
  import { Fragment as Fragment28, jsx as jsx58, jsxs as jsxs53 } from "react/jsx-runtime";
14741
+ function cameFromLine(label, groupField) {
14742
+ const grouped = groupField ? `grouped into the same columns the chart used` : `for this table`;
14743
+ return `You came here from \u201C${label}\u201D. This is every record ${grouped} \u2014 not only the ones that number counted \u2014 so the column you clicked is here with the rest beside it.`;
14744
+ }
14689
14745
  var TABLE_NOT_REACHABLE = "This table is not in the organization you are working in, so there is nothing here to show. Switch to the organization that owns it and open it again \u2014 or it may have been deleted.";
14690
14746
  var DEFAULT_VIEW_NAME = "All records";
14691
14747
  var VIEW_NOT_SAVED_YET = "This table has no saved view yet, so the layout you pick here is not being kept. Save one from the bar above and it is remembered.";
@@ -14729,6 +14785,8 @@ function TablePage({
14729
14785
  activeRecordId,
14730
14786
  activeView,
14731
14787
  onViewChanged,
14788
+ activeGroupField,
14789
+ cameFrom,
14732
14790
  className
14733
14791
  }) {
14734
14792
  const client = useRecordsClient43();
@@ -14814,7 +14872,7 @@ function TablePage({
14814
14872
  variant: surfaceChosen(surface, { rail: "field" }) ? "secondary" : "outline",
14815
14873
  "aria-pressed": surfaceChosen(surface, { rail: "field" }),
14816
14874
  onClick: () => press({ rail: "field" }),
14817
- children: "Add field"
14875
+ children: ADD_FIELD_OPEN_LABEL
14818
14876
  }
14819
14877
  ),
14820
14878
  /* @__PURE__ */ jsx58(
@@ -14922,10 +14980,24 @@ function TablePage({
14922
14980
  ] }),
14923
14981
  main === "dashboards" ? /* @__PURE__ */ jsx58(DashboardCanvas, { tableId, activeDashboardId: activeDashboardId ?? null }) : /* @__PURE__ */ jsxs53(Fragment28, { children: [
14924
14982
  opened.unknown ? /* @__PURE__ */ jsx58("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,
14983
+ cameFrom ? /* @__PURE__ */ jsx58(
14984
+ "p",
14985
+ {
14986
+ "data-testid": "came-from-a-number",
14987
+ className: "rounded-md border px-3 py-2 text-xs leading-relaxed text-muted-foreground",
14988
+ children: cameFromLine(cameFrom, activeGroupField ?? null)
14989
+ }
14990
+ ) : null,
14925
14991
  /* @__PURE__ */ jsx58(
14926
14992
  ViewSwitcher,
14927
14993
  {
14928
- view: { ...view ?? defaultView(tableId), layout: shownLayout },
14994
+ view: {
14995
+ ...view ?? defaultView(tableId),
14996
+ layout: shownLayout,
14997
+ // The address's field wins for this visit; silence keeps the
14998
+ // saved view's own choice, exactly as it always did.
14999
+ ...activeGroupField ? { groupField: activeGroupField } : {}
15000
+ },
14929
15001
  pageSize,
14930
15002
  onLayoutChange: (layout) => {
14931
15003
  setLayoutFromLink(layout);
@@ -15065,6 +15137,7 @@ export {
15065
15137
  DocRender,
15066
15138
  DocTemplate,
15067
15139
  EMPTY_PRESENTATION,
15140
+ ENGINEER_TEXT,
15068
15141
  EmbedFrame,
15069
15142
  EnrichBadge,
15070
15143
  EnrichPanel,
@@ -15188,6 +15261,7 @@ export {
15188
15261
  blockFromSpec,
15189
15262
  bodyForReading,
15190
15263
  bodyFromKeys,
15264
+ cameFromLine,
15191
15265
  cellState,
15192
15266
  chooseSurface,
15193
15267
  colorFromTheValue,
@@ -15219,6 +15293,7 @@ export {
15219
15293
  hintIsMachineIdentity,
15220
15294
  humanize,
15221
15295
  idsOf,
15296
+ isEngineerText,
15222
15297
  isId,
15223
15298
  isMachineIdentity,
15224
15299
  isPlainFieldType,