@ai-matrx/records-ui 0.93.4 → 0.93.6

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,23 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
+ ## 0.93.6
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/records-ui/v0.93.4..npm/records-ui/v0.93.6 -- apps/shared/records-ui`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.93.5
10
+ ### Unreleased
11
+ ### page-pass shared fix (2026-09-27): custom fields — the design-system Select, one-line empty, and an agent twin for "Add field"
12
+
13
+ - **"Holds" is the design-system `Select`** (it was a native `<select>`); the name box is `BasicInput`.
14
+ - **Empty is one line**: "Custom fields · None yet · Add field" — no "0", no rule under nothing.
15
+ - **New prop `agentDoor`** on `CustomFieldsSection`: the section hands its host one door (`CustomFieldsAgentDoor` — live `state()`, a whole-request `check()` that reports every problem at once, and `addField()` through `custom.entity_field_declare` followed by a read-back). New exports: `CustomFieldsAgentDoor`, `CustomFieldsAgentState`, `CustomFieldAddRequest`, `CustomFieldAddResult`, `customFieldRequestProblems`, `CUSTOM_FIELD_ADDABLE_TYPES`.
16
+ - **Proof:** `custom-fields-empty-select-and-agent-door.test.tsx` (Harbor Lane Dental's message templates).
17
+
18
+ #### Consumer action
19
+ - None required. matrx-frontend's `EntityCustomFields` passes `agentDoor` to register the platform write target `custom_fields_add` on every page that embeds the section.
20
+
3
21
  ## 0.93.4
4
22
  ### Unreleased
5
23
  ### lane RENDER-3 (2026-09-27): one cell edit redraws one row — and none of the data table's toolbar, headers or footer
package/dist/index.cjs CHANGED
@@ -54,6 +54,7 @@ __export(src_exports, {
54
54
  CHECKLIST_REQUIREMENT_LABEL: () => CHECKLIST_REQUIREMENT_LABEL,
55
55
  CONDITION_OPS: () => CONDITION_OPS,
56
56
  CONTEXT_POLICY_LABEL: () => CONTEXT_POLICY_LABEL,
57
+ CUSTOM_FIELD_ADDABLE_TYPES: () => ADDABLE_TYPES,
57
58
  CaptureRun: () => CaptureRun,
58
59
  CaptureSheet: () => CaptureSheet,
59
60
  ChartBlock: () => ChartBlock,
@@ -246,6 +247,7 @@ __export(src_exports, {
246
247
  conditionValue: () => conditionValue,
247
248
  controlFor: () => controlFor,
248
249
  currencyCodeFor: () => currencyCodeFor,
250
+ customFieldRequestProblems: () => customFieldRequestProblems,
249
251
  dashboardDeclareArgs: () => dashboardDeclareArgs,
250
252
  dashboardFromSummary: () => dashboardFromSummary,
251
253
  dateFormatFor: () => dateFormatFor,
@@ -13347,6 +13349,32 @@ var ADDABLE_TYPES = ADDABLE_TYPE_IDS.map((value) => ({
13347
13349
  value,
13348
13350
  label: fieldTypeChoice(value)?.label ?? value
13349
13351
  }));
13352
+ var ADDABLE_TYPE_SET = new Set(ADDABLE_TYPE_IDS);
13353
+ function customFieldRequestProblems(requests, existingLabels) {
13354
+ const problems = [];
13355
+ const taken = new Set(existingLabels.map((l) => l.trim().toLowerCase()));
13356
+ const asked = /* @__PURE__ */ new Set();
13357
+ if (requests.length === 0) problems.push("Name at least one field to add.");
13358
+ requests.forEach((request, index) => {
13359
+ const at = requests.length > 1 ? `Field ${index + 1}: ` : "";
13360
+ const label = typeof request?.label === "string" ? request.label.trim() : "";
13361
+ if (!label) {
13362
+ problems.push(`${at}a field needs a name.`);
13363
+ return;
13364
+ }
13365
+ const type = request.type ?? "text";
13366
+ if (!ADDABLE_TYPE_SET.has(type)) {
13367
+ problems.push(
13368
+ `${at}"${label}" cannot hold "${String(type)}". Choose one of: ${ADDABLE_TYPE_IDS.join(", ")}.`
13369
+ );
13370
+ }
13371
+ const key = label.toLowerCase();
13372
+ if (taken.has(key)) problems.push(`${at}there is already a field named "${label}".`);
13373
+ else if (asked.has(key)) problems.push(`${at}"${label}" is asked for twice.`);
13374
+ asked.add(key);
13375
+ });
13376
+ return problems;
13377
+ }
13350
13378
  function asField(f) {
13351
13379
  return {
13352
13380
  id: f.id,
@@ -13367,8 +13395,10 @@ function CustomFieldsSection({
13367
13395
  entityToken,
13368
13396
  recordId,
13369
13397
  title = "Custom fields",
13370
- className
13398
+ className,
13399
+ agentDoor
13371
13400
  }) {
13401
+ const client = (0, import_react68.useRecordsClient)();
13372
13402
  const read = (0, import_react68.useEntityRecord)(entityToken, recordId);
13373
13403
  const rights = (0, import_react68.useEntityFieldRights)(entityToken);
13374
13404
  const mutation = (0, import_react68.useEntityFieldMutation)();
@@ -13379,14 +13409,70 @@ function CustomFieldsSection({
13379
13409
  const [draft, setDraft] = (0, import_react67.useState)({});
13380
13410
  const fields = (0, import_react67.useMemo)(() => read.data?.fields ?? [], [read.data]);
13381
13411
  const mayAdd = rights.data?.may_declare === true;
13412
+ const live = (0, import_react67.useRef)({ fields, rights: rights.data, loading: rights.loading, reload: read.reload });
13413
+ (0, import_react67.useEffect)(() => {
13414
+ live.current = { fields, rights: rights.data, loading: rights.loading, reload: read.reload };
13415
+ });
13416
+ (0, import_react67.useEffect)(() => {
13417
+ if (!agentDoor) return;
13418
+ const state = () => {
13419
+ const now = live.current;
13420
+ return {
13421
+ entityToken,
13422
+ recordId,
13423
+ entityLabel: now.rights?.label ?? null,
13424
+ mayAdd: now.rights ? now.rights.may_declare === true : now.loading ? null : false,
13425
+ refusal: now.rights && !now.rights.may_declare ? now.rights.reason ?? null : null,
13426
+ fields: now.fields.map((f) => ({ key: f.key, label: f.label, type: f.type })),
13427
+ types: ADDABLE_TYPES
13428
+ };
13429
+ };
13430
+ const check = (requests) => {
13431
+ const now = state();
13432
+ if (now.mayAdd === false) {
13433
+ return [now.refusal ?? "This person may not add a field here."];
13434
+ }
13435
+ if (now.mayAdd === null) return ["The section is still reading who may add a field. Try again in a moment."];
13436
+ return customFieldRequestProblems(requests, now.fields.map((f) => f.label));
13437
+ };
13438
+ const addField = async (request) => {
13439
+ const problems = check([request]);
13440
+ if (problems.length > 0) return { ok: false, message: problems.join(" ") };
13441
+ const label = request.label.trim();
13442
+ const type = request.type ?? "text";
13443
+ const declared2 = await client.entityFieldDeclare({ token: entityToken, spec: { label, type } });
13444
+ if (!declared2.ok) {
13445
+ const hint = hintForAPerson(declared2.error.hint);
13446
+ return { ok: false, message: hint ? `${declared2.error.message} ${hint}` : declared2.error.message };
13447
+ }
13448
+ const back = await client.entityRecordRead({ token: entityToken, record_id: recordId });
13449
+ live.current.reload();
13450
+ if (!back.ok || !(back.data?.fields ?? []).some((f) => f.id === declared2.data)) {
13451
+ return {
13452
+ ok: false,
13453
+ message: `The store took "${label}" but this record does not show it yet. Nothing was lost \u2014 read the page again before adding it a second time.`
13454
+ };
13455
+ }
13456
+ return { ok: true, field_id: declared2.data, label, type };
13457
+ };
13458
+ return agentDoor({ entityToken, recordId, state, check, addField });
13459
+ }, [agentDoor, client, entityToken, recordId]);
13382
13460
  if (!read.loading && !read.error && fields.length === 0 && !mayAdd) return null;
13383
13461
  if (read.error) return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(RefusalLine, { error: read.error, className });
13384
13462
  const values = read.data?.custom ?? {};
13385
13463
  const written = read.data?.custom_written ?? {};
13464
+ const empty = fields.length === 0;
13465
+ if (empty && !adding) {
13466
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 items-center gap-2 text-xs", className), children: [
13467
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
13468
+ !read.loading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "None yet" }) : null,
13469
+ mayAdd ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", variant: "ghost", className: "ml-auto", onClick: () => setAdding(true), children: "Add field" }) : null
13470
+ ] });
13471
+ }
13386
13472
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("section", { className: (0, import_design_system38.cn)("flex min-w-0 flex-col gap-2 text-xs", className), children: [
13387
13473
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex items-center gap-2", children: [
13388
13474
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("h3", { className: "text-sm font-medium", children: title }),
13389
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }),
13475
+ !empty ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: fields.length }) : null,
13390
13476
  fields.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13391
13477
  import_design_system38.Button,
13392
13478
  {
@@ -13455,9 +13541,9 @@ function CustomFieldsSection({
13455
13541
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("label", { className: "flex flex-col gap-1", children: [
13456
13542
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Name" }),
13457
13543
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13458
- "input",
13544
+ import_design_system38.BasicInput,
13459
13545
  {
13460
- className: "h-8 rounded-md border bg-background px-2",
13546
+ className: "h-8",
13461
13547
  value: newLabel,
13462
13548
  onChange: (e) => setNewLabel(e.target.value),
13463
13549
  placeholder: "Account tier",
@@ -13465,17 +13551,12 @@ function CustomFieldsSection({
13465
13551
  }
13466
13552
  )
13467
13553
  ] }),
13468
- /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("label", { className: "flex flex-col gap-1", children: [
13554
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "flex flex-col gap-1", children: [
13469
13555
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-muted-foreground", children: "Holds" }),
13470
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
13471
- "select",
13472
- {
13473
- className: "h-8 rounded-md border bg-background px-2",
13474
- value: newType,
13475
- onChange: (e) => setNewType(e.target.value),
13476
- children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("option", { value: t.value, children: t.label }, t.value))
13477
- }
13478
- )
13556
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_design_system38.Select, { value: newType, onValueChange: setNewType, children: [
13557
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectTrigger, { size: "sm", "aria-label": "What this field holds", children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectValue, {}) }),
13558
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectContent, { children: ADDABLE_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.SelectItem, { value: t.value, children: t.label }, t.value)) })
13559
+ ] })
13479
13560
  ] }),
13480
13561
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_design_system38.Button, { size: "sm", type: "submit", disabled: mutation.saving || newLabel.trim() === "", children: mutation.saving ? "Adding\u2026" : "Add" })
13481
13562
  ]