@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
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, X, } from "lucide-react";
|
|
2
|
+
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, ChevronDown, ChevronLeft, ChevronRight, CircleHelp, Download, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, Upload, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
5
5
|
import { DatePicker, } from "../datepicker/index.js";
|
|
6
6
|
import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
|
|
7
7
|
import { Select } from "../select/index.js";
|
|
8
8
|
import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
|
|
9
|
+
// #592: shared by every direction-bearing ChevronRight usage below (the
|
|
10
|
+
// field picker's own disclosure chevron, a flattened search result's
|
|
11
|
+
// breadcrumb separator, and a filter chip's own breadcrumb separator) so
|
|
12
|
+
// the RTL mirror attribute lives in exactly one JSX tag — build/spec-check
|
|
13
|
+
// .mjs's icon sweep counts that attribute per icon NAME across the whole
|
|
14
|
+
// file, not per call site, and three independently attributed tags for
|
|
15
|
+
// the same name over-counts against that (it expects one per name).
|
|
16
|
+
const RtlChevronRight = ({ size, className, }) => (_jsx(ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
|
|
9
17
|
const OP_LABELS = {
|
|
10
18
|
contains: "contains",
|
|
11
19
|
doesnotcontain: "does not contain",
|
|
@@ -66,18 +74,53 @@ const DATE_OP_ORDER = [
|
|
|
66
74
|
"empty",
|
|
67
75
|
"notempty",
|
|
68
76
|
];
|
|
77
|
+
// #715: a closed-vocabulary column (GRC Audit's Action/Entity, e.g.) gets
|
|
78
|
+
// its own shorter set too — the text set's `contains`/`starts with`/… mean
|
|
79
|
+
// nothing against a fixed value list. Reuses the already-measured `is`/
|
|
80
|
+
// `is not`/`empty`/`not empty` labels and the same "is/isnot before the
|
|
81
|
+
// type-specific pair, empty/notempty last" ordering every other set here
|
|
82
|
+
// follows — not an independent pixel measurement the way #593's number/date
|
|
83
|
+
// sets were (a live re-check of Attio's own select-column filter menu is
|
|
84
|
+
// still open, see the PR).
|
|
85
|
+
const SELECT_OP_ORDER = ["is", "isnot", "empty", "notempty"];
|
|
69
86
|
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
70
87
|
// nothing to check off otherwise. A column's `type` picks the base set
|
|
71
|
-
// first; "isanyof" can still follow any of the
|
|
72
|
-
// declared values aren't tied to its underlying number/date/text
|
|
88
|
+
// first; "isanyof" can still follow any of the four (a column's own
|
|
89
|
+
// declared values aren't tied to its underlying number/date/text/select
|
|
90
|
+
// kind).
|
|
73
91
|
const opsFor = (col) => {
|
|
74
92
|
const base = col?.type === "number"
|
|
75
93
|
? NUMBER_OP_ORDER
|
|
76
94
|
: col?.type === "date"
|
|
77
95
|
? DATE_OP_ORDER
|
|
78
|
-
:
|
|
96
|
+
: col?.type === "select"
|
|
97
|
+
? SELECT_OP_ORDER
|
|
98
|
+
: OP_ORDER;
|
|
79
99
|
return col?.values?.length ? [...base, "isanyof"] : base;
|
|
80
100
|
};
|
|
101
|
+
// #592: a condition's `key` is either a plain top-level column key, or a
|
|
102
|
+
// dot-joined path into `nestedColumns` ("team.twitterFollowerCount"). Walks
|
|
103
|
+
// the path one segment at a time, descending into each match's own
|
|
104
|
+
// `nestedColumns` for the next — returns the resolved column at each step,
|
|
105
|
+
// in order, so a caller has both the LEAF (its own `type`/`values`/`icon`
|
|
106
|
+
// govern the operator set and value editor, unchanged from a plain column)
|
|
107
|
+
// and the FULL PATH (for the breadcrumb the field segment/picker render).
|
|
108
|
+
// Empty when any segment fails to resolve, rather than a partial path — a
|
|
109
|
+
// stale key (its column was removed upstream) should render as "unknown",
|
|
110
|
+
// not as whatever prefix still happens to match.
|
|
111
|
+
const resolveColumnPath = (cols, key) => {
|
|
112
|
+
const segments = key.split(".");
|
|
113
|
+
const path = [];
|
|
114
|
+
let list = cols;
|
|
115
|
+
for (const seg of segments) {
|
|
116
|
+
const found = list.find((c) => c.key === seg);
|
|
117
|
+
if (!found)
|
|
118
|
+
return [];
|
|
119
|
+
path.push(found);
|
|
120
|
+
list = found.nestedColumns ?? [];
|
|
121
|
+
}
|
|
122
|
+
return path;
|
|
123
|
+
};
|
|
81
124
|
const opNeedsValue = (op) => op === "contains" ||
|
|
82
125
|
op === "doesnotcontain" ||
|
|
83
126
|
op === "startswith" ||
|
|
@@ -256,7 +299,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
256
299
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
257
300
|
* Created by), which one shared list can't reproduce.
|
|
258
301
|
*/
|
|
259
|
-
export 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 }) {
|
|
302
|
+
export 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 }) {
|
|
260
303
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
261
304
|
// CrispMessagesProvider's translation of the English default below, or
|
|
262
305
|
// that default itself with no provider — unchanged from before this
|
|
@@ -283,6 +326,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
283
326
|
const reorderAttributeLabel = useCrispMessage(reorderAttributeLabelProp, "Reorder attribute");
|
|
284
327
|
const addAttributeToViewLabel = useCrispMessage(addAttributeToViewLabelProp, "Add attribute to view");
|
|
285
328
|
const searchAttributesPlaceholder = useCrispMessage(searchAttributesPlaceholderProp, "Search attributes…");
|
|
329
|
+
const importExportLabel = useCrispMessage(importExportLabelProp, "Import / Export");
|
|
330
|
+
const importCsvLabel = useCrispMessage(importCsvLabelProp, "Import CSV");
|
|
331
|
+
const exportCsvLabel = useCrispMessage(exportCsvLabelProp, "Export view as CSV");
|
|
332
|
+
const exportExcelLabel = useCrispMessage(exportExcelLabelProp, "Export view as Excel");
|
|
333
|
+
const learnImportExportLabel = useCrispMessage(learnImportExportLabelProp, "Learn about imports and exports");
|
|
334
|
+
const backLabel = useCrispMessage(backLabelProp, "Back");
|
|
286
335
|
// OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
|
|
287
336
|
// their own — a whole lookup table, not one string — so they translate
|
|
288
337
|
// straight from the provider's dictionary at the two spots they render
|
|
@@ -323,6 +372,17 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
323
372
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
324
373
|
const [dragSort, setDragSort] = React.useState(null);
|
|
325
374
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
375
|
+
// #592: which nested scope the Filter field picker is currently drilled
|
|
376
|
+
// into -- [] is the top level, [team] is inside Team's own attributes,
|
|
377
|
+
// [team, company] is Team -> Company, etc. Reset to [] wherever
|
|
378
|
+
// `filterQuery` already resets (opening the picker fresh), same lifetime.
|
|
379
|
+
const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
|
|
380
|
+
// #754: which sort row's own field picker is open (its index into
|
|
381
|
+
// `sorts`), or `null` for the normal sort-row list — the same
|
|
382
|
+
// "one sub-screen behind a flag" split View settings' own
|
|
383
|
+
// `addingAttribute` already uses, parameterized by index since more than
|
|
384
|
+
// one sort row exists.
|
|
385
|
+
const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
|
|
326
386
|
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
327
387
|
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
328
388
|
const [dragAttr, setDragAttr] = React.useState(null);
|
|
@@ -336,7 +396,6 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
336
396
|
// built here -- hiding an attribute is a trailing control instead, see
|
|
337
397
|
// the hide button below).
|
|
338
398
|
const [addingAttribute, setAddingAttribute] = React.useState(false);
|
|
339
|
-
const [addAttrQuery, setAddAttrQuery] = React.useState("");
|
|
340
399
|
// close any open menu on outside click (anything not inside a menu or its
|
|
341
400
|
// trigger) or Escape
|
|
342
401
|
React.useEffect(() => {
|
|
@@ -419,10 +478,27 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
419
478
|
};
|
|
420
479
|
// #590: shown attributes, in `shownKeys`' order -- a key with no matching
|
|
421
480
|
// `viewColumns` entry (stale state after a column is removed upstream)
|
|
422
|
-
// drops out rather than rendering a blank row.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
481
|
+
// drops out rather than rendering a blank row. #754: `shownKeys` can now
|
|
482
|
+
// carry a dot-joined nested path ("team.jobTitle") from the add-attribute
|
|
483
|
+
// screen's own nested picker, so each key resolves via the same
|
|
484
|
+
// `resolveColumnPath` the filter chip uses -- the LEAF column's icon
|
|
485
|
+
// drives the row's own icon, and the label is the full breadcrumb
|
|
486
|
+
// ("Team › Job title"), joined as plain text rather than the chip's own
|
|
487
|
+
// muted/chevron treatment, which was measured specifically for the
|
|
488
|
+
// filter chip and wasn't re-verified for this row shape.
|
|
489
|
+
const shownAttrs = shownKeys.flatMap((k) => {
|
|
490
|
+
const path = resolveColumnPath(viewColumns, k);
|
|
491
|
+
const leaf = path.at(-1);
|
|
492
|
+
if (!leaf)
|
|
493
|
+
return [];
|
|
494
|
+
return [
|
|
495
|
+
{
|
|
496
|
+
key: k,
|
|
497
|
+
icon: leaf.icon,
|
|
498
|
+
label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
});
|
|
426
502
|
const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
|
|
427
503
|
const moveAttr = (from, to) => {
|
|
428
504
|
if (from === to)
|
|
@@ -436,7 +512,8 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
436
512
|
const showAttr = (key) => {
|
|
437
513
|
putShownKeys([...shownKeys, key]);
|
|
438
514
|
setAddingAttribute(false);
|
|
439
|
-
|
|
515
|
+
setFieldPickerPath([]);
|
|
516
|
+
setFilterQuery("");
|
|
440
517
|
};
|
|
441
518
|
// #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
|
|
442
519
|
// model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
|
|
@@ -460,14 +537,62 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
460
537
|
close: () => setOpenMenu(null),
|
|
461
538
|
contentRef: opMenuRef,
|
|
462
539
|
});
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
540
|
+
// The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
|
|
541
|
+
// (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
|
|
542
|
+
// dividers between the 3 groups) — same shared keyboard model.
|
|
543
|
+
const importExportMenuRef = React.useRef(null);
|
|
544
|
+
const { onKeyDown: onImportExportKeyDown } = useMenuKeyboard({
|
|
545
|
+
open: openMenu === "importexport",
|
|
546
|
+
close: () => setOpenMenu(null),
|
|
547
|
+
contentRef: importExportMenuRef,
|
|
548
|
+
});
|
|
549
|
+
// #754: generalized from the Filter-only `filterFieldPicker` (#592) to
|
|
550
|
+
// also drive Sort's own field picker and View settings' add-attribute
|
|
551
|
+
// screen — all three offer the identical nested-drill-down construct on
|
|
552
|
+
// live Attio (confirmed: "Team" with a count badge appears, and drills
|
|
553
|
+
// into the same People-attribute screen, in all three pickers). Shared
|
|
554
|
+
// `fieldPickerPath`/`filterQuery` state across all three call sites is
|
|
555
|
+
// safe: `openMenu` already only ever names ONE open popover at a time,
|
|
556
|
+
// so only one of Filter/Sort/View-settings' own picker can be showing.
|
|
557
|
+
// `fieldPickerPath` tracks the current scope ([] is the top level),
|
|
558
|
+
// reset to [] wherever a picker opens fresh (every trigger's own
|
|
559
|
+
// onClick).
|
|
560
|
+
const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
|
|
561
|
+
const scope = fieldPickerPath.at(-1);
|
|
562
|
+
const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
|
|
563
|
+
const query = filterQuery.trim().toLowerCase();
|
|
564
|
+
const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
|
|
565
|
+
// #592: one level of flattening from wherever the picker is currently
|
|
566
|
+
// scoped — a nested column's own children get searched too, and a
|
|
567
|
+
// match renders breadcrumbed, alongside this level's direct matches.
|
|
568
|
+
// Matches the one case measured live at the TOP level ("Twitter
|
|
569
|
+
// follower" surfaces both the direct field and "Team › Twitter
|
|
570
|
+
// follower count"); applying the identical rule recursively from an
|
|
571
|
+
// already-drilled-into scope (so it also reaches Team → Company → X
|
|
572
|
+
// while already browsing inside Team) is the same construct, one level
|
|
573
|
+
// relative to wherever you are, but wasn't independently re-verified
|
|
574
|
+
// live from inside a nested scope — flagged on the PR.
|
|
575
|
+
const nested = query
|
|
576
|
+
? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
|
|
577
|
+
.filter((n) => n.label.toLowerCase().includes(query))
|
|
578
|
+
.map((n) => ({ parent: c, col: n })))
|
|
579
|
+
: [];
|
|
580
|
+
const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
|
|
581
|
+
const drillIn = (col) => {
|
|
582
|
+
setFieldPickerPath([...fieldPickerPath, col]);
|
|
583
|
+
setFilterQuery("");
|
|
584
|
+
};
|
|
585
|
+
return (_jsxs(_Fragment, { children: [scope ? (_jsxs("div", { className: "crisp-tabletoolbar-menu-nesthead", children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-menu-back", "aria-label": backLabel, onClick: () => {
|
|
586
|
+
setFieldPickerPath(fieldPickerPath.slice(0, -1));
|
|
587
|
+
setFilterQuery("");
|
|
588
|
+
}, children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), _jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
|
|
589
|
+
? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
|
|
590
|
+
: `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
|
|
591
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
592
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
|
|
593
|
+
// biome-ignore lint/a11y/noAutofocus: focus the picker on open
|
|
594
|
+
autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? (_jsx(scope.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label, _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key))), nested.map(({ parent, col }) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), _jsxs("span", { className: "crisp-tabletoolbar-menu-item-path", children: [_jsx("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), _jsx("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
|
|
595
|
+
};
|
|
471
596
|
const activeFilters = filters.length;
|
|
472
597
|
// The default phrase only works in English word order, so a caller can
|
|
473
598
|
// replace the whole thing rather than fill a template — crisp/#471's
|
|
@@ -480,7 +605,11 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
480
605
|
const sortedLabel = sortTriggerLabel
|
|
481
606
|
? sortTriggerLabel(sorts, sortColumns)
|
|
482
607
|
: sorts.length
|
|
483
|
-
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
608
|
+
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
609
|
+
// #754: sorts[0].key may be a dot-joined nested path
|
|
610
|
+
// ("team.jobTitle") — resolve it the same way the chip and sort
|
|
611
|
+
// row do, rather than a flat find that fails on a nested key.
|
|
612
|
+
resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
484
613
|
: sortLabel;
|
|
485
614
|
return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
486
615
|
if (dragSort !== null)
|
|
@@ -489,11 +618,26 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
489
618
|
if (dragSort !== null)
|
|
490
619
|
moveSort(dragSort, i);
|
|
491
620
|
setDragSort(null);
|
|
492
|
-
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }),
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
621
|
+
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
|
|
622
|
+
// #754: the sort row's own field picker -- the
|
|
623
|
+
// same nested-drill-down construct Filter's field
|
|
624
|
+
// picker already has (#592), measured live:
|
|
625
|
+
// clicking a sort row's field (or "Add sort") opens
|
|
626
|
+
// an identical picker, "Team" (a relationship
|
|
627
|
+
// field) drills into the same People-attribute
|
|
628
|
+
// screen Filter's own "Team" does.
|
|
629
|
+
const sortColPath = resolveColumnPath(sortColumns, s.key);
|
|
630
|
+
const sortCol = sortColPath.at(-1);
|
|
631
|
+
const pickerOpen = sortFieldPickerIndex === i;
|
|
632
|
+
return (_jsxs("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
|
|
633
|
+
setFieldPickerPath([]);
|
|
634
|
+
setFilterQuery("");
|
|
635
|
+
setSortFieldPickerIndex(pickerOpen ? null : i);
|
|
636
|
+
}, children: [sortCol?.icon ? (_jsx(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => (_jsxs(React.Fragment, { children: [pi > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (_jsx("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
|
|
637
|
+
setSortField(i, key);
|
|
638
|
+
setSortFieldPickerIndex(null);
|
|
639
|
+
}, searchAttributesPlaceholder, sortFieldLabel) }))] }));
|
|
640
|
+
})(), _jsx(Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
|
|
497
641
|
{
|
|
498
642
|
value: "asc",
|
|
499
643
|
label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
|
|
@@ -504,12 +648,21 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
504
648
|
label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
505
649
|
keywords: descendingLabel,
|
|
506
650
|
},
|
|
507
|
-
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () =>
|
|
651
|
+
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => {
|
|
652
|
+
setFieldPickerPath([]);
|
|
653
|
+
setOpenMenu(openMenu === "filter" ? null : "filter");
|
|
654
|
+
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
508
655
|
? filterTriggerLabel(activeFilters)
|
|
509
656
|
: activeFilters
|
|
510
657
|
? `${filterLabel} · ${activeFilters}`
|
|
511
|
-
: filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children:
|
|
512
|
-
|
|
658
|
+
: filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
|
|
659
|
+
// #592: cond.key may be a dot-joined nested path
|
|
660
|
+
// ("team.twitterFollowerCount") — resolve the full path so the
|
|
661
|
+
// field segment can render its breadcrumb; `col` (the leaf) still
|
|
662
|
+
// drives the operator set and value editor exactly as before, for
|
|
663
|
+
// both a plain and a nested key.
|
|
664
|
+
const colPath = resolveColumnPath(filterColumns, cond.key);
|
|
665
|
+
const col = colPath.at(-1);
|
|
513
666
|
const fieldOpen = openMenu === `ffield:${cond.id}`;
|
|
514
667
|
const opOpen = openMenu === `fop:${cond.id}`;
|
|
515
668
|
const valOpen = openMenu === `fval:${cond.id}`;
|
|
@@ -525,7 +678,10 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
525
678
|
// which never renders a third box in either case.
|
|
526
679
|
const showValueSeg = cond.op != null &&
|
|
527
680
|
(opNeedsValue(cond.op) || opNeedsValues(cond.op));
|
|
528
|
-
return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [_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: () =>
|
|
681
|
+
return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [_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: () => {
|
|
682
|
+
setFieldPickerPath([]);
|
|
683
|
+
setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
|
|
684
|
+
}, children: [col?.icon ? _jsx(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), _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
|
|
529
685
|
? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
|
|
530
686
|
: selectConditionLabel }), showValueSeg && (_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"
|
|
531
687
|
? cond.values?.length
|
|
@@ -535,7 +691,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
535
691
|
? col?.type === "date"
|
|
536
692
|
? formatFilterDate(cond.value)
|
|
537
693
|
: cond.value
|
|
538
|
-
: filterValuePlaceholder })), _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: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_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: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children:
|
|
694
|
+
: filterValuePlaceholder })), _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: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_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: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && (_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) => (_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 ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
539
695
|
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
540
696
|
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
541
697
|
.filter((v) => v
|
|
@@ -555,7 +711,18 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
555
711
|
values: col.values?.filter((x) => set.has(x)),
|
|
556
712
|
});
|
|
557
713
|
}, children: v }, v));
|
|
558
|
-
})] })) : col?.type === "
|
|
714
|
+
})] })) : col?.type === "select" &&
|
|
715
|
+
col?.values &&
|
|
716
|
+
(cond.op === "is" || cond.op === "isnot") ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
717
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
718
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
719
|
+
.filter((v) => v
|
|
720
|
+
.toLowerCase()
|
|
721
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
722
|
+
.map((v) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
|
|
723
|
+
patchFilter(cond.id, { value: v });
|
|
724
|
+
setOpenMenu(null);
|
|
725
|
+
}, children: [v, cond.value === v ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, v)))] })) : col?.type === "date" ? (_jsxs("div", { className: "crisp-tabletoolbar-datepop", children: [_jsx(DatePicker, { ...{
|
|
559
726
|
selectionMode: "single",
|
|
560
727
|
value: isoToDate(cond.value)
|
|
561
728
|
? toDatePickerDate(isoToDate(cond.value))
|
|
@@ -586,17 +753,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
586
753
|
}
|
|
587
754
|
else {
|
|
588
755
|
setAddingAttribute(false);
|
|
589
|
-
|
|
756
|
+
setFieldPickerPath([]);
|
|
757
|
+
setFilterQuery("");
|
|
590
758
|
setOpenMenu("viewsettings");
|
|
591
759
|
}
|
|
592
760
|
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
|
|
593
|
-
(addingAttribute ? (
|
|
594
|
-
// biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
|
|
595
|
-
autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
|
|
596
|
-
.filter((col) => col.label
|
|
597
|
-
.toLowerCase()
|
|
598
|
-
.includes(addAttrQuery.trim().toLowerCase()))
|
|
599
|
-
.map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
761
|
+
(addingAttribute ? (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
600
762
|
if (dragAttr !== null)
|
|
601
763
|
e.preventDefault();
|
|
602
764
|
}, onDrop: () => {
|
|
@@ -604,7 +766,24 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
604
766
|
moveAttr(dragAttr, i);
|
|
605
767
|
setDragAttr(null);
|
|
606
768
|
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
607
|
-
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () =>
|
|
769
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
|
|
770
|
+
setAddingAttribute(true);
|
|
771
|
+
setFieldPickerPath([]);
|
|
772
|
+
setFilterQuery("");
|
|
773
|
+
}, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
|
|
774
|
+
onExportCsv ||
|
|
775
|
+
onExportExcel ||
|
|
776
|
+
importExportHelpHref) && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && (_jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
777
|
+
onImportCsv();
|
|
778
|
+
setOpenMenu(null);
|
|
779
|
+
}, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
780
|
+
onExportCsv();
|
|
781
|
+
setOpenMenu(null);
|
|
782
|
+
}, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
783
|
+
onExportExcel();
|
|
784
|
+
setOpenMenu(null);
|
|
785
|
+
}, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
|
|
786
|
+
(onImportCsv || onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && (_jsxs("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] })), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
608
787
|
}
|
|
609
788
|
/**
|
|
610
789
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
-
import { contract as c } from "../../tokens/index.js";
|
|
1
|
+
import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
|
|
2
|
+
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
3
|
import { toolbarChromeBody, toolbarChromeHead } from "./tabletoolbar.chrome.js";
|
|
4
4
|
// The filter condition chip, TableToolbar-only (crisp/#462 follow-up,
|
|
5
5
|
// re-measured live 2026-09-25). Deliberately NOT added to the shared
|
|
@@ -33,17 +33,62 @@ const filterChipChrome = `
|
|
|
33
33
|
& .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
|
|
34
34
|
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
|
|
35
35
|
& .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${c.bgSurface}; }
|
|
36
|
-
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${c.radius.md} 0 0 ${c.radius.md}; font-weight: ${c.weight.medium}; }
|
|
36
|
+
& .crisp-tabletoolbar-fcond-seg--field { border-radius: ${c.radius.md} 0 0 ${c.radius.md}; font-weight: ${c.weight.medium}; max-width: 200px; }
|
|
37
37
|
& .crisp-tabletoolbar-fcond-seg--field svg { color: ${c.fgMuted}; }
|
|
38
|
+
/* #592: a nested pick's field segment ("Team › Twitter follower
|
|
39
|
+
count") — measured live: BOTH path segments the same colour (chip
|
|
40
|
+
context only; the picker's own menu rows keep the parent muted, see
|
|
41
|
+
tabletoolbar.chrome.ts), each independently truncated. Attio's own
|
|
42
|
+
layout gives the leaf segment priority (it stays fully readable
|
|
43
|
+
longer, the parent truncates first) via a two-part min-content
|
|
44
|
+
measurement this reproduces qualitatively with a flex-shrink
|
|
45
|
+
differential rather than byte-for-byte (the exact px split Attio
|
|
46
|
+
computes is content-length-dependent, not a fixed value) — not
|
|
47
|
+
independently re-verified at other path-segment lengths. */
|
|
48
|
+
& .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
|
|
49
|
+
& .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
|
|
50
|
+
& .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
|
|
51
|
+
& .crisp-tabletoolbar-fcond-path-sep { flex-shrink: 0; color: inherit; }
|
|
38
52
|
& .crisp-tabletoolbar-fcond-seg--op { background: ${c.bgSurface}; color: ${c.fgMuted}; }
|
|
39
53
|
& .crisp-tabletoolbar-fcond-seg--val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
|
|
40
54
|
& .crisp-tabletoolbar-fcond-seg--kebab { background: ${c.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; color: ${c.fgMuted}; }
|
|
41
55
|
& .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${c.fgPrimary}; }
|
|
56
|
+
/* #754: the sort row's own field trigger, replacing a plain Select
|
|
57
|
+
with the same nested-drill-down popover Filter's field picker
|
|
58
|
+
already has — reuses Select's own already-measured trigger chrome
|
|
59
|
+
verbatim (its class is scoped under Select's own root class, so it
|
|
60
|
+
can't be reused as a bare class name outside an actual Select tree;
|
|
61
|
+
these values are copied, not re-measured). */
|
|
62
|
+
& .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
|
|
63
|
+
& .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${c.space["1.5"]} 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
|
|
64
|
+
& .crisp-tabletoolbar-sort-field-trigger:hover { background: ${c.bgRaised}; }
|
|
65
|
+
& .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
|
|
66
|
+
& .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; background: ${c.bgSurface}; }
|
|
67
|
+
& .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
|
|
68
|
+
& .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
|
|
42
69
|
& .crisp-tabletoolbar-menu--val { min-width: 134px; }
|
|
43
70
|
& .crisp-tabletoolbar-datepop { width: 300px; }
|
|
44
71
|
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
|
|
45
72
|
& .crisp-tabletoolbar-datepop-preset { height: ${c.size.chip}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.chip}; background: ${c.bgRaised}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; cursor: pointer; }
|
|
46
73
|
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
|
|
74
|
+
// #595: the Import/Export trigger — measured live, `app.attio.com`'s own
|
|
75
|
+
// view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
|
|
76
|
+
// (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
|
|
77
|
+
// shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
|
|
78
|
+
// rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
|
|
79
|
+
// settings' plain ringed `.tool` (no background, no elevation shadow), and
|
|
80
|
+
// the only trigger of the four with a trailing chevron (2 icons total,
|
|
81
|
+
// confirmed live). Overrides `.tool`'s own base background/shadow by cascade
|
|
82
|
+
// order (this block is appended after `toolbarChromeBody`, which defines
|
|
83
|
+
// them), the same way `.tool--ghost` already does. `border`/`font-family`/
|
|
84
|
+
// `cursor` are repeated here too, even though `.tool`'s own base rule (in
|
|
85
|
+
// the shared `tabletoolbar.chrome.ts`) already sets them on every button
|
|
86
|
+
// carrying this modifier — `spec-check.mjs`'s button-reset audit only reads
|
|
87
|
+
// each component's own `<name>.recipe.ts` file verbatim, so a reset living
|
|
88
|
+
// behind `chrome.ts`'s function call is invisible to it; harmless repeats,
|
|
89
|
+
// not a real gap.
|
|
90
|
+
const importExportChrome = `
|
|
91
|
+
& .crisp-tabletoolbar-tool--raised { border: none; background: ${c.bgSurfaceMuted}; box-shadow: ${e.field}; font-family: inherit; cursor: pointer; }`;
|
|
47
92
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
48
93
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
49
94
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -61,6 +106,8 @@ const config = defineRecipe({
|
|
|
61
106
|
${toolbarChromeHead("tabletoolbar")}
|
|
62
107
|
${toolbarChromeBody("tabletoolbar")}
|
|
63
108
|
${filterChipChrome}
|
|
109
|
+
${importExportChrome}
|
|
110
|
+
${rtlFlipCss}
|
|
64
111
|
`,
|
|
65
112
|
});
|
|
66
113
|
export const tabletoolbarRecipe = resolveRecipe(config);
|
|
@@ -10,13 +10,23 @@ import { contract as c } from "../../tokens/index.js";
|
|
|
10
10
|
// the same reason (see its own doc).
|
|
11
11
|
const spec = defineSpec({
|
|
12
12
|
name: "text",
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
16
|
-
// falsy string as "no base rule",
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
13
|
+
// Needs a real, always-present declaration so `resolveRecipe` emits the
|
|
14
|
+
// `crisp-text` class at all -- an EMPTY base compiles to an empty string,
|
|
15
|
+
// and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
|
|
16
|
+
// that falsy string as "no base rule", dropping the class itself,
|
|
17
|
+
// silently, from every instance (this file's own tests catch that).
|
|
18
|
+
// `display: "inline"` filled that role until a real bug: `asChild`
|
|
19
|
+
// (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
|
|
20
|
+
// the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
|
|
21
|
+
// block default lost to `.crisp-text`'s class-specificity `display:
|
|
22
|
+
// inline`, silently collapsing its layout (internal/grc's own Settings.tsx
|
|
23
|
+
// ran two rows onto one line this way). `vertical-align: baseline` fills
|
|
24
|
+
// the same "always-present, never empty" role without that risk: it's a
|
|
25
|
+
// span's own already-inherited default (so nothing changes for the plain,
|
|
26
|
+
// non-asChild case `Primitive.span` renders), and it has NO effect at all
|
|
27
|
+
// on a block-level box, so an `asChild` div/p/li keeps its own display
|
|
28
|
+
// exactly as its own tag already gives it.
|
|
29
|
+
base: { verticalAlign: "baseline" },
|
|
20
30
|
variants: {
|
|
21
31
|
tone: {
|
|
22
32
|
default: { color: c.fgPrimary },
|