@ai-matrx/records-ui 0.115.79 → 0.115.80

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
@@ -2839,6 +2839,14 @@ interface ConditionField {
2839
2839
  * store compares.
2840
2840
  */
2841
2841
  field?: Field;
2842
+ /**
2843
+ * Set when this entry is a field of the table a relation column POINTS AT (lane NOTION-Q): `id` is
2844
+ * `<relation field id>><field id>` and `via` names the relation. Offered only by a view's filter.
2845
+ */
2846
+ via?: {
2847
+ id: string;
2848
+ fieldId: string;
2849
+ } | undefined;
2842
2850
  }
2843
2851
  interface ConditionRowProps {
2844
2852
  /** The words before the row: "Ask only when", "When". */
package/dist/index.d.ts CHANGED
@@ -2839,6 +2839,14 @@ interface ConditionField {
2839
2839
  * store compares.
2840
2840
  */
2841
2841
  field?: Field;
2842
+ /**
2843
+ * Set when this entry is a field of the table a relation column POINTS AT (lane NOTION-Q): `id` is
2844
+ * `<relation field id>><field id>` and `via` names the relation. Offered only by a view's filter.
2845
+ */
2846
+ via?: {
2847
+ id: string;
2848
+ fieldId: string;
2849
+ } | undefined;
2842
2850
  }
2843
2851
  interface ConditionRowProps {
2844
2852
  /** The words before the row: "Ask only when", "When". */
package/dist/index.js CHANGED
@@ -7486,6 +7486,33 @@ var CONDITION_OPS = [
7486
7486
  { op: "gte", label: "is at least" },
7487
7487
  { op: "lte", label: "is at most" }
7488
7488
  ];
7489
+ var LIST_TERMS = [
7490
+ { op: "contains", label: "contains" },
7491
+ { op: "not_contains", label: "does not contain" },
7492
+ { op: "is_empty", label: "is empty" },
7493
+ { op: "is_not_empty", label: "is not empty" }
7494
+ ];
7495
+ var VALUELESS_OPS = /* @__PURE__ */ new Set(["present", "is_empty", "is_not_empty"]);
7496
+ var LINK_SEP = ">";
7497
+ function linkedNode(viaId, innerId, op2, value) {
7498
+ return {
7499
+ op: "linked",
7500
+ match: op2 === "eq" ? "any" : "none",
7501
+ args: [{ field: viaId }, { op: "eq", args: [{ field: innerId }, { const: value }] }]
7502
+ };
7503
+ }
7504
+ function linkedOf(expr) {
7505
+ const node = asNode(expr);
7506
+ if (!node || node["op"] !== "linked") return null;
7507
+ const [rel, cond] = argsOf(node);
7508
+ const innerNode = asNode(cond);
7509
+ const [a, b] = innerNode ? argsOf(innerNode) : [];
7510
+ const match = node["match"] ?? "any";
7511
+ if (!rel || typeof rel["field"] !== "string" || !innerNode || innerNode["op"] !== "eq") return null;
7512
+ if (!a || typeof a["field"] !== "string" || !b || !("const" in b)) return null;
7513
+ if (match !== "any" && match !== "none") return null;
7514
+ return { via: rel["field"], inner: a["field"], op: match === "any" ? "eq" : "ne", value: b["const"] };
7515
+ }
7489
7516
  var FilterTermsContext = createContext5(false);
7490
7517
  var RELATIVE_DAY_TERMS = [
7491
7518
  { kind: "today", label: "is today" },
@@ -7598,13 +7625,15 @@ function ChoiceValue({ field: field2, value, onChange }) {
7598
7625
  }
7599
7626
  function conditionIsSimple(expr) {
7600
7627
  if (expr === null || expr === void 0) return true;
7601
- if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
7628
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null || linkedOf(expr) !== null) return true;
7602
7629
  const node = expr;
7603
- if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
7630
+ if (typeof node.op !== "string" || !(CONDITION_OPS.some((c) => c.op === node.op) || LIST_TERMS.some((c) => c.op === node.op))) {
7631
+ return false;
7632
+ }
7604
7633
  if (!Array.isArray(node.args)) return false;
7605
7634
  const first = node.args[0];
7606
7635
  if (!first || typeof first.field !== "string") return false;
7607
- if (node.op === "present") return node.args.length === 1;
7636
+ if (VALUELESS_OPS.has(node.op)) return node.args.length === 1;
7608
7637
  const second = node.args[1];
7609
7638
  return node.args.length === 2 && !!second && "const" in second;
7610
7639
  }
@@ -7622,7 +7651,13 @@ function conditionInWords(expr, fields) {
7622
7651
  lte: "is at most",
7623
7652
  and: "and",
7624
7653
  or: "or",
7625
- matches: "matches"
7654
+ matches: "matches",
7655
+ contains: "contains",
7656
+ not_contains: "does not contain",
7657
+ has_any: "has any of",
7658
+ has_all: "has all of",
7659
+ has_none: "has none of",
7660
+ is_exactly: "is exactly"
7626
7661
  };
7627
7662
  const walk = (node) => {
7628
7663
  if (node === null || node === void 0) return "nothing";
@@ -7643,6 +7678,12 @@ function conditionInWords(expr, fields) {
7643
7678
  const args = Array.isArray(n["args"]) ? n["args"] : [];
7644
7679
  if (op2 === "not") return `not ${walk(args[0])}`;
7645
7680
  if (op2 === "present") return `${walk(args[0])} has been answered`;
7681
+ if (op2 === "is_empty") return `${walk(args[0])} is empty`;
7682
+ if (op2 === "is_not_empty") return `${walk(args[0])} is not empty`;
7683
+ if (op2 === "linked") {
7684
+ const how = n["match"] === "every" ? "every" : n["match"] === "none" ? "none of" : "any";
7685
+ return `${how} linked record in ${walk(args[0])}: ${walk(args[1])}`;
7686
+ }
7646
7687
  const dayRange = relativeDayOf(n);
7647
7688
  if (dayRange) {
7648
7689
  const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
@@ -7675,12 +7716,14 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7675
7716
  const filterTerms = useContext5(FilterTermsContext);
7676
7717
  const relative = relativeDayOf(expr);
7677
7718
  const reader = meOf(expr);
7678
- const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
7679
- const onField = node?.args?.[0]?.field ?? "";
7680
- const value = relative || reader ? "" : node?.args?.[1]?.const ?? "";
7719
+ const linked = linkedOf(expr);
7720
+ const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : linked ? linked.op : node?.op ?? "";
7721
+ const onField = linked ? `${linked.via}${LINK_SEP}${linked.inner}` : node?.args?.[0]?.field ?? "";
7722
+ const value = relative || reader ? "" : linked ? linked.value : node?.args?.[1]?.const ?? "";
7681
7723
  const chosen = fields.find((f) => f.id === onField);
7682
7724
  const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
7683
- const extraTerms = !filterTerms ? [] : chosenKind === "datetime" ? RELATIVE_DAY_TERMS.map((t) => ({ op: `rel:${t.kind}`, label: t.label })) : chosenKind === "member" ? [{ op: "me", label: "is me" }, { op: "not_me", label: "is not me" }] : [];
7725
+ const extraTerms = !filterTerms || chosen?.via ? [] : chosenKind === "datetime" ? RELATIVE_DAY_TERMS.map((t) => ({ op: `rel:${t.kind}`, label: t.label })) : chosenKind === "member" ? [{ op: "me", label: "is me" }, { op: "not_me", label: "is not me" }] : chosenKind === "multi_select" ? LIST_TERMS : [];
7726
+ const baseTerms = chosen?.via ? CONDITION_OPS.filter((c) => c.op === "eq" || c.op === "ne") : CONDITION_OPS;
7684
7727
  const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
7685
7728
  function write(next) {
7686
7729
  const picked = next.op;
@@ -7693,8 +7736,11 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7693
7736
  onChange(relativeDayNode(fieldNow, relative.kind, next.days));
7694
7737
  return;
7695
7738
  }
7696
- if (next.field !== void 0 && (relative || reader)) {
7697
- onChange(next.field ? { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] } : null);
7739
+ if (next.field !== void 0 && (relative || reader || linked)) {
7740
+ const target2 = fields.find((f) => f.id === next.field);
7741
+ onChange(
7742
+ !next.field ? null : target2?.via ? linkedNode(target2.via.id, target2.via.fieldId, "eq", "") : { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] }
7743
+ );
7698
7744
  return;
7699
7745
  }
7700
7746
  const chosenOp = next.op ?? (op2 || (next.field ? CONDITION_OPS[0].op : ""));
@@ -7704,8 +7750,13 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7704
7750
  onChange(null);
7705
7751
  return;
7706
7752
  }
7753
+ const target = fields.find((f) => f.id === chosenField);
7754
+ if (target?.via) {
7755
+ onChange(linkedNode(target.via.id, target.via.fieldId, chosenOp === "ne" ? "ne" : "eq", chosenValue));
7756
+ return;
7757
+ }
7707
7758
  onChange(
7708
- chosenOp === "present" ? { op: "present", args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
7759
+ VALUELESS_OPS.has(chosenOp) ? { op: chosenOp, args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
7709
7760
  );
7710
7761
  }
7711
7762
  return /* @__PURE__ */ jsxs28("div", { className: className ?? "flex flex-wrap items-center gap-1 text-xs", children: [
@@ -7738,7 +7789,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7738
7789
  className: "h-8 w-auto min-w-[6rem] text-xs",
7739
7790
  value: op2,
7740
7791
  onChange: (next) => write({ op: next }),
7741
- options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
7792
+ options: [...baseTerms, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
7742
7793
  attrs: { "data-condition": "comparison" }
7743
7794
  }
7744
7795
  ),
@@ -7752,7 +7803,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7752
7803
  value: String(relative.days ?? 7),
7753
7804
  onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
7754
7805
  }
7755
- ) : null : reader || op2 === "present" ? null : pointsAtARecord(chosen) ? (
7806
+ ) : null : reader || VALUELESS_OPS.has(op2) ? null : pointsAtARecord(chosen) ? (
7756
7807
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
7757
7808
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
7758
7809
  // the column it filters can never disagree about what a record is called. It
@@ -7770,7 +7821,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
7770
7821
  allowCreate: false
7771
7822
  }
7772
7823
  )
7773
- ) : isChoiceColumn(chosen) && (op2 === "eq" || op2 === "ne") ? /* @__PURE__ */ jsx32(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ jsx32(
7824
+ ) : isChoiceColumn(chosen) && (op2 === "eq" || op2 === "ne" || op2 === "contains" || op2 === "not_contains") ? /* @__PURE__ */ jsx32(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ jsx32(
7774
7825
  BasicInput7,
7775
7826
  {
7776
7827
  className: "h-8 w-28 text-xs",
@@ -53121,7 +53172,7 @@ function TableDuplicateDialog({ tableId, name, organizationId, open, onOpenChang
53121
53172
  // src/ViewFilter.tsx
53122
53173
  import { useEffect as useEffect123, useMemo as useMemo81, useState as useState149 } from "react";
53123
53174
  import { Button as Button119, Popover as Popover19, PopoverContent as PopoverContent19, PopoverTrigger as PopoverTrigger19 } from "@ai-matrx/design-system";
53124
- import { useFields as useFields38 } from "@ai-matrx/records/react";
53175
+ import { useFields as useFields38, useOptionalRecordsClient as useOptionalRecordsClient21 } from "@ai-matrx/records/react";
53125
53176
  import { jsx as jsx176, jsxs as jsxs163 } from "react/jsx-runtime";
53126
53177
  function conditionCount(expr) {
53127
53178
  if (!expr || typeof expr !== "object" || Array.isArray(expr)) return 0;
@@ -53140,10 +53191,39 @@ function ViewFilter({ tableId, view, onApply, className }) {
53140
53191
  useEffect123(() => {
53141
53192
  setDraft(saved);
53142
53193
  }, [view?.id, savedKey]);
53143
- const conditionFields = useMemo81(
53144
- () => (fields.data ?? []).map((f) => ({ id: f.id, key: f.key, label: f.label || f.key, field: f })),
53194
+ const client = useOptionalRecordsClient21();
53195
+ const relations = useMemo81(
53196
+ () => (fields.data ?? []).filter((f) => f.type === "relation" && !!f.relation_target),
53145
53197
  [fields.data]
53146
53198
  );
53199
+ const [linkedFields, setLinkedFields] = useState149({});
53200
+ useEffect123(() => {
53201
+ if (!open || !client) return;
53202
+ let cancelled = false;
53203
+ for (const rel of relations) {
53204
+ const target = String(rel.relation_target);
53205
+ void client.fields({ table_id: target }).then((got) => {
53206
+ if (cancelled || !got.ok) return;
53207
+ setLinkedFields((now) => ({ ...now, [rel.id]: got.data ?? [] }));
53208
+ });
53209
+ }
53210
+ return () => {
53211
+ cancelled = true;
53212
+ };
53213
+ }, [open, client, relations]);
53214
+ const conditionFields = useMemo81(() => {
53215
+ const own3 = (fields.data ?? []).map((f) => ({ id: f.id, key: f.key, label: f.label || f.key, field: f }));
53216
+ const reached = relations.flatMap(
53217
+ (rel) => (linkedFields[rel.id] ?? []).filter((inner2) => inner2.type !== "relation").map((inner2) => ({
53218
+ id: `${rel.id}>${inner2.id}`,
53219
+ key: `${rel.key}.${inner2.key}`,
53220
+ label: `${rel.label || rel.key} \u203A ${inner2.label || inner2.key}`,
53221
+ field: inner2,
53222
+ via: { id: rel.id, fieldId: inner2.id }
53223
+ }))
53224
+ );
53225
+ return [...own3, ...reached];
53226
+ }, [fields.data, relations, linkedFields]);
53147
53227
  if (!view) return null;
53148
53228
  const count = conditionCount(saved);
53149
53229
  const changed2 = JSON.stringify(draft) !== savedKey;