@crisp-ui-kit/crisp 0.54.7 → 0.54.9
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/authshell.css +5 -1
- package/dist/authshell.layered.css +5 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +36 -8
- package/dist/board.layered.css +36 -8
- package/dist/button.css +3 -1
- package/dist/button.layered.css +3 -1
- package/dist/cjs/components/badge/badge.recipe.js +28 -5
- package/dist/cjs/components/button/button.js +33 -2
- package/dist/cjs/components/button/button.recipe.js +31 -5
- package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/cjs/components/datatable/datatable.js +590 -41
- package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
- package/dist/cjs/components/dialog/dialog.js +72 -5
- package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
- package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
- package/dist/cjs/components/tooltip/tooltip.js +23 -9
- package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/cjs/product/authshell/authshell.js +14 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
- package/dist/cjs/product/board/board.js +175 -46
- package/dist/cjs/product/board/board.recipe.js +56 -4
- package/dist/cjs/product/comments/comments.js +34 -12
- package/dist/cjs/product/comments/comments.recipe.js +31 -7
- package/dist/cjs/product/formdialog/formdialog.js +10 -4
- package/dist/cjs/product/navitem/navitem.js +7 -2
- package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
- package/dist/cjs/product/objectsettings/index.js +1 -2
- package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
- package/dist/cjs/product/recordstable/recordstable.js +1 -17
- package/dist/cjs/product/richtext/richtext.js +76 -1
- package/dist/cjs/product/savedviews/savedviews.js +55 -24
- package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/cjs/product/sidebar/sidebar.js +68 -15
- package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/cjs/tokens/palette.js +32 -1
- package/dist/cjs/tokens/semantic.js +11 -0
- package/dist/comments.css +50 -14
- package/dist/comments.layered.css +50 -14
- package/dist/contextmenu.css +24 -0
- package/dist/contextmenu.layered.css +24 -0
- package/dist/datatable.css +101 -37
- package/dist/datatable.layered.css +101 -37
- package/dist/dialog.css +13 -1
- package/dist/dialog.layered.css +13 -1
- package/dist/esm/components/badge/badge.recipe.js +28 -5
- package/dist/esm/components/button/button.js +34 -3
- package/dist/esm/components/button/button.recipe.js +31 -5
- package/dist/esm/components/contextmenu/contextmenu.js +2 -2
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/esm/components/datatable/datatable.js +590 -42
- package/dist/esm/components/datatable/datatable.recipe.js +119 -6
- package/dist/esm/components/dialog/dialog.js +72 -5
- package/dist/esm/components/dialog/dialog.recipe.js +15 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
- package/dist/esm/components/tabs/tabs.recipe.js +11 -2
- package/dist/esm/components/tooltip/tooltip.js +23 -9
- package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/esm/product/authshell/authshell.js +15 -2
- package/dist/esm/product/authshell/authshell.recipe.js +13 -1
- package/dist/esm/product/authshell/index.js +1 -1
- package/dist/esm/product/board/board.js +176 -47
- package/dist/esm/product/board/board.recipe.js +56 -4
- package/dist/esm/product/comments/comments.js +35 -13
- package/dist/esm/product/comments/comments.recipe.js +31 -7
- package/dist/esm/product/formdialog/formdialog.js +10 -4
- package/dist/esm/product/navitem/index.js +1 -1
- package/dist/esm/product/navitem/navitem.js +7 -2
- package/dist/esm/product/navitem/navitem.recipe.js +16 -0
- package/dist/esm/product/objectsettings/index.js +1 -1
- package/dist/esm/product/objectsettings/objectsettings.js +2 -12
- package/dist/esm/product/recordstable/recordstable.js +2 -18
- package/dist/esm/product/richtext/richtext.js +76 -1
- package/dist/esm/product/savedviews/savedviews.js +57 -26
- package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/esm/product/sidebar/sidebar.js +69 -16
- package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/esm/tokens/palette.js +32 -1
- package/dist/esm/tokens/semantic.js +11 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -2
- package/dist/navitem.css +7 -0
- package/dist/navitem.layered.css +7 -0
- package/dist/recordstable.css +34 -32
- package/dist/recordstable.layered.css +34 -32
- package/dist/savedviews.css +27 -0
- package/dist/savedviews.layered.css +27 -0
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +369 -106
- package/dist/styles.layered.css +369 -106
- package/dist/tabletoolbar.css +47 -3
- package/dist/tabletoolbar.layered.css +47 -3
- package/dist/tabs.css +1 -1
- package/dist/tabs.layered.css +1 -1
- package/dist/tokens.css +9 -1
- package/dist/tokens.json +16 -2
- package/dist/tooltip.css +1 -1
- package/dist/tooltip.layered.css +1 -1
- package/dist/types/components/badge/badge.recipe.d.ts +5 -1
- package/dist/types/components/button/button.recipe.d.ts +18 -5
- package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
- package/dist/types/components/datatable/datatable.d.ts +98 -1
- package/dist/types/components/dialog/dialog.d.ts +25 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
- package/dist/types/components/tooltip/tooltip.d.ts +12 -4
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
- package/dist/types/product/authshell/authshell.d.ts +26 -2
- package/dist/types/product/authshell/index.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +53 -6
- package/dist/types/product/comments/comments.d.ts +16 -3
- package/dist/types/product/formdialog/formdialog.d.ts +2 -1
- package/dist/types/product/navitem/index.d.ts +1 -1
- package/dist/types/product/navitem/navitem.d.ts +23 -0
- package/dist/types/product/objectsettings/index.d.ts +1 -1
- package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
- package/dist/types/product/recordstable/recordstable.d.ts +0 -1
- package/dist/types/product/savedviews/savedviews.d.ts +45 -8
- package/dist/types/product/sidebar/sidebar.d.ts +33 -5
- package/dist/types/tokens/contract.d.ts +4 -0
- package/dist/types/tokens/palette.d.ts +13 -1
- package/dist/types/tokens/semantic.d.ts +11 -0
- package/package.json +1 -1
|
@@ -41,9 +41,18 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
41
41
|
const lucide_react_1 = require("lucide-react");
|
|
42
42
|
const React = __importStar(require("react"));
|
|
43
43
|
const core_1 = require("../../core");
|
|
44
|
+
const datepicker_1 = require("../datepicker");
|
|
44
45
|
const menu_1 = require("../menu");
|
|
45
46
|
const select_1 = require("../select");
|
|
46
47
|
const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
|
|
48
|
+
// #592: shared by every direction-bearing ChevronRight usage below (the
|
|
49
|
+
// field picker's own disclosure chevron, a flattened search result's
|
|
50
|
+
// breadcrumb separator, and a filter chip's own breadcrumb separator) so
|
|
51
|
+
// the RTL mirror attribute lives in exactly one JSX tag — build/spec-check
|
|
52
|
+
// .mjs's icon sweep counts that attribute per icon NAME across the whole
|
|
53
|
+
// file, not per call site, and three independently attributed tags for
|
|
54
|
+
// the same name over-counts against that (it expects one per name).
|
|
55
|
+
const RtlChevronRight = ({ size, className, }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
|
|
47
56
|
const OP_LABELS = {
|
|
48
57
|
contains: "contains",
|
|
49
58
|
doesnotcontain: "does not contain",
|
|
@@ -60,11 +69,24 @@ const OP_LABELS = {
|
|
|
60
69
|
// review.
|
|
61
70
|
empty: "empty",
|
|
62
71
|
notempty: "not empty",
|
|
72
|
+
lessthan: "less than",
|
|
73
|
+
greaterthan: "greater than",
|
|
74
|
+
before: "before",
|
|
75
|
+
after: "after",
|
|
63
76
|
};
|
|
64
77
|
// #619: order and label set match Attio's own text-filter operator picker
|
|
65
78
|
// verbatim (measured live, per RecordsTable's own #578 port of the same
|
|
66
79
|
// set: contains · does not contain · starts with · ends with · is · is not ·
|
|
67
|
-
// empty · not empty).
|
|
80
|
+
// empty · not empty). #593: number and date columns get their OWN shorter
|
|
81
|
+
// sets instead — also measured live (`app.attio.com`, a Twitter follower
|
|
82
|
+
// count / Foundation date column, each filter added then discarded): number
|
|
83
|
+
// is "is / is not / less than / greater than / empty / not empty" (104.86px
|
|
84
|
+
// wide, 6 rows); date is "is / is not / before / after / empty / not empty"
|
|
85
|
+
// (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
|
|
86
|
+
// is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
|
|
87
|
+
// tabletoolbar.recipe.ts): every one of these three sets is real, live-
|
|
88
|
+
// measured content-driven width, and a floor sized for one of them
|
|
89
|
+
// overshoots the other two.
|
|
68
90
|
const OP_ORDER = [
|
|
69
91
|
"contains",
|
|
70
92
|
"doesnotcontain",
|
|
@@ -75,15 +97,79 @@ const OP_ORDER = [
|
|
|
75
97
|
"empty",
|
|
76
98
|
"notempty",
|
|
77
99
|
];
|
|
100
|
+
const NUMBER_OP_ORDER = [
|
|
101
|
+
"is",
|
|
102
|
+
"isnot",
|
|
103
|
+
"lessthan",
|
|
104
|
+
"greaterthan",
|
|
105
|
+
"empty",
|
|
106
|
+
"notempty",
|
|
107
|
+
];
|
|
108
|
+
const DATE_OP_ORDER = [
|
|
109
|
+
"is",
|
|
110
|
+
"isnot",
|
|
111
|
+
"before",
|
|
112
|
+
"after",
|
|
113
|
+
"empty",
|
|
114
|
+
"notempty",
|
|
115
|
+
];
|
|
116
|
+
// #715: a closed-vocabulary column (GRC Audit's Action/Entity, e.g.) gets
|
|
117
|
+
// its own shorter set too — the text set's `contains`/`starts with`/… mean
|
|
118
|
+
// nothing against a fixed value list. Reuses the already-measured `is`/
|
|
119
|
+
// `is not`/`empty`/`not empty` labels and the same "is/isnot before the
|
|
120
|
+
// type-specific pair, empty/notempty last" ordering every other set here
|
|
121
|
+
// follows — not an independent pixel measurement the way #593's number/date
|
|
122
|
+
// sets were (a live re-check of Attio's own select-column filter menu is
|
|
123
|
+
// still open, see the PR).
|
|
124
|
+
const SELECT_OP_ORDER = ["is", "isnot", "empty", "notempty"];
|
|
78
125
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
79
|
-
// nothing to check off otherwise.
|
|
80
|
-
|
|
126
|
+
// nothing to check off otherwise. A column's `type` picks the base set
|
|
127
|
+
// first; "isanyof" can still follow any of the four (a column's own
|
|
128
|
+
// declared values aren't tied to its underlying number/date/text/select
|
|
129
|
+
// kind).
|
|
130
|
+
const opsFor = (col) => {
|
|
131
|
+
const base = col?.type === "number"
|
|
132
|
+
? NUMBER_OP_ORDER
|
|
133
|
+
: col?.type === "date"
|
|
134
|
+
? DATE_OP_ORDER
|
|
135
|
+
: col?.type === "select"
|
|
136
|
+
? SELECT_OP_ORDER
|
|
137
|
+
: OP_ORDER;
|
|
138
|
+
return col?.values?.length ? [...base, "isanyof"] : base;
|
|
139
|
+
};
|
|
140
|
+
// #592: a condition's `key` is either a plain top-level column key, or a
|
|
141
|
+
// dot-joined path into `nestedColumns` ("team.twitterFollowerCount"). Walks
|
|
142
|
+
// the path one segment at a time, descending into each match's own
|
|
143
|
+
// `nestedColumns` for the next — returns the resolved column at each step,
|
|
144
|
+
// in order, so a caller has both the LEAF (its own `type`/`values`/`icon`
|
|
145
|
+
// govern the operator set and value editor, unchanged from a plain column)
|
|
146
|
+
// and the FULL PATH (for the breadcrumb the field segment/picker render).
|
|
147
|
+
// Empty when any segment fails to resolve, rather than a partial path — a
|
|
148
|
+
// stale key (its column was removed upstream) should render as "unknown",
|
|
149
|
+
// not as whatever prefix still happens to match.
|
|
150
|
+
const resolveColumnPath = (cols, key) => {
|
|
151
|
+
const segments = key.split(".");
|
|
152
|
+
const path = [];
|
|
153
|
+
let list = cols;
|
|
154
|
+
for (const seg of segments) {
|
|
155
|
+
const found = list.find((c) => c.key === seg);
|
|
156
|
+
if (!found)
|
|
157
|
+
return [];
|
|
158
|
+
path.push(found);
|
|
159
|
+
list = found.nestedColumns ?? [];
|
|
160
|
+
}
|
|
161
|
+
return path;
|
|
162
|
+
};
|
|
81
163
|
const opNeedsValue = (op) => op === "contains" ||
|
|
82
164
|
op === "doesnotcontain" ||
|
|
83
165
|
op === "startswith" ||
|
|
84
166
|
op === "endswith" ||
|
|
85
167
|
op === "is" ||
|
|
86
|
-
op === "isnot"
|
|
168
|
+
op === "isnot" ||
|
|
169
|
+
op === "lessthan" ||
|
|
170
|
+
op === "greaterthan" ||
|
|
171
|
+
op === "before" ||
|
|
172
|
+
op === "after";
|
|
87
173
|
const opNeedsValues = (op) => op === "isanyof";
|
|
88
174
|
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
89
175
|
// chip is one line, and a field can have far more than two facet values.
|
|
@@ -98,9 +184,10 @@ const asText = (v) => {
|
|
|
98
184
|
return v.join(", ");
|
|
99
185
|
// Before the generic object branch, which would look for `.name`, find none
|
|
100
186
|
// and return "" — turning every Date into the empty string (#185). The ISO
|
|
101
|
-
// date is stable and locale-independent, so `contains "2026-06"` works
|
|
102
|
-
//
|
|
103
|
-
//
|
|
187
|
+
// date is stable and locale-independent, so `contains "2026-06"` works and
|
|
188
|
+
// (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
|
|
189
|
+
// condition value — `before`/`after` get a real chronological compare
|
|
190
|
+
// instead, below.
|
|
104
191
|
if (v instanceof Date)
|
|
105
192
|
return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
|
|
106
193
|
if (typeof v === "object") {
|
|
@@ -109,13 +196,67 @@ const asText = (v) => {
|
|
|
109
196
|
}
|
|
110
197
|
return String(v);
|
|
111
198
|
};
|
|
199
|
+
// #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
|
|
200
|
+
// columns) need a real numeric/chronological compare, not `asText`'s string
|
|
201
|
+
// one — `null` on anything that doesn't parse, so a condition against a
|
|
202
|
+
// malformed or missing value never matches rather than throwing.
|
|
203
|
+
const toNumberOrNull = (v) => {
|
|
204
|
+
if (typeof v === "number")
|
|
205
|
+
return Number.isNaN(v) ? null : v;
|
|
206
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
207
|
+
const n = Number(v);
|
|
208
|
+
return Number.isNaN(n) ? null : n;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
};
|
|
212
|
+
const toDateOrNull = (v) => {
|
|
213
|
+
if (v instanceof Date)
|
|
214
|
+
return Number.isNaN(v.getTime()) ? null : v;
|
|
215
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
216
|
+
const d = new Date(v);
|
|
217
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
// A `type: "date"` condition's own value is stored as a plain ISO date
|
|
222
|
+
// string ("2026-09-25") — machine-comparable (`before`/`after` above,
|
|
223
|
+
// `is`/`is not` via `asText`) and locale-independent, the same choice #185
|
|
224
|
+
// already made for `asText`'s own `Date` branch. Parsed/formatted at local
|
|
225
|
+
// midnight, not UTC, so the picked calendar day and the displayed day are
|
|
226
|
+
// always the same one regardless of timezone offset.
|
|
227
|
+
const isoToDate = (iso) => {
|
|
228
|
+
if (!iso)
|
|
229
|
+
return null;
|
|
230
|
+
const d = new Date(`${iso}T00:00:00`);
|
|
231
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
232
|
+
};
|
|
233
|
+
const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
234
|
+
// e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
|
|
235
|
+
// uses, and what Attio's chip itself showed after picking "Today" live.
|
|
236
|
+
const formatFilterDate = (iso) => {
|
|
237
|
+
const d = isoToDate(iso);
|
|
238
|
+
return d
|
|
239
|
+
? d.toLocaleDateString(undefined, {
|
|
240
|
+
month: "short",
|
|
241
|
+
day: "numeric",
|
|
242
|
+
year: "numeric",
|
|
243
|
+
})
|
|
244
|
+
: iso;
|
|
245
|
+
};
|
|
246
|
+
const toDatePickerDate = (d) => ({
|
|
247
|
+
year: d.getFullYear(),
|
|
248
|
+
month: d.getMonth() + 1,
|
|
249
|
+
day: d.getDate(),
|
|
250
|
+
});
|
|
251
|
+
const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
|
|
112
252
|
/**
|
|
113
253
|
* Applies a toolbar's filter conditions to any row array. Reads the raw value
|
|
114
254
|
* at `row[key]` (override with `get` for derived fields) — never a formatted
|
|
115
255
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
116
256
|
*/
|
|
117
257
|
function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
118
|
-
const active = filters.filter((f) =>
|
|
258
|
+
const active = filters.filter((f) => f.op != null &&
|
|
259
|
+
(!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
119
260
|
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
120
261
|
if (!active.length)
|
|
121
262
|
return rows;
|
|
@@ -125,6 +266,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
125
266
|
return isEmptyValue(raw);
|
|
126
267
|
if (f.op === "notempty")
|
|
127
268
|
return !isEmptyValue(raw);
|
|
269
|
+
if (f.op === "lessthan" || f.op === "greaterthan") {
|
|
270
|
+
const n = toNumberOrNull(raw);
|
|
271
|
+
const cn = toNumberOrNull(f.value);
|
|
272
|
+
if (n == null || cn == null)
|
|
273
|
+
return false;
|
|
274
|
+
return f.op === "lessthan" ? n < cn : n > cn;
|
|
275
|
+
}
|
|
276
|
+
if (f.op === "before" || f.op === "after") {
|
|
277
|
+
const d = toDateOrNull(raw);
|
|
278
|
+
const cd = toDateOrNull(f.value);
|
|
279
|
+
if (d == null || cd == null)
|
|
280
|
+
return false;
|
|
281
|
+
return f.op === "before" ? d < cd : d > cd;
|
|
282
|
+
}
|
|
128
283
|
const t = asText(raw).toLowerCase();
|
|
129
284
|
if (f.op === "isanyof")
|
|
130
285
|
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
@@ -183,7 +338,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
183
338
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
184
339
|
* Created by), which one shared list can't reproduce.
|
|
185
340
|
*/
|
|
186
|
-
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
|
|
341
|
+
function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
|
|
187
342
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
188
343
|
// CrispMessagesProvider's translation of the English default below, or
|
|
189
344
|
// that default itself with no provider — unchanged from before this
|
|
@@ -210,6 +365,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
210
365
|
const reorderAttributeLabel = (0, core_1.useCrispMessage)(reorderAttributeLabelProp, "Reorder attribute");
|
|
211
366
|
const addAttributeToViewLabel = (0, core_1.useCrispMessage)(addAttributeToViewLabelProp, "Add attribute to view");
|
|
212
367
|
const searchAttributesPlaceholder = (0, core_1.useCrispMessage)(searchAttributesPlaceholderProp, "Search attributes…");
|
|
368
|
+
const importExportLabel = (0, core_1.useCrispMessage)(importExportLabelProp, "Import / Export");
|
|
369
|
+
const importCsvLabel = (0, core_1.useCrispMessage)(importCsvLabelProp, "Import CSV");
|
|
370
|
+
const exportCsvLabel = (0, core_1.useCrispMessage)(exportCsvLabelProp, "Export view as CSV");
|
|
371
|
+
const exportExcelLabel = (0, core_1.useCrispMessage)(exportExcelLabelProp, "Export view as Excel");
|
|
372
|
+
const learnImportExportLabel = (0, core_1.useCrispMessage)(learnImportExportLabelProp, "Learn about imports and exports");
|
|
373
|
+
const backLabel = (0, core_1.useCrispMessage)(backLabelProp, "Back");
|
|
213
374
|
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
214
375
|
// their own — a whole lookup table, not one string — so they translate
|
|
215
376
|
// straight from the provider's dictionary at the two spots they render
|
|
@@ -250,6 +411,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
250
411
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
251
412
|
const [dragSort, setDragSort] = React.useState(null);
|
|
252
413
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
414
|
+
// #592: which nested scope the Filter field picker is currently drilled
|
|
415
|
+
// into -- [] is the top level, [team] is inside Team's own attributes,
|
|
416
|
+
// [team, company] is Team -> Company, etc. Reset to [] wherever
|
|
417
|
+
// `filterQuery` already resets (opening the picker fresh), same lifetime.
|
|
418
|
+
const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
|
|
253
419
|
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
254
420
|
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
255
421
|
const [dragAttr, setDragAttr] = React.useState(null);
|
|
@@ -317,11 +483,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
317
483
|
};
|
|
318
484
|
const addFilter = (key) => {
|
|
319
485
|
const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
|
|
320
|
-
putFilters([...filters, { id, key,
|
|
486
|
+
putFilters([...filters, { id, key, value: "" }]);
|
|
321
487
|
setFilterQuery("");
|
|
322
|
-
|
|
488
|
+
// matches Attio: picking a field opens the operator menu next, not a
|
|
489
|
+
// combined dialog — measured live, 2026-09-25.
|
|
490
|
+
setOpenMenu(`fop:${id}`);
|
|
323
491
|
};
|
|
324
492
|
const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
|
|
493
|
+
// The field segment's own picker (#462 follow-up): changing the field
|
|
494
|
+
// replaces it in place — confirmed live, one chip, not a second — and
|
|
495
|
+
// resets op/value/values, since neither is guaranteed to make sense for
|
|
496
|
+
// the new field's type (a text op on a select field, e.g.).
|
|
497
|
+
const changeFilterField = (id, key) => {
|
|
498
|
+
patchFilter(id, { key, op: undefined, value: "", values: undefined });
|
|
499
|
+
setFilterQuery("");
|
|
500
|
+
setOpenMenu(`fop:${id}`);
|
|
501
|
+
};
|
|
502
|
+
// The operator segment's own menu: picking an operator that needs a value
|
|
503
|
+
// auto-opens the value popover, focused; "empty"/"not empty" need nothing
|
|
504
|
+
// further, so the menu just closes — both measured live, 2026-09-25.
|
|
505
|
+
const pickOp = (id, op) => {
|
|
506
|
+
patchFilter(id, { op });
|
|
507
|
+
setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
|
|
508
|
+
};
|
|
325
509
|
const removeFilter = (id) => {
|
|
326
510
|
putFilters(filters.filter((c) => c.id !== id));
|
|
327
511
|
setOpenMenu(null);
|
|
@@ -360,6 +544,66 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
360
544
|
close: () => setOpenMenu(null),
|
|
361
545
|
contentRef: kebabMenuRef,
|
|
362
546
|
});
|
|
547
|
+
// The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
|
|
548
|
+
// so it gets the same shared keyboard model as the kebab above, one hook
|
|
549
|
+
// call tracking whichever condition's operator menu is open.
|
|
550
|
+
const opMenuRef = React.useRef(null);
|
|
551
|
+
const { onKeyDown: onOpKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
552
|
+
open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
|
|
553
|
+
close: () => setOpenMenu(null),
|
|
554
|
+
contentRef: opMenuRef,
|
|
555
|
+
});
|
|
556
|
+
// The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
|
|
557
|
+
// (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
|
|
558
|
+
// dividers between the 3 groups) — same shared keyboard model.
|
|
559
|
+
const importExportMenuRef = React.useRef(null);
|
|
560
|
+
const { onKeyDown: onImportExportKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
561
|
+
open: openMenu === "importexport",
|
|
562
|
+
close: () => setOpenMenu(null),
|
|
563
|
+
contentRef: importExportMenuRef,
|
|
564
|
+
});
|
|
565
|
+
// Shared between the "Filter" trigger's own add-condition picker and each
|
|
566
|
+
// chip's field segment (#462 follow-up) — both search the same
|
|
567
|
+
// `filterColumns` list and differ only in what a pick does. #592: also
|
|
568
|
+
// handles drilling into a `nestedColumns` column — `fieldPickerPath`
|
|
569
|
+
// tracks the current scope ([] is the top level), reset to [] wherever
|
|
570
|
+
// the picker opens fresh (both trigger onClicks, above).
|
|
571
|
+
const filterFieldPicker = (onSelect) => {
|
|
572
|
+
const scope = fieldPickerPath.at(-1);
|
|
573
|
+
const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
|
|
574
|
+
const query = filterQuery.trim().toLowerCase();
|
|
575
|
+
const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
|
|
576
|
+
// #592: one level of flattening from wherever the picker is currently
|
|
577
|
+
// scoped — a nested column's own children get searched too, and a
|
|
578
|
+
// match renders breadcrumbed, alongside this level's direct matches.
|
|
579
|
+
// Matches the one case measured live at the TOP level ("Twitter
|
|
580
|
+
// follower" surfaces both the direct field and "Team › Twitter
|
|
581
|
+
// follower count"); applying the identical rule recursively from an
|
|
582
|
+
// already-drilled-into scope (so it also reaches Team → Company → X
|
|
583
|
+
// while already browsing inside Team) is the same construct, one level
|
|
584
|
+
// relative to wherever you are, but wasn't independently re-verified
|
|
585
|
+
// live from inside a nested scope — flagged on the PR.
|
|
586
|
+
const nested = query
|
|
587
|
+
? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
|
|
588
|
+
.filter((n) => n.label.toLowerCase().includes(query))
|
|
589
|
+
.map((n) => ({ parent: c, col: n })))
|
|
590
|
+
: [];
|
|
591
|
+
const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
|
|
592
|
+
const drillIn = (col) => {
|
|
593
|
+
setFieldPickerPath([...fieldPickerPath, col]);
|
|
594
|
+
setFilterQuery("");
|
|
595
|
+
};
|
|
596
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [scope ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-nesthead", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-menu-back", "aria-label": backLabel, onClick: () => {
|
|
597
|
+
setFieldPickerPath(fieldPickerPath.slice(0, -1));
|
|
598
|
+
setFilterQuery("");
|
|
599
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
|
|
600
|
+
? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
|
|
601
|
+
: `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
|
|
602
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
603
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
|
|
604
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
605
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? ((0, jsx_runtime_1.jsx)(scope.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label, (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key))), nested.map(({ parent, col }) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-menu-item-path", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), (0, jsx_runtime_1.jsx)("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
|
|
606
|
+
};
|
|
363
607
|
const activeFilters = filters.length;
|
|
364
608
|
// The default phrase only works in English word order, so a caller can
|
|
365
609
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -374,7 +618,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
374
618
|
: sorts.length
|
|
375
619
|
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
376
620
|
: sortLabel;
|
|
377
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
621
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
378
622
|
if (dragSort !== null)
|
|
379
623
|
e.preventDefault();
|
|
380
624
|
}, onDrop: () => {
|
|
@@ -396,47 +640,106 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
396
640
|
label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
397
641
|
keywords: descendingLabel,
|
|
398
642
|
},
|
|
399
|
-
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () =>
|
|
643
|
+
], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => {
|
|
644
|
+
setFieldPickerPath([]);
|
|
645
|
+
setOpenMenu(openMenu === "filter" ? null : "filter");
|
|
646
|
+
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
400
647
|
? filterTriggerLabel(activeFilters)
|
|
401
648
|
: activeFilters
|
|
402
649
|
? `${filterLabel} · ${activeFilters}`
|
|
403
|
-
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const col =
|
|
411
|
-
const
|
|
650
|
+
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
|
|
651
|
+
// #592: cond.key may be a dot-joined nested path
|
|
652
|
+
// ("team.twitterFollowerCount") — resolve the full path so the
|
|
653
|
+
// field segment can render its breadcrumb; `col` (the leaf) still
|
|
654
|
+
// drives the operator set and value editor exactly as before, for
|
|
655
|
+
// both a plain and a nested key.
|
|
656
|
+
const colPath = resolveColumnPath(filterColumns, cond.key);
|
|
657
|
+
const col = colPath.at(-1);
|
|
658
|
+
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
659
|
+
const opOpen = openMenu === `fop:${cond.id}`;
|
|
660
|
+
const valOpen = openMenu === `fval:${cond.id}`;
|
|
412
661
|
const kebabOpen = openMenu === `fkebab:${cond.id}`;
|
|
413
|
-
|
|
662
|
+
// Gates the field/value segments' background (white once the
|
|
663
|
+
// condition is usable, the measured `f6f7f7` otherwise) — NOT
|
|
664
|
+
// whether the value popover renders; see `showValueSeg` below.
|
|
665
|
+
const complete = cond.op != null &&
|
|
666
|
+
(!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
414
667
|
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
668
|
+
// No value segment at all for "empty"/"not empty" (nothing to
|
|
669
|
+
// show) or before an operator is even chosen — matches Attio,
|
|
670
|
+
// which never renders a third box in either case.
|
|
671
|
+
const showValueSeg = cond.op != null &&
|
|
672
|
+
(opNeedsValue(cond.op) || opNeedsValues(cond.op));
|
|
673
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => {
|
|
674
|
+
setFieldPickerPath([]);
|
|
675
|
+
setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
|
|
676
|
+
}, children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [i > 0 && ((0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
|
|
677
|
+
? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
|
|
678
|
+
: selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
|
|
679
|
+
? cond.values?.length
|
|
680
|
+
? isAnyOfSummary(cond.values)
|
|
681
|
+
: filterValuePlaceholder
|
|
682
|
+
: cond.value.trim()
|
|
683
|
+
? col?.type === "date"
|
|
684
|
+
? formatFilterDate(cond.value)
|
|
685
|
+
: cond.value
|
|
686
|
+
: filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
687
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
688
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
689
|
+
.filter((v) => v
|
|
690
|
+
.toLowerCase()
|
|
691
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
692
|
+
.map((v) => {
|
|
693
|
+
const checked = (cond.values ?? []).includes(v);
|
|
694
|
+
return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
|
|
695
|
+
const set = new Set(cond.values ?? []);
|
|
696
|
+
if (next)
|
|
697
|
+
set.add(v);
|
|
698
|
+
else
|
|
699
|
+
set.delete(v);
|
|
700
|
+
// the column's own order, not click order —
|
|
701
|
+
// what the chip summary reads out
|
|
702
|
+
patchFilter(cond.id, {
|
|
703
|
+
values: col.values?.filter((x) => set.has(x)),
|
|
704
|
+
});
|
|
705
|
+
}, children: v }, v));
|
|
706
|
+
})] })) : col?.type === "select" &&
|
|
707
|
+
col?.values &&
|
|
708
|
+
(cond.op === "is" || cond.op === "isnot") ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
709
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
710
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
711
|
+
.filter((v) => v
|
|
712
|
+
.toLowerCase()
|
|
713
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
714
|
+
.map((v) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
|
|
715
|
+
patchFilter(cond.id, { value: v });
|
|
716
|
+
setOpenMenu(null);
|
|
717
|
+
}, children: [v, cond.value === v ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, v)))] })) : col?.type === "date" ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-datepop", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker, { ...{
|
|
718
|
+
selectionMode: "single",
|
|
719
|
+
value: isoToDate(cond.value)
|
|
720
|
+
? toDatePickerDate(isoToDate(cond.value))
|
|
721
|
+
: undefined,
|
|
722
|
+
onValueChange: (next) => {
|
|
723
|
+
if (!next)
|
|
724
|
+
return;
|
|
725
|
+
patchFilter(cond.id, {
|
|
726
|
+
value: dateToISO(fromDatePickerDate(next)),
|
|
727
|
+
});
|
|
728
|
+
setOpenMenu(null);
|
|
729
|
+
},
|
|
730
|
+
} }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
|
|
731
|
+
[crispMessages.Today ?? "Today", 0],
|
|
732
|
+
[crispMessages.Tomorrow ?? "Tomorrow", 1],
|
|
733
|
+
[crispMessages["Next week"] ?? "Next week", 7],
|
|
734
|
+
].map(([label, addDays]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
|
|
735
|
+
const d = new Date();
|
|
736
|
+
d.setDate(d.getDate() + addDays);
|
|
737
|
+
patchFilter(cond.id, { value: dateToISO(d) });
|
|
738
|
+
setOpenMenu(null);
|
|
739
|
+
}, children: label }, label))) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
740
|
+
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
741
|
+
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
|
|
742
|
+
}), showViewSettings && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
|
|
440
743
|
if (openMenu === "viewsettings") {
|
|
441
744
|
setOpenMenu(null);
|
|
442
745
|
}
|
|
@@ -460,7 +763,20 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
460
763
|
moveAttr(dragAttr, i);
|
|
461
764
|
setDragAttr(null);
|
|
462
765
|
}, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
463
|
-
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })
|
|
766
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
|
|
767
|
+
onExportCsv ||
|
|
768
|
+
onExportExcel ||
|
|
769
|
+
importExportHelpHref) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
770
|
+
onImportCsv();
|
|
771
|
+
setOpenMenu(null);
|
|
772
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
773
|
+
onExportCsv();
|
|
774
|
+
setOpenMenu(null);
|
|
775
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
776
|
+
onExportExcel();
|
|
777
|
+
setOpenMenu(null);
|
|
778
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
|
|
779
|
+
(onImportCsv || onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
464
780
|
}
|
|
465
781
|
/**
|
|
466
782
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|