Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m37s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / updater-image (push) Successful in 38s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 8s
314 lines
9.0 KiB
TypeScript
314 lines
9.0 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
CSSProperties,
|
|
Fragment,
|
|
ReactNode,
|
|
useEffect,
|
|
useId,
|
|
useRef,
|
|
useState,
|
|
} from "react"
|
|
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
|
|
import {
|
|
DataGridTableBase,
|
|
DataGridTableBody,
|
|
DataGridTableBodyRow,
|
|
DataGridTableBodyRowCell,
|
|
DataGridTableBodyRowExpandded,
|
|
DataGridTableBodyRowSkeleton,
|
|
DataGridTableBodyRowSkeletonCell,
|
|
DataGridTableEmpty,
|
|
DataGridTableFoot,
|
|
DataGridTableHead,
|
|
DataGridTableHeadRow,
|
|
DataGridTableHeadRowCell,
|
|
DataGridTableHeadRowCellResize,
|
|
DataGridTableRowSpacer,
|
|
DataGridTableViewport,
|
|
} from "@/components/reui/data-grid/data-grid-table"
|
|
import {
|
|
closestCenter,
|
|
DndContext,
|
|
KeyboardSensor,
|
|
Modifier,
|
|
MouseSensor,
|
|
TouchSensor,
|
|
useSensor,
|
|
useSensors,
|
|
type DragEndEvent,
|
|
} from "@dnd-kit/core"
|
|
import {
|
|
horizontalListSortingStrategy,
|
|
SortableContext,
|
|
useSortable,
|
|
} from "@dnd-kit/sortable"
|
|
import { CSS } from "@dnd-kit/utilities"
|
|
import {
|
|
Cell,
|
|
flexRender,
|
|
Header,
|
|
HeaderGroup,
|
|
Row,
|
|
} from "@tanstack/react-table"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { GripVerticalIcon } from "lucide-react"
|
|
|
|
function DataGridTableDndHeader<TData>({
|
|
header,
|
|
}: {
|
|
header: Header<TData, unknown>
|
|
}) {
|
|
const { props } = useDataGrid()
|
|
const { column } = header
|
|
|
|
// Check if column ordering is enabled for this column
|
|
const canOrder =
|
|
(column.columnDef as { enableColumnOrdering?: boolean })
|
|
.enableColumnOrdering !== false
|
|
|
|
const {
|
|
attributes,
|
|
isDragging,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
} = useSortable({
|
|
id: header.column.id,
|
|
})
|
|
|
|
const style: CSSProperties = {
|
|
opacity: isDragging ? 0.8 : 1,
|
|
position: "relative",
|
|
transform: CSS.Translate.toString(transform),
|
|
transition,
|
|
cursor: isDragging ? "grabbing" : undefined,
|
|
whiteSpace: "nowrap",
|
|
width: props.tableLayout?.columnsResizable
|
|
? `calc(var(--header-${header.id}-size) * 1px)`
|
|
: header.column.getSize(),
|
|
zIndex: isDragging ? 1 : 0,
|
|
}
|
|
|
|
return (
|
|
<DataGridTableHeadRowCell
|
|
header={header}
|
|
dndStyle={style}
|
|
dndRef={setNodeRef}
|
|
>
|
|
<div className="flex items-center justify-start gap-0.5">
|
|
{canOrder && (
|
|
<Button
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
className={`-ms-2 size-6 ${isDragging ? "cursor-grabbing" : "cursor-grab active:cursor-grabbing"}`}
|
|
{...attributes}
|
|
{...listeners}
|
|
aria-label="Drag to reorder"
|
|
>
|
|
<GripVerticalIcon className="opacity-60 hover:opacity-100" aria-hidden="true" />
|
|
</Button>
|
|
)}
|
|
<span className="grow truncate">
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
</span>
|
|
{props.tableLayout?.columnsResizable && column.getCanResize() && (
|
|
<DataGridTableHeadRowCellResize header={header} />
|
|
)}
|
|
</div>
|
|
</DataGridTableHeadRowCell>
|
|
)
|
|
}
|
|
|
|
function DataGridTableDndCell<TData>({ cell }: { cell: Cell<TData, unknown> }) {
|
|
const { props } = useDataGrid()
|
|
const { isDragging, setNodeRef, transform, transition } = useSortable({
|
|
id: cell.column.id,
|
|
})
|
|
|
|
const style: CSSProperties = {
|
|
opacity: isDragging ? 0.8 : 1,
|
|
position: "relative",
|
|
transform: CSS.Translate.toString(transform),
|
|
transition,
|
|
cursor: isDragging ? "grabbing" : undefined,
|
|
width: props.tableLayout?.columnsResizable
|
|
? `calc(var(--col-${cell.column.id}-size) * 1px)`
|
|
: cell.column.getSize(),
|
|
zIndex: isDragging ? 1 : 0,
|
|
}
|
|
|
|
return (
|
|
<DataGridTableBodyRowCell cell={cell} dndStyle={style} dndRef={setNodeRef}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</DataGridTableBodyRowCell>
|
|
)
|
|
}
|
|
|
|
function DataGridTableDnd<TData>({
|
|
handleDragEnd,
|
|
footerContent,
|
|
}: {
|
|
handleDragEnd: (event: DragEndEvent) => void
|
|
footerContent?: ReactNode
|
|
}) {
|
|
const { table, isLoading, props } = useDataGrid()
|
|
const pagination = table.getState().pagination
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const [isDraggingColumn, setIsDraggingColumn] = useState(false)
|
|
|
|
const sensors = useSensors(
|
|
useSensor(MouseSensor, {}),
|
|
useSensor(TouchSensor, {}),
|
|
useSensor(KeyboardSensor, {})
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!isDraggingColumn) return
|
|
|
|
const { body, documentElement } = document
|
|
const previousBodyCursor = body.style.cursor
|
|
const previousDocumentCursor = documentElement.style.cursor
|
|
|
|
body.style.cursor = "grabbing"
|
|
documentElement.style.cursor = "grabbing"
|
|
|
|
return () => {
|
|
body.style.cursor = previousBodyCursor
|
|
documentElement.style.cursor = previousDocumentCursor
|
|
}
|
|
}, [isDraggingColumn])
|
|
|
|
// Custom modifier to restrict dragging within table bounds with edge offset
|
|
const restrictToTableBounds: Modifier = ({ draggingNodeRect, transform }) => {
|
|
if (!draggingNodeRect || !containerRef.current) {
|
|
return { ...transform, y: 0 }
|
|
}
|
|
|
|
const containerRect = containerRef.current.getBoundingClientRect()
|
|
const edgeOffset = 0
|
|
|
|
const minX = containerRect.left - draggingNodeRect.left - edgeOffset
|
|
const maxX =
|
|
containerRect.right -
|
|
draggingNodeRect.left -
|
|
draggingNodeRect.width +
|
|
edgeOffset
|
|
|
|
return {
|
|
...transform,
|
|
x: Math.min(Math.max(transform.x, minX), maxX),
|
|
y: 0, // Lock vertical movement
|
|
}
|
|
}
|
|
|
|
return (
|
|
<DndContext
|
|
collisionDetection={closestCenter}
|
|
id={useId()}
|
|
modifiers={[restrictToTableBounds]}
|
|
onDragCancel={() => setIsDraggingColumn(false)}
|
|
onDragEnd={(event) => {
|
|
setIsDraggingColumn(false)
|
|
handleDragEnd(event)
|
|
}}
|
|
onDragStart={() => setIsDraggingColumn(true)}
|
|
sensors={sensors}
|
|
>
|
|
<DataGridTableViewport
|
|
viewportRef={containerRef}
|
|
className={
|
|
isDraggingColumn
|
|
? "relative cursor-grabbing [&_*]:cursor-grabbing!"
|
|
: "relative"
|
|
}
|
|
>
|
|
<DataGridTableBase>
|
|
<DataGridTableHead>
|
|
{table
|
|
.getHeaderGroups()
|
|
.map((headerGroup: HeaderGroup<TData>, index) => {
|
|
return (
|
|
<DataGridTableHeadRow headerGroup={headerGroup} key={index}>
|
|
<SortableContext
|
|
items={table.getState().columnOrder}
|
|
strategy={horizontalListSortingStrategy}
|
|
>
|
|
{headerGroup.headers.map((header) => (
|
|
<DataGridTableDndHeader
|
|
header={header}
|
|
key={header.id}
|
|
/>
|
|
))}
|
|
</SortableContext>
|
|
</DataGridTableHeadRow>
|
|
)
|
|
})}
|
|
</DataGridTableHead>
|
|
|
|
{(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (
|
|
<DataGridTableRowSpacer />
|
|
)}
|
|
|
|
<DataGridTableBody>
|
|
{props.loadingMode === "skeleton" &&
|
|
isLoading &&
|
|
pagination?.pageSize ? (
|
|
Array.from({ length: pagination.pageSize }).map((_, rowIndex) => (
|
|
<DataGridTableBodyRowSkeleton key={rowIndex}>
|
|
{table.getVisibleFlatColumns().map((column, colIndex) => {
|
|
return (
|
|
<DataGridTableBodyRowSkeletonCell
|
|
column={column}
|
|
key={colIndex}
|
|
>
|
|
{column.columnDef.meta?.skeleton}
|
|
</DataGridTableBodyRowSkeletonCell>
|
|
)
|
|
})}
|
|
</DataGridTableBodyRowSkeleton>
|
|
))
|
|
) : table.getRowModel().rows.length ? (
|
|
table.getRowModel().rows.map((row: Row<TData>) => {
|
|
return (
|
|
<Fragment key={row.id}>
|
|
<DataGridTableBodyRow row={row}>
|
|
{row
|
|
.getVisibleCells()
|
|
.map((cell: Cell<TData, unknown>) => {
|
|
return (
|
|
<SortableContext
|
|
key={cell.id}
|
|
items={table.getState().columnOrder}
|
|
strategy={horizontalListSortingStrategy}
|
|
>
|
|
<DataGridTableDndCell cell={cell} />
|
|
</SortableContext>
|
|
)
|
|
})}
|
|
</DataGridTableBodyRow>
|
|
{row.getIsExpanded() && (
|
|
<DataGridTableBodyRowExpandded row={row} />
|
|
)}
|
|
</Fragment>
|
|
)
|
|
})
|
|
) : (
|
|
<DataGridTableEmpty />
|
|
)}
|
|
</DataGridTableBody>
|
|
|
|
{footerContent && (
|
|
<DataGridTableFoot>{footerContent}</DataGridTableFoot>
|
|
)}
|
|
</DataGridTableBase>
|
|
</DataGridTableViewport>
|
|
</DndContext>
|
|
)
|
|
}
|
|
|
|
export { DataGridTableDnd } |