@ai-matrx/records-ui 0.67.0 → 0.69.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.d.cts CHANGED
@@ -1406,8 +1406,18 @@ interface RelationPickerProps {
1406
1406
  disabled?: boolean | undefined;
1407
1407
  id?: string | undefined;
1408
1408
  className?: string | undefined;
1409
+ /**
1410
+ * WHETHER A PERSON MAY MAKE THE THING SHE IS LOOKING FOR (RELATION-CREATE, 2026-09-22).
1411
+ *
1412
+ * On by default, because that is what this control is for on every surface a
1413
+ * person fills a record in: the grid cell, the generated form, a public form
1414
+ * runner. The one surface that passes `false` is the filter builder — picking
1415
+ * a value to FILTER by is not a reason to put a new customer in the book, and
1416
+ * a Create button there would write a row nobody asked for.
1417
+ */
1418
+ allowCreate?: boolean | undefined;
1409
1419
  }
1410
- declare function RelationPicker({ field, value, onChange, disabled, id, className }: RelationPickerProps): react.JSX.Element;
1420
+ declare function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate }: RelationPickerProps): react.JSX.Element;
1411
1421
 
1412
1422
  interface FieldEditorControlProps {
1413
1423
  field: Field;
package/dist/index.d.ts CHANGED
@@ -1406,8 +1406,18 @@ interface RelationPickerProps {
1406
1406
  disabled?: boolean | undefined;
1407
1407
  id?: string | undefined;
1408
1408
  className?: string | undefined;
1409
+ /**
1410
+ * WHETHER A PERSON MAY MAKE THE THING SHE IS LOOKING FOR (RELATION-CREATE, 2026-09-22).
1411
+ *
1412
+ * On by default, because that is what this control is for on every surface a
1413
+ * person fills a record in: the grid cell, the generated form, a public form
1414
+ * runner. The one surface that passes `false` is the filter builder — picking
1415
+ * a value to FILTER by is not a reason to put a new customer in the book, and
1416
+ * a Create button there would write a row nobody asked for.
1417
+ */
1418
+ allowCreate?: boolean | undefined;
1409
1419
  }
1410
- declare function RelationPicker({ field, value, onChange, disabled, id, className }: RelationPickerProps): react.JSX.Element;
1420
+ declare function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate }: RelationPickerProps): react.JSX.Element;
1411
1421
 
