mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
use existing ColumnHeader to enable sort, column menu
This commit is contained in:
@@ -1,36 +1,33 @@
|
||||
import { useRef } from "react";
|
||||
import { forwardRef, useRef } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
|
||||
import {
|
||||
styled,
|
||||
alpha,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
tooltipClasses,
|
||||
Fade,
|
||||
Grid,
|
||||
GridProps,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import DropdownIcon from "@mui/icons-material/MoreHoriz";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
|
||||
import ColumnHeaderSort from "./ColumnHeaderSort";
|
||||
import ColumnHeaderSort, { SORT_STATES } from "./ColumnHeaderSort";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
userRolesAtom,
|
||||
altPressAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { projectScope, altPressAtom } from "@src/atoms/projectScope";
|
||||
import {
|
||||
tableScope,
|
||||
updateColumnAtom,
|
||||
columnMenuAtom,
|
||||
tableSortsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { spreadSx } from "@src/utils/ui";
|
||||
|
||||
export { COLUMN_HEADER_HEIGHT };
|
||||
|
||||
@@ -47,36 +44,20 @@ const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
},
|
||||
}));
|
||||
|
||||
export interface IDraggableHeaderRendererProps {
|
||||
export interface IColumnHeaderProps extends Partial<GridProps> {
|
||||
column: ColumnConfig;
|
||||
width: number;
|
||||
focusInsideCell: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DraggableHeaderRenderer({
|
||||
column,
|
||||
}: IDraggableHeaderRendererProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
|
||||
export const ColumnHeader = forwardRef(function ColumnHeader(
|
||||
{ column, width, focusInsideCell, children, ...props }: IColumnHeaderProps,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const openColumnMenu = useSetAtom(columnMenuAtom, tableScope);
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
|
||||
const [{ isDragging }, dragRef] = useDrag({
|
||||
type: "COLUMN_DRAG",
|
||||
item: { key: column.key },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ isOver }, dropRef] = useDrop({
|
||||
accept: "COLUMN_DRAG",
|
||||
drop: ({ key }: { key: string }) => {
|
||||
updateColumn({ key, config: {}, index: column.index });
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
const [tableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -85,14 +66,26 @@ export default function DraggableHeaderRenderer({
|
||||
openColumnMenu({ column, anchorEl: buttonRef.current });
|
||||
};
|
||||
|
||||
const _sortKey = getFieldProp("sortKey", (column as any).type);
|
||||
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
|
||||
const currentSort: typeof SORT_STATES[number] =
|
||||
tableSorts[0]?.key !== sortKey
|
||||
? "none"
|
||||
: tableSorts[0]?.direction || "none";
|
||||
|
||||
return (
|
||||
<Grid
|
||||
key={column.key}
|
||||
role="columnheader"
|
||||
id={`column-header-${column.key}`}
|
||||
ref={(ref) => {
|
||||
dragRef(ref);
|
||||
dropRef(ref);
|
||||
}}
|
||||
ref={ref}
|
||||
{...props}
|
||||
aria-sort={
|
||||
currentSort === "none"
|
||||
? "none"
|
||||
: currentSort === "asc"
|
||||
? "ascending"
|
||||
: "descending"
|
||||
}
|
||||
container
|
||||
alignItems="center"
|
||||
wrap="nowrap"
|
||||
@@ -100,7 +93,8 @@ export default function DraggableHeaderRenderer({
|
||||
sx={[
|
||||
{
|
||||
height: "100%",
|
||||
"& svg, & button": { display: "block" },
|
||||
"& svg, & button": { display: "block", zIndex: 1 },
|
||||
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
|
||||
color: "text.secondary",
|
||||
transition: (theme) =>
|
||||
@@ -109,29 +103,18 @@ export default function DraggableHeaderRenderer({
|
||||
}),
|
||||
"&:hover": { color: "text.primary" },
|
||||
|
||||
cursor: "move",
|
||||
position: "relative",
|
||||
|
||||
py: 0,
|
||||
pr: 0.5,
|
||||
pl: 1,
|
||||
width: "100%",
|
||||
},
|
||||
isDragging
|
||||
? { opacity: 0.5 }
|
||||
: isOver
|
||||
? {
|
||||
backgroundColor: (theme) =>
|
||||
alpha(
|
||||
theme.palette.primary.main,
|
||||
theme.palette.action.focusOpacity
|
||||
),
|
||||
color: "primary.main",
|
||||
}
|
||||
: {},
|
||||
...spreadSx(props.sx),
|
||||
]}
|
||||
className="column-header"
|
||||
>
|
||||
{(column.width as number) > 140 && (
|
||||
{width > 140 && (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
@@ -149,6 +132,7 @@ export default function DraggableHeaderRenderer({
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(column.key);
|
||||
}}
|
||||
style={{ position: "relative", zIndex: 2 }}
|
||||
>
|
||||
{column.editable === false ? (
|
||||
<LockIcon />
|
||||
@@ -190,6 +174,8 @@ export default function DraggableHeaderRenderer({
|
||||
fontWeight: "fontWeightMedium",
|
||||
lineHeight: `${COLUMN_HEADER_HEIGHT}px`,
|
||||
textOverflow: "clip",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
}}
|
||||
component="div"
|
||||
color="inherit"
|
||||
@@ -205,15 +191,22 @@ export default function DraggableHeaderRenderer({
|
||||
</LightTooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<ColumnHeaderSort column={column as any} />
|
||||
</Grid>
|
||||
{column.type !== FieldType.id && (
|
||||
<Grid item>
|
||||
<ColumnHeaderSort
|
||||
sortKey={sortKey}
|
||||
currentSort={currentSort}
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<Tooltip title="Column settings">
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={`Column settings for ${column.name as string}`}
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
id={`column-settings-${column.key}`}
|
||||
color="inherit"
|
||||
onClick={handleOpenMenu}
|
||||
@@ -225,13 +218,17 @@ export default function DraggableHeaderRenderer({
|
||||
}),
|
||||
|
||||
color: "text.disabled",
|
||||
".column-header:hover &": { color: "text.primary" },
|
||||
".column-header:hover &, &:focus": { color: "text.primary" },
|
||||
}}
|
||||
>
|
||||
<DropdownIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ColumnHeader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { colord } from "colord";
|
||||
|
||||
import { Tooltip, IconButton } from "@mui/material";
|
||||
@@ -8,27 +8,22 @@ import IconSlash, {
|
||||
} from "@src/components/IconSlash";
|
||||
|
||||
import { tableScope, tableSortsAtom } from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
|
||||
const SORT_STATES = ["none", "desc", "asc"] as const;
|
||||
export const SORT_STATES = ["none", "desc", "asc"] as const;
|
||||
|
||||
export interface IColumnHeaderSortProps {
|
||||
column: ColumnConfig;
|
||||
sortKey: string;
|
||||
currentSort: typeof SORT_STATES[number];
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
export default function ColumnHeaderSort({
|
||||
sortKey,
|
||||
currentSort,
|
||||
tabIndex,
|
||||
}: IColumnHeaderSortProps) {
|
||||
const setTableSorts = useSetAtom(tableSortsAtom, tableScope);
|
||||
|
||||
const _sortKey = getFieldProp("sortKey", (column as any).type);
|
||||
const sortKey = _sortKey ? `${column.key}.${_sortKey}` : column.key;
|
||||
|
||||
const currentSort: typeof SORT_STATES[number] =
|
||||
tableSorts[0]?.key !== sortKey
|
||||
? "none"
|
||||
: tableSorts[0]?.direction || "none";
|
||||
const nextSort =
|
||||
SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0];
|
||||
|
||||
@@ -37,8 +32,6 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
else setTableSorts([{ key: sortKey, direction: nextSort }]);
|
||||
};
|
||||
|
||||
if (column.type === FieldType.id) return null;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={nextSort === "none" ? "Unsort" : `Sort by ${nextSort}ending`}
|
||||
@@ -48,9 +41,10 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
size="small"
|
||||
onClick={handleSortClick}
|
||||
color="inherit"
|
||||
tabIndex={tabIndex}
|
||||
sx={{
|
||||
bgcolor: "background.default",
|
||||
"&:hover": {
|
||||
"&:hover, &:focus": {
|
||||
backgroundColor: (theme) =>
|
||||
colord(theme.palette.background.default)
|
||||
.mix(
|
||||
@@ -74,7 +68,8 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
|
||||
position: "relative",
|
||||
opacity: currentSort !== "none" ? 1 : 0,
|
||||
".column-header:hover &": { opacity: 1 },
|
||||
"[role='columnheader']:hover &, [role='columnheader']:focus &, [role='columnheader']:focus-within &, &:focus":
|
||||
{ opacity: 1 },
|
||||
|
||||
transition: (theme) =>
|
||||
theme.transitions.create(["background-color", "opacity"], {
|
||||
@@ -89,7 +84,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
|
||||
transform: currentSort === "asc" ? "rotate(180deg)" : "none",
|
||||
},
|
||||
"&:hover .arrow": {
|
||||
"&:hover .arrow, &:focus .arrow": {
|
||||
transform:
|
||||
currentSort === "asc" || nextSort === "asc"
|
||||
? "rotate(180deg)"
|
||||
@@ -100,7 +95,7 @@ export default function ColumnHeaderSort({ column }: IColumnHeaderSortProps) {
|
||||
strokeDashoffset:
|
||||
currentSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
|
||||
},
|
||||
"&:hover .icon-slash": {
|
||||
"&:hover .icon-slash, &:focus .icon-slash": {
|
||||
strokeDashoffset:
|
||||
nextSort === "none" ? 0 : ICON_SLASH_STROKE_DASHOFFSET,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ export const StyledResizer = styled("div", {
|
||||
shouldForwardProp: (prop) => prop !== "isResizing",
|
||||
})<IStyledResizerProps>(({ theme, isResizing }) => ({
|
||||
position: "absolute",
|
||||
zIndex: 5,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
@@ -39,7 +40,7 @@ export const StyledResizer = styled("div", {
|
||||
height: "50%",
|
||||
width: 4,
|
||||
borderRadius: 2,
|
||||
marginRight: 3,
|
||||
marginRight: 2,
|
||||
|
||||
background: isResizing
|
||||
? theme.palette.primary.main
|
||||
@@ -51,3 +52,5 @@ export const StyledResizer = styled("div", {
|
||||
},
|
||||
}));
|
||||
StyledResizer.displayName = "StyledResizer";
|
||||
|
||||
export default StyledResizer;
|
||||
|
||||
@@ -32,3 +32,5 @@ export const StyledRow = styled("div")(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
StyledRow.displayName = "StyledRow";
|
||||
|
||||
export default StyledRow;
|
||||
|
||||
@@ -31,3 +31,5 @@ export const StyledTable = styled("div")(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
StyledTable.displayName = "StyledTable";
|
||||
|
||||
export default StyledTable;
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { useMemo, useRef, useState, useEffect, useCallback } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useThrottledCallback } from "use-debounce";
|
||||
import {
|
||||
DragDropContext,
|
||||
DropResult,
|
||||
Droppable,
|
||||
Draggable,
|
||||
} from "react-beautiful-dnd";
|
||||
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
DragDropContext,
|
||||
DropResult,
|
||||
Droppable,
|
||||
Draggable,
|
||||
} from "react-beautiful-dnd";
|
||||
import { Portal } from "@mui/material";
|
||||
|
||||
import { StyledTable } from "./Styled/StyledTable";
|
||||
import { StyledRow } from "./Styled/StyledRow";
|
||||
import { StyledResizer } from "./Styled/StyledResizer";
|
||||
import ColumnHeaderComponent from "./Column";
|
||||
import StyledTable from "./Styled/StyledTable";
|
||||
import StyledRow from "./Styled/StyledRow";
|
||||
import ColumnHeader from "./ColumnHeader";
|
||||
import StyledResizer from "./Styled/StyledResizer";
|
||||
import OutOfOrderIndicator from "./OutOfOrderIndicator";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import { IconButton, Portal } from "@mui/material";
|
||||
|
||||
import ColumnHeader, { COLUMN_HEADER_HEIGHT } from "./ColumnHeader";
|
||||
import FinalColumnHeader from "./FinalColumnHeader";
|
||||
import FinalColumn from "./formatters/FinalColumn";
|
||||
// import TableRow from "./TableRow";
|
||||
@@ -31,8 +29,6 @@ import EmptyState from "@src/components/EmptyState";
|
||||
// import BulkActions from "./BulkActions";
|
||||
import AddRow from "@src/components/TableToolbar/AddRow";
|
||||
import { AddRow as AddRowIcon } from "@src/assets/icons";
|
||||
import Loading from "@src/components/Loading";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
@@ -291,6 +287,8 @@ export default function TableComponent() {
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
if (!header.column.columnDef.meta) return null;
|
||||
|
||||
const isSelectedCell =
|
||||
(!selectedCell && header.index === 0) ||
|
||||
(selectedCell?.path === "_rowy_header" &&
|
||||
@@ -305,7 +303,7 @@ export default function TableComponent() {
|
||||
disableInteractiveElementBlocking
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<ColumnHeaderComponent
|
||||
<ColumnHeader
|
||||
key={header.id}
|
||||
data-row-id={"_rowy_header"}
|
||||
data-col-id={header.id}
|
||||
@@ -317,16 +315,11 @@ export default function TableComponent() {
|
||||
}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
role="columnheader"
|
||||
tabIndex={isSelectedCell ? 0 : -1}
|
||||
aria-colindex={header.index + 1}
|
||||
aria-readonly={!canEditColumn}
|
||||
// TODO: aria-sort={"none" | "ascending" | "descending" | "other" | undefined}
|
||||
aria-selected={isSelectedCell}
|
||||
label={
|
||||
header.column.columnDef.meta?.name || header.id
|
||||
}
|
||||
type={header.column.columnDef.meta?.type}
|
||||
column={header.column.columnDef.meta!}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
left: header.column.getIsPinned()
|
||||
@@ -335,6 +328,7 @@ export default function TableComponent() {
|
||||
: undefined,
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
width={header.getSize()}
|
||||
sx={[
|
||||
snapshot.isDragging
|
||||
? {}
|
||||
@@ -347,6 +341,9 @@ export default function TableComponent() {
|
||||
});
|
||||
(e.target as HTMLDivElement).focus();
|
||||
}}
|
||||
focusInsideCell={
|
||||
isSelectedCell && focusInsideCell
|
||||
}
|
||||
>
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
@@ -360,7 +357,11 @@ export default function TableComponent() {
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
style={{ position: "absolute", inset: 0 }}
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{header.column.getCanResize() && (
|
||||
@@ -370,7 +371,7 @@ export default function TableComponent() {
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
/>
|
||||
)}
|
||||
</ColumnHeaderComponent>
|
||||
</ColumnHeader>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user