@ai-matrx/records-ui 0.68.0 → 0.70.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
@@ -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
  ] });
@@ -3221,6 +3332,7 @@ var NO_COLUMNS_WHY = "A column is what a record holds \u2014 a name, a date, an
3221
3332
  function Grid({
3222
3333
  tableId,
3223
3334
  pageSize = 50,
3335
+ filter,
3224
3336
  onOpenRecord,
3225
3337
  onAddField,
3226
3338
  onNewRecordForm,
@@ -3236,7 +3348,7 @@ function Grid({
3236
3348
  const table = useTable2(tableId);
3237
3349
  const fields = useFields3(tableId);
3238
3350
  const [page, setPage] = useState10(0);
3239
- const records = useRecords2(tableId, { pageSize, page });
3351
+ const records = useRecords2(tableId, { pageSize, page, ...filter ? { filter } : {} });
3240
3352
  const rights = useTableRights(table.data);
3241
3353
  const rows = useMemo9(() => records.data?.rows ?? [], [records.data]);
3242
3354
  const rowIds = useMemo9(() => rows.map((row) => row.id), [rows]);
@@ -5210,7 +5322,8 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5210
5322
  value: value === "" ? null : value,
5211
5323
  onChange: (next) => write({ value: next === null || next === void 0 ? "" : String(next) }),
5212
5324
  id: "condition-value-record",
5213
- className: "min-w-[8rem]"
5325
+ className: "min-w-[8rem]",
5326
+ allowCreate: false
5214
5327
  }
5215
5328
  )
5216
5329
  ) : /* @__PURE__ */ jsx18(
@@ -7019,9 +7132,12 @@ function unplacedName(record, field, labels) {
7019
7132
  // src/ViewSwitcher.tsx
7020
7133
  import { jsx as jsx26, jsxs as jsxs22 } from "react/jsx-runtime";
7021
7134
  var NO_FIELDS = [];
7022
- function useViewRecords(view, pageSize = 200) {
7135
+ function useViewRecords(view, pageSize = 200, filter) {
7023
7136
  const client = useRecordsClient16();
7024
- const table = useRecords5(view.ruleId ? null : view.subject, { pageSize });
7137
+ const table = useRecords5(view.ruleId ? null : view.subject, {
7138
+ pageSize,
7139
+ ...filter ? { filter } : {}
7140
+ });
7025
7141
  const [ruled, setRuled] = useState24({
7026
7142
  rows: [],
7027
7143
  loading: Boolean(view.ruleId),
@@ -7076,6 +7192,7 @@ function ViewSwitcher({
7076
7192
  onNewRecordForm,
7077
7193
  onAskWhoChanged,
7078
7194
  pageSize = 200,
7195
+ filter,
7079
7196
  className
7080
7197
  }) {
7081
7198
  const [layout, setLayout] = useState24(view.layout);
@@ -7113,6 +7230,7 @@ function ViewSwitcher({
7113
7230
  {
7114
7231
  tableId: view.subject,
7115
7232
  pageSize,
7233
+ ...filter ? { filter } : {},
7116
7234
  ...local.presentation ?? view.presentation ? { presentation: local.presentation ?? view.presentation } : {},
7117
7235
  ...onViewChange ? { onPresentationChange: (next) => change({ presentation: next }) } : {},
7118
7236
  ...onOpenRecord ? { onOpenRecord } : {},
@@ -7124,6 +7242,7 @@ function ViewSwitcher({
7124
7242
  {
7125
7243
  view: { ...view, ...local, layout },
7126
7244
  pageSize,
7245
+ ...filter ? { filter } : {},
7127
7246
  onChange: change,
7128
7247
  remembered: Boolean(onViewChange),
7129
7248
  ...onOpenRecord ? { onOpenRecord } : {},
@@ -7171,13 +7290,14 @@ function boardColumnChoice(fields, chosenKey, stage) {
7171
7290
  function Board({
7172
7291
  view,
7173
7292
  pageSize,
7293
+ filter,
7174
7294
  onChange,
7175
7295
  remembered,
7176
7296
  onOpenRecord,
7177
7297
  onMoved
7178
7298
  }) {
7179
7299
  const fields = useFields12(view.subject);
7180
- const records = useViewRecords(view, pageSize);
7300
+ const records = useViewRecords(view, pageSize, filter);
7181
7301
  const stage = useStageField(view.subject);
7182
7302
  if (fields.error) return /* @__PURE__ */ jsx26(RefusalNotice, { error: fields.error });
7183
7303
  if (records.error) return /* @__PURE__ */ jsx26(RefusalNotice, { error: records.error });
@@ -12740,14 +12860,20 @@ function DashboardCanvas({ tableId, activeDashboardId, filter, className }) {
12740
12860
  rights.admin ? /* @__PURE__ */ jsx47(Button39, { size: "sm", variant: "ghost", onClick: () => void remove(board), children: "Delete" }) : null
12741
12861
  ] }) : null
12742
12862
  ] }),
12743
- board && scheduling ? /* @__PURE__ */ jsx47(
12744
- DigestScheduler,
12745
- {
12746
- tableId,
12747
- subjectName: board.name,
12748
- ...filter ? { filters: filter } : {},
12749
- onClose: () => setScheduling(false)
12750
- }
12863
+ board && scheduling ? (
12864
+ // ONE SHAPE, SO NO CAST. The dashboard's own filter, the saved view it
12865
+ // schedules over and the drill-through it opens are all the same
12866
+ // `RecordFilter`; the `as Record<string, unknown>` that used to be here
12867
+ // was the seam where they could have drifted.
12868
+ /* @__PURE__ */ jsx47(
12869
+ DigestScheduler,
12870
+ {
12871
+ tableId,
12872
+ subjectName: board.name,
12873
+ ...filter ? { filters: filter } : {},
12874
+ onClose: () => setScheduling(false)
12875
+ }
12876
+ )
12751
12877
  ) : null,
12752
12878
  board ? /* @__PURE__ */ jsxs43("div", { className: "flex flex-col gap-1", children: [
12753
12879
  /* @__PURE__ */ jsxs43("div", { className: "flex items-center gap-1", children: [
@@ -14750,12 +14876,38 @@ function TablesHome({ onOpenTable, className }) {
14750
14876
 
14751
14877
  // src/TablePage.tsx
14752
14878
  import { useCallback as useCallback37, useEffect as useEffect41, useState as useState53 } from "react";
14753
- import { useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14879
+ import { useFields as useFields23, useRecordsClient as useRecordsClient43, useTable as useTable22 } from "@ai-matrx/records/react";
14754
14880
  import { Button as Button48, Separator as Separator12, Skeleton as Skeleton34, cn as cn51 } from "@ai-matrx/design-system";
14755
14881
  import { Fragment as Fragment28, jsx as jsx58, jsxs as jsxs53 } from "react/jsx-runtime";
14756
- function cameFromLine(label, groupField) {
14757
- const grouped = groupField ? `grouped into the same columns the chart used` : `for this table`;
14758
- 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.`;
14882
+ function filterInWords(filter, fields) {
14883
+ const nameOf = (key) => {
14884
+ const field = fields.find((f) => f.key === key);
14885
+ return field ? fieldName(field) : humanize(key);
14886
+ };
14887
+ const parts = Object.keys(filter).map((key) => {
14888
+ const value = filter[key];
14889
+ if (value === null || value === void 0) return `${nameOf(key)} is not filled in`;
14890
+ if (typeof value === "object") {
14891
+ const from = (value.from ?? "").toString().trim();
14892
+ const to = (value.to ?? "").toString().trim();
14893
+ if (from !== "" && to !== "") return `${nameOf(key)} from ${from} up to ${to}`;
14894
+ if (from !== "") return `${nameOf(key)} from ${from} onwards`;
14895
+ if (to !== "") return `${nameOf(key)} up to ${to}`;
14896
+ return `${nameOf(key)} in a period nobody named`;
14897
+ }
14898
+ return `${nameOf(key)} is ${String(value)}`;
14899
+ });
14900
+ if (parts.length === 0) return "";
14901
+ if (parts.length === 1) return parts[0];
14902
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
14903
+ }
14904
+ function cameFromLine(label, groupField, question) {
14905
+ const asked = (question ?? "").trim();
14906
+ if (asked === "") {
14907
+ const grouped = groupField ? " grouped into the same columns the chart used" : "";
14908
+ return `You came here from \u201C${label}\u201D. This is every record in this table${grouped} \u2014 that number was counted over all of them.`;
14909
+ }
14910
+ return `You came here from \u201C${label}\u201D. These are only the records that number counted: ${asked}. Everything else in this table is being held back \u2014 clear the filter to see all of it.`;
14759
14911
  }
14760
14912
  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.";
14761
14913
  var DEFAULT_VIEW_NAME = "All records";
@@ -14802,11 +14954,14 @@ function TablePage({
14802
14954
  onViewChanged,
14803
14955
  activeGroupField,
14804
14956
  cameFrom,
14957
+ filter,
14805
14958
  className
14806
14959
  }) {
14807
14960
  const client = useRecordsClient43();
14808
14961
  const table = useTable22(tableId);
14809
14962
  const rights = useTableRights(table.data);
14963
+ const pageFields = useFields23(filter ? tableId : null);
14964
+ const askedInWords = filter ? filterInWords(filter, pageFields.data ?? []) : null;
14810
14965
  const organizationId = useRecordsClient43().config.organizationId;
14811
14966
  const [view, setView] = useState53(null);
14812
14967
  const opening = openingRail(activeRecordId);
@@ -15000,7 +15155,7 @@ function TablePage({
15000
15155
  {
15001
15156
  "data-testid": "came-from-a-number",
15002
15157
  className: "rounded-md border px-3 py-2 text-xs leading-relaxed text-muted-foreground",
15003
- children: cameFromLine(cameFrom, activeGroupField ?? null)
15158
+ children: cameFromLine(cameFrom, activeGroupField ?? null, askedInWords)
15004
15159
  }
15005
15160
  ) : null,
15006
15161
  /* @__PURE__ */ jsx58(
@@ -15014,6 +15169,7 @@ function TablePage({
15014
15169
  ...activeGroupField ? { groupField: activeGroupField } : {}
15015
15170
  },
15016
15171
  pageSize,
15172
+ ...filter ? { filter } : {},
15017
15173
  onLayoutChange: (layout) => {
15018
15174
  setLayoutFromLink(layout);
15019
15175
  onViewChanged?.(layout);