@ai-matrx/records-ui 0.115.78 → 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/pickers.cjs CHANGED
@@ -4032,46 +4032,63 @@ function AttachmentChip({ id, onRemove, onPreview }) {
4032
4032
  const words = read?.name ?? (read ? FILE_CHIP_UNREAD : "\u2026");
4033
4033
  const href = read?.fileId ? host.hrefForFile?.({ fileId: read.fileId }) : void 0;
4034
4034
  const title = onPreview ? `Preview ${words}` : href ? `Open ${words}` : read?.why ?? FILE_CHIP_NO_PAGE;
4035
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_design_system19.Badge, { variant: "secondary", className: "max-w-[14rem] gap-1 text-[11px]", "data-records-file-chip": id, title, children: [
4036
- onPreview ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4037
- "button",
4038
- {
4039
- type: "button",
4040
- className: "truncate underline-offset-2 hover:underline",
4041
- "data-records-file-preview": id,
4035
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
4036
+ import_design_system19.Badge,
4037
+ {
4038
+ variant: "secondary",
4039
+ className: (0, import_design_system19.cn)("max-w-[14rem] gap-1 text-[11px]", onPreview && "cursor-pointer"),
4040
+ "data-records-file-chip": id,
4041
+ title,
4042
+ ...onPreview ? {
4042
4043
  onClick: (event) => {
4043
4044
  event.stopPropagation();
4044
4045
  onPreview();
4045
4046
  },
4046
4047
  onMouseDown: (event) => event.stopPropagation(),
4047
- onDoubleClick: (event) => event.stopPropagation(),
4048
- children: words
4049
- }
4050
- ) : href ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4051
- import_outbound_react4.OutboundLink,
4052
- {
4053
- href,
4054
- className: "truncate underline-offset-2 hover:underline",
4055
- "data-records-file-link": read?.fileId ?? "",
4056
- onClick: (event) => event.stopPropagation(),
4057
- onMouseDown: (event) => event.stopPropagation(),
4058
- children: words
4059
- }
4060
- ) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "truncate", children: words }),
4061
- onRemove ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4062
- "button",
4063
- {
4064
- type: "button",
4065
- "aria-label": `Remove ${words}`,
4066
- className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
4067
- onClick: (event) => {
4068
- event.stopPropagation();
4069
- onRemove();
4070
- },
4071
- children: "\xD7"
4072
- }
4073
- ) : null
4074
- ] });
4048
+ onDoubleClick: (event) => event.stopPropagation()
4049
+ } : {},
4050
+ children: [
4051
+ onPreview ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4052
+ "button",
4053
+ {
4054
+ type: "button",
4055
+ className: "truncate underline-offset-2 hover:underline",
4056
+ "data-records-file-preview": id,
4057
+ onClick: (event) => {
4058
+ event.stopPropagation();
4059
+ onPreview();
4060
+ },
4061
+ onMouseDown: (event) => event.stopPropagation(),
4062
+ onDoubleClick: (event) => event.stopPropagation(),
4063
+ children: words
4064
+ }
4065
+ ) : href ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4066
+ import_outbound_react4.OutboundLink,
4067
+ {
4068
+ href,
4069
+ className: "truncate underline-offset-2 hover:underline",
4070
+ "data-records-file-link": read?.fileId ?? "",
4071
+ onClick: (event) => event.stopPropagation(),
4072
+ onMouseDown: (event) => event.stopPropagation(),
4073
+ children: words
4074
+ }
4075
+ ) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "truncate", children: words }),
4076
+ onRemove ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4077
+ "button",
4078
+ {
4079
+ type: "button",
4080
+ "aria-label": `Remove ${words}`,
4081
+ className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
4082
+ onClick: (event) => {
4083
+ event.stopPropagation();
4084
+ onRemove();
4085
+ },
4086
+ children: "\xD7"
4087
+ }
4088
+ ) : null
4089
+ ]
4090
+ }
4091
+ );
4075
4092
  }