1412
1422
  interface FieldEditorControlProps {
1413
1423
  field: Field;
package/dist/index.js CHANGED
@@ -667,22 +667,28 @@ import {
667
667
  cn as cn2
668
668
  } from "@ai-matrx/design-system";
669
669
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
670
- function RelationPicker({ field, value, onChange, disabled, id, className }) {
670
+ function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true }) {
671
671
  const client = useRecordsClient();
672
672
  const [rows, setRows] = useState2(null);
673
673
  const [words, setWords] = useState2({});
674
674
  const [refusal, setRefusal] = useState2(null);
675
675
  const [search, setSearch] = useState2("");
676
676
  const [open, setOpen] = useState2(false);
677
+ const [target, setTarget] = useState2(null);
678
+ const [targetRefusal, setTargetRefusal] = useState2(null);
679
+ const [creating, setCreating] = useState2(false);
680
+ const [newTitle, setNewTitle] = useState2("");
681
+ const [busy, setBusy] = useState2(false);
682
+ const [createRefusal, setCreateRefusal] = useState2(null);
677
683
  const picked = useMemo3(
678
684
  () => Array.isArray(value) ? value.map(String) : value === null || value === void 0 ? [] : [String(value)],
679
685
  [value]
680
686
  );
681
687
  useEffect(() => {
682
- const target = field.relation_target;
683
- if (!target) return;
688
+ const target2 = field.relation_target;
689
+ if (!target2) return;
684
690
  let cancelled = false;
685
- void client.list({ table_id: target, limit: 200 }).then((result) => {
691
+ void client.list({ table_id: target2, limit: 200 }).then((result) => {
686
692
  if (cancelled) return;
687
693
  if (result.ok) setRows(result.data.rows);
688
694
  else setRefusal(refusalLineForAPerson(result.error));
@@ -711,6 +717,28 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
711
717
  cancelled = true;
712
718
  };
713
719
  }, [client, field.id, rows, picked, words]);
720
+ useEffect(() => {
721
+ if (!open || !allowCreate || target || targetRefusal) return;
722
+ const id2 = field.relation_target;
723
+ if (!id2) return;
724
+ let cancelled = false;
725
+ void client.tableList().then((result) => {
726
+ if (cancelled) return;
727
+ if (!result.ok) {
728
+ setTargetRefusal(refusalLineForAPerson(result.error));
729
+ return;
730
+ }
731
+ const found = result.data.find((t) => t.id === id2) ?? null;
732
+ if (found) setTarget(found);
733
+ else
734
+ setTargetRefusal(
735
+ "That Table is not one you can open, so a new record cannot be added to it from here."
736
+ );
737
+ });
738
+ return () => {
739
+ cancelled = true;
740
+ };
741
+ }, [client, open, allowCreate, target, targetRefusal, field.relation_target]);
714
742
  if (!field.relation_target) {
715
743
  return /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
716
744
  fieldName(field),
@@ -730,6 +758,39 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
730
758
  else if (!full) next.add(recordId);
731
759
  onChange(next.size === 0 ? null : [...next]);
732
760
  };
761
+ const targetWord = (target?.label_singular ?? "").trim() || (target?.name ?? "").trim() || "record";
762
+ const create = async (rawTitle) => {
763
+ const title = rawTitle.trim();
764
+ const tableId = field.relation_target;
765
+ if (!tableId || title === "" || busy) return;
766
+ if (!target) {
767
+ setCreateRefusal("Still reading that Table. Try again in a moment.");
768
+ return;
769
+ }
770
+ const titleField = target.title_field;
771
+ if (!titleField) {
772
+ setCreateRefusal(
773
+ `${target.name} has no column that names a record yet, so a new one would have nothing to show here. A table admin picks its title column in that table's settings.`
774
+ );
775
+ return;
776
+ }
777
+ setBusy(true);
778
+ setCreateRefusal(null);
779
+ const written = await client.recordWrite({ table_id: tableId, data: { [titleField]: title } });
780
+ if (!written.ok) {
781
+ setBusy(false);
782
+ setCreateRefusal(refusalLineForAPerson(written.error));
783
+ return;
784
+ }
785
+ const createdId = String(written.data);
786
+ const refreshed = await client.list({ table_id: tableId, limit: 200 });
787
+ if (refreshed.ok) setRows(refreshed.data.rows);
788
+ setBusy(false);
789
+ setCreating(false);
790
+ setNewTitle("");
791
+ setSearch("");
792
+ toggle(createdId);
793
+ };
733
794
  const visible = (rows ?? []).filter(
734
795
  (row) => search.trim() === "" ? true : titleOf(row.id).toLowerCase().includes(search.trim().toLowerCase())
735
796
  );
@@ -776,7 +837,57 @@ function RelationPicker({ field, value, onChange, disabled, id, className }) {
776
837
  }
777
838
  ) }, row.id)) }),
778
839
  rows && visible.length === 0 ? /* @__PURE__ */ jsx3("p", { className: "p-2 text-xs text-muted-foreground", children: "Nothing in that table matches." }) : null
779
- ] })
840
+ ] }),
841
+ allowCreate && !full ? /* @__PURE__ */ jsxs2("div", { className: "mt-1 border-t pt-1", children: [
842
+ targetRefusal ? /* @__PURE__ */ jsx3("p", { className: "p-2 text-xs text-destructive", children: targetRefusal }) : !target ? /* @__PURE__ */ jsx3("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading that table\u2026" }) : creating ? /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 p-1", children: [
843
+ /* @__PURE__ */ jsx3(
844
+ BasicInput2,
845
+ {
846
+ autoFocus: true,
847
+ value: newTitle,
848
+ onChange: (e) => setNewTitle(e.target.value),
849
+ onKeyDown: (e) => {
850
+ if (e.key === "Enter") {
851
+ e.preventDefault();
852
+ void create(newTitle);
853
+ }
854
+ },
855
+ placeholder: `New ${targetWord}`,
856
+ className: "h-7 flex-1 text-xs",
857
+ "aria-label": `Name the new ${targetWord}`
858
+ }
859
+ ),
860
+ /* @__PURE__ */ jsx3(
861
+ Button2,
862
+ {
863
+ type: "button",
864
+ size: "sm",
865
+ variant: "default",
866
+ disabled: busy || newTitle.trim() === "",
867
+ onClick: () => void create(newTitle),
868
+ children: busy ? "Creating\u2026" : "Create"
869
+ }
870
+ )
871
+ ] }) : /* @__PURE__ */ jsx3(
872
+ Button2,
873
+ {
874
+ type: "button",
875
+ size: "sm",
876
+ variant: "ghost",
877
+ className: "w-full justify-start text-xs",
878
+ disabled: busy,
879
+ onClick: () => {
880
+ if (search.trim() !== "") void create(search);
881
+ else {
882
+ setNewTitle("");
883
+ setCreating(true);
884
+ }
885
+ },
886
+ children: search.trim() !== "" ? `Create \u201C${search.trim()}\u201D` : `New ${targetWord}`
887
+ }
888
+ ),
889
+ createRefusal ? /* @__PURE__ */ jsx3("p", { className: "p-2 text-xs text-destructive", children: createRefusal }) : null
890
+ ] }) : null
780
891
  ] })
