feat(ui): add column visibility and drag-and-drop reordering support (#13198)

This commit is contained in:
Sebastian Rindom
2025-09-01 14:03:56 +02:00
committed by GitHub
parent 5a46372afd
commit 2a94dbd243
19 changed files with 1846 additions and 310 deletions

View File

@@ -19,7 +19,6 @@ export class Views {
}) })
} }
// View configurations
async listConfigurations( async listConfigurations(
entity: string, entity: string,
query?: HttpTypes.AdminGetViewConfigurationsParams, query?: HttpTypes.AdminGetViewConfigurationsParams,

View File

@@ -81,6 +81,9 @@
"vitest": "^3.0.5" "vitest": "^3.0.5"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.0.0",
"@dnd-kit/sortable": "^7.0.0",
"@dnd-kit/utilities": "^3.2.0",
"@medusajs/icons": "2.10.1", "@medusajs/icons": "2.10.1",
"@tanstack/react-table": "8.20.5", "@tanstack/react-table": "8.20.5",
"clsx": "^1.2.1", "clsx": "^1.2.1",

View File

@@ -0,0 +1,102 @@
import React from "react"
import { Column } from "@tanstack/react-table"
import { Checkbox } from "@/components/checkbox"
import { DropdownMenu } from "@/components/dropdown-menu"
import { IconButton } from "@/components/icon-button"
import { Tooltip } from "@/components/tooltip"
import { Adjustments } from "@medusajs/icons"
import { clx } from "@/utils/clx"
import { useDataTableContext } from "../context/use-data-table-context"
interface DataTableColumnVisibilityMenuProps {
className?: string
tooltip?: string
}
const DataTableColumnVisibilityMenu = ({
className,
tooltip,
}: DataTableColumnVisibilityMenuProps) => {
const { instance, enableColumnVisibility } = useDataTableContext()
if (!enableColumnVisibility) {
return null
}
const columns = instance
.getAllColumns()
.filter((column) => column.getCanHide())
const handleToggleColumn = (column: Column<any, any>) => {
column.toggleVisibility()
}
const handleToggleAll = (value: boolean) => {
instance.setColumnVisibility(
Object.fromEntries(
columns.map((column: Column<any, any>) => [column.id, value])
)
)
}
const allColumnsVisible = columns.every((column: Column<any, any>) => column.getIsVisible())
const someColumnsVisible = columns.some((column: Column<any, any>) => column.getIsVisible())
const Wrapper = tooltip ? Tooltip : React.Fragment
const wrapperProps = tooltip ? { content: tooltip } : ({} as any)
return (
<DropdownMenu>
<Wrapper {...wrapperProps}>
<DropdownMenu.Trigger asChild>
<IconButton size="small" className={className}>
<Adjustments />
</IconButton>
</DropdownMenu.Trigger>
</Wrapper>
<DropdownMenu.Content align="end" className="min-w-[200px] max-h-[400px] overflow-hidden">
<DropdownMenu.Label>Toggle columns</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={(e: Event) => {
e.preventDefault()
handleToggleAll(!allColumnsVisible)
}}
>
<div className="flex items-center gap-x-2">
<Checkbox
checked={allColumnsVisible ? true : (someColumnsVisible && !allColumnsVisible) ? "indeterminate" : false}
/>
<span>Toggle all</span>
</div>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<div className="max-h-[250px] overflow-y-auto">
{columns.map((column: Column<any, any>) => {
return (
<DropdownMenu.Item
key={column.id}
onSelect={(e: Event) => {
e.preventDefault()
handleToggleColumn(column)
}}
>
<div className="flex items-center gap-x-2">
<Checkbox checked={column.getIsVisible()} />
<span className="truncate">
{(column.columnDef.meta as any)?.name || column.id}
</span>
</div>
</DropdownMenu.Item>
)
})}
</div>
</DropdownMenu.Content>
</DropdownMenu>
)
}
export { DataTableColumnVisibilityMenu }
export type { DataTableColumnVisibilityMenuProps }

View File

@@ -3,28 +3,114 @@
import * as React from "react" import * as React from "react"
import { DataTableFilter } from "@/blocks/data-table/components/data-table-filter" import { DataTableFilter } from "@/blocks/data-table/components/data-table-filter"
import { DataTableFilterMenu } from "@/blocks/data-table/components/data-table-filter-menu"
import { DataTableSortingMenu } from "@/blocks/data-table/components/data-table-sorting-menu"
import { DataTableColumnVisibilityMenu } from "@/blocks/data-table/components/data-table-column-visibility-menu"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context" import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { Button } from "@/components/button" import { Button } from "@/components/button"
import { Skeleton } from "@/components/skeleton" import { Skeleton } from "@/components/skeleton"
interface DataTableFilterBarProps { interface DataTableFilterBarProps {
clearAllFiltersLabel?: string clearAllFiltersLabel?: string
alwaysShow?: boolean
sortingTooltip?: string
columnsTooltip?: string
children?: React.ReactNode
}
interface LocalFilter {
id: string
value: unknown
isNew: boolean
} }
const DataTableFilterBar = ({ const DataTableFilterBar = ({
clearAllFiltersLabel = "Clear all", clearAllFiltersLabel = "Clear all",
alwaysShow = false,
sortingTooltip,
columnsTooltip,
children,
}: DataTableFilterBarProps) => { }: DataTableFilterBarProps) => {
const { instance } = useDataTableContext() const { instance, enableColumnVisibility } = useDataTableContext()
const filterState = instance.getFiltering() // Local state for managing intermediate filters
const [localFilters, setLocalFilters] = React.useState<LocalFilter[]>([])
const parentFilterState = instance.getFiltering()
const availableFilters = instance.getFilters()
// Sync parent filters with local state
React.useEffect(() => {
setLocalFilters(prevLocalFilters => {
const parentIds = Object.keys(parentFilterState)
const localIds = prevLocalFilters.map(f => f.id)
// Remove local filters that have been removed from parent
const updatedLocalFilters = prevLocalFilters.filter(f =>
parentIds.includes(f.id) || f.isNew
)
// Add parent filters that don't exist locally
parentIds.forEach(id => {
if (!localIds.includes(id)) {
updatedLocalFilters.push({
id,
value: parentFilterState[id],
isNew: false
})
}
})
// Only update if there's an actual change
if (updatedLocalFilters.length !== prevLocalFilters.length ||
updatedLocalFilters.some((f, i) => f.id !== prevLocalFilters[i]?.id)) {
return updatedLocalFilters
}
return prevLocalFilters
})
}, [parentFilterState])
// Add a new filter locally
const addLocalFilter = React.useCallback((id: string, value: unknown) => {
setLocalFilters(prev => [...prev, { id, value, isNew: true }])
}, [])
// Update a local filter's value
const updateLocalFilter = React.useCallback((id: string, value: unknown) => {
setLocalFilters(prev => prev.map(f =>
f.id === id ? { ...f, value, isNew: false } : f
))
// If the filter has a meaningful value, propagate to parent
if (value !== undefined && value !== null && value !== '' &&
!(Array.isArray(value) && value.length === 0)) {
instance.updateFilter({ id, value })
}
}, [instance])
// Remove a local filter
const removeLocalFilter = React.useCallback((id: string) => {
setLocalFilters(prev => prev.filter(f => f.id !== id))
// Also remove from parent if it exists there
if (parentFilterState[id] !== undefined) {
instance.removeFilter(id)
}
}, [instance, parentFilterState])
const clearFilters = React.useCallback(() => { const clearFilters = React.useCallback(() => {
setLocalFilters([])
instance.clearFilters() instance.clearFilters()
}, [instance]) }, [instance])
const filterCount = Object.keys(filterState).length const filterCount = localFilters.length
const hasAvailableFilters = availableFilters.length > 0
// Check if sorting is enabled
const sortableColumns = instance.getAllColumns().filter((column) => column.getCanSort())
const hasSorting = instance.enableSorting && sortableColumns.length > 0
if (filterCount === 0) { // Always show the filter bar when there are available filters, sorting, column visibility, or when forced
if (filterCount === 0 && !hasAvailableFilters && !hasSorting && !enableColumnVisibility && !alwaysShow && !children) {
return null return null
} }
@@ -33,21 +119,27 @@ const DataTableFilterBar = ({
} }
return ( return (
<div className="bg-ui-bg-subtle flex w-full flex-nowrap items-center gap-2 overflow-x-auto border-t px-6 py-2 md:flex-wrap"> <div className="bg-ui-bg-subtle flex w-full flex-nowrap items-center justify-between gap-2 overflow-x-auto border-t px-6 py-2">
{Object.entries(filterState).map(([id, filter]) => ( <div className="flex flex-nowrap items-center gap-2 md:flex-wrap">
<DataTableFilter key={id} id={id} filter={filter} /> {localFilters.map((localFilter) => (
))} <DataTableFilter
{filterCount > 0 ? ( key={localFilter.id}
<Button id={localFilter.id}
variant="transparent" filter={localFilter.value}
size="small" isNew={localFilter.isNew}
className="text-ui-fg-muted hover:text-ui-fg-subtle flex-shrink-0 whitespace-nowrap" onUpdate={(value) => updateLocalFilter(localFilter.id, value)}
type="button" onRemove={() => removeLocalFilter(localFilter.id)}
onClick={clearFilters} />
> ))}
{clearAllFiltersLabel} {hasAvailableFilters && (
</Button> <DataTableFilterMenu onAddFilter={addLocalFilter} />
) : null} )}
</div>
<div className="flex flex-shrink-0 items-center gap-2">
{hasSorting && <DataTableSortingMenu tooltip={sortingTooltip} />}
{enableColumnVisibility && <DataTableColumnVisibilityMenu tooltip={columnsTooltip} />}
{children}
</div>
</div> </div>
) )
} }

View File

@@ -12,13 +12,17 @@ interface DataTableFilterMenuProps {
* The tooltip to show when hovering over the filter menu. * The tooltip to show when hovering over the filter menu.
*/ */
tooltip?: string tooltip?: string
/**
* Callback when a filter is added
*/
onAddFilter?: (id: string, value: unknown) => void
} }
/** /**
* This component adds a filter menu to the data table, allowing users * This component adds a filter menu to the data table, allowing users
* to filter the table's data. * to filter the table's data.
*/ */
const DataTableFilterMenu = (props: DataTableFilterMenuProps) => { const DataTableFilterMenu = ({ tooltip, onAddFilter }: DataTableFilterMenuProps) => {
const { instance } = useDataTableContext() const { instance } = useDataTableContext()
const enabledFilters = Object.keys(instance.getFiltering()) const enabledFilters = Object.keys(instance.getFiltering())
@@ -33,9 +37,9 @@ const DataTableFilterMenu = (props: DataTableFilterMenuProps) => {
) )
} }
const Wrapper = props.tooltip ? Tooltip : React.Fragment const Wrapper = tooltip ? Tooltip : React.Fragment
const wrapperProps = props.tooltip const wrapperProps = tooltip
? { content: props.tooltip, hidden: filterOptions.length === 0 } ? { content: tooltip, hidden: filterOptions.length === 0 }
: ({} as any) : ({} as any)
if (instance.showSkeleton) { if (instance.showSkeleton) {
@@ -51,17 +55,45 @@ const DataTableFilterMenu = (props: DataTableFilterMenuProps) => {
</IconButton> </IconButton>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
</Wrapper> </Wrapper>
<DropdownMenu.Content side="bottom"> <DropdownMenu.Content side="bottom" align="start">
{filterOptions.map((filter) => ( {filterOptions.map((filter) => {
<DropdownMenu.Item const getDefaultValue = () => {
key={filter.id} switch (filter.type) {
onClick={() => { case "select":
instance.addFilter({ id: filter.id, value: undefined }) case "multiselect":
}} return []
> case "string":
{filter.label} return ""
</DropdownMenu.Item> case "number":
))} return null
case "date":
return null
case "radio":
return null
case "custom":
return null
default:
return null
}
}
return (
<DropdownMenu.Item
key={filter.id}
onClick={(e) => {
// Prevent any bubbling that might interfere
e.stopPropagation()
if (onAddFilter) {
onAddFilter(filter.id, getDefaultValue())
} else {
instance.addFilter({ id: filter.id, value: getDefaultValue() })
}
}}
>
{filter.label}
</DropdownMenu.Item>
)
})}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu> </DropdownMenu>
) )

View File

@@ -0,0 +1,64 @@
import * as React from "react"
import { useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { Table } from "@/components/table"
interface DataTableNonSortableHeaderCellProps extends React.HTMLAttributes<HTMLTableCellElement> {
id: string
children: React.ReactNode
isFirstColumn?: boolean
}
export const DataTableNonSortableHeaderCell = React.forwardRef<
HTMLTableCellElement,
DataTableNonSortableHeaderCellProps
>(({ id, children, className, style: propStyle, isFirstColumn, ...props }, ref) => {
// Still use sortable hook but without listeners
const {
setNodeRef,
transform,
transition,
} = useSortable({
id,
disabled: true, // Disable dragging
})
// Only apply horizontal transform for smooth shifting
const transformStyle = transform ? {
x: transform.x,
y: 0,
scaleX: transform.scaleX,
scaleY: transform.scaleY,
} : null
const style: React.CSSProperties = {
...propStyle,
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
transition,
position: 'relative' as const,
}
const combineRefs = (element: HTMLTableCellElement | null) => {
setNodeRef(element)
if (ref) {
if (typeof ref === 'function') {
ref(element)
} else {
ref.current = element
}
}
}
return (
<Table.HeaderCell
ref={combineRefs}
style={style}
className={className}
{...props}
>
{children}
</Table.HeaderCell>
)
})
DataTableNonSortableHeaderCell.displayName = "DataTableNonSortableHeaderCell"

View File

@@ -0,0 +1,71 @@
import * as React from "react"
import { useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { clx } from "@/utils/clx"
import { Table } from "@/components/table"
interface DataTableSortableHeaderCellProps extends React.HTMLAttributes<HTMLTableCellElement> {
id: string
children: React.ReactNode
isFirstColumn?: boolean
}
export const DataTableSortableHeaderCell = React.forwardRef<
HTMLTableCellElement,
DataTableSortableHeaderCellProps
>(({ id, children, className, style: propStyle, isFirstColumn, ...props }, ref) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id,
})
// Only apply horizontal transform, ignore vertical movement
const transformStyle = transform ? {
x: transform.x,
y: 0,
scaleX: 1,
scaleY: 1
} : null
const style: React.CSSProperties = {
...propStyle,
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
transition,
opacity: isDragging ? 0.8 : 1,
zIndex: isDragging ? 50 : undefined,
backgroundColor: "white",
position: 'relative' as const,
}
const combineRefs = (element: HTMLTableCellElement | null) => {
setNodeRef(element)
if (ref) {
if (typeof ref === 'function') {
ref(element)
} else {
ref.current = element
}
}
}
return (
<Table.HeaderCell
ref={combineRefs}
style={style}
className={clx(className, "group/header-cell relative")}
{...attributes}
{...listeners}
{...props}
>
{children}
</Table.HeaderCell>
)
})
DataTableSortableHeaderCell.displayName = "DataTableSortableHeaderCell"

