feat(ui): add column visibility and drag-and-drop reordering support (#13198)
This commit is contained in:
@@ -19,7 +19,6 @@ export class Views {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// View configurations
|
|
||||||
async listConfigurations(
|
async listConfigurations(
|
||||||
entity: string,
|
entity: string,
|
||||||
query?: HttpTypes.AdminGetViewConfigurationsParams,
|
query?: HttpTypes.AdminGetViewConfigurationsParams,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
yarn.lock
43
yarn.lock
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user