@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/CHANGELOG.md CHANGED
@@ -1,10 +1,25 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
- ## 0.115.79
3
+ ## 0.115.80
4
4
 
5
5
  UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
6
  Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/records-ui/src`.
7
7
 
8
+ ## 0.115.79
9
+
10
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
11
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
12
+ Diff: `git diff c3135c57b9 aa17cdaf9c -- apps/shared/records-ui/src`
13
+
14
+ Changed shipped source files (7):
15
+ - `apps/shared/records-ui/src/ImportWizard.tsx`
16
+ - `apps/shared/records-ui/src/TablePage.tsx`
17
+ - `apps/shared/records-ui/src/ViewBar.tsx`
18
+ - `apps/shared/records-ui/src/gridAttachments.tsx`
19
+ - `apps/shared/records-ui/src/gridColumns.tsx`
20
+ - `apps/shared/records-ui/src/importRuleCheck.ts`
21
+ - `apps/shared/records-ui/src/viewActions.ts`
22
+
8
23
  ## 0.115.78
9
24
 
10
25
  SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
@@ -31,6 +46,7 @@ Changed shipped source files (3):
31
46
  ## 0.115.76
32
47
  ### Unreleased
33
48
 
49
+ - `ViewFilter` / `Condition` (NOTION-Q): a view's filter asks a multi-select column Notion's list questions (contains, does not contain, is empty, is not empty) and offers the fields of every table a relation points at ("Client › Client Type is / is not Past Client", the store's `linked` node: "is" = some linked record is, "is not" = none is). New exports `LIST_TERMS`, `linkedNode`, `linkedOf`; `ConditionField.via`. Needs the store migration `notionq_a_a_multi_pick_and_a_linked_field_filter_a_list`.
34
50
  - `ViewSwitcher newRowValues`: the host hands the values a new row starts with (Spaces: a view filtered to "This page's row" hands the relation), and every add path of every layout — the grid's add line and toolbar, board and pipeline "+", gallery, list, calendar, timeline — writes them with the row. A path's own values (a board column's stage) win. Absent: a blank row, as before.
35
51
  - The formula editor's function list follows the store's vocabulary: Airtable's remaining functions (ROUNDUP, FLOOR, COUNTA,
36
52
  IS_BEFORE, DATETIME_DIFF, WORKDAY_DIFF, ARRAYFLATTEN, REGEX_EXTRACT, RECORD_ID, …) are offered, and CREATED_TIME() and
package/dist/index.cjs CHANGED
@@ -8006,6 +8006,33 @@ var CONDITION_OPS = [
8006
8006
  { op: "gte", label: "is at least" },
8007
8007
  { op: "lte", label: "is at most" }
8008
8008
  ];
8009
+ var LIST_TERMS = [
8010
+ { op: "contains", label: "contains" },
8011
+ { op: "not_contains", label: "does not contain" },
8012
+ { op: "is_empty", label: "is empty" },
8013
+ { op: "is_not_empty", label: "is not empty" }
8014
+ ];
8015
+ var VALUELESS_OPS = /* @__PURE__ */ new Set(["present", "is_empty", "is_not_empty"]);
8016
+ var LINK_SEP = ">";
8017
+ function linkedNode(viaId, innerId, op2, value) {
8018
+ return {
8019
+ op: "linked",
8020
+ match: op2 === "eq" ? "any" : "none",
8021
+ args: [{ field: viaId }, { op: "eq", args: [{ field: innerId }, { const: value }] }]
8022
+ };
8023
+ }
8024
+ function linkedOf(expr) {
8025
+ const node = asNode(expr);
8026
+ if (!node || node["op"] !== "linked") return null;
8027
+ const [rel, cond] = argsOf(node);
8028
+ const innerNode = asNode(cond);
8029
+ const [a, b] = innerNode ? argsOf(innerNode) : [];
8030
+ const match = node["match"] ?? "any";
8031
+ if (!rel || typeof rel["field"] !== "string" || !innerNode || innerNode["op"] !== "eq") return null;
8032
+ if (!a || typeof a["field"] !== "string" || !b || !("const" in b)) return null;
8033
+ if (match !== "any" && match !== "none") return null;
8034
+ return { via: rel["field"], inner: a["field"], op: match === "any" ? "eq" : "ne", value: b["const"] };
8035
+ }
8009
8036
  var FilterTermsContext = (0, import_react46.createContext)(false);
8010
8037
  var RELATIVE_DAY_TERMS = [
8011
8038
  { kind: "today", label: "is today" },
@@ -8118,13 +8145,15 @@ function ChoiceValue({ field: field2, value, onChange }) {
8118
8145
  }
8119
8146
  function conditionIsSimple(expr) {
8120
8147
  if (expr === null || expr === void 0) return true;
8121
- if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
8148
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null || linkedOf(expr) !== null) return true;
8122
8149
  const node = expr;
8123
- if (typeof node.op !== "string" || !CONDITION_OPS.some((c) => c.op === node.op)) return false;
8150
+ if (typeof node.op !== "string" || !(CONDITION_OPS.some((c) => c.op === node.op) || LIST_TERMS.some((c) => c.op === node.op))) {
8151
+ return false;
8152
+ }
8124
8153
  if (!Array.isArray(node.args)) return false;
8125
8154
  const first = node.args[0];
8126
8155
  if (!first || typeof first.field !== "string") return false;
8127
- if (node.op === "present") return node.args.length === 1;
8156
+ if (VALUELESS_OPS.has(node.op)) return node.args.length === 1;
8128
8157
  const second = node.args[1];
8129
8158
  return node.args.length === 2 && !!second && "const" in second;
8130
8159
  }
@@ -8142,7 +8171,13 @@ function conditionInWords(expr, fields) {
8142
8171
  lte: "is at most",
8143
8172
  and: "and",
8144
8173
  or: "or",
8145
- matches: "matches"
8174
+ matches: "matches",
8175
+ contains: "contains",
8176
+ not_contains: "does not contain",
8177
+ has_any: "has any of",
8178
+ has_all: "has all of",
8179
+ has_none: "has none of",
8180
+ is_exactly: "is exactly"
8146
8181
  };
8147
8182
  const walk = (node) => {
8148
8183
  if (node === null || node === void 0) return "nothing";
@@ -8163,6 +8198,12 @@ function conditionInWords(expr, fields) {
8163
8198
  const args = Array.isArray(n["args"]) ? n["args"] : [];
8164
8199
  if (op2 === "not") return `not ${walk(args[0])}`;
8165
8200
  if (op2 === "present") return `${walk(args[0])} has been answered`;
8201
+ if (op2 === "is_empty") return `${walk(args[0])} is empty`;
8202
+ if (op2 === "is_not_empty") return `${walk(args[0])} is not empty`;
8203
+ if (op2 === "linked") {
8204
+ const how = n["match"] === "every" ? "every" : n["match"] === "none" ? "none of" : "any";
8205
+ return `${how} linked record in ${walk(args[0])}: ${walk(args[1])}`;
8206
+ }
8166
8207
  const dayRange = relativeDayOf(n);
8167
8208
  if (dayRange) {
8168
8209
  const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
@@ -8195,12 +8236,14 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8195
8236
  const filterTerms = (0, import_react46.useContext)(FilterTermsContext);
8196
8237
  const relative = relativeDayOf(expr);
8197
8238
  const reader = meOf(expr);
8198
- const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
8199
- const onField = node?.args?.[0]?.field ?? "";
8200
- const value = relative || reader ? "" : node?.args?.[1]?.const ?? "";
8239
+ const linked = linkedOf(expr);
8240
+ const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : linked ? linked.op : node?.op ?? "";
8241
+ const onField = linked ? `${linked.via}${LINK_SEP}${linked.inner}` : node?.args?.[0]?.field ?? "";
8242
+ const value = relative || reader ? "" : linked ? linked.value : node?.args?.[1]?.const ?? "";
8201
8243
  const chosen = fields.find((f) => f.id === onField);
8202
8244
  const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
8203
- 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" }] : [];
8245
+ 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 : [];
8246
+ const baseTerms = chosen?.via ? CONDITION_OPS.filter((c) => c.op === "eq" || c.op === "ne") : CONDITION_OPS;
8204
8247
  const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
8205
8248
  function write(next) {
8206
8249
  const picked = next.op;
@@ -8213,8 +8256,11 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8213
8256
  onChange(relativeDayNode(fieldNow, relative.kind, next.days));
8214
8257
  return;
8215
8258
  }
8216
- if (next.field !== void 0 && (relative || reader)) {
8217
- onChange(next.field ? { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] } : null);
8259
+ if (next.field !== void 0 && (relative || reader || linked)) {
8260
+ const target2 = fields.find((f) => f.id === next.field);
8261
+ onChange(
8262
+ !next.field ? null : target2?.via ? linkedNode(target2.via.id, target2.via.fieldId, "eq", "") : { op: CONDITION_OPS[0].op, args: [{ field: next.field }, { const: "" }] }
8263
+ );
8218
8264
  return;
8219
8265
  }
8220
8266
  const chosenOp = next.op ?? (op2 || (next.field ? CONDITION_OPS[0].op : ""));
@@ -8224,8 +8270,13 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8224
8270
  onChange(null);
8225
8271
  return;
8226
8272
  }
8273
+ const target = fields.find((f) => f.id === chosenField);
8274
+ if (target?.via) {
8275
+ onChange(linkedNode(target.via.id, target.via.fieldId, chosenOp === "ne" ? "ne" : "eq", chosenValue));
8276
+ return;
8277
+ }
8227
8278
  onChange(
8228
- chosenOp === "present" ? { op: "present", args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
8279
+ VALUELESS_OPS.has(chosenOp) ? { op: chosenOp, args: [{ field: chosenField }] } : { op: chosenOp, args: [{ field: chosenField }, { const: chosenValue }] }
8229
8280
  );
8230
8281
  }
8231
8282
  return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: className ?? "flex flex-wrap items-center gap-1 text-xs", children: [
@@ -8258,7 +8309,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8258
8309
  className: "h-8 w-auto min-w-[6rem] text-xs",
8259
8310
  value: op2,
8260
8311
  onChange: (next) => write({ op: next }),
8261
- options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
8312
+ options: [...baseTerms, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
8262
8313
  attrs: { "data-condition": "comparison" }
8263
8314
  }
8264
8315
  ),
@@ -8272,7 +8323,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8272
8323
  value: String(relative.days ?? 7),
8273
8324
  onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
8274
8325
  }
8275
- ) : null : reader || op2 === "present" ? null : pointsAtARecord(chosen) ? (
8326
+ ) : null : reader || VALUELESS_OPS.has(op2) ? null : pointsAtARecord(chosen) ? (
8276
8327
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
8277
8328
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
8278
8329
  // the column it filters can never disagree about what a record is called. It
@@ -8290,7 +8341,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8290
8341
  allowCreate: false
8291
8342
  }
8292
8343
  )
8293
- ) : isChoiceColumn(chosen) && (op2 === "eq" || op2 === "ne") ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
8344
+ ) : isChoiceColumn(chosen) && (op2 === "eq" || op2 === "ne" || op2 === "contains" || op2 === "not_contains") ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(ChoiceValue, { field: chosen.field, value: String(value ?? ""), onChange: (word) => write({ value: word }) }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
8294
8345
  import_design_system28.BasicInput,
8295
8346
  {
8296
8347
  className: "h-8 w-28 text-xs",
@@ -53157,10 +53208,39 @@ function ViewFilter({ tableId, view, onApply, className }) {
53157
53208
  (0, import_react310.useEffect)(() => {
53158
53209
  setDraft(saved);
53159
53210
  }, [view?.id, savedKey]);
53160
- const conditionFields = (0, import_react310.useMemo)(
53161
- () => (fields.data ?? []).map((f) => ({ id: f.id, key: f.key, label: f.label || f.key, field: f })),
53211
+ const client = (0, import_react311.useOptionalRecordsClient)();
53212
+ const relations = (0, import_react310.useMemo)(
53213
+ () => (fields.data ?? []).filter((f) => f.type === "relation" && !!f.relation_target),
53162
53214
  [fields.data]
53163
53215
  );
53216
+ const [linkedFields, setLinkedFields] = (0, import_react310.useState)({});
53217
+ (0, import_react310.useEffect)(() => {
53218
+ if (!open || !client) return;
53219
+ let cancelled = false;
53220
+ for (const rel of relations) {
53221
+ const target = String(rel.relation_target);
53222
+ void client.fields({ table_id: target }).then((got) => {
53223
+ if (cancelled || !got.ok) return;
53224
+ setLinkedFields((now) => ({ ...now, [rel.id]: got.data ?? [] }));
53225
+ });
53226
+ }
53227
+ return () => {
53228
+ cancelled = true;
53229
+ };
53230
+ }, [open, client, relations]);
53231
+ const conditionFields = (0, import_react310.useMemo)(() => {
53232
+ const own3 = (fields.data ?? []).map((f) => ({ id: f.id, key: f.key, label: f.label || f.key, field: f }));
53233
+ const reached = relations.flatMap(
53234
+ (rel) => (linkedFields[rel.id] ?? []).filter((inner2) => inner2.type !== "relation").map((inner2) => ({
53235
+ id: `${rel.id}>${inner2.id}`,
53236
+ key: `${rel.key}.${inner2.key}`,
53237
+ label: `${rel.label || rel.key} \u203A ${inner2.label || inner2.key}`,
53238
+ field: inner2,
53239
+ via: { id: rel.id, fieldId: inner2.id }
53240
+ }))
53241
+ );
53242
+ return [...own3, ...reached];
53243
+ }, [fields.data, relations, linkedFields]);
53164
53244
  if (!view) return null;
53165
53245
  const count = conditionCount(saved);
53166
53246
  const changed2 = JSON.stringify(draft) !== savedKey;