View File

@@ -4,6 +4,22 @@ import * as React from "react"
import { Table } from "@/components/table" import { Table } from "@/components/table"
import { flexRender } from "@tanstack/react-table" import { flexRender } from "@tanstack/react-table"
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
DragStartEvent,
} from "@dnd-kit/core"
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
horizontalListSortingStrategy,
} from "@dnd-kit/sortable"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context" import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { Skeleton } from "@/components/skeleton" import { Skeleton } from "@/components/skeleton"
@@ -15,6 +31,8 @@ import {
DataTableEmptyStateProps, DataTableEmptyStateProps,
} from "../types" } from "../types"
import { DataTableSortingIcon } from "./data-table-sorting-icon" import { DataTableSortingIcon } from "./data-table-sorting-icon"
import { DataTableSortableHeaderCell } from "./data-table-sortable-header-cell"
import { DataTableNonSortableHeaderCell } from "./data-table-non-sortable-header-cell"
interface DataTableTableProps { interface DataTableTableProps {
/** /**
@@ -42,6 +60,59 @@ const DataTableTable = (props: DataTableTableProps) => {
const hasSelect = columns.find((c) => c.id === "select") const hasSelect = columns.find((c) => c.id === "select")
const hasActions = columns.find((c) => c.id === "action") const hasActions = columns.find((c) => c.id === "action")
// Create list of all column IDs for SortableContext
// Use current order if available, otherwise use default order
const sortableItems = React.useMemo(() => {
if (instance.columnOrder && instance.columnOrder.length > 0) {
return instance.columnOrder
}
return columns.map(col => col.id)
}, [columns, instance.columnOrder])
// Setup drag-and-drop sensors
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (active.id !== over?.id && over?.id) {
const activeId = active.id as string
const overId = over.id as string
// Don't allow dragging fixed columns
if (activeId === "select" || activeId === "action") {
return
}
// Don't allow dropping on fixed columns
if (overId === "select" || overId === "action") {
return
}
// Use the current column order from the instance
const currentOrder = instance.columnOrder && instance.columnOrder.length > 0
? instance.columnOrder
: columns.map(col => col.id)
const oldIndex = currentOrder.indexOf(activeId)
const newIndex = currentOrder.indexOf(overId)
if (oldIndex !== -1 && newIndex !== -1) {
const newOrder = arrayMove(currentOrder, oldIndex, newIndex)
instance.setColumnOrderFromArray(newOrder)
}
}
}
React.useEffect(() => { React.useEffect(() => {
const onKeyDownHandler = (event: KeyboardEvent) => { const onKeyDownHandler = (event: KeyboardEvent) => {
// If an editable element is focused, we don't want to select a row // If an editable element is focused, we don't want to select a row
@@ -100,155 +171,349 @@ const DataTableTable = (props: DataTableTableProps) => {
return ( return (
<div className="flex w-full flex-1 flex-col overflow-hidden"> <div className="flex w-full flex-1 flex-col overflow-hidden">
{instance.emptyState === DataTableEmptyState.POPULATED && ( {instance.emptyState === DataTableEmptyState.POPULATED && (
<div instance.enableColumnOrder ? (
ref={scrollableRef} <DndContext
onScroll={handleHorizontalScroll} sensors={sensors}
className="min-h-0 w-full flex-1 overflow-auto overscroll-none border-y" collisionDetection={closestCenter}
> onDragEnd={handleDragEnd}
<Table className="relative isolate w-full"> >
<Table.Header <div
className="shadow-ui-border-base sticky inset-x-0 top-0 z-[1] w-full border-b-0 border-t-0 shadow-[0_1px_1px_0]" ref={scrollableRef}
style={{ transform: "translate3d(0,0,0)" }} onScroll={handleHorizontalScroll}
className="min-h-0 w-full flex-1 overflow-auto overscroll-none border-y"
> >
{instance.getHeaderGroups().map((headerGroup) => ( <Table className="relative isolate w-full">
<Table.Row <Table.Header
key={headerGroup.id} className="shadow-ui-border-base sticky inset-x-0 top-0 z-[1] w-full border-b-0 border-t-0 shadow-[0_1px_1px_0]"
className={clx("border-b-0", { style={{ transform: "translate3d(0,0,0)" }}
"[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
hasActions,
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
hasSelect,
})}
> >
{headerGroup.headers.map((header, idx) => { {instance.getHeaderGroups().map((headerGroup) => (
const canSort = header.column.getCanSort() <Table.Row
const sortDirection = header.column.getIsSorted() key={headerGroup.id}
const sortHandler = header.column.getToggleSortingHandler() className={clx("border-b-0", {
"[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
hasActions,
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
hasSelect,
})}
>
<SortableContext
items={sortableItems}
strategy={horizontalListSortingStrategy}
>
{headerGroup.headers.map((header, idx) => {
const canSort = header.column.getCanSort()
const sortDirection = header.column.getIsSorted()
const sortHandler = header.column.getToggleSortingHandler()
const isActionHeader = header.id === "action" const isActionHeader = header.id === "action"
const isSelectHeader = header.id === "select" const isSelectHeader = header.id === "select"
const isSpecialHeader = isActionHeader || isSelectHeader const isSpecialHeader = isActionHeader || isSelectHeader
const isDraggable = !isSpecialHeader
const Wrapper = canSort ? "button" : "div" const Wrapper = canSort ? "button" : "div"
const isFirstColumn = hasSelect ? idx === 1 : idx === 0 const isFirstColumn = hasSelect ? idx === 1 : idx === 0
return ( // Get header alignment from column metadata
<Table.HeaderCell const headerAlign = (header.column.columnDef.meta as any)?.___alignMetaData?.headerAlign || 'left'
key={header.id} const isRightAligned = headerAlign === 'right'
className={clx("whitespace-nowrap", { const isCenterAligned = headerAlign === 'center'
"w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]":
isSelectHeader, const HeaderCellComponent = isDraggable ? DataTableSortableHeaderCell : DataTableNonSortableHeaderCell
"w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]":
isActionHeader, return (
"after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']": <HeaderCellComponent
isFirstColumn, key={header.id}
"after:bg-ui-border-base": id={header.id}
showStickyBorder && isFirstColumn, isFirstColumn={isFirstColumn}
"bg-ui-bg-subtle sticky": className={clx("whitespace-nowrap", {
isFirstColumn || isSelectHeader, "w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]":
"left-0": isSelectHeader,
isSelectHeader || (isFirstColumn && !hasSelect), "w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]":
"left-[calc(20px+24px+24px)]": isActionHeader,
isFirstColumn && hasSelect, "after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
isFirstColumn,
"after:bg-ui-border-base":
showStickyBorder && isFirstColumn,
"bg-ui-bg-subtle sticky":
isFirstColumn || isSelectHeader,
"left-0":
isSelectHeader || (isFirstColumn && !hasSelect),
"left-[calc(20px+24px+24px)]":
isFirstColumn && hasSelect,
})}
style={
!isSpecialHeader
? {
width: header.column.columnDef.size,
maxWidth: header.column.columnDef.maxSize,
minWidth: header.column.columnDef.minSize,
}
: undefined
}
>
<Wrapper
type={canSort ? "button" : undefined}
onClick={canSort ? sortHandler : undefined}
onMouseDown={canSort ? (e) => e.stopPropagation() : undefined}
className={clx(
"group flex cursor-default items-center gap-2",
{
"cursor-pointer": canSort,
"w-full": isRightAligned || isCenterAligned,
"w-fit": !isRightAligned && !isCenterAligned,
"justify-end": isRightAligned,
"justify-center": isCenterAligned,
}
)}
>
{canSort && isRightAligned && (
<DataTableSortingIcon direction={sortDirection} />
)}
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{canSort && !isRightAligned && (
<DataTableSortingIcon direction={sortDirection} />
)}
</Wrapper>
</HeaderCellComponent>
)
})} })}
style={ </SortableContext>
!isSpecialHeader </Table.Row>
? { ))}
</Table.Header>
<Table.Body className="border-b-0 border-t-0">
{instance.getRowModel().rows.map((row) => {
return (
<Table.Row
key={row.id}
onMouseEnter={() => (hoveredRowId.current = row.id)}
onMouseLeave={() => (hoveredRowId.current = null)}
onClick={(e) => instance.onRowClick?.(e, row)}
className={clx("group/row last-of-type:border-b-0", {
"cursor-pointer": !!instance.onRowClick,
})}
>
{row.getVisibleCells().map((cell, idx) => {
const isSelectCell = cell.column.id === "select"
const isActionCell = cell.column.id === "action"
const isSpecialCell = isSelectCell || isActionCell
const isFirstColumn = hasSelect ? idx === 1 : idx === 0
return (
<Table.Cell
key={cell.id}
className={clx(
"items-stretch truncate whitespace-nowrap",
{
"w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]":
isSelectCell,
"w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]":
isActionCell,
"bg-ui-bg-base group-hover/row:bg-ui-bg-base-hover transition-fg sticky h-full":
isFirstColumn || isSelectCell,
"after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
isFirstColumn,
"after:bg-ui-border-base":
showStickyBorder && isFirstColumn,
"left-0":
isSelectCell || (isFirstColumn && !hasSelect),
"left-[calc(20px+24px+24px)]":
isFirstColumn && hasSelect,
}
)}
style={
!isSpecialCell
? {
width: cell.column.columnDef.size,
maxWidth: cell.column.columnDef.maxSize,
minWidth: cell.column.columnDef.minSize,
}
: undefined
}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Cell>
)
})}
</Table.Row>
)
})}
</Table.Body>
</Table>
</div>
</DndContext>
) : (
<div
ref={scrollableRef}
onScroll={handleHorizontalScroll}
className="min-h-0 w-full flex-1 overflow-auto overscroll-none border-y"
>
<Table className="relative isolate w-full">
<Table.Header
className="shadow-ui-border-base sticky inset-x-0 top-0 z-[1] w-full border-b-0 border-t-0 shadow-[0_1px_1px_0]"
style={{ transform: "translate3d(0,0,0)" }}
>
{instance.getHeaderGroups().map((headerGroup) => (
<Table.Row
key={headerGroup.id}
className={clx("border-b-0", {
"[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap":
hasActions,
"[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap":
hasSelect,
})}
>
{headerGroup.headers.map((header, idx) => {
const canSort = header.column.getCanSort()
const sortDirection = header.column.getIsSorted()
const sortHandler = header.column.getToggleSortingHandler()
const isActionHeader = header.id === "action"
const isSelectHeader = header.id === "select"
const isSpecialHeader = isActionHeader || isSelectHeader
const Wrapper = canSort ? "button" : "div"
const isFirstColumn = hasSelect ? idx === 1 : idx === 0
// Get header alignment from column metadata
const headerAlign = (header.column.columnDef.meta as any)?.___alignMetaData?.headerAlign || 'left'
const isRightAligned = headerAlign === 'right'
const isCenterAligned = headerAlign === 'center'
return (
<Table.HeaderCell
key={header.id}
className={clx("whitespace-nowrap", {
"w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]":
isSelectHeader,
"w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]":
isActionHeader,
"after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
isFirstColumn,
"after:bg-ui-border-base":
showStickyBorder && isFirstColumn,
"bg-ui-bg-subtle sticky":
isFirstColumn || isSelectHeader,
"left-0":
isSelectHeader || (isFirstColumn && !hasSelect),
"left-[calc(20px+24px+24px)]":
isFirstColumn && hasSelect,
})}
style={
!isSpecialHeader
? {
width: header.column.columnDef.size, width: header.column.columnDef.size,
maxWidth: header.column.columnDef.maxSize, maxWidth: header.column.columnDef.maxSize,
minWidth: header.column.columnDef.minSize, minWidth: header.column.columnDef.minSize,
} }
: undefined : undefined
} }
>
<Wrapper
type={canSort ? "button" : undefined}
onClick={canSort ? sortHandler : undefined}
className={clx(
"group flex w-fit cursor-default items-center gap-2",
{
"cursor-pointer": canSort,
}
)}
> >
{flexRender( <Wrapper
header.column.columnDef.header, type={canSort ? "button" : undefined}
header.getContext() onClick={canSort ? sortHandler : undefined}
)} onMouseDown={canSort ? (e) => e.stopPropagation() : undefined}
{canSort && ( className={clx(
<DataTableSortingIcon direction={sortDirection} /> "group flex cursor-default items-center gap-2",
)} {
</Wrapper> "cursor-pointer": canSort,
</Table.HeaderCell> "w-full": isRightAligned || isCenterAligned,
) "w-fit": !isRightAligned && !isCenterAligned,
})} "justify-end": isRightAligned,
</Table.Row> "justify-center": isCenterAligned,
))} }
</Table.Header> )}
<Table.Body className="border-b-0 border-t-0"> >
{instance.getRowModel().rows.map((row) => { {canSort && isRightAligned && (
return ( <DataTableSortingIcon direction={sortDirection} />
<Table.Row )}
key={row.id} {flexRender(
onMouseEnter={() => (hoveredRowId.current = row.id)} header.column.columnDef.header,
onMouseLeave={() => (hoveredRowId.current = null)} header.getContext()
onClick={(e) => instance.onRowClick?.(e, row)} )}
className={clx("group/row last-of-type:border-b-0", { {canSort && !isRightAligned && (
"cursor-pointer": !!instance.onRowClick, <DataTableSortingIcon direction={sortDirection} />
)}
</Wrapper>
</Table.HeaderCell>
)
})} })}
> </Table.Row>
{row.getVisibleCells().map((cell, idx) => { ))}
const isSelectCell = cell.column.id === "select" </Table.Header>
const isActionCell = cell.column.id === "action" <Table.Body className="border-b-0 border-t-0">
const isSpecialCell = isSelectCell || isActionCell {instance.getRowModel().rows.map((row) => {
return (
<Table.Row
key={row.id}
onMouseEnter={() => (hoveredRowId.current = row.id)}
onMouseLeave={() => (hoveredRowId.current = null)}
onClick={(e) => instance.onRowClick?.(e, row)}
className={clx("group/row last-of-type:border-b-0", {
"cursor-pointer": !!instance.onRowClick,
})}
>
{row.getVisibleCells().map((cell, idx) => {
const isSelectCell = cell.column.id === "select"
const isActionCell = cell.column.id === "action"
const isSpecialCell = isSelectCell || isActionCell
const isFirstColumn = hasSelect ? idx === 1 : idx === 0 const isFirstColumn = hasSelect ? idx === 1 : idx === 0
return ( return (
<Table.Cell <Table.Cell
key={cell.id} key={cell.id}
className={clx( className={clx(
"items-stretch truncate whitespace-nowrap", "items-stretch truncate whitespace-nowrap",
{ {
"w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]": "w-[calc(20px+24px+24px)] min-w-[calc(20px+24px+24px)] max-w-[calc(20px+24px+24px)]":
isSelectCell, isSelectCell,
"w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]": "w-[calc(28px+24px+4px)] min-w-[calc(28px+24px+4px)] max-w-[calc(28px+24px+4px)]":
isActionCell, isActionCell,
"bg-ui-bg-base group-hover/row:bg-ui-bg-base-hover transition-fg sticky h-full": "bg-ui-bg-base group-hover/row:bg-ui-bg-base-hover transition-fg sticky h-full":
isFirstColumn || isSelectCell, isFirstColumn || isSelectCell,
"after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']": "after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
isFirstColumn, isFirstColumn,
"after:bg-ui-border-base": "after:bg-ui-border-base":
showStickyBorder && isFirstColumn, showStickyBorder && isFirstColumn,
"left-0": "left-0":
isSelectCell || (isFirstColumn && !hasSelect), isSelectCell || (isFirstColumn && !hasSelect),
"left-[calc(20px+24px+24px)]": "left-[calc(20px+24px+24px)]":
isFirstColumn && hasSelect, isFirstColumn && hasSelect,
} }
)} )}
style={ style={
!isSpecialCell !isSpecialCell
? { ? {
width: cell.column.columnDef.size, width: cell.column.columnDef.size,
maxWidth: cell.column.columnDef.maxSize, maxWidth: cell.column.columnDef.maxSize,
minWidth: cell.column.columnDef.minSize, minWidth: cell.column.columnDef.minSize,
} }
: undefined : undefined
} }
> >
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext()
)} )}
</Table.Cell> </Table.Cell>
) )
})} })}
</Table.Row> </Table.Row>
) )
})} })}
</Table.Body> </Table.Body>
</Table> </Table>
</div> </div>
)
)} )}
<DataTableEmptyStateDisplay <DataTableEmptyStateDisplay
state={instance.emptyState} state={instance.emptyState}

View File

@@ -1,4 +1,5 @@
import { DataTableFilterBar } from "@/blocks/data-table/components/data-table-filter-bar" import { DataTableFilterBar } from "@/blocks/data-table/components/data-table-filter-bar"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { clx } from "@/utils/clx" import { clx } from "@/utils/clx"
import * as React from "react" import * as React from "react"
@@ -8,6 +9,14 @@ interface DataTableToolbarTranslations {
* @default "Clear all" * @default "Clear all"
*/ */
clearAll?: string clearAll?: string
/**
* The tooltip for the sorting menu
*/
sort?: string
/**
* The tooltip for the columns menu
*/
columns?: string
} }
interface DataTableToolbarProps { interface DataTableToolbarProps {
@@ -23,12 +32,19 @@ interface DataTableToolbarProps {
* The translations of strings in the toolbar. * The translations of strings in the toolbar.
*/ */
translations?: DataTableToolbarTranslations translations?: DataTableToolbarTranslations
/**
* Custom content to render in the filter bar
*/
filterBarContent?: React.ReactNode
} }
/** /**
* Toolbar shown for the data table. * Toolbar shown for the data table.
*/ */
const DataTableToolbar = (props: DataTableToolbarProps) => { const DataTableToolbar = (props: DataTableToolbarProps) => {
const { instance } = useDataTableContext()
const hasFilters = instance.getFilters().length > 0
return ( return (
<div className="flex flex-col divide-y"> <div className="flex flex-col divide-y">
<div className={clx("flex items-center px-6 py-4", props.className)}> <div className={clx("flex items-center px-6 py-4", props.className)}>
@@ -36,7 +52,12 @@ const DataTableToolbar = (props: DataTableToolbarProps) => {
</div> </div>
<DataTableFilterBar <DataTableFilterBar
clearAllFiltersLabel={props.translations?.clearAll} clearAllFiltersLabel={props.translations?.clearAll}
/> alwaysShow={hasFilters}
sortingTooltip={props.translations?.sort}
columnsTooltip={props.translations?.columns}
>
{props.filterBarContent}
</DataTableFilterBar>
</div> </div>
) )
} }

View File

@@ -15,7 +15,13 @@ const DataTableContextProvider = <TData,>({
children, children,
}: DataTableContextProviderProps<TData>) => { }: DataTableContextProviderProps<TData>) => {
return ( return (
<DataTableContext.Provider value={{ instance }}> <DataTableContext.Provider
value={{
instance,
enableColumnVisibility: instance.enableColumnVisibility,
enableColumnOrder: instance.enableColumnOrder
}}
>
{children} {children}
</DataTableContext.Provider> </DataTableContext.Provider>
) )

View File

@@ -3,6 +3,8 @@ import { UseDataTableReturn } from "../use-data-table"
export interface DataTableContextValue<TData> { export interface DataTableContextValue<TData> {
instance: UseDataTableReturn<TData> instance: UseDataTableReturn<TData>
enableColumnVisibility: boolean
enableColumnOrder: boolean
} }
export const DataTableContext = export const DataTableContext =

View File

@@ -248,24 +248,24 @@ const columns = [
[ [
{ {
label: "Edit", label: "Edit",
onClick: () => {}, onClick: () => { },
icon: <PencilSquare />, icon: <PencilSquare />,
}, },
{ {
label: "Edit", label: "Edit",
onClick: () => {}, onClick: () => { },
icon: <PencilSquare />, icon: <PencilSquare />,
}, },
{ {
label: "Edit", label: "Edit",
onClick: () => {}, onClick: () => { },
icon: <PencilSquare />, icon: <PencilSquare />,
}, },
], ],
[ [
{ {
label: "Delete", label: "Delete",
onClick: () => {}, onClick: () => { },
icon: <Trash />, icon: <Trash />,
}, },
], ],
@@ -385,6 +385,13 @@ const KitchenSinkDemo = () => {
}, },
}) })
const handleFilteringChange = (
state: DataTableFilteringState,
) => {
console.log("Filtering changed:", state)
setFiltering(state)
}
const [pagination, setPagination] = React.useState<DataTablePaginationState>({ const [pagination, setPagination] = React.useState<DataTablePaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
@@ -414,7 +421,7 @@ const KitchenSinkDemo = () => {
}, },
filtering: { filtering: {
state: filtering, state: filtering,
onFilteringChange: setFiltering, onFilteringChange: handleFilteringChange,
}, },
rowSelection: { rowSelection: {
state: rowSelection, state: rowSelection,

View File

@@ -5,6 +5,8 @@ import * as React from "react"
import { clx } from "@/utils/clx" import { clx } from "@/utils/clx"
import { DataTableCommandBar } from "./components/data-table-command-bar" import { DataTableCommandBar } from "./components/data-table-command-bar"
import { DataTableColumnVisibilityMenu } from "./components/data-table-column-visibility-menu"
import { DataTableFilterBar } from "./components/data-table-filter-bar"
import { DataTableFilterMenu } from "./components/data-table-filter-menu" import { DataTableFilterMenu } from "./components/data-table-filter-menu"
import { DataTablePagination } from "./components/data-table-pagination" import { DataTablePagination } from "./components/data-table-pagination"
import { DataTableSearch } from "./components/data-table-search" import { DataTableSearch } from "./components/data-table-search"
@@ -58,6 +60,8 @@ const DataTable = Object.assign(Root, {
Search: DataTableSearch, Search: DataTableSearch,
SortingMenu: DataTableSortingMenu, SortingMenu: DataTableSortingMenu,
FilterMenu: DataTableFilterMenu, FilterMenu: DataTableFilterMenu,
FilterBar: DataTableFilterBar,
ColumnVisibilityMenu: DataTableColumnVisibilityMenu,
Pagination: DataTablePagination, Pagination: DataTablePagination,
CommandBar: DataTableCommandBar, CommandBar: DataTableCommandBar,
}) })

View File

@@ -12,6 +12,7 @@ export type {
DataTableColumnFilter, DataTableColumnFilter,
DataTableCommand, DataTableCommand,
DataTableDateComparisonOperator, DataTableDateComparisonOperator,
DataTableNumberComparisonOperator,
DataTableEmptyState, DataTableEmptyState,
DataTableEmptyStateContent, DataTableEmptyStateContent,
DataTableEmptyStateProps, DataTableEmptyStateProps,
@@ -25,3 +26,6 @@ export type {
DataTableSortDirection, DataTableSortDirection,
DataTableSortingState, DataTableSortingState,
} from "./types" } from "./types"
// Re-export types from @tanstack/react-table that are used in the public API
export type { VisibilityState, ColumnOrderState } from "@tanstack/react-table"

View File

@@ -73,10 +73,24 @@ export type DataTableSortableColumnDef = {
enableSorting?: boolean enableSorting?: boolean
} }
export type DataTableHeaderAlignment = 'left' | 'center' | 'right'
export type DataTableAlignableColumnDef = {
/**
* The alignment of the header content.
* @default 'left'
*/
headerAlign?: DataTableHeaderAlignment
}
export type DataTableSortableColumnDefMeta = { export type DataTableSortableColumnDefMeta = {
___sortMetaData?: DataTableSortableColumnDef ___sortMetaData?: DataTableSortableColumnDef
} }
export type DataTableAlignableColumnDefMeta = {
___alignMetaData?: DataTableAlignableColumnDef
}
export type DataTableActionColumnDefMeta<TData> = { export type DataTableActionColumnDefMeta<TData> = {
___actions?: ___actions?:
| DataTableAction<TData>[] | DataTableAction<TData>[]
@@ -151,8 +165,8 @@ export interface DataTableColumnHelper<TData> {
>( >(
accessor: TAccessor, accessor: TAccessor,
column: TAccessor extends AccessorFn<TData> column: TAccessor extends AccessorFn<TData>
? DataTableDisplayColumnDef<TData, TValue> & DataTableSortableColumnDef ? DataTableDisplayColumnDef<TData, TValue> & DataTableSortableColumnDef & DataTableAlignableColumnDef
: DataTableIdentifiedColumnDef<TData, TValue> & DataTableSortableColumnDef : DataTableIdentifiedColumnDef<TData, TValue> & DataTableSortableColumnDef & DataTableAlignableColumnDef
) => TAccessor extends AccessorFn<TData> ) => TAccessor extends AccessorFn<TData>
? AccessorFnColumnDef<TData, TValue> ? AccessorFnColumnDef<TData, TValue>
: AccessorKeyColumnDef<TData, TValue> : AccessorKeyColumnDef<TData, TValue>
@@ -192,7 +206,7 @@ export type DataTableFilteringState<
[K in keyof T]: T[K] [K in keyof T]: T[K]
} }
export type DataTableFilterType = "radio" | "select" | "date" export type DataTableFilterType = "radio" | "select" | "date" | "multiselect" | "string" | "number" | "custom"
export type DataTableFilterOption<T = string> = { export type DataTableFilterOption<T = string> = {
label: string label: string
value: T value: T
@@ -259,10 +273,57 @@ export interface DataTableDateFilterProps extends DataTableBaseFilterProps {
options: DataTableFilterOption<DataTableDateComparisonOperator>[] options: DataTableFilterOption<DataTableDateComparisonOperator>[]
} }
export interface DataTableMultiselectFilterProps extends DataTableBaseFilterProps {
type: "multiselect"
options: DataTableFilterOption[]
/**
* Whether to show a search input for the options.
* @default true
*/
searchable?: boolean
}
export interface DataTableStringFilterProps extends DataTableBaseFilterProps {
type: "string"
/**
* Placeholder text for the input.
*/
placeholder?: string
}
export interface DataTableNumberFilterProps extends DataTableBaseFilterProps {
type: "number"
/**
* Placeholder text for the input.
*/
placeholder?: string
/**
* Whether to include comparison operators.
* @default true
*/
includeOperators?: boolean
}
export interface DataTableCustomFilterProps extends DataTableBaseFilterProps {
type: "custom"
/**
* Custom render function for the filter.
*/
render: (props: {
value: any
onChange: (value: any) => void
onRemove: () => void
}) => React.ReactNode
}
export type DataTableFilterProps = export type DataTableFilterProps =
| DataTableRadioFilterProps | DataTableRadioFilterProps
| DataTableSelectFilterProps | DataTableSelectFilterProps
| DataTableDateFilterProps | DataTableDateFilterProps
| DataTableMultiselectFilterProps
| DataTableStringFilterProps
| DataTableNumberFilterProps
| DataTableCustomFilterProps
export type DataTableFilter< export type DataTableFilter<
T extends DataTableFilterProps = DataTableFilterProps T extends DataTableFilterProps = DataTableFilterProps
@@ -295,6 +356,29 @@ export type DataTableDateComparisonOperator = {
$gt?: string $gt?: string
} }
export type DataTableNumberComparisonOperator = {
/**
* The filtered number must be greater than or equal to this value.
*/
$gte?: number
/**
* The filtered number must be less than or equal to this value.
*/
$lte?: number
/**
* The filtered number must be less than this value.
*/
$lt?: number
/**
* The filtered number must be greater than this value.
*/
$gt?: number
/**
* The filtered number must be equal to this value.
*/
$eq?: number
}
type DataTableCommandAction = ( type DataTableCommandAction = (
selection: DataTableRowSelectionState selection: DataTableRowSelectionState
) => void | Promise<void> ) => void | Promise<void>

View File

@@ -2,6 +2,7 @@ import {
ColumnFilter, ColumnFilter,
ColumnFiltersState, ColumnFiltersState,
type ColumnSort, type ColumnSort,
type ColumnOrderState,
getCoreRowModel, getCoreRowModel,
PaginationState, PaginationState,
type RowSelectionState, type RowSelectionState,
@@ -9,6 +10,7 @@ import {
type TableOptions, type TableOptions,
type Updater, type Updater,
useReactTable, useReactTable,
type VisibilityState,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import * as React from "react" import * as React from "react"
import { import {
@@ -106,6 +108,20 @@ interface DataTableOptions<TData>
* @default true * @default true
*/ */
autoResetPageIndex?: boolean autoResetPageIndex?: boolean
/**
* The state and callback for the column visibility.
*/
columnVisibility?: {
state: VisibilityState
onColumnVisibilityChange: (state: VisibilityState) => void
}
/**
* The state and callback for the column order.
*/
columnOrder?: {
state: ColumnOrderState
onColumnOrderChange: (state: ColumnOrderState) => void
}
} }
interface UseDataTableReturn<TData> interface UseDataTableReturn<TData>
@@ -119,6 +135,8 @@ interface UseDataTableReturn<TData>
| "previousPage" | "previousPage"
| "getPageCount" | "getPageCount"
| "getAllColumns" | "getAllColumns"
| "setColumnVisibility"
| "setColumnOrder"
> { > {
getSorting: () => DataTableSortingState | null getSorting: () => DataTableSortingState | null
setSorting: ( setSorting: (
@@ -156,6 +174,10 @@ interface UseDataTableReturn<TData>
enableFiltering: boolean enableFiltering: boolean
enableSorting: boolean enableSorting: boolean
enableSearch: boolean enableSearch: boolean
enableColumnVisibility: boolean
enableColumnOrder: boolean
columnOrder: ColumnOrderState
setColumnOrderFromArray: (order: string[]) => void
} }
const useDataTable = <TData,>({ const useDataTable = <TData,>({
@@ -170,6 +192,8 @@ const useDataTable = <TData,>({
onRowClick, onRowClick,
autoResetPageIndex = true, autoResetPageIndex = true,
isLoading = false, isLoading = false,
columnVisibility,
columnOrder,
...options ...options
}: DataTableOptions<TData>): UseDataTableReturn<TData> => { }: DataTableOptions<TData>): UseDataTableReturn<TData> => {
const { state: sortingState, onSortingChange } = sorting ?? {} const { state: sortingState, onSortingChange } = sorting ?? {}
@@ -180,6 +204,10 @@ const useDataTable = <TData,>({
onRowSelectionChange, onRowSelectionChange,
enableRowSelection, enableRowSelection,
} = rowSelection ?? {} } = rowSelection ?? {}
const { state: columnVisibilityState, onColumnVisibilityChange } = columnVisibility ?? {}
const { state: columnOrderState, onColumnOrderChange } = columnOrder ?? {}
// Store filter metadata like openOnMount
const autoResetPageIndexHandler = React.useCallback(() => { const autoResetPageIndexHandler = React.useCallback(() => {
return autoResetPageIndex return autoResetPageIndex
@@ -230,6 +258,32 @@ const useDataTable = <TData,>({
: undefined : undefined
}, [onPaginationChange, paginationState]) }, [onPaginationChange, paginationState])
const columnVisibilityStateHandler = React.useCallback(() => {
return onColumnVisibilityChange
? (updaterOrValue: Updater<VisibilityState>) => {
const value =
typeof updaterOrValue === "function"
? updaterOrValue(columnVisibilityState ?? {})
: updaterOrValue
onColumnVisibilityChange(value)
}
: undefined
}, [onColumnVisibilityChange, columnVisibilityState])
const columnOrderStateHandler = React.useCallback(() => {
return onColumnOrderChange
? (updaterOrValue: Updater<ColumnOrderState>) => {
const value =
typeof updaterOrValue === "function"
? updaterOrValue(columnOrderState ?? [])
: updaterOrValue
onColumnOrderChange(value)
}
: undefined
}, [onColumnOrderChange, columnOrderState])
const instance = useReactTable({ const instance = useReactTable({
...options, ...options,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
@@ -243,6 +297,8 @@ const useDataTable = <TData,>({
}) })
), ),
pagination: paginationState, pagination: paginationState,
columnVisibility: columnVisibilityState ?? {},
columnOrder: columnOrderState ?? [],
}, },
enableRowSelection, enableRowSelection,
rowCount, rowCount,
@@ -250,6 +306,8 @@ const useDataTable = <TData,>({
onRowSelectionChange: rowSelectionStateHandler(), onRowSelectionChange: rowSelectionStateHandler(),
onSortingChange: sortingStateHandler(), onSortingChange: sortingStateHandler(),
onPaginationChange: paginationStateHandler(), onPaginationChange: paginationStateHandler(),
onColumnVisibilityChange: columnVisibilityStateHandler(),
onColumnOrderChange: columnOrderStateHandler(),
manualSorting: true, manualSorting: true,
manualPagination: true, manualPagination: true,
manualFiltering: true, manualFiltering: true,
@@ -289,7 +347,7 @@ const useDataTable = <TData,>({
return null return null
} }
return filter.options as DataTableFilterOption<T>[] return ((filter as any).options as DataTableFilterOption<T>[]) || null
}, },
[getFilters] [getFilters]
) )
@@ -307,11 +365,11 @@ const useDataTable = <TData,>({
}, [instance]) }, [instance])
const addFilter = React.useCallback( const addFilter = React.useCallback(
(filter: ColumnFilter) => { (filter: DataTableColumnFilter) => {
if (filter.value) { const currentFilters = getFiltering()
autoResetPageIndexHandler()?.() const newFilters = { ...currentFilters, [filter.id]: filter.value }
} autoResetPageIndexHandler()?.()
onFilteringChange?.({ ...getFiltering(), [filter.id]: filter.value }) onFilteringChange?.(newFilters)
}, },
[onFilteringChange, getFiltering, autoResetPageIndexHandler] [onFilteringChange, getFiltering, autoResetPageIndexHandler]
) )
@@ -424,12 +482,20 @@ const useDataTable = <TData,>({
const enableFiltering: boolean = !!filtering const enableFiltering: boolean = !!filtering
const enableSorting: boolean = !!sorting const enableSorting: boolean = !!sorting
const enableSearch: boolean = !!search const enableSearch: boolean = !!search
const enableColumnVisibility: boolean = !!columnVisibility
const enableColumnOrder: boolean = !!columnOrder
const setColumnOrderFromArray = React.useCallback((order: string[]) => {
instance.setColumnOrder(order)
}, [instance])
return { return {
// Table // Table
getHeaderGroups: instance.getHeaderGroups, getHeaderGroups: instance.getHeaderGroups,
getRowModel: instance.getRowModel, getRowModel: instance.getRowModel,
getAllColumns: instance.getAllColumns, getAllColumns: instance.getAllColumns,
setColumnVisibility: instance.setColumnVisibility,
setColumnOrder: instance.setColumnOrder,
// Pagination // Pagination
enablePagination, enablePagination,
getCanNextPage: instance.getCanNextPage, getCanNextPage: instance.getCanNextPage,
@@ -468,6 +534,12 @@ const useDataTable = <TData,>({
// Loading // Loading
isLoading, isLoading,
showSkeleton, showSkeleton,
// Column Visibility
enableColumnVisibility,
// Column Order
enableColumnOrder,
columnOrder: instance.getState().columnOrder,
setColumnOrderFromArray,
} }
} }
@@ -516,7 +588,7 @@ function onFilteringChangeTransformer(
: updaterOrValue : updaterOrValue
const transformedValue = Object.fromEntries( const transformedValue = Object.fromEntries(
value.map((filter) => [filter.id, filter]) value.map((filter) => [filter.id, filter.value])
) )
onFilteringChange(transformedValue) onFilteringChange(transformedValue)

View File

@@ -13,6 +13,8 @@ import {
DataTableSelectColumnDef, DataTableSelectColumnDef,
DataTableSortableColumnDef, DataTableSortableColumnDef,
DataTableSortableColumnDefMeta, DataTableSortableColumnDefMeta,
DataTableAlignableColumnDef,
DataTableAlignableColumnDefMeta,
} from "../types" } from "../types"
const createDataTableColumnHelper = < const createDataTableColumnHelper = <
@@ -27,13 +29,15 @@ const createDataTableColumnHelper = <
sortLabel, sortLabel,
sortAscLabel, sortAscLabel,
sortDescLabel, sortDescLabel,
headerAlign,
meta, meta,
enableSorting, enableSorting,
...rest ...rest
} = column as any & DataTableSortableColumnDef } = column as any & DataTableSortableColumnDef & DataTableAlignableColumnDef
const extendedMeta: DataTableSortableColumnDefMeta = { const extendedMeta: DataTableSortableColumnDefMeta & DataTableAlignableColumnDefMeta = {
___sortMetaData: { sortLabel, sortAscLabel, sortDescLabel }, ___sortMetaData: { sortLabel, sortAscLabel, sortDescLabel },
___alignMetaData: { headerAlign },
...(meta || {}), ...(meta || {}),
} }

View File

@@ -3753,6 +3753,31 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@dnd-kit/accessibility@npm:^3.1.1":
version: 3.1.1
resolution: "@dnd-kit/accessibility@npm:3.1.1"
dependencies:
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
checksum: be0bf41716dc58f9386bc36906ec1ce72b7b42b6d1d0e631d347afe9bd8714a829bd6f58a346dd089b1519e93918ae2f94497411a61a4f5e4d9247c6cfd1fef8
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.0.0":
version: 6.3.1
resolution: "@dnd-kit/core@npm:6.3.1"
dependencies:
"@dnd-kit/accessibility": ^3.1.1
"@dnd-kit/utilities": ^3.2.2
tslib: ^2.0.0
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 196db95d81096d9dc248983533eab91ba83591770fa5c894b1ac776f42af0d99522b3fd5bb3923411470e4733fcfa103e6ee17adc17b9b7eb54c7fbec5ff7c52
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.1.0": "@dnd-kit/core@npm:^6.1.0":
version: 6.1.0 version: 6.1.0
resolution: "@dnd-kit/core@npm:6.1.0" resolution: "@dnd-kit/core@npm:6.1.0"
@@ -3767,6 +3792,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@dnd-kit/sortable@npm:^7.0.0":
version: 7.0.2
resolution: "@dnd-kit/sortable@npm:7.0.2"
dependencies:
"@dnd-kit/utilities": ^3.2.0
tslib: ^2.0.0
peerDependencies:
"@dnd-kit/core": ^6.0.7
react: ">=16.8.0"
checksum: 06aeb113eeeb470bb2443bf1c48d597157bb3a1caa9740e60c2fa73a3076e753cd083a2d381f0556bd7e9873e851a49ce8ea14796ac02e2d796eabea4e27196d
languageName: node
linkType: hard
"@dnd-kit/sortable@npm:^8.0.0": "@dnd-kit/sortable@npm:^8.0.0":
version: 8.0.0 version: 8.0.0
resolution: "@dnd-kit/sortable@npm:8.0.0" resolution: "@dnd-kit/sortable@npm:8.0.0"
@@ -3780,7 +3818,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@dnd-kit/utilities@npm:^3.2.2": "@dnd-kit/utilities@npm:^3.2.0, @dnd-kit/utilities@npm:^3.2.2":
version: 3.2.2 version: 3.2.2
resolution: "@dnd-kit/utilities@npm:3.2.2" resolution: "@dnd-kit/utilities@npm:3.2.2"
dependencies: dependencies:
@@ -7533,6 +7571,9 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/ui@workspace:packages/design-system/ui" resolution: "@medusajs/ui@workspace:packages/design-system/ui"
dependencies: dependencies:
"@dnd-kit/core": ^6.0.0
"@dnd-kit/sortable": ^7.0.0
"@dnd-kit/utilities": ^3.2.0
"@faker-js/faker": ^9.2.0 "@faker-js/faker": ^9.2.0
"@medusajs/icons": 2.10.1 "@medusajs/icons": 2.10.1
"@medusajs/ui-preset": 2.10.1 "@medusajs/ui-preset": 2.10.1