feat(base): header select-all with tri-state checkbox

This commit is contained in:
Philipinho
2026-04-18 16:42:06 +01:00
parent 3fca962c9f
commit 4c4bbe9b15
5 changed files with 87 additions and 3 deletions
@@ -210,6 +210,7 @@ export function GridContainer({
table={table} table={table}
baseId={baseId} baseId={baseId}
columnOrder={table.getState().columnOrder} columnOrder={table.getState().columnOrder}
loadedRowIds={rowIds}
onPropertyCreated={handlePropertyCreated} onPropertyCreated={handlePropertyCreated}
/> />
</SortableContext> </SortableContext>
@@ -23,6 +23,8 @@ import {
IconUserEdit, IconUserEdit,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { PropertyMenuContent } from "@/features/base/components/property/property-menu"; import { PropertyMenuContent } from "@/features/base/components/property/property-menu";
import { RowNumberHeaderCell } from "./row-number-header-cell";
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
import classes from "@/features/base/styles/grid.module.css"; import classes from "@/features/base/styles/grid.module.css";
const typeIcons: Record<string, typeof IconLetterT> = { const typeIcons: Record<string, typeof IconLetterT> = {
@@ -44,10 +46,12 @@ const typeIcons: Record<string, typeof IconLetterT> = {
type GridHeaderCellProps = { type GridHeaderCellProps = {
header: Header<IBaseRow, unknown>; header: Header<IBaseRow, unknown>;
loadedRowIds: string[];
}; };
export const GridHeaderCell = memo(function GridHeaderCell({ export const GridHeaderCell = memo(function GridHeaderCell({
header, header,
loadedRowIds,
}: GridHeaderCellProps) { }: GridHeaderCellProps) {
const property = header.column.columnDef.meta?.property as const property = header.column.columnDef.meta?.property as
| IBaseProperty | IBaseProperty
@@ -55,6 +59,8 @@ export const GridHeaderCell = memo(function GridHeaderCell({
const isRowNumber = header.column.id === "__row_number"; const isRowNumber = header.column.id === "__row_number";
const isPinned = header.column.getIsPinned(); const isPinned = header.column.getIsPinned();
const pinOffset = isPinned ? header.column.getStart("left") : undefined; const pinOffset = isPinned ? header.column.getStart("left") : undefined;
const { selectionCount } = useRowSelection();
const hasSelection = selectionCount > 0;
const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void]; const [activePropertyMenu, setActivePropertyMenu] = useAtom(activePropertyMenuAtom) as unknown as [string | null, (val: string | null) => void];
const menuOpened = activePropertyMenu === header.column.id; const menuOpened = activePropertyMenu === header.column.id;
@@ -118,7 +124,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
return ( return (
<div <div
ref={combinedRef} ref={combinedRef}
className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""}`} className={`${classes.headerCell} ${isPinned ? classes.headerCellPinned : ""} ${hasSelection ? classes.hasSelection : ""}`}
style={{ style={{
...(isPinned ? { left: pinOffset } : {}), ...(isPinned ? { left: pinOffset } : {}),
...(isRowNumber ? {} : { cursor: "pointer" }), ...(isRowNumber ? {} : { cursor: "pointer" }),
@@ -129,7 +135,7 @@ export const GridHeaderCell = memo(function GridHeaderCell({
{...(isSortableDisabled ? {} : listeners)} {...(isSortableDisabled ? {} : listeners)}
> >
{isRowNumber ? ( {isRowNumber ? (
flexRender(header.column.columnDef.header, header.getContext()) <RowNumberHeaderCell loadedRowIds={loadedRowIds} />
) : ( ) : (
<div className={classes.headerCellContent}> <div className={classes.headerCellContent}>
{TypeIcon && ( {TypeIcon && (
@@ -11,6 +11,7 @@ type GridHeaderProps = {
// Passed explicitly to break memo when columns change // Passed explicitly to break memo when columns change
// (table ref is stable from useReactTable, so memo won't fire without this) // (table ref is stable from useReactTable, so memo won't fire without this)
columnOrder: ColumnOrderState; columnOrder: ColumnOrderState;
loadedRowIds: string[];
onPropertyCreated?: () => void; onPropertyCreated?: () => void;
}; };
@@ -19,6 +20,7 @@ export const GridHeader = memo(function GridHeader({
baseId, baseId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
columnOrder: _columnOrder, columnOrder: _columnOrder,
loadedRowIds,
onPropertyCreated, onPropertyCreated,
}: GridHeaderProps) { }: GridHeaderProps) {
const headerGroups = table.getHeaderGroups(); const headerGroups = table.getHeaderGroups();
@@ -26,7 +28,7 @@ export const GridHeader = memo(function GridHeader({
return ( return (
<div className={classes.headerRow} role="row"> <div className={classes.headerRow} role="row">
{headerGroups[0]?.headers.map((header) => ( {headerGroups[0]?.headers.map((header) => (
<GridHeaderCell key={header.id} header={header} /> <GridHeaderCell key={header.id} header={header} loadedRowIds={loadedRowIds} />
))} ))}
{baseId && ( {baseId && (
<CreatePropertyPopover <CreatePropertyPopover
@@ -0,0 +1,48 @@
import { memo, useMemo } from "react";
import { Checkbox, Tooltip } from "@mantine/core";
import { useRowSelection } from "@/features/base/hooks/use-row-selection";
import classes from "@/features/base/styles/grid.module.css";
type RowNumberHeaderCellProps = {
loadedRowIds: string[];
};
export const RowNumberHeaderCell = memo(function RowNumberHeaderCell({
loadedRowIds,
}: RowNumberHeaderCellProps) {
const { selectedIds, toggleAll } = useRowSelection();
const { checked, indeterminate } = useMemo(() => {
if (loadedRowIds.length === 0) {
return { checked: false, indeterminate: false };
}
const selectedInLoaded = loadedRowIds.reduce(
(acc, id) => (selectedIds.has(id) ? acc + 1 : acc),
0,
);
return {
checked: selectedInLoaded === loadedRowIds.length,
indeterminate:
selectedInLoaded > 0 && selectedInLoaded < loadedRowIds.length,
};
}, [loadedRowIds, selectedIds]);
if (loadedRowIds.length === 0) return null;
return (
<div className={classes.rowNumberHeaderInner}>
<span className={classes.rowNumberHeaderHash}>#</span>
<span className={classes.rowNumberHeaderCheckbox}>
<Tooltip label="Select all loaded rows" withinPortal>
<Checkbox
size="xs"
checked={checked}
indeterminate={indeterminate}
onChange={() => toggleAll(loadedRowIds)}
aria-label="Select all loaded rows"
/>
</Tooltip>
</span>
</div>
);
});
@@ -347,3 +347,30 @@
.rowSelected .cell { .rowSelected .cell {
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6)); background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
} }
.rowNumberHeaderInner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.rowNumberHeaderHash {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
font-size: var(--mantine-font-size-xs);
}
.rowNumberHeaderCheckbox {
display: none;
}
.headerCell:hover .rowNumberHeaderHash,
.hasSelection .rowNumberHeaderHash {
display: none;
}
.headerCell:hover .rowNumberHeaderCheckbox,
.hasSelection .rowNumberHeaderCheckbox {
display: inline-flex;
}