@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/CHANGELOG.md CHANGED
@@ -1,10 +1,37 @@
1
1
  # Changelog — @ai-matrx/records-ui
2
2
 
3
- ## 0.115.78
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
+
23
+ ## 0.115.78
24
+
25
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
26
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
27
+ Diff: `git diff 254d4d3f8b c3135c57b9 -- apps/shared/records-ui/src`
28
+
29
+ Changed shipped source files (4):
30
+ - `apps/shared/records-ui/src/Grid.tsx`
31
+ - `apps/shared/records-ui/src/ViewSwitcher.tsx`
32
+ - `apps/shared/records-ui/src/newRowValues.tsx`
33
+ - `apps/shared/records-ui/src/recordMoves.tsx`
34
+
8
35
  ## 0.115.77
9
36
 
10
37
  SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
@@ -19,6 +46,7 @@ Changed shipped source files (3):
19
46
  ## 0.115.76
20
47
  ### Unreleased
21
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`.
22
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.
23
51
  - The formula editor's function list follows the store's vocabulary: Airtable's remaining functions (ROUNDUP, FLOOR, COUNTA,
24
52
  IS_BEFORE, DATETIME_DIFF, WORKDAY_DIFF, ARRAYFLATTEN, REGEX_EXTRACT, RECORD_ID, …) are offered, and CREATED_TIME() and
package/dist/index.cjs CHANGED
@@ -6886,46 +6886,63 @@ function AttachmentChip({ id, onRemove, onPreview }) {
6886
6886
  const words2 = read?.name ?? (read ? FILE_CHIP_UNREAD : "\u2026");
6887
6887
  const href = read?.fileId ? host.hrefForFile?.({ fileId: read.fileId }) : void 0;
6888
6888
  const title = onPreview ? `Preview ${words2}` : href ? `Open ${words2}` : read?.why ?? FILE_CHIP_NO_PAGE;
6889
- return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_design_system22.Badge, { variant: "secondary", className: "max-w-[14rem] gap-1 text-[11px]", "data-records-file-chip": id, title, children: [
6890
- onPreview ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6891
- "button",
6892
- {
6893
- type: "button",
6894
- className: "truncate underline-offset-2 hover:underline",
6895
- "data-records-file-preview": id,
6889
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
6890
+ import_design_system22.Badge,
6891
+ {
6892
+ variant: "secondary",
6893
+ className: (0, import_design_system22.cn)("max-w-[14rem] gap-1 text-[11px]", onPreview && "cursor-pointer"),
6894
+ "data-records-file-chip": id,
6895
+ title,
6896
+ ...onPreview ? {
6896
6897
  onClick: (event) => {
6897
6898
  event.stopPropagation();
6898
6899
  onPreview();
6899
6900
  },
6900
6901
  onMouseDown: (event) => event.stopPropagation(),
6901
- onDoubleClick: (event) => event.stopPropagation(),
6902
- children: words2
6903
- }
6904
- ) : href ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6905
- import_outbound_react4.OutboundLink,
6906
- {
6907
- href,
6908
- className: "truncate underline-offset-2 hover:underline",
6909
- "data-records-file-link": read?.fileId ?? "",
6910
- onClick: (event) => event.stopPropagation(),
6911
- onMouseDown: (event) => event.stopPropagation(),
6912
- children: words2
6913
- }
6914
- ) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "truncate", children: words2 }),
6915
- onRemove ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6916
- "button",
6917
- {
6918
- type: "button",
6919
- "aria-label": `Remove ${words2}`,
6920
- className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
6921
- onClick: (event) => {
6922
- event.stopPropagation();
6923
- onRemove();
6924
- },
6925
- children: "\xD7"
6926
- }
6927
- ) : null
6928
- ] });
6902
+ onDoubleClick: (event) => event.stopPropagation()
6903
+ } : {},
6904
+ children: [
6905
+ onPreview ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6906
+ "button",
6907
+ {
6908
+ type: "button",
6909
+ className: "truncate underline-offset-2 hover:underline",
6910
+ "data-records-file-preview": id,
6911
+ onClick: (event) => {
6912
+ event.stopPropagation();
6913
+ onPreview();
6914
+ },
6915
+ onMouseDown: (event) => event.stopPropagation(),
6916
+ onDoubleClick: (event) => event.stopPropagation(),
6917
+ children: words2
6918
+ }
6919
+ ) : href ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6920
+ import_outbound_react4.OutboundLink,
6921
+ {
6922
+ href,
6923
+ className: "truncate underline-offset-2 hover:underline",
6924
+ "data-records-file-link": read?.fileId ?? "",
6925
+ onClick: (event) => event.stopPropagation(),
6926
+ onMouseDown: (event) => event.stopPropagation(),
6927
+ children: words2
6928
+ }
6929
+ ) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "truncate", children: words2 }),
6930
+ onRemove ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
6931
+ "button",
6932
+ {
6933
+ type: "button",
6934
+ "aria-label": `Remove ${words2}`,
6935
+ className: "shrink-0 rounded px-0.5 text-muted-foreground hover:text-foreground",
6936
+ onClick: (event) => {
6937
+ event.stopPropagation();
6938
+ onRemove();
6939
+ },
6940
+ children: "\xD7"
6941
+ }
6942
+ ) : null
6943
+ ]
6944
+ }
6945
+ );
6929
6946
  }
6930
6947
  function AttachmentChips({ value, onRemove }) {
6931
6948
  const host = useRecordsUi();
@@ -6966,6 +6983,18 @@ function AttachmentViewer({
6966
6983
  {
6967
6984
  className: "sm:max-w-3xl",
6968
6985
  "data-records-attachment-viewer": id,
6986
+ tabIndex: -1,
6987
+ onOpenAutoFocus: (event) => {
6988
+ event.preventDefault();
6989
+ event.currentTarget.focus();
6990
+ },
6991
+ onCloseAutoFocus: (event) => {
6992
+ const chip = document.querySelector(`[data-records-file-preview="${ids[index]}"]`);
6993
+ if (chip) {
6994
+ event.preventDefault();
6995
+ chip.focus();
6996
+ }
6997
+ },
6969
6998
  onKeyDown: (event) => {
6970
6999
  if (event.key === "ArrowLeft") {
6971
7000
  event.preventDefault();
@@ -7977,6 +8006,33 @@ var CONDITION_OPS = [
7977
8006
  { op: "gte", label: "is at least" },
7978
8007
  { op: "lte", label: "is at most" }
7979
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
+ }
7980
8036
  var FilterTermsContext = (0, import_react46.createContext)(false);
7981
8037
  var RELATIVE_DAY_TERMS = [
7982
8038
  { kind: "today", label: "is today" },
@@ -8089,13 +8145,15 @@ function ChoiceValue({ field: field2, value, onChange }) {
8089
8145
  }
8090
8146
  function conditionIsSimple(expr) {
8091
8147
  if (expr === null || expr === void 0) return true;
8092
- if (relativeDayOf(expr) !== null || meOf(expr) !== null) return true;
8148
+ if (relativeDayOf(expr) !== null || meOf(expr) !== null || linkedOf(expr) !== null) return true;
8093
8149
  const node = expr;
8094
- 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
+ }
8095
8153
  if (!Array.isArray(node.args)) return false;
8096
8154
  const first = node.args[0];
8097
8155
  if (!first || typeof first.field !== "string") return false;
8098
- if (node.op === "present") return node.args.length === 1;
8156
+ if (VALUELESS_OPS.has(node.op)) return node.args.length === 1;
8099
8157
  const second = node.args[1];
8100
8158
  return node.args.length === 2 && !!second && "const" in second;
8101
8159
  }
@@ -8113,7 +8171,13 @@ function conditionInWords(expr, fields) {
8113
8171
  lte: "is at most",
8114
8172
  and: "and",
8115
8173
  or: "or",
8116
- 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"
8117
8181
  };
8118
8182
  const walk = (node) => {
8119
8183
  if (node === null || node === void 0) return "nothing";
@@ -8134,6 +8198,12 @@ function conditionInWords(expr, fields) {
8134
8198
  const args = Array.isArray(n["args"]) ? n["args"] : [];
8135
8199
  if (op2 === "not") return `not ${walk(args[0])}`;
8136
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
+ }
8137
8207
  const dayRange = relativeDayOf(n);
8138
8208
  if (dayRange) {
8139
8209
  const term = RELATIVE_DAY_TERMS.find((t) => t.kind === dayRange.kind);
@@ -8166,12 +8236,14 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8166
8236
  const filterTerms = (0, import_react46.useContext)(FilterTermsContext);
8167
8237
  const relative = relativeDayOf(expr);
8168
8238
  const reader = meOf(expr);
8169
- const op2 = relative ? `rel:${relative.kind}` : reader ? reader.which : node?.op ?? "";
8170
- const onField = node?.args?.[0]?.field ?? "";
8171
- 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 ?? "";
8172
8243
  const chosen = fields.find((f) => f.id === onField);
8173
8244
  const chosenKind = chosen?.field ? String(editorKindFor(chosen.field)) : "";
8174
- 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;
8175
8247
  const relativeTerm = relative ? RELATIVE_DAY_TERMS.find((t) => t.kind === relative.kind) : void 0;
8176
8248
  function write(next) {
8177
8249
  const picked = next.op;
@@ -8184,8 +8256,11 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8184
8256
  onChange(relativeDayNode(fieldNow, relative.kind, next.days));
8185
8257
  return;
8186
8258
  }
8187
- if (next.field !== void 0 && (relative || reader)) {
8188
- 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
+ );
8189
8264
  return;
8190
8265
  }
8191
8266
  const chosenOp = next.op ?? (op2 || (next.field ? CONDITION_OPS[0].op : ""));
@@ -8195,8 +8270,13 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8195
8270
  onChange(null);
8196
8271
  return;
8197
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
+ }
8198
8278
  onChange(
8199
- 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 }] }
8200
8280
  );
8201
8281
  }
8202
8282
  return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: className ?? "flex flex-wrap items-center gap-1 text-xs", children: [
@@ -8229,7 +8309,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8229
8309
  className: "h-8 w-auto min-w-[6rem] text-xs",
8230
8310
  value: op2,
8231
8311
  onChange: (next) => write({ op: next }),
8232
- options: [...CONDITION_OPS, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
8312
+ options: [...baseTerms, ...extraTerms].map((c) => ({ value: c.op, label: c.label })),
8233
8313
  attrs: { "data-condition": "comparison" }
8234
8314
  }
8235
8315
  ),
@@ -8243,7 +8323,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8243
8323
  value: String(relative.days ?? 7),
8244
8324
  onChange: (e) => write({ days: Math.max(0, Math.floor(Number(e.target.value) || 0)) })
8245
8325
  }
8246
- ) : null : reader || op2 === "present" ? null : pointsAtARecord(chosen) ? (
8326
+ ) : null : reader || VALUELESS_OPS.has(op2) ? null : pointsAtARecord(chosen) ? (
8247
8327
  // A RECORD IS PICKED BY ITS WORDS, NEVER TYPED AS AN ID. The same picker the
8248
8328
  // cell uses, on the same `relation_words_many` the cell reads, so a filter and
8249
8329
  // the column it filters can never disagree about what a record is called. It
@@ -8261,7 +8341,7 @@ function ConditionRow({ lead, expr, fields, onChange, emptyLabel = "always", cla
8261
8341
  allowCreate: false
8262
8342
  }
8263
8343
  )
8264
- ) : 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)(
8265
8345
  import_design_system28.BasicInput,
8266
8346
  {
8267
8347
  className: "h-8 w-28 text-xs",
@@ -20700,6 +20780,8 @@ function columnForField(field2, editing, mayWriteRow, whyNotRow, look, onAskWhoC
20700
20780
  ] }) : name
20701
20781
  ),
20702
20782
  label: name,
20783
+ // The field's own description, one hover away on every header (not only the type mark's glyph).
20784
+ ...fieldHelpText(field2) ? { helpText: fieldHelpText(field2) } : {},
20703
20785
  accessorFn: (row) => row.document?.[field2.key],
20704
20786
  // A column the store keeps no value for is not offered a sort it would refuse (`sortRefusal`).
20705
20787
  sortable: sortRefusal(field2) === null,
@@ -21853,9 +21935,9 @@ var import_records43 = require("@ai-matrx/records");
21853
21935
 
21854
21936
  // src/importRuleCheck.ts
21855
21937
  var import_core13 = require("@ai-matrx/records/core");
21856
- function checkImportRules(fields, rows, mapping, choiceOptions, relationNames) {
21938
+ function refusedCellsOf(fields, rows, mapping, choiceOptions, relationNames) {
21857
21939
  const byKey = new Map(fields.map((f) => [f.key, f]));
21858
- const verdicts = [];
21940
+ const cells = [];
21859
21941
  for (const [header, key] of Object.entries(mapping)) {
21860
21942
  const field2 = byKey.get(key);
21861
21943
  if (!field2) continue;
@@ -21865,9 +21947,8 @@ function checkImportRules(fields, rows, mapping, choiceOptions, relationNames) {
21865
21947
  if (isChoice && !options) continue;
21866
21948
  const named = relationNames?.get(key);
21867
21949
  if (kind2 === "relation" && !named) continue;
21868
- let refused = 0;
21869
21950
  let judged = 0;
21870
- let first = null;
21951
+ const mine = [];
21871
21952
  rows.forEach((row, i) => {
21872
21953
  const raw = String(row[header] ?? "").trim();
21873
21954
  if (raw === "") return;
@@ -21883,17 +21964,20 @@ function checkImportRules(fields, rows, mapping, choiceOptions, relationNames) {
21883
21964
  );
21884
21965
  if (predicted.length > 0) why = predicted[0].message;
21885
21966
  }
21886
- if (why !== null) {
21887
- refused += 1;
21888
- if (!first) first = { why, line: i + 1, raw };
21889
- }
21967
+ if (why !== null) mine.push({ header, line: i + 1, raw, why });
21890
21968
  });
21891
- if (refused > 0 && first) {
21892
- const f = first;
21893
- verdicts.push({ header, field: field2, refused, judged, firstWhy: f.why, firstLine: f.line, firstRaw: f.raw });
21894
- }
21969
+ for (const c of mine) cells.push({ ...c, judged, field: field2 });
21970
+ }
21971
+ return cells;
21972
+ }
21973
+ function checkImportRules(fields, rows, mapping, choiceOptions, relationNames) {
21974
+ const verdicts = /* @__PURE__ */ new Map();
21975
+ for (const c of refusedCellsOf(fields, rows, mapping, choiceOptions, relationNames)) {
21976
+ const have = verdicts.get(c.header);
21977
+ if (have) have.refused += 1;
21978
+ else verdicts.set(c.header, { header: c.header, field: c.field, refused: 1, judged: c.judged, firstWhy: c.why, firstLine: c.line, firstRaw: c.raw });
21895
21979
  }
21896
- return verdicts;
21980
+ return [...verdicts.values()];
21897
21981
  }
21898
21982
 
21899
21983
  // src/ImportWizard.tsx
@@ -22305,7 +22389,7 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22305
22389
  },
22306
22390
  [client, openRun]
22307
22391
  );
22308
- const declared2 = fields.data ?? [];
22392
+ const declared2 = (0, import_react122.useMemo)(() => fields.data ?? [], [fields.data]);
22309
22393
  const titleKey = table.data?.title_field ?? null;
22310
22394
  const titleHeader = titleFrom ?? (titleKey ? Object.entries(mapping).find(([h, key]) => key === titleKey && h !== TITLE_COPY)?.[0] ?? null : null);
22311
22395
  const distinctRows = (0, import_react122.useMemo)(() => {
@@ -22599,6 +22683,12 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22599
22683
  () => exactDuplicateRows(effectiveRows, (plan?.columns ?? []).map((c) => c.header).filter((h) => !leftOut.has(h))),
22600
22684
  [effectiveRows, plan, leftOut]
22601
22685
  );
22686
+ const [goAheadAnyway, setGoAheadAnyway] = (0, import_react122.useState)(false);
22687
+ const refusedCells = (0, import_react122.useMemo)(
22688
+ () => goAheadAnyway ? refusedCellsOf(declared2, effectiveRows, mapping, choiceIndex, relationIndex) : [],
22689
+ [goAheadAnyway, declared2, effectiveRows, mapping, choiceIndex, relationIndex]
22690
+ );
22691
+ const [keptAside, setKeptAside] = (0, import_react122.useState)([]);
22602
22692
  const rowsToSend = (0, import_react122.useCallback)(
22603
22693
  (index) => {
22604
22694
  const repeated = new Set(skipRepeats ? repeats.map((r) => r.row) : []);
@@ -22608,6 +22698,7 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22608
22698
  if (repeated.has(at)) return;
22609
22699
  const out = {};
22610
22700
  for (const [header, value] of Object.entries(row)) if (!leftOut.has(header)) out[header] = value;
22701
+ for (const c of refusedCells) if (c.line === at + 1 && c.header in out) out[c.header] = "";
22611
22702
  if (titleFrom) out[TITLE_COPY] = String(row[titleFrom] ?? "").trim();
22612
22703
  for (const { header, field: field2 } of mappedRelations) {
22613
22704
  const named = index.get(field2.key);
@@ -22637,7 +22728,7 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22637
22728
  return { rows: sent, lines };
22638
22729
  },
22639
22730
  // eslint-disable-next-line react-hooks/exhaustive-deps -- choiceFor reads unlinkedChoice
22640
- [effectiveRows, combined, titleFrom, leftOut, mappedRelations, unlinkedChoice, dateAsks, dateMiss, repeats, skipRepeats]
22731
+ [effectiveRows, combined, titleFrom, leftOut, mappedRelations, unlinkedChoice, dateAsks, dateMiss, repeats, skipRepeats, refusedCells]
22641
22732
  );
22642
22733
  const run = (0, import_react122.useCallback)(async () => {
22643
22734
  if (!plan) return;
@@ -22705,6 +22796,7 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22705
22796
  index = reread;
22706
22797
  }
22707
22798
  const { rows: sendRows, lines: sendLines } = rowsToSend(index);
22799
+ setKeptAside(refusedCells.map(({ header, line, raw, why }) => ({ header, line, raw, why })));
22708
22800
  setSentLines(sendLines);
22709
22801
  setRepeatsSkipped(effectiveRows.length - sendRows.length);
22710
22802
  let sendMapping = mapping;
@@ -22838,7 +22930,7 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22838
22930
  setPhase("done");
22839
22931
  onWrote?.(finished.data.rows_written);
22840
22932
  void loadRuns();
22841
- }, [newColumns, makesColumns, kindChoice, guessedKind, addChoiceWords, client, dedupeKey, effectiveRows, fileBytes, fileHash, fileName, format, leftOut, loadRuns, mappedRelations, mapping, newChoiceWords, nudge, onDuplicate, onWrote, plan, readRelationNames, relationAsks, relationIndex, rights.admin, rowsToSend, tableId, unmapped, unlinkedChoice]);
22933
+ }, [newColumns, makesColumns, kindChoice, guessedKind, addChoiceWords, client, dedupeKey, effectiveRows, fileBytes, fileHash, fileName, format, leftOut, loadRuns, mappedRelations, mapping, newChoiceWords, nudge, onDuplicate, onWrote, plan, readRelationNames, refusedCells, relationAsks, relationIndex, rights.admin, rowsToSend, tableId, unmapped, unlinkedChoice]);
22842
22934
  const counts = (0, import_react122.useMemo)(() => {
22843
22935
  let landed = 0;
22844
22936
  let duplicate = 0;
@@ -22867,8 +22959,8 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
22867
22959
  );
22868
22960
  const ruleVerdictOf = (0, import_react122.useMemo)(() => new Map(ruleVerdicts.map((v) => [v.header, v])), [ruleVerdicts]);
22869
22961
  const refusedByRules = ruleVerdicts.reduce((n, v) => n + v.refused, 0);
22870
- const [goAheadAnyway, setGoAheadAnyway] = (0, import_react122.useState)(false);
22871
- (0, import_react122.useEffect)(() => setGoAheadAnyway(false), [ruleVerdicts]);
22962
+ const verdictKey = ruleVerdicts.map((v) => `${v.header}:${v.refused}/${v.judged}`).join("|");
22963
+ (0, import_react122.useEffect)(() => setGoAheadAnyway(false), [verdictKey]);
22872
22964
  const [autoRan, setAutoRan] = (0, import_react122.useState)(false);
22873
22965
  (0, import_react122.useEffect)(() => {
22874
22966
  if (!autoRun || autoRan || !source || phase !== "ready" || !plan) return;
@@ -23458,6 +23550,23 @@ function ImportWizard({ tableId, source, onDone, onWrote, autoRun, className })
23458
23550
  ] })
23459
23551
  }
23460
23552
  ) }) : null,
23553
+ afterWrite && keptAside.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime77.jsxs)("div", { "data-import-kept-aside": "", className: "flex flex-col gap-0.5", children: [
23554
+ /* @__PURE__ */ (0, import_jsx_runtime77.jsxs)("p", { children: [
23555
+ keptAside.length,
23556
+ " cell",
23557
+ keptAside.length === 1 ? " was" : "s were",
23558
+ " kept aside and left empty; every other value in those rows was written."
23559
+ ] }),
23560
+ /* @__PURE__ */ (0, import_jsx_runtime77.jsx)("ul", { className: "flex flex-col gap-0.5 text-muted-foreground", children: keptAside.map((c) => /* @__PURE__ */ (0, import_jsx_runtime77.jsxs)("li", { children: [
23561
+ c.header,
23562
+ ", ",
23563
+ source ? `row ${c.line}` : `line ${lineInYourFile(c.line)}`,
23564
+ ": ",
23565
+ c.raw,
23566
+ " \u2014 ",
23567
+ c.why
23568
+ ] }, `${c.header}:${c.line}`)) })
23569
+ ] }) : null,
23461
23570
  repeatsSkipped > 0 ? /* @__PURE__ */ (0, import_jsx_runtime77.jsxs)("p", { className: "text-muted-foreground", "data-import-repeats-skipped": "", children: [
23462
23571
  repeatsSkipped,
23463
23572
  " repeated row",
@@ -39885,6 +39994,9 @@ function settingsToCopy(source, target) {
39885
39994
  const same3 = source.layout === target.layout;
39886
39995
  return {
39887
39996
  where: source.where ?? null,
39997
+ // The flat filter map a view may carry instead of (or beside) `where`: every stored filter shape travels.
39998
+ filters: source.filters ?? null,
39999
+ // saveViewPatch sends null to the door, which clears it
39888
40000
  sorts: source.sorts ?? [],
39889
40001
  groupField: source.groupField ?? null,
39890
40002
  presentation: look,
@@ -39919,7 +40031,7 @@ function SortableRow({ id, disabled, children }) {
39919
40031
  }
39920
40032
  );
39921
40033
  }
39922
- function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews, initialViews, className }) {
40034
+ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews, onSettingsCopied, initialViews, className }) {
39923
40035
  const client = (0, import_react233.useRecordsClient)();
39924
40036
  const claimSeed = useSeedGuard();
39925
40037
  const table = (0, import_react234.useTable)(tableId);
@@ -40092,6 +40204,7 @@ function ViewBar({ tableId, activeViewId, onActiveView, seed, settings, onViews,
40092
40204
  return;
40093
40205
  }
40094
40206
  setCopying(false);
40207
+ onSettingsCopied?.(into.id);
40095
40208
  await load();
40096
40209
  notify?.success(`Copied settings from \u201C${source.name}\u201D`);
40097
40210
  }
@@ -53095,10 +53208,39 @@ function ViewFilter({ tableId, view, onApply, className }) {
53095
53208
  (0, import_react310.useEffect)(() => {
53096
53209
  setDraft(saved);
53097
53210
  }, [view?.id, savedKey]);
53098
- const conditionFields = (0, import_react310.useMemo)(
53099
- () => (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),
53100
53214
  [fields.data]
53101
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]);
53102
53244
  if (!view) return null;
53103
53245
  const count = conditionCount(saved);
53104
53246
  const changed2 = JSON.stringify(draft) !== savedKey;
@@ -54534,6 +54676,14 @@ function TablePage({
54534
54676
  }
54535
54677
  },
54536
54678
  onViews: setViews,
54679
+ onSettingsCopied: (copiedId) => {
54680
+ setAddressResetEpoch((n) => n + 1);
54681
+ setLooks((all) => {
54682
+ const { [copiedId]: _gone, ...rest } = all ?? {};
54683
+ return rest;
54684
+ });
54685
+ void keepLook(client, tableId, copiedId, null);
54686
+ },
54537
54687
  ...seededViews && viewsEpoch === 0 ? { initialViews: seededViews } : {},
54538
54688
  seed: seedViews ?? [defaultView(tableId)],
54539
54689
  settings: view && main === "records" && shownLayout === "grid" ? /* @__PURE__ */ (0, import_jsx_runtime178.jsxs)("div", { className: "flex flex-col gap-0.5 px-1 py-1", role: "radiogroup", "aria-label": FOOTER_WORDS.label, children: [