mirror of
https://github.com/docmost/docmost.git
synced 2026-05-19 07:54:05 +08:00
feat(base): header select-all with tri-state checkbox
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user