@ai-matrx/records-ui 0.68.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
  ] });
@@ -5210,7 +5321,8 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5210
5321
  value: value === "" ? null : value,
5211
5322
  onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5212
5323
  id: "condition-value-record",
5213
- className: "min-w-[8rem]"
5324
+ className: "min-w-[8rem]",
5325
+ allowCreate: false
5214
5326
  }
5215
5327
  )
5216
5328
  ) : /* @__PURE__ */ jsx18(