4076
4093
  function AttachmentChips({ value, onRemove }) {
4077
4094
  const host = useRecordsUi();
@@ -4112,6 +4129,18 @@ function AttachmentViewer({
4112
4129
  {
4113
4130
  className: "sm:max-w-3xl",
4114
4131
  "data-records-attachment-viewer": id,
4132
+ tabIndex: -1,
4133
+ onOpenAutoFocus: (event) => {
4134
+ event.preventDefault();
4135
+ event.currentTarget.focus();
4136
+ },
4137
+ onCloseAutoFocus: (event) => {
4138
+ const chip = document.querySelector(`[data-records-file-preview="${ids[index]}"]`);
4139
+ if (chip) {
4140
+ event.preventDefault();
4141
+ chip.focus();
4142
+ }
4143
+ },
4115
4144
  onKeyDown: (event) => {
4116
4145
  if (event.key === "ArrowLeft") {
4117
4146
  event.preventDefault();
@@ -4749,6 +4778,33 @@ var CONDITION_OPS = [
4749
4778
  { op: "gte", label: "is at least" },
4750
4779
  { op: "lte", label: "is at most" }
4751
4780
  ];
4781
+ var LIST_TERMS = [
4782
+ { op: "contains", label: "contains" },
4783
+ { op: "not_contains", label: "does not contain" },
4784
+ { op: "is_empty", label: "is empty" },
4785
+ { op: "is_not_empty", label: "is not empty" }
4786
+ ];
4787
+ var VALUELESS_OPS = /* @__PURE__ */ new Set(["present", "is_empty", "is_not_empty"]);
4788
+ var LINK_SEP = ">";
4789
+ function linkedNode(viaId, innerId, op, value) {
4790
+ return {
4791
+ op: "linked",
4792
+ match: op === "eq" ? "any" : "none",
4793
+ args: [{ field: viaId }, { op: "eq", args: [{ field: innerId }, { const: value }] }]
4794
+ };
4795
+ }
4796
+ function linkedOf(expr) {
4797
+ const node = asNode(expr);
4798
+ if (!node || node["op"] !== "linked") return null;
4799
+ const [rel, cond] = argsOf(node);
4800
+ const innerNode = asNode(cond);
4801
+ const [a, b] = innerNode ? argsOf(innerNode) : [];
4802
+ const match = node["match"] ?? "any";
4803
+ if (!rel || typeof rel["field"] !== "string" || !innerNode || innerNode["op"] !== "eq") return null;
4804
+ if (!a || typeof a["field"] !== "string" || !b || !("const" in b)) return null;
4805
+ if (match !== "any" && match !== "none") return null;
4806
+ return { via: rel["field"], inner: a["field"], op: match === "any" ? "eq" : "ne", value: b["const"] };
4807
+ }
4752
4808
  var FilterTermsContext = (0, import_react36.createContext)(false);
4753
4809
  var RELATIVE_DAY_TERMS = [
4754
4810
  { kind: "today", label: "is today" },
@@ -4861,13 +4917,15 @@ function ChoiceValue({ field, value, onChange }) {
4861
4917
  }
4862
4918
  function conditionIsSimple(expr) {
4863
4919
  if (expr === null || expr === void 0) return true;
4864
- if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
4920
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null || linkedOf(expr) !== null) return true;
4865
4921
  const node = expr;
4866
- if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
4922
+ if (typeof node.op !== "string" || !(CONDITION_OPS.some((c) => c.op === node.op) || LIST_TERMS.some((c) => c.op === node.op))) {
4923
+ return false;
4924
+ }
4867
4925
  if (!Array.isArray(node.args)) return false;
4868
4926
  const first = node.args[0];
4869
4927
  if (!first || typeof first.field !== "string") return false;
4870
- if (node.op === "present") return node.args.length === 1;
4928
+ if (VALUELESS_OPS.has(node.op)) return node.args.length === 1;
4871
4929
  const second = node.args[1];
4872
4930
  return node.args.length === 2 && !!second && "const" in second;
4873
4931
  }
@@ -4885,7 +4943,13 @@ function conditionInWords(expr, fields) {
4885
4943
  lte: "is at most",
4886
4944
  and: "and",
4887
4945
  or: "or",
4888
- matches: "matches"
4946
+ matches: "matches",
4947
+ contains: "contains",
4948
+ not_contains: "does not contain",
4949
+ has_any: "has any of",
4950
+ has_all: "has all of",
4951
+ has_none: "has none of",
4952
+ is_exactly: "is exactly"
4889
4953
  };
4890
4954
  const walk = (node) => {
4891
4955
  if (node === null || node === void 0) return "nothing";
@@ -4906,6 +4970,12 @@ function conditionInWords(expr, fields) {
4906
4970
  const args = Array.isArray(n["args"]) ? n["args"] : [];
4907
4971
  if (op === "not") return `not ${walk(args[0])}`;
4908
4972
  if (op === "present") return `${walk(args[0])} has been answered`;
4973
+ if (op === "is_empty") return `${walk(args[0])} is empty`;
4974
+ if (op === "is_not_empty") return `${walk(args[0])} is not empty`;
4975
+ if (op === "linked") {
4976
+ const how = n["match"] === "every" ? "every" : n["match"] === "none" ? "none of" : "any";
4977
+ return `${how} linked record in ${walk(args[0])}: ${walk(args[1])}`;
4978
+ }
4909
4979
  const dayRange = relativeDayOf(n);
4910
4980
  if (dayRange) {
4911
4981
  const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
@@ -4938,12 +5008,14 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
4938
5008
  const filterTerms = (0, import_react36.useContext)(FilterTermsContext);
4939
5009
  const relative = relativeDayOf(expr);
4940
5010
  const reader = meOf(expr);
4941
- const op = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
4942
- const onField = node?.args?.[0]?.field ?? "";
4943
- const value = relative || reader ? "" : node?.args?.[1]?.const ?? "";
5011
+ const linked = linkedOf(expr);
5012
+ const op = relative ? `rel:${relative.kind}` : reader ? reader.which : linked ? linked.op : node?.op ?? "";
5013
+ const onField = linked ? `${linked.via}${LINK_SEP}${linked.inner}` : node?.args?.[0]?.field ?? "";
5014
+ const value = relative || reader ? "" : linked ? linked.value : node?.args?.[1]?.const ?? "";
4944
5015
  const chosen = fields.find((f) => f.id === onField);
4945
5016
  const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
4946
- 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" }] : [];
5017
+ 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 : [];
5018
+ const baseTerms = chosen?.via ? CONDITION_OPS.filter((c) => c.op === "eq" || c.op === "ne") : CONDITION_OPS;
4947
5019
  const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
4948
5020
  function write(next) {
4949
5021
  const picked = next.op;
@@ -4956,8 +5028,11 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
4956
5028
  onChange(relativeDayNode(fieldNow, relative.kind, next.days));
4957
5029
  return;
4958
5030
  }
4959
- if (next.field !== void 0 && (relative || reader)) {
4960
- onChange(next.field ? { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] } : null);
5031
+ if (next.field !== void 0 && (relative || reader || linked)) {
5032
+ const target2 = fields.find((f) => f.id === next.field);
5033
+ onChange(
5034
+ !next.field ? null : target2?.via ? linkedNode(target2.via.id, target2.via.fieldId, "eq", "") : { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] }
5035
+ );
4961
5036
  return;
4962
5037
  }
4963
5038
  const chosenOp = next.op ?? (op || (next.field ? CONDITION_OPS[0].op : ""));
@@ -4967,8 +5042,13 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
4967
5042
  onChange(null);
4968
5043
  return;
4969
5044
  }
5045
+ const target = fields.find((f) => f.id === chosenField);
5046
+ if (target?.via) {
5047
+ onChange(linkedNode(target.via.id, target.via.fieldId, chosenOp === "ne" ? "ne" : "eq", chosenValue));
5048
+ return;
5049
+ }
4970
5050
  onChange(
4971
- chosenOp === "present" ? { op: "present", args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
5051
+ VALUELESS_OPS.has(chosenOp) ? { op: chosenOp, args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
4972
5052
  );
4973
5053
  }
4974
5054
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: className ?? "flex flex-wrap items-center gap-1 text-xs", children: [
@@ -5001,7 +5081,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5001
5081
  className: "h-8 w-auto min-w-[6rem] text-xs",
5002
5082
  value: op,
5003
5083
  onChange: (next) => write({ op: next }),
5004
- options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
5084
+ options: [...baseTerms, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
5005
5085
  attrs: { "data-condition": "comparison" }
5006
5086
  }
5007
5087
  ),
@@ -5015,7 +5095,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5015
5095
  value: String(relative.days ?? 7),
5016
5096
  onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
5017
5097
  }
5018
- ) : null : reader || op === "present" ? null : pointsAtARecord(chosen) ? (
5098
+ ) : null : reader || VALUELESS_OPS.has(op) ? null : pointsAtARecord(chosen) ? (
5019
5099
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
5020
5100
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
5021
5101
  // the column it filters can never disagree about what a record is called. It
@@ -5033,7 +5113,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
5033
5113
  allowCreate: false
5034
5114
  }
5035
5115
  )
5036
- ) : isChoiceColumn(chosen) && (op === "eq" || op === "ne") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5116
+ ) : isChoiceColumn(chosen) && (op === "eq" || op === "ne" || op === "contains" || op === "not_contains") ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5037
5117
  import_design_system23.BasicInput,
5038
5118
  {
5039
5119
  className: "h-8 w-28 text-xs",