Feat/datatable core enhancements (#13193)

**What**

This PR adds core DataTable enhancements to support view configurations in the admin dashboard. This is part of a set of stacked PRs to add the feature to Medusa.

- Puts handlers in place to update the visible columns in a table and the order in which they appear.
- Adds a ViewPills component for displaying and switching between saved view configurations
- Integrated view configuration hooks (useViewConfigurations) with the DataTable

Note: Column drag-and-drop reordering and the column visibility UI controls are not included in this PR as they require additional UI library updates - which will come in the next PR.

Example of what this looks like with the feature flag turned on - note the view pills with "default" in the top. This will expand when the data table behavior adds configuration.
<img width="2492" height="758" alt="CleanShot 2025-08-13 at 2  31 35@2x" src="https://github.com/user-attachments/assets/ee770f1c-dae1-49da-b255-1a6d615789de" />
This commit is contained in:
Sebastian Rindom
2025-09-01 12:30:05 +02:00
committed by GitHub
parent fa10c78ed3
commit 5a46372afd
12 changed files with 1368 additions and 83 deletions

View File

@@ -0,0 +1,160 @@
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { HttpTypes } from "@medusajs/types"
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
interface UseColumnStateReturn {
visibleColumns: Record<string, boolean>
columnOrder: string[]
currentColumns: {
visible: string[]
order: string[]
}
setVisibleColumns: (visibility: Record<string, boolean>) => void
setColumnOrder: (order: string[]) => void
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
handleViewChange: (view: ViewConfiguration | null, apiColumns: HttpTypes.AdminViewColumn[]) => void
initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void
}
export function useColumnState(
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
activeView?: ViewConfiguration | null
): UseColumnStateReturn {
// Initialize state lazily to avoid unnecessary re-renders
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(() => {
if (apiColumns?.length && activeView) {
// If there's an active view, initialize with its configuration
const visibility: Record<string, boolean> = {}
apiColumns.forEach(column => {
visibility[column.field] = activeView.configuration.visible_columns.includes(column.field)
})
return visibility
} else if (apiColumns?.length) {
return getInitialColumnVisibility(apiColumns)
}
return {}
})
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
if (activeView) {
// If there's an active view, use its column order
return activeView.configuration.column_order || []
} else if (apiColumns?.length) {
return getInitialColumnOrder(apiColumns)
}
return []
})
const currentColumns = useMemo(() => {
const visible = Object.entries(visibleColumns)
.filter(([_, isVisible]) => isVisible)
.map(([field]) => field)
return {
visible,
order: columnOrder,
}
}, [visibleColumns, columnOrder])
const handleColumnVisibilityChange = useCallback((visibility: Record<string, boolean>) => {
setVisibleColumns(visibility)
}, [])
const handleViewChange = useCallback((
view: ViewConfiguration | null,
apiColumns: HttpTypes.AdminViewColumn[]
) => {
if (view) {
// Apply view configuration
const newVisibility: Record<string, boolean> = {}
apiColumns.forEach(column => {
newVisibility[column.field] = view.configuration.visible_columns.includes(column.field)
})
setVisibleColumns(newVisibility)
setColumnOrder(view.configuration.column_order)
} else {
// Reset to default visibility when no view is selected
setVisibleColumns(getInitialColumnVisibility(apiColumns))
setColumnOrder(getInitialColumnOrder(apiColumns))
}
}, [])
const initializeColumns = useCallback((apiColumns: HttpTypes.AdminViewColumn[]) => {
// Only initialize if we don't already have column state
if (Object.keys(visibleColumns).length === 0) {
setVisibleColumns(getInitialColumnVisibility(apiColumns))
}
if (columnOrder.length === 0) {
setColumnOrder(getInitialColumnOrder(apiColumns))
}
}, [])
// Track previous active view to detect changes
const prevActiveViewRef = useRef<ViewConfiguration | null | undefined>()
// Sync local state when active view updates (e.g., after saving)
useEffect(() => {
if (apiColumns?.length && activeView && prevActiveViewRef.current) {
// Check if the active view has been updated (same ID but different updated_at)
if (
prevActiveViewRef.current.id === activeView.id &&
prevActiveViewRef.current.updated_at !== activeView.updated_at
) {
// Sync local state with the updated view configuration
const newVisibility: Record<string, boolean> = {}
apiColumns.forEach(column => {
newVisibility[column.field] = activeView.configuration.visible_columns.includes(column.field)
})
setVisibleColumns(newVisibility)
setColumnOrder(activeView.configuration.column_order)
}
}
prevActiveViewRef.current = activeView
}, [activeView, apiColumns])
return {
visibleColumns,
columnOrder,
currentColumns,
setVisibleColumns,
setColumnOrder,
handleColumnVisibilityChange,
handleViewChange,
initializeColumns,
}
}
// Utility functions
const DEFAULT_COLUMN_ORDER = 500
/**
* Gets the initial column visibility state from API columns
*/
function getInitialColumnVisibility(
apiColumns: HttpTypes.AdminViewColumn[]
): Record<string, boolean> {
const visibility: Record<string, boolean> = {}
apiColumns.forEach(column => {
visibility[column.field] = column.default_visible
})
return visibility
}
/**
* Gets the initial column order from API columns
*/
function getInitialColumnOrder(
apiColumns: HttpTypes.AdminViewColumn[]
): string[] {
const sortedColumns = [...apiColumns].sort((a, b) => {
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
const orderB = b.default_order ?? DEFAULT_COLUMN_ORDER
return orderA - orderB
})
return sortedColumns.map(col => col.field)
}