@crisp-ui-kit/crisp 0.54.7 → 0.54.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authshell.css +5 -1
- package/dist/authshell.layered.css +5 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +36 -8
- package/dist/board.layered.css +36 -8
- package/dist/button.css +3 -1
- package/dist/button.layered.css +3 -1
- package/dist/cjs/components/badge/badge.recipe.js +28 -5
- package/dist/cjs/components/button/button.js +33 -2
- package/dist/cjs/components/button/button.recipe.js +31 -5
- package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/cjs/components/datatable/datatable.js +590 -41
- package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
- package/dist/cjs/components/dialog/dialog.js +72 -5
- package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
- package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
- package/dist/cjs/components/tooltip/tooltip.js +23 -9
- package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/cjs/product/authshell/authshell.js +14 -1
- package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
- package/dist/cjs/product/board/board.js +175 -46
- package/dist/cjs/product/board/board.recipe.js +56 -4
- package/dist/cjs/product/comments/comments.js +34 -12
- package/dist/cjs/product/comments/comments.recipe.js +31 -7
- package/dist/cjs/product/formdialog/formdialog.js +10 -4
- package/dist/cjs/product/navitem/navitem.js +7 -2
- package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
- package/dist/cjs/product/objectsettings/index.js +1 -2
- package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
- package/dist/cjs/product/recordstable/recordstable.js +1 -17
- package/dist/cjs/product/richtext/richtext.js +76 -1
- package/dist/cjs/product/savedviews/savedviews.js +55 -24
- package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/cjs/product/sidebar/sidebar.js +68 -15
- package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/cjs/tokens/palette.js +32 -1
- package/dist/cjs/tokens/semantic.js +11 -0
- package/dist/comments.css +50 -14
- package/dist/comments.layered.css +50 -14
- package/dist/contextmenu.css +24 -0
- package/dist/contextmenu.layered.css +24 -0
- package/dist/datatable.css +101 -37
- package/dist/datatable.layered.css +101 -37
- package/dist/dialog.css +13 -1
- package/dist/dialog.layered.css +13 -1
- package/dist/esm/components/badge/badge.recipe.js +28 -5
- package/dist/esm/components/button/button.js +34 -3
- package/dist/esm/components/button/button.recipe.js +31 -5
- package/dist/esm/components/contextmenu/contextmenu.js +2 -2
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
- package/dist/esm/components/datatable/datatable.js +590 -42
- package/dist/esm/components/datatable/datatable.recipe.js +119 -6
- package/dist/esm/components/dialog/dialog.js +72 -5
- package/dist/esm/components/dialog/dialog.recipe.js +15 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
- package/dist/esm/components/tabs/tabs.recipe.js +11 -2
- package/dist/esm/components/tooltip/tooltip.js +23 -9
- package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
- package/dist/esm/product/authshell/authshell.js +15 -2
- package/dist/esm/product/authshell/authshell.recipe.js +13 -1
- package/dist/esm/product/authshell/index.js +1 -1
- package/dist/esm/product/board/board.js +176 -47
- package/dist/esm/product/board/board.recipe.js +56 -4
- package/dist/esm/product/comments/comments.js +35 -13
- package/dist/esm/product/comments/comments.recipe.js +31 -7
- package/dist/esm/product/formdialog/formdialog.js +10 -4
- package/dist/esm/product/navitem/index.js +1 -1
- package/dist/esm/product/navitem/navitem.js +7 -2
- package/dist/esm/product/navitem/navitem.recipe.js +16 -0
- package/dist/esm/product/objectsettings/index.js +1 -1
- package/dist/esm/product/objectsettings/objectsettings.js +2 -12
- package/dist/esm/product/recordstable/recordstable.js +2 -18
- package/dist/esm/product/richtext/richtext.js +76 -1
- package/dist/esm/product/savedviews/savedviews.js +57 -26
- package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
- package/dist/esm/product/sidebar/sidebar.js +69 -16
- package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
- package/dist/esm/tokens/palette.js +32 -1
- package/dist/esm/tokens/semantic.js +11 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +16 -2
- package/dist/navitem.css +7 -0
- package/dist/navitem.layered.css +7 -0
- package/dist/recordstable.css +34 -32
- package/dist/recordstable.layered.css +34 -32
- package/dist/savedviews.css +27 -0
- package/dist/savedviews.layered.css +27 -0
- package/dist/sidebar.css +6 -1
- package/dist/sidebar.layered.css +6 -1
- package/dist/styles.css +369 -106
- package/dist/styles.layered.css +369 -106
- package/dist/tabletoolbar.css +47 -3
- package/dist/tabletoolbar.layered.css +47 -3
- package/dist/tabs.css +1 -1
- package/dist/tabs.layered.css +1 -1
- package/dist/tokens.css +9 -1
- package/dist/tokens.json +16 -2
- package/dist/tooltip.css +1 -1
- package/dist/tooltip.layered.css +1 -1
- package/dist/types/components/badge/badge.recipe.d.ts +5 -1
- package/dist/types/components/button/button.recipe.d.ts +18 -5
- package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
- package/dist/types/components/datatable/datatable.d.ts +98 -1
- package/dist/types/components/dialog/dialog.d.ts +25 -2
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
- package/dist/types/components/tooltip/tooltip.d.ts +12 -4
- package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
- package/dist/types/product/authshell/authshell.d.ts +26 -2
- package/dist/types/product/authshell/index.d.ts +1 -1
- package/dist/types/product/board/board.d.ts +53 -6
- package/dist/types/product/comments/comments.d.ts +16 -3
- package/dist/types/product/formdialog/formdialog.d.ts +2 -1
- package/dist/types/product/navitem/index.d.ts +1 -1
- package/dist/types/product/navitem/navitem.d.ts +23 -0
- package/dist/types/product/objectsettings/index.d.ts +1 -1
- package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
- package/dist/types/product/recordstable/recordstable.d.ts +0 -1
- package/dist/types/product/savedviews/savedviews.d.ts +45 -8
- package/dist/types/product/sidebar/sidebar.d.ts +33 -5
- package/dist/types/tokens/contract.d.ts +4 -0
- package/dist/types/tokens/palette.d.ts +13 -1
- package/dist/types/tokens/semantic.d.ts +11 -0
- package/package.json +1 -1
|
@@ -34,13 +34,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.isLabelTruncated = isLabelTruncated;
|
|
37
|
+
exports.reorderColumns = reorderColumns;
|
|
37
38
|
exports.DataTable = DataTable;
|
|
38
39
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
40
|
const lucide_react_1 = require("lucide-react");
|
|
40
41
|
const React = __importStar(require("react"));
|
|
41
42
|
const core_1 = require("../../core");
|
|
43
|
+
// crisp/#601 supersession: a deliberate exception to "DS atoms stay
|
|
44
|
+
// generic; product components compose them" (AUTHORING.md) -- the row's
|
|
45
|
+
// own comment affordance is `CommentCountBadge` (product/comments, #625),
|
|
46
|
+
// already correctly built and measured for the dot/hover-count states this
|
|
47
|
+
// plain icon path can't express on its own. Reaching into `product/` from
|
|
48
|
+
// `components/` runs backwards from every other import in this file; it's
|
|
49
|
+
// scoped to this one component, not a precedent for the rest of DataTable.
|
|
50
|
+
const comments_1 = require("../../product/comments");
|
|
42
51
|
const avatar_1 = require("../avatar");
|
|
43
52
|
const checkbox_1 = require("../checkbox");
|
|
53
|
+
const columncalc_1 = require("../columncalc");
|
|
54
|
+
const contextmenu_1 = require("../contextmenu");
|
|
44
55
|
const menu_1 = require("../menu");
|
|
45
56
|
const select_1 = require("../select");
|
|
46
57
|
const spinner_1 = require("../spinner");
|
|
@@ -55,6 +66,35 @@ const datatable_recipe_1 = require("./datatable.recipe");
|
|
|
55
66
|
function isLabelTruncated(el) {
|
|
56
67
|
return el.scrollWidth > el.clientWidth;
|
|
57
68
|
}
|
|
69
|
+
// #731: the pure resequencing behind both the controlled `columnOrder` prop
|
|
70
|
+
// and the drag/Move-left-right mutations that produce one -- exported so the
|
|
71
|
+
// "does this actually reorder, and does it survive a stale/unknown key"
|
|
72
|
+
// logic can be mutation-tested without mounting the table. Every key in
|
|
73
|
+
// `order` that matches a real column is placed first, in `order`'s own
|
|
74
|
+
// sequence; every column NOT named in `order` (a brand-new column a
|
|
75
|
+
// consumer's persisted order hasn't caught up to yet) keeps its relative
|
|
76
|
+
// position at the end, in `cols`' own order -- so a column can never be
|
|
77
|
+
// dropped or duplicated by this, only resequenced. An `order` entry with no
|
|
78
|
+
// matching column (one that was removed, or a stale value from a saved
|
|
79
|
+
// view) is silently skipped rather than throwing.
|
|
80
|
+
function reorderColumns(cols, order) {
|
|
81
|
+
if (!order || order.length === 0)
|
|
82
|
+
return cols;
|
|
83
|
+
const byKey = new Map(cols.map((c) => [c.key, c]));
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const ordered = [];
|
|
86
|
+
for (const key of order) {
|
|
87
|
+
const c = byKey.get(key);
|
|
88
|
+
if (c && !seen.has(key)) {
|
|
89
|
+
ordered.push(c);
|
|
90
|
+
seen.add(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const c of cols)
|
|
94
|
+
if (!seen.has(c.key))
|
|
95
|
+
ordered.push(c);
|
|
96
|
+
return ordered;
|
|
97
|
+
}
|
|
58
98
|
/**
|
|
59
99
|
* The header label span: a `Tooltip` (full text, `asChild` so the wrapper
|
|
60
100
|
* never displaces the ellipsis CSS the span already carries -- see
|
|
@@ -260,7 +300,7 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
260
300
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
261
301
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
262
302
|
// raw value keeps driving structure and tone.
|
|
263
|
-
function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
303
|
+
function renderTyped(col, value, avatarTone, row, display, locale = "en-US", openPreviewLabel = "Open preview", rowHovered = false) {
|
|
264
304
|
// Attio's own empty cell is genuinely blank -- no placeholder character at
|
|
265
305
|
// all, for any column type (#412).
|
|
266
306
|
const empty = null;
|
|
@@ -279,7 +319,10 @@ function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
|
279
319
|
// at a DIFFERENT record than the row itself (#501).
|
|
280
320
|
ev.stopPropagation();
|
|
281
321
|
col.onEntityClick?.(row);
|
|
282
|
-
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name }))
|
|
322
|
+
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name })), col.onOpenComments ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)(comments_1.CommentCountBadge, { count: col.commentCount?.(row) ?? 0, hovered: rowHovered, onOpen: () => col.onOpenComments?.(row), onOpenPreview: col.onOpenPreview ? () => col.onOpenPreview?.(row) : undefined, previewLabel: openPreviewLabel }) })) : (col.onOpenPreview && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-actions", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-action", "aria-label": openPreviewLabel, onClick: (ev) => {
|
|
323
|
+
ev.stopPropagation();
|
|
324
|
+
col.onOpenPreview?.(row);
|
|
325
|
+
}, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PanelRight, { "data-rtl-flip": "", size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) })))] }));
|
|
283
326
|
}
|
|
284
327
|
case "tags": {
|
|
285
328
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -452,7 +495,7 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
|
|
|
452
495
|
onCommit(v);
|
|
453
496
|
} }) }));
|
|
454
497
|
}
|
|
455
|
-
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel: selectAllLabelProp, selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, onEdit, stickyFirst = true, stickyLast = false, scrollProps, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel: addRowLabelProp, empty: emptyProp, showCount = true, countLabel, toolbar, height, className, reorderRowLabel: reorderRowLabelProp, sortAscendingLabel: sortAscendingLabelProp, sortDescendingLabel: sortDescendingLabelProp, moveColumnLeftLabel: moveColumnLeftLabelProp, moveColumnRightLabel: moveColumnRightLabelProp, editColumnLabel: editColumnLabelProp, hideColumnLabel: hideColumnLabelProp, renameColumnLabel: renameColumnLabelProp, viewSettingsLabel: viewSettingsLabelProp, columnsGroupLabel: columnsGroupLabelProp, ...rest }) {
|
|
498
|
+
function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel: selectAllLabelProp, selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement, columnMenu = false, onColumnsChange, columnOrder, onColumnOrderChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, onEdit, onViewEditHistory, stickyFirst = true, stickyLast = false, scrollProps, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel: addRowLabelProp, empty: emptyProp, showCount = true, countLabel, toolbar, height, className, reorderRowLabel: reorderRowLabelProp, openPreviewLabel: openPreviewLabelProp, sortAscendingLabel: sortAscendingLabelProp, sortDescendingLabel: sortDescendingLabelProp, moveColumnLeftLabel: moveColumnLeftLabelProp, moveColumnRightLabel: moveColumnRightLabelProp, editColumnLabel: editColumnLabelProp, hideColumnLabel: hideColumnLabelProp, renameColumnLabel: renameColumnLabelProp, viewSettingsLabel: viewSettingsLabelProp, columnsGroupLabel: columnsGroupLabelProp, cellMenuLabel: cellMenuLabelProp, copyLabel: copyLabelProp, pasteLabel: pasteLabelProp, viewEditHistoryLabel: viewEditHistoryLabelProp, clearValueLabel: clearValueLabelProp, ...rest }) {
|
|
456
499
|
// crisp/#471: same resolution order as TableToolbar's own — an explicit
|
|
457
500
|
// prop wins, then the provider's translation of the built-in English
|
|
458
501
|
// default, then that default itself with no provider in the tree.
|
|
@@ -469,6 +512,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
469
512
|
// wants shown); only the built-in string default translates.
|
|
470
513
|
const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
|
|
471
514
|
const reorderRowLabel = (0, core_1.useCrispMessage)(reorderRowLabelProp, "Reorder row");
|
|
515
|
+
const openPreviewLabel = (0, core_1.useCrispMessage)(openPreviewLabelProp, "Open preview");
|
|
472
516
|
const sortAscendingLabel = (0, core_1.useCrispMessage)(sortAscendingLabelProp, "Sort ascending");
|
|
473
517
|
const sortDescendingLabel = (0, core_1.useCrispMessage)(sortDescendingLabelProp, "Sort descending");
|
|
474
518
|
const moveColumnLeftLabel = (0, core_1.useCrispMessage)(moveColumnLeftLabelProp, "Move left");
|
|
@@ -478,6 +522,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
478
522
|
const renameColumnLabel = (0, core_1.useCrispMessage)(renameColumnLabelProp, "Rename column");
|
|
479
523
|
const viewSettingsLabel = (0, core_1.useCrispMessage)(viewSettingsLabelProp, "View settings");
|
|
480
524
|
const columnsGroupLabel = (0, core_1.useCrispMessage)(columnsGroupLabelProp, "Columns");
|
|
525
|
+
const cellMenuLabel = (0, core_1.useCrispMessage)(cellMenuLabelProp, "Cell options");
|
|
526
|
+
const copyLabel = (0, core_1.useCrispMessage)(copyLabelProp, "Copy");
|
|
527
|
+
const pasteLabel = (0, core_1.useCrispMessage)(pasteLabelProp, "Paste");
|
|
528
|
+
const viewEditHistoryLabel = (0, core_1.useCrispMessage)(viewEditHistoryLabelProp, "View edit history");
|
|
529
|
+
const clearValueLabel = (0, core_1.useCrispMessage)(clearValueLabelProp, "Clear value");
|
|
481
530
|
// countLabel/selectionAnnouncement stay formatter props, same escape hatch
|
|
482
531
|
// as TableToolbar's sortTriggerLabel — word order and pluralisation rules
|
|
483
532
|
// differ too much across languages for a template. Their BUILT-IN default
|
|
@@ -506,14 +555,32 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
506
555
|
// menu mutates this copy and reports upward via `onColumnsChange`.
|
|
507
556
|
const [internalCols, setInternalCols] = React.useState(columnsProp);
|
|
508
557
|
React.useEffect(() => setInternalCols(columnsProp), [columnsProp]);
|
|
509
|
-
/** Every column, hidden ones included — what the column manager lists.
|
|
510
|
-
|
|
558
|
+
/** Every column, hidden ones included — what the column manager lists.
|
|
559
|
+
* `columnOrder`, controlled or not, is the one source of truth for ORDER;
|
|
560
|
+
* `internalCols` still carries each column's hidden/renamed state, so
|
|
561
|
+
* resequencing it here never loses either. */
|
|
562
|
+
const allColumns = React.useMemo(() => reorderColumns(internalCols, columnOrder), [internalCols, columnOrder]);
|
|
511
563
|
/** What the grid actually renders. */
|
|
512
|
-
const columns = React.useMemo(() =>
|
|
564
|
+
const columns = React.useMemo(() => allColumns.filter((c) => !c.hidden), [allColumns]);
|
|
513
565
|
const applyColumns = React.useCallback((next) => {
|
|
514
566
|
setInternalCols(next);
|
|
515
567
|
onColumnsChange?.(next);
|
|
516
568
|
}, [onColumnsChange]);
|
|
569
|
+
// Reordering specifically (a header drag, or the menu's Move left/right) —
|
|
570
|
+
// separate from `applyColumns` above (hide/rename, which never touch
|
|
571
|
+
// order) because this is the one path that also owns `columnOrder`:
|
|
572
|
+
// uncontrolled, it seeds `internalCols`' own order same as any other
|
|
573
|
+
// mutation; controlled, the next render's `allColumns` re-derives order
|
|
574
|
+
// from the (now stale, until the consumer updates it) `columnOrder` prop
|
|
575
|
+
// instead — the same contract `sort`/`selectedIds` already use — so this
|
|
576
|
+
// deliberately does NOT touch `internalCols`' order when controlled.
|
|
577
|
+
const applyOrder = React.useCallback((nextOrder) => {
|
|
578
|
+
const next = reorderColumns(allColumns, nextOrder);
|
|
579
|
+
if (columnOrder === undefined)
|
|
580
|
+
setInternalCols(next);
|
|
581
|
+
onColumnsChange?.(next);
|
|
582
|
+
onColumnOrderChange?.(next.map((c) => c.key));
|
|
583
|
+
}, [allColumns, columnOrder, onColumnsChange, onColumnOrderChange]);
|
|
517
584
|
const moveColumn = React.useCallback((key, dir) => {
|
|
518
585
|
const i = allColumns.findIndex((c) => c.key === key);
|
|
519
586
|
const j = i + dir;
|
|
@@ -521,14 +588,72 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
521
588
|
return;
|
|
522
589
|
const next = [...allColumns];
|
|
523
590
|
[next[i], next[j]] = [next[j], next[i]];
|
|
524
|
-
|
|
525
|
-
}, [allColumns,
|
|
591
|
+
applyOrder(next.map((c) => c.key));
|
|
592
|
+
}, [allColumns, applyOrder]);
|
|
526
593
|
const hideColumn = React.useCallback((key) => applyColumns(allColumns.map((c) => (c.key === key ? { ...c, hidden: true } : c))), [allColumns, applyColumns]);
|
|
527
594
|
const renameColumn = React.useCallback((key, header) => applyColumns(allColumns.map((c) => (c.key === key ? { ...c, header } : c))), [allColumns, applyColumns]);
|
|
528
595
|
const [renaming, setRenaming] = React.useState(null);
|
|
529
596
|
const showColumn = React.useCallback((key) => applyColumns(allColumns.map((c) => (c.key === key ? { ...c, hidden: false } : c))), [allColumns, applyColumns]);
|
|
530
597
|
const [selCell, setSelCell] = React.useState(null);
|
|
598
|
+
// crisp/#625/#601 supersession: `CommentCountBadge` branches its own
|
|
599
|
+
// render (a plain dot vs. the icon+count row) on a `hovered` boolean it
|
|
600
|
+
// takes as a prop -- unlike the plain #601 icons this replaces, which
|
|
601
|
+
// reveal on pure `tr:hover` CSS, the badge has no hover detection of its
|
|
602
|
+
// own (see its own doc comment) and needs the row's hover state IN JS.
|
|
603
|
+
// A single id, not a Set: only one row is ever hovered at a time, so this
|
|
604
|
+
// is one extra re-render on enter/leave, not per-row state.
|
|
605
|
+
const [hoveredRowId, setHoveredRowId] = React.useState(null);
|
|
606
|
+
// crisp/#645, checklist G-101: a Shift+Click extends a RANGE from the last
|
|
607
|
+
// plain-click anchor to the newly clicked cell (spreadsheet convention --
|
|
608
|
+
// Attio's own trigger is "Click cell A, Shift+Click cell B"). `selAnchor`
|
|
609
|
+
// is the fixed corner; `selCell` (above) keeps doubling as the moving
|
|
610
|
+
// corner/focus cell, so every existing single-cell consumer (editing,
|
|
611
|
+
// arrow-key nav, Enter) is unaffected -- a plain click resets `selAnchor`
|
|
612
|
+
// to null, collapsing back to single-cell mode.
|
|
613
|
+
const [selAnchor, setSelAnchor] = React.useState(null);
|
|
531
614
|
const gridRef = React.useRef(null);
|
|
615
|
+
// The native-copy-event proxy (crisp/#645): Attio "has no Mousetrap/
|
|
616
|
+
// JS-keydown binding for c/mod+c ... it copies purely via the native
|
|
617
|
+
// browser copy event" -- confirmed live, `document.execCommand('copy')`
|
|
618
|
+
// on the focused element is what reliably fires it, a synthetic keydown
|
|
619
|
+
// does not. A plain focused `<div>` (what `gridRef` is) never receives a
|
|
620
|
+
// native `copy` event at all (no selection, nothing editable) -- verified
|
|
621
|
+
// empirically (Playwright, a focused non-editable div, real Ctrl+C: no
|
|
622
|
+
// `copy` event fires; the same page's `<input>` with selected text does).
|
|
623
|
+
// This hidden, always-selected textarea is what a spreadsheet-style grid
|
|
624
|
+
// needs to give the browser something copyable while the VISIBLE focus
|
|
625
|
+
// styling stays on the grid: it's the same visually-hidden-but-focusable
|
|
626
|
+
// shape as `.crisp-datatable-live` below, just interactive instead of
|
|
627
|
+
// `aria-live`.
|
|
628
|
+
const clipboardProxyRef = React.useRef(null);
|
|
629
|
+
// #579: the click-to-expand overlay for a truncated cell. Only one is ever
|
|
630
|
+
// open (a second click-to-expand elsewhere closes the first, matching "one
|
|
631
|
+
// live session"-style singular UI state already used for `selCell`).
|
|
632
|
+
const [expandedCell, setExpandedCell] = React.useState(null);
|
|
633
|
+
// Closes on any click outside the overlay itself (a click ON it is
|
|
634
|
+
// stopped from propagating, see the overlay's own onClick) and on
|
|
635
|
+
// Escape, kept for accessibility even though Attio's own Escape handling
|
|
636
|
+
// wasn't reliable when this was measured live (crisp#579).
|
|
637
|
+
React.useEffect(() => {
|
|
638
|
+
if (!expandedCell)
|
|
639
|
+
return;
|
|
640
|
+
const close = (e) => {
|
|
641
|
+
const target = e.target;
|
|
642
|
+
if (target?.closest?.(".crisp-datatable-cell-overlay"))
|
|
643
|
+
return;
|
|
644
|
+
setExpandedCell(null);
|
|
645
|
+
};
|
|
646
|
+
const onKey = (e) => {
|
|
647
|
+
if (e.key === "Escape")
|
|
648
|
+
setExpandedCell(null);
|
|
649
|
+
};
|
|
650
|
+
document.addEventListener("click", close, true);
|
|
651
|
+
document.addEventListener("keydown", onKey);
|
|
652
|
+
return () => {
|
|
653
|
+
document.removeEventListener("click", close, true);
|
|
654
|
+
document.removeEventListener("keydown", onKey);
|
|
655
|
+
};
|
|
656
|
+
}, [expandedCell]);
|
|
532
657
|
const sort = sortProp !== undefined ? sortProp : internalSort;
|
|
533
658
|
// One shape downstream: [] when unsorted, ordered by precedence otherwise.
|
|
534
659
|
const sorts = React.useMemo(() => (sort == null ? [] : Array.isArray(sort) ? sort : [sort]), [sort]);
|
|
@@ -644,6 +769,25 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
644
769
|
const [widths, setWidths] = React.useState({});
|
|
645
770
|
const [resizingKey, setResizingKey] = React.useState(null);
|
|
646
771
|
const resizeRef = React.useRef(null);
|
|
772
|
+
// crisp/#731 -- column drag-to-reorder. Pointer events, not native HTML5
|
|
773
|
+
// `draggable`: measured live on Attio's own grid (#731's issue comment) --
|
|
774
|
+
// `aria-roledescription="draggable"` etc. on every draggable header is
|
|
775
|
+
// dnd-kit's own signature, itself pointer-events-based, the same primitive
|
|
776
|
+
// `board.tsx` moved to in #724 (mousedown -> several mousemoves -> mouseup,
|
|
777
|
+
// no `[draggable]` anywhere). Mirrors that fix's own shape: a ref for the
|
|
778
|
+
// in-flight gesture (so a pointermove before the 2px threshold never
|
|
779
|
+
// re-renders), React state for what actually needs to paint.
|
|
780
|
+
const [dragColKey, setDragColKey] = React.useState(null);
|
|
781
|
+
const [dropCol, setDropCol] = React.useState(null);
|
|
782
|
+
const colDragRef = React.useRef(null);
|
|
783
|
+
// Keyed by column key, not index -- #577's own column virtualization means
|
|
784
|
+
// an off-screen column's `TableHead` unmounts entirely (confirmed live:
|
|
785
|
+
// Attio does the same, a column scrolled far enough away disappears from
|
|
786
|
+
// the DOM), so this only ever holds the currently-mounted ones, same as
|
|
787
|
+
// `board.tsx`'s `colRefs`.
|
|
788
|
+
const colRefs = React.useRef(new Map());
|
|
789
|
+
const autoScrollRef = React.useRef(null);
|
|
790
|
+
const COL_DRAG_THRESHOLD = 2; // crisp/#736 review fix: measured ~2px, not 4px
|
|
647
791
|
const colWidth = (col) => widths[col.key] ?? col.width ?? (col.type === "entity" ? 220 : 160);
|
|
648
792
|
const minWidth = (showGrip ? GRIP_W : 0) +
|
|
649
793
|
(selectable ? CHECK_W : 0) +
|
|
@@ -892,21 +1036,166 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
892
1036
|
return { type: "text", inputMode: "text" };
|
|
893
1037
|
}
|
|
894
1038
|
}
|
|
1039
|
+
/** The rectangular range's index bounds (inclusive), or null outside range
|
|
1040
|
+
* mode (`selAnchor` unset -- single-cell selection, the common case).
|
|
1041
|
+
* Indices, not ids/keys, so it always reads against the CURRENT rendered
|
|
1042
|
+
* `rows`/`columns` order -- the same order arrow-key nav already uses. */
|
|
1043
|
+
function getSelRangeIndices() {
|
|
1044
|
+
if (!selAnchor || !selCell)
|
|
1045
|
+
return null;
|
|
1046
|
+
const ar = rows.findIndex((row) => row.id === selAnchor.id);
|
|
1047
|
+
const ac = columns.findIndex((col) => col.key === selAnchor.key);
|
|
1048
|
+
const fr = rows.findIndex((row) => row.id === selCell.id);
|
|
1049
|
+
const fc = columns.findIndex((col) => col.key === selCell.key);
|
|
1050
|
+
if (ar < 0 || ac < 0 || fr < 0 || fc < 0)
|
|
1051
|
+
return null;
|
|
1052
|
+
return {
|
|
1053
|
+
r0: Math.min(ar, fr),
|
|
1054
|
+
r1: Math.max(ar, fr),
|
|
1055
|
+
c0: Math.min(ac, fc),
|
|
1056
|
+
c1: Math.max(ac, fc),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* crisp/#645: Attio copies the RAW, unformatted value (`asText`, not
|
|
1061
|
+
* `formatCell` -- G-070: a formatted "28,946,065" cell copies as raw
|
|
1062
|
+
* "28946065"), writes nothing at all for a `readOnly`/computed cell
|
|
1063
|
+
* (G-100 -- no clipboard write occurs, not an empty string), and a
|
|
1064
|
+
* Shift+Click range copies as TSV: `\t` between columns, `\r\n` between
|
|
1065
|
+
* rows, no header row, raw values (G-101). Returns null for "no clipboard
|
|
1066
|
+
* write" (the single-readOnly-cell case, or nothing selected) rather than
|
|
1067
|
+
* "" -- the caller must not fall back to writing an empty string either.
|
|
1068
|
+
*/
|
|
1069
|
+
function buildCopyText() {
|
|
1070
|
+
const range = getSelRangeIndices();
|
|
1071
|
+
if (range) {
|
|
1072
|
+
const { r0, r1, c0, c1 } = range;
|
|
1073
|
+
const lines = [];
|
|
1074
|
+
for (let ri = r0; ri <= r1; ri++) {
|
|
1075
|
+
const row = rows[ri];
|
|
1076
|
+
const cells = [];
|
|
1077
|
+
for (let ci = c0; ci <= c1; ci++) {
|
|
1078
|
+
const col = columns[ci];
|
|
1079
|
+
cells.push(col.readOnly ? "" : asText(cellValue(col, row)));
|
|
1080
|
+
}
|
|
1081
|
+
lines.push(cells.join("\t"));
|
|
1082
|
+
}
|
|
1083
|
+
return lines.join("\r\n");
|
|
1084
|
+
}
|
|
1085
|
+
if (!selCell)
|
|
1086
|
+
return null;
|
|
1087
|
+
const row = rows.find((rw) => rw.id === selCell.id);
|
|
1088
|
+
const col = columns.find((cc) => cc.key === selCell.key);
|
|
1089
|
+
if (!row || !col || col.readOnly)
|
|
1090
|
+
return null;
|
|
1091
|
+
return asText(cellValue(col, row));
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* crisp/#645: the native `copy`/`paste` events, not a keydown handler --
|
|
1095
|
+
* Attio "has no Mousetrap/JS-keydown binding for c/mod+c ... it copies
|
|
1096
|
+
* purely via the native browser copy event" (confirmed live). These fire
|
|
1097
|
+
* on `clipboardProxyRef`'s hidden textarea (see its own comment), which
|
|
1098
|
+
* `onGridKeyDown` below no longer intercepts Cmd/Ctrl+C for.
|
|
1099
|
+
*/
|
|
1100
|
+
function onGridCopy(e) {
|
|
1101
|
+
e.preventDefault();
|
|
1102
|
+
const text = buildCopyText();
|
|
1103
|
+
if (text != null)
|
|
1104
|
+
e.clipboardData.setData("text/plain", text);
|
|
1105
|
+
}
|
|
1106
|
+
/** crisp/#598, G-071: Attio's own context-menu Paste "looked like it would
|
|
1107
|
+
* commit immediately with no safe cancel step" -- matched here: paste
|
|
1108
|
+
* commits straight to `onEdit`, it doesn't open the cell's editor first.
|
|
1109
|
+
* Single-cell only -- a range-fill paste isn't measured (G-071 was
|
|
1110
|
+
* deliberately not exercised against real clipboard content), so it isn't
|
|
1111
|
+
* built rather than guessed. */
|
|
1112
|
+
function onGridPaste(e) {
|
|
1113
|
+
e.preventDefault();
|
|
1114
|
+
if (!selCell || editing)
|
|
1115
|
+
return;
|
|
1116
|
+
const col = columns.find((cc) => cc.key === selCell.key);
|
|
1117
|
+
if (!col?.editable || col.readOnly)
|
|
1118
|
+
return;
|
|
1119
|
+
const text = e.clipboardData.getData("text/plain");
|
|
1120
|
+
onEdit?.(selCell.id, selCell.key, text);
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* crisp/#598, checklist G-043: Attio's cell context menu is "identical
|
|
1124
|
+
* regardless of field type" — Copy, Paste, View edit history, Clear
|
|
1125
|
+
* value, every time, just some items disabled. No row-level menu exists
|
|
1126
|
+
* at all (G-044 — RecordsTable's own Copy/Duplicate/Pin/Delete row menu
|
|
1127
|
+
* was dropped rather than carried onto DataTable, per the consolidation
|
|
1128
|
+
* plan's own decision: Attio has no row menu to match).
|
|
1129
|
+
*/
|
|
1130
|
+
function cellMenuItems(row, col) {
|
|
1131
|
+
return [
|
|
1132
|
+
{
|
|
1133
|
+
label: copyLabel,
|
|
1134
|
+
icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 16, strokeWidth: 1.75 }),
|
|
1135
|
+
// G-100: Attio has "no copy handler at all" for a computed/
|
|
1136
|
+
// read-only field -- disabled here, not just silently a no-op, so
|
|
1137
|
+
// the menu itself tells the reader why.
|
|
1138
|
+
disabled: !!col.readOnly,
|
|
1139
|
+
onSelect: () => {
|
|
1140
|
+
if (col.readOnly)
|
|
1141
|
+
return;
|
|
1142
|
+
navigator.clipboard
|
|
1143
|
+
?.writeText(asText(cellValue(col, row)))
|
|
1144
|
+
.catch(() => { });
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
label: pasteLabel,
|
|
1149
|
+
icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ClipboardPaste, { size: 16, strokeWidth: 1.75 }),
|
|
1150
|
+
disabled: !col.editable || !!col.readOnly,
|
|
1151
|
+
onSelect: () => {
|
|
1152
|
+
navigator.clipboard
|
|
1153
|
+
?.readText()
|
|
1154
|
+
.then((text) => onEdit?.(row.id, col.key, text))
|
|
1155
|
+
.catch(() => { });
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
{ separator: true },
|
|
1159
|
+
{
|
|
1160
|
+
label: viewEditHistoryLabel,
|
|
1161
|
+
icon: (0, jsx_runtime_1.jsx)(lucide_react_1.History, { size: 16, strokeWidth: 1.75 }),
|
|
1162
|
+
// Attio's own item carries a "Pro" pill (crisp has no billing
|
|
1163
|
+
// concept to gate on, see this prop's own doc comment) -- disabled
|
|
1164
|
+
// only when the consumer gave nothing for it to open, not gated on
|
|
1165
|
+
// a plan crisp doesn't model.
|
|
1166
|
+
disabled: !onViewEditHistory,
|
|
1167
|
+
onSelect: () => onViewEditHistory?.(row.id, col.key),
|
|
1168
|
+
},
|
|
1169
|
+
{ separator: true },
|
|
1170
|
+
{
|
|
1171
|
+
label: clearValueLabel,
|
|
1172
|
+
icon: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 1.75 }),
|
|
1173
|
+
danger: true,
|
|
1174
|
+
disabled: !col.editable,
|
|
1175
|
+
onSelect: () => onEdit?.(row.id, col.key, ""),
|
|
1176
|
+
},
|
|
1177
|
+
];
|
|
1178
|
+
}
|
|
895
1179
|
/**
|
|
896
1180
|
* Spreadsheet keyboard model, ported from RecordsTable: arrows move the
|
|
897
1181
|
* selected cell, Enter opens its editor (or, on a non-editable cell,
|
|
898
|
-
* activates the row via `onRowClick`), Escape clears
|
|
899
|
-
*
|
|
1182
|
+
* activates the row via `onRowClick`), Escape clears. Cmd/Ctrl+C is
|
|
1183
|
+
* deliberately NOT handled here (crisp/#645) -- copy is the native `copy`
|
|
1184
|
+
* event on `clipboardProxyRef`, see `onGridCopy` above.
|
|
900
1185
|
*/
|
|
901
1186
|
function onGridKeyDown(e) {
|
|
902
1187
|
if (!selCell || editing)
|
|
903
1188
|
return;
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1189
|
+
// crisp/#607, G-069: Attio's own Cmd+A selects every row (confirmed
|
|
1190
|
+
// live, `metaKey:true`, all 37/37 rows, header checkbox fully checked)
|
|
1191
|
+
// -- a real SELECT ALL, not `toggleAll`'s flip-if-already-all-selected,
|
|
1192
|
+
// so a second Cmd+A with everything already picked stays a no-op
|
|
1193
|
+
// instead of clearing the selection out from under the user.
|
|
1194
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "a" || e.key === "A")) {
|
|
1195
|
+
if (!selectable)
|
|
1196
|
+
return;
|
|
909
1197
|
e.preventDefault();
|
|
1198
|
+
setSelected(new Set(rows.map((rw) => rw.id)));
|
|
910
1199
|
return;
|
|
911
1200
|
}
|
|
912
1201
|
const r = rows.findIndex((row) => row.id === selCell.id);
|
|
@@ -926,8 +1215,27 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
926
1215
|
const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
927
1216
|
nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
|
|
928
1217
|
}
|
|
1218
|
+
// crisp/#607, G-066/G-068: End jumps to the last row (confirmed live --
|
|
1219
|
+
// it also triggers Attio's own lazy-load, which is a data-loading
|
|
1220
|
+
// concern outside this component; the #577 scroll-into-view effect
|
|
1221
|
+
// below already re-runs on every `selCell` change, on a virtualized
|
|
1222
|
+
// grid too, so jumping straight to the last row is enough to reveal it
|
|
1223
|
+
// the same way arrowing down one row at a time would). PageUp/PageDown
|
|
1224
|
+
// move a viewport's worth of rows -- `scrollRef`'s own rendered height
|
|
1225
|
+
// divided by the uniform row height, at least one row so a viewport
|
|
1226
|
+
// shorter than one row can't get stuck.
|
|
1227
|
+
else if (e.key === "End")
|
|
1228
|
+
nr = rows.length - 1;
|
|
1229
|
+
else if (e.key === "PageDown" || e.key === "PageUp") {
|
|
1230
|
+
const viewportRows = Math.max(1, Math.floor((scrollRef.current?.clientHeight ?? 0) / ROW_HEIGHT));
|
|
1231
|
+
nr =
|
|
1232
|
+
e.key === "PageDown"
|
|
1233
|
+
? Math.min(rows.length - 1, r + viewportRows)
|
|
1234
|
+
: Math.max(0, r - viewportRows);
|
|
1235
|
+
}
|
|
929
1236
|
else if (e.key === "Escape") {
|
|
930
1237
|
setSelCell(null);
|
|
1238
|
+
setSelAnchor(null);
|
|
931
1239
|
return;
|
|
932
1240
|
}
|
|
933
1241
|
else if (e.key === "Enter") {
|
|
@@ -949,7 +1257,16 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
949
1257
|
else
|
|
950
1258
|
return;
|
|
951
1259
|
e.preventDefault();
|
|
1260
|
+
// A plain arrow key moves the single-cell selection and drops any active
|
|
1261
|
+
// range -- Attio's own range trigger is Shift+Click only (G-101), arrow
|
|
1262
|
+
// keys were never measured as a range-extend gesture, so this doesn't
|
|
1263
|
+
// invent one.
|
|
1264
|
+
setSelAnchor(null);
|
|
952
1265
|
setSelCell({ id: rows[nr].id, key: columns[nc].key });
|
|
1266
|
+
// keeps the clipboard proxy's selection non-collapsed after a move, so
|
|
1267
|
+
// a Cmd+C right after an arrow key still has something native `copy`
|
|
1268
|
+
// can fire on (see `clipboardProxyRef`'s own comment).
|
|
1269
|
+
clipboardProxyRef.current?.select();
|
|
953
1270
|
}
|
|
954
1271
|
// Opening a cell's editor also clears any stale error left over from a
|
|
955
1272
|
// PREVIOUS failed commit on it (#385) -- otherwise a rejection from three
|
|
@@ -957,6 +1274,10 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
957
1274
|
function openEditing(id, key) {
|
|
958
1275
|
setEditError(null);
|
|
959
1276
|
setEditing({ id, key });
|
|
1277
|
+
// #579: the editing branch pre-empts the plain-cell render (and its
|
|
1278
|
+
// overlay) entirely, but `expandedCell` itself would survive a later
|
|
1279
|
+
// cancel-back-to-plain-view unless cleared here too.
|
|
1280
|
+
setExpandedCell(null);
|
|
960
1281
|
}
|
|
961
1282
|
// Async-aware (#385): a sync `onEdit` (returns undefined) commits exactly
|
|
962
1283
|
// as before -- close the editor immediately. A Promise-returning `onEdit`
|
|
@@ -1057,7 +1378,8 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1057
1378
|
renderItems,
|
|
1058
1379
|
]);
|
|
1059
1380
|
const totalCols = columns.length + (selectable ? 1 : 0) + (showGrip ? 1 : 0);
|
|
1060
|
-
const hasSummary = columns.some((c) => c.summary);
|
|
1381
|
+
const hasSummary = columns.some((c) => c.summary || c.calc);
|
|
1382
|
+
const hasCommentColumn = columns.some((c) => c.onOpenComments);
|
|
1061
1383
|
// sticky attrs for the leading column(s): grip col (index -2) + selection
|
|
1062
1384
|
// col (-1) + first data column (0). Each offsets by whatever leading
|
|
1063
1385
|
// columns come before it, so the group stays contiguous when scrolled.
|
|
@@ -1154,13 +1476,135 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1154
1476
|
setResizingKey(null);
|
|
1155
1477
|
e.target.releasePointerCapture(e.pointerId);
|
|
1156
1478
|
}
|
|
1479
|
+
// Which mounted column (if any) `clientX` is over, and which side of it --
|
|
1480
|
+
// X only, not the full box: like `board.tsx`'s own `columnAt` (#724), the
|
|
1481
|
+
// header row is a single non-wrapping row, so X alone already identifies a
|
|
1482
|
+
// column unambiguously, and checking Y too only risks the same short-row
|
|
1483
|
+
// miss that #724 found the hard way. The midpoint split is what turns
|
|
1484
|
+
// "over column C" into an insertion point: left half means "before C",
|
|
1485
|
+
// right half means "after C" -- exposed as `dropCol` so the render can draw
|
|
1486
|
+
// the measured blue line on the correct edge.
|
|
1487
|
+
function columnDropAt(clientX) {
|
|
1488
|
+
for (const [key, el] of colRefs.current) {
|
|
1489
|
+
const r = el.getBoundingClientRect();
|
|
1490
|
+
if (clientX >= r.left && clientX <= r.right) {
|
|
1491
|
+
return {
|
|
1492
|
+
key,
|
|
1493
|
+
side: clientX < r.left + r.width / 2 ? "before" : "after",
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
function stopColAutoScroll() {
|
|
1500
|
+
if (autoScrollRef.current != null) {
|
|
1501
|
+
cancelAnimationFrame(autoScrollRef.current);
|
|
1502
|
+
autoScrollRef.current = null;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// Edge auto-scroll (measured live, #731's issue comment: dragging a header
|
|
1506
|
+
// toward the viewport edge scrolls the grid to reveal more columns). A
|
|
1507
|
+
// `requestAnimationFrame` loop, not a one-shot nudge per pointermove: the
|
|
1508
|
+
// scroll has to keep going even while the pointer itself sits still near
|
|
1509
|
+
// the edge, which a per-event nudge can't do. Guarded for environments with
|
|
1510
|
+
// no `requestAnimationFrame` (bun test's happy-dom) -- never throws, just a
|
|
1511
|
+
// no-op there, same as the `ResizeObserver` guards elsewhere in this file.
|
|
1512
|
+
function updateColAutoScroll(clientX) {
|
|
1513
|
+
if (typeof requestAnimationFrame === "undefined")
|
|
1514
|
+
return;
|
|
1515
|
+
const el = scrollRef.current;
|
|
1516
|
+
if (!el)
|
|
1517
|
+
return;
|
|
1518
|
+
const r = el.getBoundingClientRect();
|
|
1519
|
+
const EDGE = 40;
|
|
1520
|
+
const dir = clientX < r.left + EDGE ? -1 : clientX > r.right - EDGE ? 1 : 0;
|
|
1521
|
+
stopColAutoScroll();
|
|
1522
|
+
if (dir === 0)
|
|
1523
|
+
return;
|
|
1524
|
+
const step = () => {
|
|
1525
|
+
el.scrollLeft += dir * 12;
|
|
1526
|
+
autoScrollRef.current = requestAnimationFrame(step);
|
|
1527
|
+
};
|
|
1528
|
+
autoScrollRef.current = requestAnimationFrame(step);
|
|
1529
|
+
}
|
|
1530
|
+
function onColHeaderPointerDown(e, key) {
|
|
1531
|
+
if (!e.isPrimary)
|
|
1532
|
+
return;
|
|
1533
|
+
colDragRef.current = {
|
|
1534
|
+
key,
|
|
1535
|
+
pointerId: e.pointerId,
|
|
1536
|
+
startX: e.clientX,
|
|
1537
|
+
startY: e.clientY,
|
|
1538
|
+
active: false,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function onColHeaderPointerMove(e) {
|
|
1542
|
+
const st = colDragRef.current;
|
|
1543
|
+
if (!st || st.pointerId !== e.pointerId)
|
|
1544
|
+
return;
|
|
1545
|
+
if (!st.active) {
|
|
1546
|
+
if (Math.hypot(e.clientX - st.startX, e.clientY - st.startY) <
|
|
1547
|
+
COL_DRAG_THRESHOLD)
|
|
1548
|
+
return;
|
|
1549
|
+
st.active = true;
|
|
1550
|
+
setDragColKey(st.key);
|
|
1551
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1552
|
+
}
|
|
1553
|
+
const at = columnDropAt(e.clientX);
|
|
1554
|
+
setDropCol(at && at.key !== st.key ? at : null);
|
|
1555
|
+
updateColAutoScroll(e.clientX);
|
|
1556
|
+
}
|
|
1557
|
+
function endColHeaderDrag(e, commit) {
|
|
1558
|
+
const st = colDragRef.current;
|
|
1559
|
+
if (!st || st.pointerId !== e.pointerId)
|
|
1560
|
+
return;
|
|
1561
|
+
if (commit && st.active && dropCol) {
|
|
1562
|
+
const keys = allColumns.map((c) => c.key);
|
|
1563
|
+
const from = keys.indexOf(st.key);
|
|
1564
|
+
const to = keys.indexOf(dropCol.key);
|
|
1565
|
+
if (from >= 0 && to >= 0) {
|
|
1566
|
+
keys.splice(from, 1);
|
|
1567
|
+
let insertAt = keys.indexOf(dropCol.key);
|
|
1568
|
+
if (dropCol.side === "after")
|
|
1569
|
+
insertAt += 1;
|
|
1570
|
+
keys.splice(insertAt, 0, st.key);
|
|
1571
|
+
applyOrder(keys);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
colDragRef.current = null;
|
|
1575
|
+
setDragColKey(null);
|
|
1576
|
+
setDropCol(null);
|
|
1577
|
+
stopColAutoScroll();
|
|
1578
|
+
}
|
|
1579
|
+
// Escape cancels the drag without committing (measured live, #731's issue
|
|
1580
|
+
// comment: verified twice against the real grid) -- a document listener,
|
|
1581
|
+
// not a React `onKeyDown` on the header, for the same reason
|
|
1582
|
+
// `useEscapeExitsEditing` above uses one: focus during a pointer drag isn't
|
|
1583
|
+
// reliably inside the dragged element.
|
|
1584
|
+
React.useEffect(() => {
|
|
1585
|
+
if (!dragColKey)
|
|
1586
|
+
return;
|
|
1587
|
+
const onKey = (e) => {
|
|
1588
|
+
if (e.key !== "Escape")
|
|
1589
|
+
return;
|
|
1590
|
+
colDragRef.current = null;
|
|
1591
|
+
setDragColKey(null);
|
|
1592
|
+
setDropCol(null);
|
|
1593
|
+
if (autoScrollRef.current != null) {
|
|
1594
|
+
cancelAnimationFrame(autoScrollRef.current);
|
|
1595
|
+
autoScrollRef.current = null;
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
document.addEventListener("keydown", onKey);
|
|
1599
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1600
|
+
}, [dragColKey]);
|
|
1157
1601
|
function ChevronFor({ colKey }) {
|
|
1158
1602
|
const dir = sortDirByKey.get(colKey);
|
|
1159
1603
|
if (dir)
|
|
1160
1604
|
return dir === "asc" ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
1161
1605
|
return ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
|
|
1162
1606
|
}
|
|
1163
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface,
|
|
1607
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (0, jsx_runtime_1.jsx)("textarea", { ref: clipboardProxyRef, className: "crisp-datatable-clipboard-proxy", readOnly: true, value: " ", "aria-label": cellMenuLabel, tabIndex: -1, onCopy: onGridCopy, onPaste: onGridPaste }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface,
|
|
1164
1608
|
// crisp/#581: on the <table> itself (`tableStyle`), not the outer
|
|
1165
1609
|
// wrapper (`style`) -- see `Table`'s own doc comment on why. This is
|
|
1166
1610
|
// what lets the grid actually scroll horizontally INSIDE its own
|
|
@@ -1212,7 +1656,17 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1212
1656
|
// aria-sort is specified as one header per table, and with the
|
|
1213
1657
|
// multi-sort the `sort` prop allows, a second "ascending" says
|
|
1214
1658
|
// nothing about which column came first.
|
|
1215
|
-
|
|
1659
|
+
// crisp/#605: the primary/entity column is inert -- no menu,
|
|
1660
|
+
// no click-to-sort, no aria-sort claim -- matching Attio's
|
|
1661
|
+
// own measured behaviour exactly ("no popover opens, no
|
|
1662
|
+
// right-click menu either... primary column is fixed by
|
|
1663
|
+
// construction, not a menu target", checklist G-023). A
|
|
1664
|
+
// consumer setting `sortable`/`columnMenu` on an entity
|
|
1665
|
+
// column doesn't get a sort affordance there; this is
|
|
1666
|
+
// `!isPrimary` gating every interactive branch below, not
|
|
1667
|
+
// just the menu.
|
|
1668
|
+
const isPrimary = col.type === "entity";
|
|
1669
|
+
const sortable = !isPrimary && (col.sortable || !!columnMenu);
|
|
1216
1670
|
const ariaSort = !sortable
|
|
1217
1671
|
? undefined
|
|
1218
1672
|
: sorts[0]?.key === col.key
|
|
@@ -1221,7 +1675,30 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1221
1675
|
: "descending"
|
|
1222
1676
|
: "none";
|
|
1223
1677
|
const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)(ThLabel, { header: col.header, referencedType: col.referencedType }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
|
|
1224
|
-
return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, {
|
|
1678
|
+
return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { ref: isPrimary
|
|
1679
|
+
? undefined
|
|
1680
|
+
: (el) => {
|
|
1681
|
+
if (el)
|
|
1682
|
+
colRefs.current.set(col.key, el);
|
|
1683
|
+
else
|
|
1684
|
+
colRefs.current.delete(col.key);
|
|
1685
|
+
}, align: alignFor(col), "aria-sort": ariaSort, "aria-colindex": colAriaIndex(i), "data-dragging": dragColKey === col.key ? "" : undefined, "data-drop-before": dropCol?.key === col.key && dropCol.side === "before"
|
|
1686
|
+
? ""
|
|
1687
|
+
: undefined, "data-drop-after": dropCol?.key === col.key && dropCol.side === "after"
|
|
1688
|
+
? ""
|
|
1689
|
+
: undefined,
|
|
1690
|
+
// #731: column drag-to-reorder lives on the TH itself, not
|
|
1691
|
+
// a dedicated grip -- Attio's own header is draggable by
|
|
1692
|
+
// its whole surface (measured live). A plain click (no
|
|
1693
|
+
// movement past the 2px threshold) never calls
|
|
1694
|
+
// `setPointerCapture`/`preventDefault`, so the sort
|
|
1695
|
+
// button/menu trigger/resize handle nested inside still get
|
|
1696
|
+
// their own click normally; `startResize`'s own
|
|
1697
|
+
// `stopPropagation` (above) keeps a resize drag from ever
|
|
1698
|
+
// reaching this handler in the first place.
|
|
1699
|
+
onPointerDown: isPrimary
|
|
1700
|
+
? undefined
|
|
1701
|
+
: (e) => onColHeaderPointerDown(e, col.key), onPointerMove: isPrimary ? undefined : onColHeaderPointerMove, onPointerUp: isPrimary ? undefined : (e) => endColHeaderDrag(e, true), onPointerCancel: isPrimary ? undefined : (e) => endColHeaderDrag(e, false), ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
|
|
1225
1702
|
// biome-ignore lint/a11y/noAutofocus: the rename item opens this field
|
|
1226
1703
|
autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
|
|
1227
1704
|
renameColumn(col.key, e.currentTarget.value);
|
|
@@ -1233,9 +1710,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1233
1710
|
}
|
|
1234
1711
|
else if (e.key === "Escape")
|
|
1235
1712
|
setRenaming(null);
|
|
1236
|
-
} })) : columnMenu ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
|
|
1713
|
+
} })) : columnMenu && !isPrimary ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
|
|
1237
1714
|
? col.header
|
|
1238
|
-
: col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
|
|
1715
|
+
: col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable && !isPrimary ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.readOnly && ((0, jsx_runtime_1.jsx)(lucide_react_1.Lock, { className: "crisp-datatable-readonly", size: 12, strokeWidth: 1.75, "aria-hidden": "true" })), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
|
|
1239
1716
|
})] }) }), (0, jsx_runtime_1.jsx)(table_1.TableBody, { children: error ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: empty }) }) })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [topSpacerHeight > 0 && ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("td", { colSpan: totalCols, style: {
|
|
1240
1717
|
height: topSpacerHeight,
|
|
1241
1718
|
padding: 0,
|
|
@@ -1253,7 +1730,9 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1253
1730
|
dragRowId !== row.id &&
|
|
1254
1731
|
dropRowId === row.id
|
|
1255
1732
|
? ""
|
|
1256
|
-
: undefined, onClick: onRowClick ? () => onRowClick(row) : undefined, style: onRowClick ? { cursor: "pointer" } : undefined,
|
|
1733
|
+
: undefined, onClick: onRowClick ? () => onRowClick(row) : undefined, style: onRowClick ? { cursor: "pointer" } : undefined, onMouseEnter: hasCommentColumn
|
|
1734
|
+
? () => setHoveredRowId(row.id)
|
|
1735
|
+
: undefined, onMouseLeave: hasCommentColumn ? () => setHoveredRowId(null) : undefined, onDragOver: showGrip && dragRowId
|
|
1257
1736
|
? (e) => {
|
|
1258
1737
|
e.preventDefault();
|
|
1259
1738
|
if (dropRowId !== row.id)
|
|
@@ -1337,29 +1816,99 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
1337
1816
|
commitEdit(row.id, col.key, e.currentTarget.value);
|
|
1338
1817
|
} }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-datatable-edit-pending" }))] }, col.key));
|
|
1339
1818
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1819
|
+
// `itemIndex` is the row's position in the full
|
|
1820
|
+
// `rows` array (getSelRangeIndices' own r0/r1 are
|
|
1821
|
+
// indices into that same array) for the common,
|
|
1822
|
+
// ungrouped case. A `groupBy` view interleaves group-
|
|
1823
|
+
// header items into `visibleItems`, which this
|
|
1824
|
+
// doesn't correct for -- Shift+Click ranging across a
|
|
1825
|
+
// grouped view isn't a measured Attio interaction
|
|
1826
|
+
// (G-101's own trigger is a plain table), so it's a
|
|
1827
|
+
// known gap rather than a guess at behaviour there.
|
|
1828
|
+
const inRange = (() => {
|
|
1829
|
+
const range = getSelRangeIndices();
|
|
1830
|
+
if (!range)
|
|
1831
|
+
return false;
|
|
1832
|
+
return (itemIndex >= range.r0 &&
|
|
1833
|
+
itemIndex <= range.r1 &&
|
|
1834
|
+
i >= range.c0 &&
|
|
1835
|
+
i <= range.c1);
|
|
1836
|
+
})();
|
|
1837
|
+
const rendered = col.render
|
|
1838
|
+
? col.render(value, row)
|
|
1839
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale, openPreviewLabel, hoveredRowId === row.id);
|
|
1840
|
+
const isExpanded = expandedCell?.id === row.id &&
|
|
1841
|
+
expandedCell?.key === col.key;
|
|
1842
|
+
const cellContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isExpanded && rendered, errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere })), isExpanded && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-cell-overlay", onClick: (e) => e.stopPropagation(), children: rendered }))] }));
|
|
1843
|
+
return ((0, jsx_runtime_1.jsx)(contextmenu_1.ContextMenu, { asChild: true, variant: "dark", menuLabel: cellMenuLabel, items: cellMenuItems(row, col), children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), "aria-colindex": colAriaIndex(i), ...sticky, className: col.editable
|
|
1844
|
+
? "crisp-datatable-editable"
|
|
1845
|
+
: undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
|
|
1846
|
+
? ""
|
|
1847
|
+
: undefined, "data-in-range": inRange ? "" : undefined, "data-cell-expanded": isExpanded ? "" : undefined, onClick: (e) => {
|
|
1848
|
+
// the keyboard model's anchor -- a single click
|
|
1849
|
+
// only selects. A genuine double-click (or Enter
|
|
1850
|
+
// on the selected cell) opens the editor (#411),
|
|
1851
|
+
// matching RecordsTable's own onDoubleClick.
|
|
1852
|
+
// crisp/#645, G-101: Shift+Click extends a
|
|
1853
|
+
// range from the last plain-click anchor
|
|
1854
|
+
// ("Click cell A, Shift+Click cell B") --
|
|
1855
|
+
// `selCell` keeps doubling as the moving
|
|
1856
|
+
// corner, `selAnchor` is the fixed one.
|
|
1857
|
+
if (e.shiftKey && selCell) {
|
|
1858
|
+
setSelAnchor(selAnchor ?? selCell);
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
setSelAnchor(null);
|
|
1862
|
+
}
|
|
1863
|
+
setSelCell({ id: row.id, key: col.key });
|
|
1864
|
+
// Focuses the clipboard proxy, not `gridRef`
|
|
1865
|
+
// itself (crisp/#645) -- it's a DESCENDANT of
|
|
1866
|
+
// `gridRef`, so `onGridKeyDown` still sees
|
|
1867
|
+
// every key via bubbling, but now the browser
|
|
1868
|
+
// also has a focused, selectable text node to
|
|
1869
|
+
// fire a native `copy`/`paste` on. `focus()`
|
|
1870
|
+
// then `select()`, not `select()` alone --
|
|
1871
|
+
// `select()` only guarantees the SELECTION,
|
|
1872
|
+
// focus is a side effect some engines don't
|
|
1873
|
+
// give it for free.
|
|
1874
|
+
clipboardProxyRef.current?.focus();
|
|
1875
|
+
clipboardProxyRef.current?.select();
|
|
1876
|
+
// #579: a truncated cell also opens a read-only
|
|
1877
|
+
// overlay with its full value -- settled live
|
|
1878
|
+
// that this is click, not Attio's usual
|
|
1879
|
+
// hover-expand (a 1s hover on a truncated
|
|
1880
|
+
// Description cell produced nothing). Measured
|
|
1881
|
+
// the same way as `ThLabel`'s header tooltip
|
|
1882
|
+
// (#534): scrollWidth vs clientWidth on the
|
|
1883
|
+
// element the CSS ellipsis actually applies to,
|
|
1884
|
+
// which for a data cell is the `td` itself
|
|
1885
|
+
// (table.recipe.ts's own `overflow: hidden;
|
|
1886
|
+
// text-overflow: ellipsis`), not a nested span.
|
|
1887
|
+
// Skipped on a Shift+Click (crisp/#645) -- that
|
|
1888
|
+
// gesture's own intent is extending the range,
|
|
1889
|
+
// not popping open a read-only overlay on top
|
|
1890
|
+
// of it.
|
|
1891
|
+
setExpandedCell(!e.shiftKey && isLabelTruncated(e.currentTarget)
|
|
1892
|
+
? { id: row.id, key: col.key }
|
|
1893
|
+
: null);
|
|
1894
|
+
}, onDoubleClick: col.editable
|
|
1895
|
+
? () => openEditing(row.id, col.key)
|
|
1896
|
+
: undefined, onContextMenu: () => {
|
|
1897
|
+
// Right-clicking targets THAT cell for the
|
|
1898
|
+
// menu regardless of what was previously
|
|
1899
|
+
// selected (crisp/#598) -- collapses any
|
|
1900
|
+
// active range, same as a plain left-click.
|
|
1901
|
+
setSelAnchor(null);
|
|
1902
|
+
setSelCell({ id: row.id, key: col.key });
|
|
1903
|
+
clipboardProxyRef.current?.focus();
|
|
1904
|
+
clipboardProxyRef.current?.select();
|
|
1905
|
+
}, children: cellContent }) }, col.key));
|
|
1356
1906
|
})] }, row.id));
|
|
1357
1907
|
}), bottomSpacerHeight > 0 && ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("td", { colSpan: totalCols, style: {
|
|
1358
1908
|
height: bottomSpacerHeight,
|
|
1359
1909
|
padding: 0,
|
|
1360
1910
|
border: "none",
|
|
1361
|
-
} }) }))] })) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), colSlots.map((slot) => slot.type === "spacer" ? ((0, jsx_runtime_1.jsx)("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`)) : ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(columns[slot.i]), "aria-colindex": colAriaIndex(slot.i), ...dataStickyCol(slot.i), children: columns[slot.i].
|
|
1362
|
-
|
|
1363
|
-
: null }, columns[slot.i].key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
1911
|
+
} }) }))] })) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), colSlots.map((slot) => slot.type === "spacer" ? ((0, jsx_runtime_1.jsx)("td", { role: "presentation", "aria-hidden": "true", style: { padding: 0, border: "none" } }, `__spacer_${slot.side}`)) : ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(columns[slot.i]), "aria-colindex": colAriaIndex(slot.i), ...dataStickyCol(slot.i), children: columns[slot.i].calc ? ((0, jsx_runtime_1.jsx)(columncalc_1.ColumnCalc, { rows: rows, value: (row) => cellValue(columns[slot.i], row), numeric: columns[slot.i].type === "number" ||
|
|
1912
|
+
columns[slot.i].type === "currency", defaultOp: columns[slot.i].calc?.defaultOp, op: columns[slot.i].calc?.op, onOpChange: columns[slot.i].calc?.onOpChange, align: alignFor(columns[slot.i]) })) : columns[slot.i].summary ? (columns[slot.i].summary?.(rows)) : null }, columns[slot.i].key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
1364
1913
|
}
|
|
1365
1914
|
DataTable.displayName = "DataTable";
|