Skip to content

Commit f4515d0

Browse files
committed
feat: schema introspected columns
1 parent 3e9aa25 commit f4515d0

20 files changed

Lines changed: 1502 additions & 52 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# DataTable Column Visibility Customization Guide
2+
3+
## Overview
4+
5+
The new `@medusajs/ui` DataTable component provides built-in column visibility functionality through `Primitive.ColumnVisibilityMenu`. However, this component doesn't support custom implementations out of the box. This guide explains how to integrate custom column visibility with the DataTable component.
6+
7+
## Current Architecture
8+
9+
### Default DataTable Component
10+
The default DataTable from `@medusajs/ui` uses:
11+
- `Primitive.ColumnVisibilityMenu` - A built-in component that cannot be customized
12+
- `columnVisibility` state managed through `useDataTable` hook
13+
- No props to pass custom column visibility components
14+
15+
### Custom Column Visibility Requirements
16+
To integrate with the column visibility API, we need:
17+
1. Access to API column metadata (names, IDs, etc.)
18+
2. Ability to save/load column preferences
19+
3. Custom UI for column selection
20+
4. Integration with the table's visibility state
21+
22+
## Solution Approaches
23+
24+
### Approach 1: Custom DataTable Wrapper (Recommended)
25+
26+
Create a wrapper component that replaces the built-in column visibility menu with a custom implementation:
27+
28+
```tsx
29+
// data-table-with-custom-columns.tsx
30+
import { DataTable as Primitive, useDataTable } from "@medusajs/ui"
31+
import { CustomColumnVisibility } from "./custom-column-visibility"
32+
33+
export const DataTableWithCustomColumns = (props) => {
34+
const instance = useDataTable({
35+
// ... table configuration
36+
columnVisibility: {
37+
state: columnVisibility,
38+
onColumnVisibilityChange: setColumnVisibility,
39+
}
40+
})
41+
42+
return (
43+
<Primitive instance={instance}>
44+
<Primitive.Toolbar>
45+
{/* Replace Primitive.ColumnVisibilityMenu with custom component */}
46+
{enableColumnVisibility && (
47+
<CustomColumnVisibility
48+
table={instance}
49+
apiColumns={apiColumns}
50+
onSaveColumns={onSaveColumns}
51+
/>
52+
)}
53+
</Primitive.Toolbar>
54+
<Primitive.Table />
55+
</Primitive>
56+
)
57+
}
58+
```
59+
60+
### Approach 2: Extend Existing DataTable
61+
62+
Extend the existing DataTable component with additional props:
63+
64+
```tsx
65+
// Extended DataTable usage
66+
<DataTable
67+
// ... standard props
68+
enableColumnVisibility={true}
69+
apiColumns={apiColumns}
70+
onSaveColumns={handleSaveColumns}
71+
/>
72+
```
73+
74+
### Approach 3: Hook-based Column Management
75+
76+
Create a hook that manages column visibility state:
77+
78+
```tsx
79+
const useColumnVisibilityManager = (entity: string) => {
80+
// Load/save column preferences
81+
// Sync with API
82+
// Return visibility state and handlers
83+
}
84+
```
85+
86+
## Implementation Details
87+
88+
### Custom Column Visibility Component
89+
90+
The custom component should:
91+
1. Display column names from the API
92+
2. Allow toggling individual columns
93+
3. Support "toggle all" functionality
94+
4. Save preferences when changed
95+
5. Show loading state while fetching column metadata
96+
97+
```tsx
98+
export const CustomColumnVisibility = ({
99+
table,
100+
apiColumns,
101+
isLoading,
102+
onSaveColumns,
103+
}) => {
104+
// Get hideable columns from table
105+
const columns = table.getAllColumns().filter(col => col.getCanHide())
106+
107+
// Handle column toggle
108+
const handleToggle = (columnId) => {
109+
table.getColumn(columnId)?.toggleVisibility()
110+
// Save preferences
111+
const visibleColumns = getVisibleColumns()
112+
onSaveColumns(visibleColumns)
113+
}
114+
115+
// Render dropdown with column list
116+
return (
117+
<DropdownMenu>
118+
{/* Column checkboxes */}
119+
</DropdownMenu>
120+
)
121+
}
122+
```
123+
124+
### Integration with Column API
125+
126+
Use the views API to get column metadata:
127+
128+
```tsx
129+
import { useOrderColumns } from "../../hooks/api/views"
130+
131+
const OrderTable = () => {
132+
const { columns: apiColumns, isLoading } = useOrderColumns()
133+
134+
return (
135+
<DataTableWithCustomColumns
136+
apiColumns={apiColumns}
137+
apiColumnsLoading={isLoading}
138+
onSaveColumns={saveColumnPreferences}
139+
/>
140+
)
141+
}
142+
```
143+
144+
## Usage Example
145+
146+
```tsx
147+
import { DataTableWithCustomColumns } from "./data-table-with-custom-columns"
148+
import { useOrderColumns } from "../../hooks/api/views"
149+
150+
export const OrderListTable = () => {
151+
const { columns: apiColumns } = useOrderColumns()
152+
153+
const handleSaveColumns = async (columns) => {
154+
// Save to local storage
155+
localStorage.setItem('order-columns', JSON.stringify(columns))
156+
157+
// Or save to API
158+
await sdk.admin.views.updateColumns('orders', columns)
159+
}
160+
161+
return (
162+
<DataTableWithCustomColumns
163+
data={orders}
164+
columns={tableColumns}
165+
enableColumnVisibility
166+
apiColumns={apiColumns}
167+
onSaveColumns={handleSaveColumns}
168+
/>
169+
)
170+
}
171+
```
172+
173+
## Benefits
174+
175+
1. **Full control** over column visibility UI
176+
2. **Integration** with backend column metadata API
177+
3. **Persistence** of user preferences
178+
4. **Consistent** with existing DataTable API
179+
5. **Reusable** across different table implementations
180+
181+
## Limitations
182+
183+
1. Requires maintaining a custom DataTable wrapper
184+
2. Must keep in sync with @medusajs/ui updates
185+
3. Cannot use the built-in Primitive.ColumnVisibilityMenu
186+
187+
## Future Considerations
188+
189+
Ideally, the `@medusajs/ui` library would support:
190+
- Custom render props for column visibility
191+
- Column metadata integration
192+
- Persistence callbacks
193+
- Custom component injection
194+
195+
Until then, the wrapper approach provides the most flexibility while maintaining compatibility with the existing DataTable API.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { Button, Checkbox, DropdownMenu } from "@medusajs/ui"
2+
import { Adjustments, Spinner } from "@medusajs/icons"
3+
import { Table as ReactTable } from "@tanstack/react-table"
4+
import { useTranslation } from "react-i18next"
5+
import { useState, useEffect } from "react"
6+
7+
interface CustomColumnVisibilityProps<TData> {
8+
table: ReactTable<TData>
9+
apiColumns?: Array<{ id: string; name: string }>
10+
isLoading?: boolean
11+
onSaveColumns?: (columns: string[]) => void
12+
}
13+
14+
export const CustomColumnVisibility = <TData,>({
15+
table,
16+
apiColumns,
17+
isLoading = false,
18+
onSaveColumns,
19+
}: CustomColumnVisibilityProps<TData>) => {
20+
const { t } = useTranslation()
21+
const [open, setOpen] = useState(false)
22+
23+
const columns = table
24+
.getAllColumns()
25+
.filter((column) => column.getCanHide())
26+
27+
if (columns.length === 0) {
28+
return null
29+
}
30+
31+
const handleToggleColumn = (columnId: string) => {
32+
const column = table.getColumn(columnId)
33+
if (column) {
34+
column.toggleVisibility()
35+
36+
// Save column visibility state if callback provided
37+
if (onSaveColumns) {
38+
const visibleColumns = table
39+
.getAllColumns()
40+
.filter((col) => col.getIsVisible() && col.getCanHide())
41+
.map((col) => col.id)
42+
onSaveColumns(visibleColumns)
43+
}
44+
}
45+
}
46+
47+
const handleToggleAll = (value: boolean) => {
48+
table.setColumnVisibility(
49+
Object.fromEntries(
50+
columns.map((column) => [column.id, value])
51+
)
52+
)
53+
54+
// Save column visibility state if callback provided
55+
if (onSaveColumns) {
56+
const visibleColumns = value
57+
? columns.map((col) => col.id)
58+
: []
59+
onSaveColumns(visibleColumns)
60+
}
61+
}
62+
63+
const allColumnsVisible = columns.every((column) =>
64+
column.getIsVisible()
65+
)
66+
const someColumnsVisible = columns.some((column) =>
67+
column.getIsVisible()
68+
)
69+
70+
const getColumnLabel = (columnId: string) => {
71+
// First check API columns
72+
const apiColumn = apiColumns?.find(c => c.id === columnId)
73+
if (apiColumn) {
74+
return apiColumn.name
75+
}
76+
77+
// Then check column meta
78+
const column = table.getColumn(columnId)
79+
const meta = column?.columnDef.meta as any
80+
if (meta?.name) {
81+
return meta.name
82+
}
83+
84+
// Fall back to column ID
85+
return columnId
86+
}
87+
88+
return (
89+
<DropdownMenu open={open} onOpenChange={setOpen}>
90+
<DropdownMenu.Trigger asChild>
91+
<Button
92+
variant="secondary"
93+
size="small"
94+
className="gap-x-1.5"
95+
>
96+
<Adjustments />
97+
{t("general.columns")}
98+
</Button>
99+
</DropdownMenu.Trigger>
100+
<DropdownMenu.Content align="end" className="min-w-[200px]">
101+
<DropdownMenu.Label>{t("general.toggleColumns")}</DropdownMenu.Label>
102+
<DropdownMenu.Separator />
103+
{isLoading ? (
104+
<div className="flex items-center justify-center py-6">
105+
<Spinner className="animate-spin" />
106+
</div>
107+
) : (
108+
<>
109+
<DropdownMenu.Item
110+
onSelect={(e: Event) => {
111+
e.preventDefault()
112+
handleToggleAll(!allColumnsVisible)
113+
}}
114+
>
115+
<div className="flex items-center gap-x-2">
116+
<Checkbox
117+
checked={allColumnsVisible}
118+
indeterminate={someColumnsVisible && !allColumnsVisible}
119+
/>
120+
<span>{t("general.toggleAll")}</span>
121+
</div>
122+
</DropdownMenu.Item>
123+
<DropdownMenu.Separator />
124+
<div className="max-h-[300px] overflow-y-auto">
125+
{columns.map((column) => {
126+
return (
127+
<DropdownMenu.Item
128+
key={column.id}
129+
onSelect={(e: Event) => {
130+
e.preventDefault()
131+
handleToggleColumn(column.id)
132+
}}
133+
>
134+
<div className="flex items-center gap-x-2">
135+
<Checkbox checked={column.getIsVisible()} />
136+
<span className="truncate">
137+
{getColumnLabel(column.id)}
138+
</span>
139+
</div>
140+
</DropdownMenu.Item>
141+
)
142+
})}
143+
</div>
144+
</>
145+
)}
146+
</DropdownMenu.Content>
147+
</DropdownMenu>
148+
)
149+
}

0 commit comments

Comments
 (0)