781
892
  ] })
782
893
  ] });
@@ -1240,7 +1351,16 @@ function RefusalNotice({
1240
1351
  /* @__PURE__ */ jsxs5(AlertDescription, { className: "space-y-1 text-xs", children: [
1241
1352
  plain.sentence.replace(/\.$/, "") === plain.title ? null : /* @__PURE__ */ jsx6("p", { children: plain.sentence }),
1242
1353
  /* @__PURE__ */ jsx6("p", { className: "opacity-80", children: plain.remedy }),
1243
- plain.forEngineers ? /* @__PURE__ */ jsx6("p", { className: "sr-only", ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {}, children: plain.forEngineers }) : null,
1354
+ plain.forEngineers ? /* @__PURE__ */ jsx6(
1355
+ "p",
1356
+ {
1357
+ className: "sr-only",
1358
+ "aria-hidden": "true",
1359
+ "data-for-engineers": "",
1360
+ ...error.sqlstate ? { "data-sqlstate": error.sqlstate } : {},
1361
+ children: plain.forEngineers
1362
+ }
1363
+ ) : null,
1244
1364
  actions
1245
1365
  ] })
1246
1366
  ]
@@ -5201,7 +5321,8 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5201
5321
  value: value === "" ? null : value,
5202
5322
  onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5203
5323
  id: "condition-value-record",
5204
- className: "min-w-[8rem]"
5324
+ className: "min-w-[8rem]",
5325
+ allowCreate: false
5205
5326
  }
5206
5327
  )
5207
5328
  ) : /* @__PURE__ */ jsx18(
@@ -7239,7 +7360,9 @@ function FieldPicker({
7239
7360
  }) {
7240
7361
  const want = LAYOUT_FIELD_KIND[layout];
7241
7362
  const offerable = want ? offerableFields(all, want) : [];
7242
- if (offerable.length === 0) {
7363
+ const chosenField = chosen ? all.find((field) => field.key === chosen) : void 0;
7364
+ const unusual = chosenField && !offerable.some((field) => field.key === chosenField.key) ? chosenField : void 0;
7365
+ if (offerable.length === 0 && !unusual) {
7243
7366
  return /* @__PURE__ */ jsx26("p", { className: "text-xs text-muted-foreground", children: LAYOUT_NO_FIELD[layout] });
7244
7367
  }
7245
7368
  return /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-1.5 text-xs", children: [
@@ -7254,7 +7377,11 @@ function FieldPicker({
7254
7377
  onChange: (event) => onPick(event.target.value === "" ? null : event.target.value),
7255
7378
  children: [
7256
7379
  /* @__PURE__ */ jsx26("option", { value: "", children: "Choose a field" }),
7257
- offerable.map((field) => /* @__PURE__ */ jsx26("option", { value: field.key, children: fieldName(field) }, field.key))
7380
+ offerable.map((field) => /* @__PURE__ */ jsx26("option", { value: field.key, children: fieldName(field) }, field.key)),
7381
+ unusual ? /* @__PURE__ */ jsxs22("option", { value: unusual.key, children: [
7382
+ fieldName(unusual),
7383
+ " \u2014 an unusual column to use here"
7384
+ ] }, unusual.key) : null
7258
7385
  ]
7259
7386
  }
7260
7387
  ),