@crisp-ui-kit/crisp 0.54.8 → 0.54.10
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/board.css +32 -8
- package/dist/board.layered.css +32 -8
- 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 +593 -51
- package/dist/cjs/components/datatable/datatable.recipe.js +67 -3
- package/dist/cjs/components/dialog/dialog.js +65 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +37 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +218 -39
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +48 -1
- package/dist/cjs/components/text/text.recipe.js +17 -7
- package/dist/cjs/product/board/board.js +62 -28
- package/dist/cjs/product/board/board.recipe.js +42 -2
- package/dist/cjs/product/comments/comments.js +11 -2
- package/dist/cjs/product/comments/comments.recipe.js +14 -4
- package/dist/cjs/product/recordstable/recordstable.js +1 -17
- package/dist/cjs/product/savedviews/savedviews.js +55 -24
- package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/comments.css +14 -4
- package/dist/comments.layered.css +14 -4
- package/dist/contextmenu.css +24 -0
- package/dist/contextmenu.layered.css +24 -0
- package/dist/datatable.css +106 -77
- package/dist/datatable.layered.css +106 -77
- 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 +593 -52
- package/dist/esm/components/datatable/datatable.recipe.js +68 -4
- package/dist/esm/components/dialog/dialog.js +65 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +37 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +219 -40
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +50 -3
- package/dist/esm/components/text/text.recipe.js +17 -7
- package/dist/esm/product/board/board.js +63 -29
- package/dist/esm/product/board/board.recipe.js +42 -2
- package/dist/esm/product/comments/comments.js +12 -3
- package/dist/esm/product/comments/comments.recipe.js +14 -4
- package/dist/esm/product/recordstable/recordstable.js +2 -18
- package/dist/esm/product/savedviews/savedviews.js +57 -26
- package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/recordstable.css +33 -31
- package/dist/recordstable.layered.css +33 -31
- package/dist/savedviews.css +27 -0
- package/dist/savedviews.layered.css +27 -0
- package/dist/styles.css +282 -125
- package/dist/styles.layered.css +282 -125
- package/dist/tabletoolbar.css +45 -4
- package/dist/tabletoolbar.layered.css +45 -4
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
- package/dist/types/components/datatable/datatable.d.ts +98 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +72 -2
- package/dist/types/components/text/text.recipe.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +48 -2
- package/dist/types/product/comments/comments.d.ts +6 -1
- package/dist/types/product/recordstable/recordstable.d.ts +0 -1
- package/dist/types/product/savedviews/savedviews.d.ts +45 -8
- package/package.json +1 -1
|
@@ -45,6 +45,14 @@ const datepicker_1 = require("../datepicker");
|
|
|
45
45
|
const menu_1 = require("../menu");
|
|
46
46
|
const select_1 = require("../select");
|
|
47
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 }));
|
|
48
56
|
const OP_LABELS = {
|
|
49
57
|
contains: "contains",
|
|
50
58
|
doesnotcontain: "does not contain",
|
|
@@ -105,18 +113,53 @@ const DATE_OP_ORDER = [
|
|
|
105
113
|
"empty",
|
|
106
114
|
"notempty",
|
|
107
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"];
|
|
108
125
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
109
126
|
// nothing to check off otherwise. A column's `type` picks the base set
|
|
110
|
-
// first; "isanyof" can still follow any of the
|
|
111
|
-
// declared values aren't tied to its underlying number/date/text
|
|
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).
|
|
112
130
|
const opsFor = (col) => {
|
|
113
131
|
const base = col?.type === "number"
|
|
114
132
|
? NUMBER_OP_ORDER
|
|
115
133
|
: col?.type === "date"
|
|
116
134
|
? DATE_OP_ORDER
|
|
117
|
-
:
|
|
135
|
+
: col?.type === "select"
|
|
136
|
+
? SELECT_OP_ORDER
|
|
137
|
+
: OP_ORDER;
|
|
118
138
|
return col?.values?.length ? [...base, "isanyof"] : base;
|
|
119
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
|
+
};
|
|
120
163
|
const opNeedsValue = (op) => op === "contains" ||
|
|
121
164
|
op === "doesnotcontain" ||
|
|
122
165
|
op === "startswith" ||
|
|
@@ -295,7 +338,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
295
338
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
296
339
|
* Created by), which one shared list can't reproduce.
|
|
297
340
|
*/
|
|
298
|
-
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, ...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 }) {
|
|
299
342
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
300
343
|
// CrispMessagesProvider's translation of the English default below, or
|
|
301
344
|
// that default itself with no provider — unchanged from before this
|
|
@@ -322,6 +365,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
322
365
|
const reorderAttributeLabel = (0, core_1.useCrispMessage)(reorderAttributeLabelProp, "Reorder attribute");
|
|
323
366
|
const addAttributeToViewLabel = (0, core_1.useCrispMessage)(addAttributeToViewLabelProp, "Add attribute to view");
|
|
324
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");
|
|
325
374
|
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
326
375
|
// their own — a whole lookup table, not one string — so they translate
|
|
327
376
|
// straight from the provider's dictionary at the two spots they render
|
|
@@ -362,6 +411,17 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
362
411
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
363
412
|
const [dragSort, setDragSort] = React.useState(null);
|
|
364
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([]);
|
|
419
|
+
// #754: which sort row's own field picker is open (its index into
|
|
420
|
+
// `sorts`), or `null` for the normal sort-row list — the same
|
|
421
|
+
// "one sub-screen behind a flag" split View settings' own
|
|
422
|
+
// `addingAttribute` already uses, parameterized by index since more than
|
|
423
|
+
// one sort row exists.
|
|
424
|
+
const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
|
|
365
425
|
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
366
426
|
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
367
427
|
const [dragAttr, setDragAttr] = React.useState(null);
|
|
@@ -375,7 +435,6 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
375
435
|
// built here -- hiding an attribute is a trailing control instead, see
|
|
376
436
|
// the hide button below).
|
|
377
437
|
const [addingAttribute, setAddingAttribute] = React.useState(false);
|
|
378
|
-
const [addAttrQuery, setAddAttrQuery] = React.useState("");
|
|
379
438
|
// close any open menu on outside click (anything not inside a menu or its
|
|
380
439
|
// trigger) or Escape
|
|
381
440
|
React.useEffect(() => {
|
|
@@ -458,10 +517,27 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
458
517
|
};
|
|
459
518
|
// #590: shown attributes, in `shownKeys`' order -- a key with no matching
|
|
460
519
|
// `viewColumns` entry (stale state after a column is removed upstream)
|
|
461
|
-
// drops out rather than rendering a blank row.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
520
|
+
// drops out rather than rendering a blank row. #754: `shownKeys` can now
|
|
521
|
+
// carry a dot-joined nested path ("team.jobTitle") from the add-attribute
|
|
522
|
+
// screen's own nested picker, so each key resolves via the same
|
|
523
|
+
// `resolveColumnPath` the filter chip uses -- the LEAF column's icon
|
|
524
|
+
// drives the row's own icon, and the label is the full breadcrumb
|
|
525
|
+
// ("Team › Job title"), joined as plain text rather than the chip's own
|
|
526
|
+
// muted/chevron treatment, which was measured specifically for the
|
|
527
|
+
// filter chip and wasn't re-verified for this row shape.
|
|
528
|
+
const shownAttrs = shownKeys.flatMap((k) => {
|
|
529
|
+
const path = resolveColumnPath(viewColumns, k);
|
|
530
|
+
const leaf = path.at(-1);
|
|
531
|
+
if (!leaf)
|
|
532
|
+
return [];
|
|
533
|
+
return [
|
|
534
|
+
{
|
|
535
|
+
key: k,
|
|
536
|
+
icon: leaf.icon,
|
|
537
|
+
label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
|
|
538
|
+
},
|
|
539
|
+
];
|
|
540
|
+
});
|
|
465
541
|
const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
|
|
466
542
|
const moveAttr = (from, to) => {
|
|
467
543
|
if (from === to)
|
|
@@ -475,7 +551,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
475
551
|
const showAttr = (key) => {
|
|
476
552
|
putShownKeys([...shownKeys, key]);
|
|
477
553
|
setAddingAttribute(false);
|
|
478
|
-
|
|
554
|
+
setFieldPickerPath([]);
|
|
555
|
+
setFilterQuery("");
|
|
479
556
|
};
|
|
480
557
|
// #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
|
|
481
558
|
// model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
|
|
@@ -499,14 +576,62 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
499
576
|
close: () => setOpenMenu(null),
|
|
500
577
|
contentRef: opMenuRef,
|
|
501
578
|
});
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
579
|
+
// The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
|
|
580
|
+
// (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
|
|
581
|
+
// dividers between the 3 groups) — same shared keyboard model.
|
|
582
|
+
const importExportMenuRef = React.useRef(null);
|
|
583
|
+
const { onKeyDown: onImportExportKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
584
|
+
open: openMenu === "importexport",
|
|
585
|
+
close: () => setOpenMenu(null),
|
|
586
|
+
contentRef: importExportMenuRef,
|
|
587
|
+
});
|
|
588
|
+
// #754: generalized from the Filter-only `filterFieldPicker` (#592) to
|
|
589
|
+
// also drive Sort's own field picker and View settings' add-attribute
|
|
590
|
+
// screen — all three offer the identical nested-drill-down construct on
|
|
591
|
+
// live Attio (confirmed: "Team" with a count badge appears, and drills
|
|
592
|
+
// into the same People-attribute screen, in all three pickers). Shared
|
|
593
|
+
// `fieldPickerPath`/`filterQuery` state across all three call sites is
|
|
594
|
+
// safe: `openMenu` already only ever names ONE open popover at a time,
|
|
595
|
+
// so only one of Filter/Sort/View-settings' own picker can be showing.
|
|
596
|
+
// `fieldPickerPath` tracks the current scope ([] is the top level),
|
|
597
|
+
// reset to [] wherever a picker opens fresh (every trigger's own
|
|
598
|
+
// onClick).
|
|
599
|
+
const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
|
|
600
|
+
const scope = fieldPickerPath.at(-1);
|
|
601
|
+
const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
|
|
602
|
+
const query = filterQuery.trim().toLowerCase();
|
|
603
|
+
const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
|
|
604
|
+
// #592: one level of flattening from wherever the picker is currently
|
|
605
|
+
// scoped — a nested column's own children get searched too, and a
|
|
606
|
+
// match renders breadcrumbed, alongside this level's direct matches.
|
|
607
|
+
// Matches the one case measured live at the TOP level ("Twitter
|
|
608
|
+
// follower" surfaces both the direct field and "Team › Twitter
|
|
609
|
+
// follower count"); applying the identical rule recursively from an
|
|
610
|
+
// already-drilled-into scope (so it also reaches Team → Company → X
|
|
611
|
+
// while already browsing inside Team) is the same construct, one level
|
|
612
|
+
// relative to wherever you are, but wasn't independently re-verified
|
|
613
|
+
// live from inside a nested scope — flagged on the PR.
|
|
614
|
+
const nested = query
|
|
615
|
+
? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
|
|
616
|
+
.filter((n) => n.label.toLowerCase().includes(query))
|
|
617
|
+
.map((n) => ({ parent: c, col: n })))
|
|
618
|
+
: [];
|
|
619
|
+
const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
|
|
620
|
+
const drillIn = (col) => {
|
|
621
|
+
setFieldPickerPath([...fieldPickerPath, col]);
|
|
622
|
+
setFilterQuery("");
|
|
623
|
+
};
|
|
624
|
+
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: () => {
|
|
625
|
+
setFieldPickerPath(fieldPickerPath.slice(0, -1));
|
|
626
|
+
setFilterQuery("");
|
|
627
|
+
}, 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
|
|
628
|
+
? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
|
|
629
|
+
: `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
|
|
630
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
631
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
|
|
632
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
633
|
+
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}`)))] }));
|
|
634
|
+
};
|
|
510
635
|
const activeFilters = filters.length;
|
|
511
636
|
// The default phrase only works in English word order, so a caller can
|
|
512
637
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -519,7 +644,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
519
644
|
const sortedLabel = sortTriggerLabel
|
|
520
645
|
? sortTriggerLabel(sorts, sortColumns)
|
|
521
646
|
: sorts.length
|
|
522
|
-
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
647
|
+
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
648
|
+
// #754: sorts[0].key may be a dot-joined nested path
|
|
649
|
+
// ("team.jobTitle") — resolve it the same way the chip and sort
|
|
650
|
+
// row do, rather than a flat find that fails on a nested key.
|
|
651
|
+
resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
523
652
|
: sortLabel;
|
|
524
653
|
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) => {
|
|
525
654
|
if (dragSort !== null)
|
|
@@ -528,11 +657,26 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
528
657
|
if (dragSort !== null)
|
|
529
658
|
moveSort(dragSort, i);
|
|
530
659
|
setDragSort(null);
|
|
531
|
-
}, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
660
|
+
}, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
|
|
661
|
+
// #754: the sort row's own field picker -- the
|
|
662
|
+
// same nested-drill-down construct Filter's field
|
|
663
|
+
// picker already has (#592), measured live:
|
|
664
|
+
// clicking a sort row's field (or "Add sort") opens
|
|
665
|
+
// an identical picker, "Team" (a relationship
|
|
666
|
+
// field) drills into the same People-attribute
|
|
667
|
+
// screen Filter's own "Team" does.
|
|
668
|
+
const sortColPath = resolveColumnPath(sortColumns, s.key);
|
|
669
|
+
const sortCol = sortColPath.at(-1);
|
|
670
|
+
const pickerOpen = sortFieldPickerIndex === i;
|
|
671
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
|
|
672
|
+
setFieldPickerPath([]);
|
|
673
|
+
setFilterQuery("");
|
|
674
|
+
setSortFieldPickerIndex(pickerOpen ? null : i);
|
|
675
|
+
}, children: [sortCol?.icon ? ((0, jsx_runtime_1.jsx)(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [pi > 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))) })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
|
|
676
|
+
setSortField(i, key);
|
|
677
|
+
setSortFieldPickerIndex(null);
|
|
678
|
+
}, searchAttributesPlaceholder, sortFieldLabel) }))] }));
|
|
679
|
+
})(), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
|
|
536
680
|
{
|
|
537
681
|
value: "asc",
|
|
538
682
|
label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
|
|
@@ -543,12 +687,21 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
543
687
|
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] })),
|
|
544
688
|
keywords: descendingLabel,
|
|
545
689
|
},
|
|
546
|
-
], "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: () =>
|
|
690
|
+
], "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: () => {
|
|
691
|
+
setFieldPickerPath([]);
|
|
692
|
+
setOpenMenu(openMenu === "filter" ? null : "filter");
|
|
693
|
+
}, "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
|
|
547
694
|
? filterTriggerLabel(activeFilters)
|
|
548
695
|
: activeFilters
|
|
549
696
|
? `${filterLabel} · ${activeFilters}`
|
|
550
|
-
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children:
|
|
551
|
-
|
|
697
|
+
: filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
|
|
698
|
+
// #592: cond.key may be a dot-joined nested path
|
|
699
|
+
// ("team.twitterFollowerCount") — resolve the full path so the
|
|
700
|
+
// field segment can render its breadcrumb; `col` (the leaf) still
|
|
701
|
+
// drives the operator set and value editor exactly as before, for
|
|
702
|
+
// both a plain and a nested key.
|
|
703
|
+
const colPath = resolveColumnPath(filterColumns, cond.key);
|
|
704
|
+
const col = colPath.at(-1);
|
|
552
705
|
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
553
706
|
const opOpen = openMenu === `fop:${cond.id}`;
|
|
554
707
|
const valOpen = openMenu === `fval:${cond.id}`;
|
|
@@ -564,7 +717,10 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
564
717
|
// which never renders a third box in either case.
|
|
565
718
|
const showValueSeg = cond.op != null &&
|
|
566
719
|
(opNeedsValue(cond.op) || opNeedsValues(cond.op));
|
|
567
|
-
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: () =>
|
|
720
|
+
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: () => {
|
|
721
|
+
setFieldPickerPath([]);
|
|
722
|
+
setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
|
|
723
|
+
}, 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
|
|
568
724
|
? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
|
|
569
725
|
: 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"
|
|
570
726
|
? cond.values?.length
|
|
@@ -574,7 +730,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
574
730
|
? col?.type === "date"
|
|
575
731
|
? formatFilterDate(cond.value)
|
|
576
732
|
: cond.value
|
|
577
|
-
: 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:
|
|
733
|
+
: 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: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), 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`,
|
|
578
734
|
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
579
735
|
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
580
736
|
.filter((v) => v
|
|
@@ -594,7 +750,18 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
594
750
|
values: col.values?.filter((x) => set.has(x)),
|
|
595
751
|
});
|
|
596
752
|
}, children: v }, v));
|
|
597
|
-
})] })) : col?.type === "
|
|
753
|
+
})] })) : col?.type === "select" &&
|
|
754
|
+
col?.values &&
|
|
755
|
+
(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`,
|
|
756
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
757
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
758
|
+
.filter((v) => v
|
|
759
|
+
.toLowerCase()
|
|
760
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
761
|
+
.map((v) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
|
|
762
|
+
patchFilter(cond.id, { value: v });
|
|
763
|
+
setOpenMenu(null);
|
|
764
|
+
}, 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, { ...{
|
|
598
765
|
selectionMode: "single",
|
|
599
766
|
value: isoToDate(cond.value)
|
|
600
767
|
? toDatePickerDate(isoToDate(cond.value))
|
|
@@ -625,17 +792,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
625
792
|
}
|
|
626
793
|
else {
|
|
627
794
|
setAddingAttribute(false);
|
|
628
|
-
|
|
795
|
+
setFieldPickerPath([]);
|
|
796
|
+
setFilterQuery("");
|
|
629
797
|
setOpenMenu("viewsettings");
|
|
630
798
|
}
|
|
631
799
|
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
|
|
632
|
-
(addingAttribute ? ((0, jsx_runtime_1.
|
|
633
|
-
// biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
|
|
634
|
-
autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
|
|
635
|
-
.filter((col) => col.label
|
|
636
|
-
.toLowerCase()
|
|
637
|
-
.includes(addAttrQuery.trim().toLowerCase()))
|
|
638
|
-
.map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), 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)))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
800
|
+
(addingAttribute ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
639
801
|
if (dragAttr !== null)
|
|
640
802
|
e.preventDefault();
|
|
641
803
|
}, onDrop: () => {
|
|
@@ -643,7 +805,24 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
|
|
|
643
805
|
moveAttr(dragAttr, i);
|
|
644
806
|
setDragAttr(null);
|
|
645
807
|
}, 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) ??
|
|
646
|
-
`${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: () =>
|
|
808
|
+
`${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: () => {
|
|
809
|
+
setAddingAttribute(true);
|
|
810
|
+
setFieldPickerPath([]);
|
|
811
|
+
setFilterQuery("");
|
|
812
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
|
|
813
|
+
onExportCsv ||
|
|
814
|
+
onExportExcel ||
|
|
815
|
+
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: () => {
|
|
816
|
+
onImportCsv();
|
|
817
|
+
setOpenMenu(null);
|
|
818
|
+
}, 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: () => {
|
|
819
|
+
onExportCsv();
|
|
820
|
+
setOpenMenu(null);
|
|
821
|
+
}, 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: () => {
|
|
822
|
+
onExportExcel();
|
|
823
|
+
setOpenMenu(null);
|
|
824
|
+
}, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
|
|
825
|
+
(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] }) }));
|
|
647
826
|
}
|
|
648
827
|
/**
|
|
649
828
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -36,17 +36,62 @@ const filterChipChrome = `
|
|
|
36
36
|
& .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
|
|
37
37
|
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
|
|
38
38
|
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${tokens_1.contract.bgSurface}; }
|
|
39
|
-
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
39
|
+
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; max-width: 200px; }
|
|
40
40
|
& .crisp-tabletoolbar-fcond-seg--field svg { color: ${tokens_1.contract.fgMuted}; }
|
|
41
|
+
/* #592: a nested pick's field segment ("Team › Twitter follower
|
|
42
|
+
count") — measured live: BOTH path segments the same colour (chip
|
|
43
|
+
context only; the picker's own menu rows keep the parent muted, see
|
|
44
|
+
tabletoolbar.chrome.ts), each independently truncated. Attio's own
|
|
45
|
+
layout gives the leaf segment priority (it stays fully readable
|
|
46
|
+
longer, the parent truncates first) via a two-part min-content
|
|
47
|
+
measurement this reproduces qualitatively with a flex-shrink
|
|
48
|
+
differential rather than byte-for-byte (the exact px split Attio
|
|
49
|
+
computes is content-length-dependent, not a fixed value) — not
|
|
50
|
+
independently re-verified at other path-segment lengths. */
|
|
51
|
+
& .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
|
|
52
|
+
& .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
|
|
53
|
+
& .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
|
|
54
|
+
& .crisp-tabletoolbar-fcond-path-sep { flex-shrink: 0; color: inherit; }
|
|
41
55
|
& .crisp-tabletoolbar-fcond-seg--op { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; }
|
|
42
56
|
& .crisp-tabletoolbar-fcond-seg--val { font-weight: ${tokens_1.contract.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
43
57
|
& .crisp-tabletoolbar-fcond-seg--kebab { background: ${tokens_1.contract.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${tokens_1.contract.radius.md} ${tokens_1.contract.radius.md} 0; color: ${tokens_1.contract.fgMuted}; }
|
|
44
58
|
& .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${tokens_1.contract.fgPrimary}; }
|
|
59
|
+
/* #754: the sort row's own field trigger, replacing a plain Select
|
|
60
|
+
with the same nested-drill-down popover Filter's field picker
|
|
61
|
+
already has — reuses Select's own already-measured trigger chrome
|
|
62
|
+
verbatim (its class is scoped under Select's own root class, so it
|
|
63
|
+
can't be reused as a bare class name outside an actual Select tree;
|
|
64
|
+
these values are copied, not re-measured). */
|
|
65
|
+
& .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
|
|
66
|
+
& .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]} 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
|
|
67
|
+
& .crisp-tabletoolbar-sort-field-trigger:hover { background: ${tokens_1.contract.bgRaised}; }
|
|
68
|
+
& .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
|
|
69
|
+
& .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; background: ${tokens_1.contract.bgSurface}; }
|
|
70
|
+
& .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
|
|
71
|
+
& .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
|
|
45
72
|
& .crisp-tabletoolbar-menu--val { min-width: 134px; }
|
|
46
73
|
& .crisp-tabletoolbar-datepop { width: 300px; }
|
|
47
74
|
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
|
|
48
75
|
& .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
|
|
49
76
|
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
|
|
77
|
+
// #595: the Import/Export trigger — measured live, `app.attio.com`'s own
|
|
78
|
+
// view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
|
|
79
|
+
// (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
|
|
80
|
+
// shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
|
|
81
|
+
// rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
|
|
82
|
+
// settings' plain ringed `.tool` (no background, no elevation shadow), and
|
|
83
|
+
// the only trigger of the four with a trailing chevron (2 icons total,
|
|
84
|
+
// confirmed live). Overrides `.tool`'s own base background/shadow by cascade
|
|
85
|
+
// order (this block is appended after `toolbarChromeBody`, which defines
|
|
86
|
+
// them), the same way `.tool--ghost` already does. `border`/`font-family`/
|
|
87
|
+
// `cursor` are repeated here too, even though `.tool`'s own base rule (in
|
|
88
|
+
// the shared `tabletoolbar.chrome.ts`) already sets them on every button
|
|
89
|
+
// carrying this modifier — `spec-check.mjs`'s button-reset audit only reads
|
|
90
|
+
// each component's own `<name>.recipe.ts` file verbatim, so a reset living
|
|
91
|
+
// behind `chrome.ts`'s function call is invisible to it; harmless repeats,
|
|
92
|
+
// not a real gap.
|
|
93
|
+
const importExportChrome = `
|
|
94
|
+
& .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.bgSurfaceMuted}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; cursor: pointer; }`;
|
|
50
95
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
51
96
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
52
97
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -64,6 +109,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
64
109
|
${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
|
|
65
110
|
${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
|
|
66
111
|
${filterChipChrome}
|
|
112
|
+
${importExportChrome}
|
|
113
|
+
${core_1.rtlFlipCss}
|
|
67
114
|
`,
|
|
68
115
|
});
|
|
69
116
|
exports.tabletoolbarRecipe = (0, core_1.resolveRecipe)(config);
|
|
@@ -13,13 +13,23 @@ const tokens_1 = require("../../tokens");
|
|
|
13
13
|
// the same reason (see its own doc).
|
|
14
14
|
const spec = (0, core_1.defineSpec)({
|
|
15
15
|
name: "text",
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
19
|
-
// falsy string as "no base rule",
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
16
|
+
// Needs a real, always-present declaration so `resolveRecipe` emits the
|
|
17
|
+
// `crisp-text` class at all -- an EMPTY base compiles to an empty string,
|
|
18
|
+
// and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
19
|
+
// that falsy string as "no base rule", dropping the class itself,
|
|
20
|
+
// silently, from every instance (this file's own tests catch that).
|
|
21
|
+
// `display: "inline"` filled that role until a real bug: `asChild`
|
|
22
|
+
// (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
|
|
23
|
+
// the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
|
|
24
|
+
// block default lost to `.crisp-text`'s class-specificity `display:
|
|
25
|
+
// inline`, silently collapsing its layout (internal/grc's own Settings.tsx
|
|
26
|
+
// ran two rows onto one line this way). `vertical-align: baseline` fills
|
|
27
|
+
// the same "always-present, never empty" role without that risk: it's a
|
|
28
|
+
// span's own already-inherited default (so nothing changes for the plain,
|
|
29
|
+
// non-asChild case `Primitive.span` renders), and it has NO effect at all
|
|
30
|
+
// on a block-level box, so an `asChild` div/p/li keeps its own display
|
|
31
|
+
// exactly as its own tag already gives it.
|
|
32
|
+
base: { verticalAlign: "baseline" },
|
|
23
33
|
variants: {
|
|
24
34
|
tone: {
|
|
25
35
|
default: { color: tokens_1.contract.fgPrimary },
|