feat(admin): add configurable order views (#13211)
Adds support for configurable order views. https://github.com/user-attachments/assets/ed4a5f61-1667-4ed7-9478-423894f3eba6
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
// import { HttpTypes } from "@medusajs/types"
|
||||
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
|
||||
|
||||
interface ColumnState {
|
||||
visibility: Record<string, boolean>
|
||||
order: string[]
|
||||
}
|
||||
|
||||
interface UseColumnStateReturn {
|
||||
visibleColumns: Record<string, boolean>
|
||||
columnOrder: string[]
|
||||
columnState: ColumnState
|
||||
currentColumns: {
|
||||
visible: string[]
|
||||
order: string[]
|
||||
@@ -12,7 +18,10 @@ interface UseColumnStateReturn {
|
||||
setVisibleColumns: (visibility: Record<string, boolean>) => void
|
||||
setColumnOrder: (order: string[]) => void
|
||||
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||
handleViewChange: (view: ViewConfiguration | null, apiColumns: HttpTypes.AdminViewColumn[]) => void
|
||||
handleViewChange: (
|
||||
view: ViewConfiguration | null,
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
) => void
|
||||
initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void
|
||||
}
|
||||
|
||||
@@ -21,30 +30,42 @@ export function useColumnState(
|
||||
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)
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
() => {
|
||||
if (apiColumns?.length && activeView?.configuration) {
|
||||
// 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) ||
|
||||
false
|
||||
})
|
||||
return visibility
|
||||
} else if (apiColumns?.length) {
|
||||
return getInitialColumnVisibility(apiColumns)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
)
|
||||
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
|
||||
if (activeView) {
|
||||
if (activeView?.configuration?.column_order) {
|
||||
// If there's an active view, use its column order
|
||||
return activeView.configuration.column_order || []
|
||||
return activeView.configuration.column_order
|
||||
} else if (apiColumns?.length) {
|
||||
return getInitialColumnOrder(apiColumns)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const columnState = useMemo<ColumnState>(
|
||||
() => ({
|
||||
visibility: visibleColumns,
|
||||
order: columnOrder,
|
||||
}),
|
||||
[visibleColumns, columnOrder]
|
||||
)
|
||||
|
||||
const currentColumns = useMemo(() => {
|
||||
const visible = Object.entries(visibleColumns)
|
||||
.filter(([_, isVisible]) => isVisible)
|
||||
@@ -56,66 +77,89 @@ export function useColumnState(
|
||||
}
|
||||
}, [visibleColumns, columnOrder])
|
||||
|
||||
const handleColumnVisibilityChange = useCallback((visibility: Record<string, boolean>) => {
|
||||
setVisibleColumns(visibility)
|
||||
}, [])
|
||||
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 handleViewChange = useCallback(
|
||||
(
|
||||
view: ViewConfiguration | null,
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
) => {
|
||||
if (view?.configuration) {
|
||||
// Apply view configuration
|
||||
const newVisibility: Record<string, boolean> = {}
|
||||
apiColumns.forEach((column) => {
|
||||
newVisibility[column.field] =
|
||||
view.configuration.visible_columns?.includes(column.field) || false
|
||||
})
|
||||
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))
|
||||
}
|
||||
}, [])
|
||||
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)
|
||||
|
||||
// Sync local state when active view changes
|
||||
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 &&
|
||||
if (apiColumns?.length) {
|
||||
// Check if this is a different view or an update to the same view
|
||||
const viewChanged = prevActiveViewRef.current?.id !== activeView?.id
|
||||
const viewUpdated =
|
||||
activeView &&
|
||||
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)
|
||||
|
||||
if (viewChanged || viewUpdated) {
|
||||
if (activeView?.configuration) {
|
||||
// Apply the active view's configuration
|
||||
const newVisibility: Record<string, boolean> = {}
|
||||
apiColumns.forEach((column) => {
|
||||
newVisibility[column.field] =
|
||||
activeView.configuration?.visible_columns?.includes(
|
||||
column.field
|
||||
) || false
|
||||
})
|
||||
setVisibleColumns(newVisibility)
|
||||
setColumnOrder(activeView.configuration?.column_order || [])
|
||||
} else {
|
||||
// No active view - reset to defaults
|
||||
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
||||
setColumnOrder(getInitialColumnOrder(apiColumns))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
prevActiveViewRef.current = activeView
|
||||
}, [activeView, apiColumns])
|
||||
|
||||
return {
|
||||
visibleColumns,
|
||||
columnOrder,
|
||||
columnState,
|
||||
currentColumns,
|
||||
setVisibleColumns,
|
||||
setColumnOrder,
|
||||
@@ -135,12 +179,16 @@ const DEFAULT_COLUMN_ORDER = 500
|
||||
function getInitialColumnVisibility(
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
): Record<string, boolean> {
|
||||
if (!apiColumns || apiColumns.length === 0) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const visibility: Record<string, boolean> = {}
|
||||
|
||||
apiColumns.forEach(column => {
|
||||
visibility[column.field] = column.default_visible
|
||||
|
||||
apiColumns.forEach((column) => {
|
||||
visibility[column.field] = column.default_visible ?? true
|
||||
})
|
||||
|
||||
|
||||
return visibility
|
||||
}
|
||||
|
||||
@@ -150,11 +198,15 @@ function getInitialColumnVisibility(
|
||||
function getInitialColumnOrder(
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
): string[] {
|
||||
if (!apiColumns || apiColumns.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return sortedColumns.map((col) => col.field)
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { createDataTableColumnHelper } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getDisplayStrategy, getEntityAccessor } from "../../../lib/table-display-utils"
|
||||
import { getColumnAlignment } from "../../../routes/orders/order-list/components/order-list-table/utils/column-utils"
|
||||
|
||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminOrder>()
|
||||
|
||||
export function useConfigurableOrderTableColumns(apiColumns: any[] | undefined) {
|
||||
return useMemo(() => {
|
||||
if (!apiColumns?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return apiColumns.map(apiColumn => {
|
||||
// Get the display strategy for this column
|
||||
const displayStrategy = getDisplayStrategy(apiColumn)
|
||||
|
||||
// Get the entity-specific accessor or use default
|
||||
const accessor = getEntityAccessor('orders', apiColumn.field, apiColumn)
|
||||
|
||||
// Determine header alignment
|
||||
const headerAlign = getColumnAlignment(apiColumn)
|
||||
|
||||
return columnHelper.accessor(accessor, {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue, row }) => {
|
||||
const value = getValue()
|
||||
|
||||
// If the value is already a React element (from computed columns), return it directly
|
||||
if (React.isValidElement(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Otherwise, use the display strategy to format the value
|
||||
return displayStrategy(value, row.original)
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn, // Store column metadata for future use
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false, // Disable sorting for all columns
|
||||
headerAlign, // Pass the header alignment to the DataTable
|
||||
} as any)
|
||||
})
|
||||
}, [apiColumns])
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
DateCell,
|
||||
DateHeader,
|
||||
} from "../../../components/table/table-cells/common/date-cell"
|
||||
import { CountryCell } from "../../../components/table/table-cells/order/country-cell"
|
||||
import {
|
||||
CustomerCell,
|
||||
CustomerHeader,
|
||||
} from "../../../components/table/table-cells/order/customer-cell"
|
||||
import {
|
||||
DisplayIdCell,
|
||||
DisplayIdHeader,
|
||||
} from "../../../components/table/table-cells/order/display-id-cell"
|
||||
import {
|
||||
FulfillmentStatusCell,
|
||||
FulfillmentStatusHeader,
|
||||
} from "../../../components/table/table-cells/order/fulfillment-status-cell"
|
||||
import {
|
||||
PaymentStatusCell,
|
||||
PaymentStatusHeader,
|
||||
} from "../../../components/table/table-cells/order/payment-status-cell"
|
||||
import {
|
||||
SalesChannelCell,
|
||||
SalesChannelHeader,
|
||||
} from "../../../components/table/table-cells/order/sales-channel-cell"
|
||||
import {
|
||||
TotalCell,
|
||||
TotalHeader,
|
||||
} from "../../../components/table/table-cells/order/total-cell"
|
||||
import { TextCell, TextHeader } from "../../../components/table/table-cells/common/text-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminOrder>()
|
||||
|
||||
/**
|
||||
* Hook to build columns dynamically based on API columns response
|
||||
*/
|
||||
export const useOrderDataTableColumns = (
|
||||
apiColumns: HttpTypes.AdminOrderColumn[] | undefined,
|
||||
visibleColumns: string[]
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!apiColumns || apiColumns.length === 0) {
|
||||
// Return default columns if no API columns
|
||||
return [
|
||||
columnHelper.accessor("display_id", {
|
||||
header: () => <DisplayIdHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const id = getValue()
|
||||
return <DisplayIdCell displayId={id!} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: () => <DateHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const date = new Date(getValue())
|
||||
return <DateCell date={date} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("customer", {
|
||||
header: () => <CustomerHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const customer = getValue()
|
||||
return <CustomerCell customer={customer} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channel", {
|
||||
header: () => <SalesChannelHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const channel = getValue()
|
||||
return <SalesChannelCell channel={channel} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("payment_status", {
|
||||
header: () => <PaymentStatusHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
return <PaymentStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("fulfillment_status", {
|
||||
header: () => <FulfillmentStatusHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
return <FulfillmentStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("total", {
|
||||
header: () => <TotalHeader />,
|
||||
cell: ({ getValue, row }) => {
|
||||
const total = getValue()
|
||||
const currencyCode = row.original.currency_code
|
||||
return <TotalCell currencyCode={currencyCode} total={total} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "country",
|
||||
cell: ({ row }) => {
|
||||
const country = row.original.shipping_address?.country
|
||||
return <CountryCell country={country} />
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
// Build columns from API response
|
||||
return apiColumns
|
||||
.filter((col) => visibleColumns.includes(col.id))
|
||||
.sort((a, b) => {
|
||||
const aIndex = visibleColumns.indexOf(a.id)
|
||||
const bIndex = visibleColumns.indexOf(b.id)
|
||||
return aIndex - bIndex
|
||||
})
|
||||
.map((col) => {
|
||||
// Handle special columns with custom cells
|
||||
switch (col.id) {
|
||||
case "display_id":
|
||||
return columnHelper.accessor("display_id", {
|
||||
header: () => <DisplayIdHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const id = getValue()
|
||||
return <DisplayIdCell displayId={id!} />
|
||||
},
|
||||
})
|
||||
|
||||
case "created_at":
|
||||
case "updated_at":
|
||||
return columnHelper.accessor(col.field as any, {
|
||||
header: () => <DateHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const date = getValue() ? new Date(getValue() as string) : null
|
||||
return date ? <DateCell date={date} /> : null
|
||||
},
|
||||
})
|
||||
|
||||
case "email":
|
||||
return columnHelper.accessor("email", {
|
||||
header: () => <TextHeader text={col.name} />,
|
||||
cell: ({ getValue }) => {
|
||||
const email = getValue()
|
||||
return <TextCell text={email || ""} />
|
||||
},
|
||||
})
|
||||
|
||||
case "customer_display":
|
||||
return columnHelper.accessor("customer", {
|
||||
header: () => <CustomerHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const customer = getValue()
|
||||
return <CustomerCell customer={customer} />
|
||||
},
|
||||
})
|
||||
|
||||
case "sales_channel.name":
|
||||
return columnHelper.accessor("sales_channel", {
|
||||
header: () => <SalesChannelHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const channel = getValue()
|
||||
return <SalesChannelCell channel={channel} />
|
||||
},
|
||||
})
|
||||
|
||||
case "payment_status":
|
||||
return columnHelper.accessor("payment_status", {
|
||||
header: () => <PaymentStatusHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
return <PaymentStatusCell status={status} />
|
||||
},
|
||||
})
|
||||
|
||||
case "fulfillment_status":
|
||||
return columnHelper.accessor("fulfillment_status", {
|
||||
header: () => <FulfillmentStatusHeader />,
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
return <FulfillmentStatusCell status={status} />
|
||||
},
|
||||
})
|
||||
|
||||
case "total":
|
||||
return columnHelper.accessor("total", {
|
||||
header: () => <TotalHeader />,
|
||||
cell: ({ getValue, row }) => {
|
||||
const total = getValue()
|
||||
const currencyCode = row.original.currency_code
|
||||
return <TotalCell currencyCode={currencyCode} total={total} />
|
||||
},
|
||||
})
|
||||
|
||||
case "country":
|
||||
return columnHelper.display({
|
||||
id: "country",
|
||||
cell: ({ row }) => {
|
||||
const country = row.original.shipping_address?.country
|
||||
return <CountryCell country={country} />
|
||||
},
|
||||
})
|
||||
|
||||
default:
|
||||
// Handle relationship fields (e.g., customer.email)
|
||||
if (col.field.includes(".")) {
|
||||
const [relation, field] = col.field.split(".")
|
||||
return columnHelper.accessor((row: any) => {
|
||||
const relationData = row[relation]
|
||||
return relationData?.[field] || ""
|
||||
}, {
|
||||
id: col.id,
|
||||
header: () => <TextHeader text={col.name} />,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return <TextCell text={value || ""} />
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Default text column
|
||||
return columnHelper.accessor(col.field as any, {
|
||||
header: () => <TextHeader text={col.name} />,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return <TextCell text={value || ""} />
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [apiColumns, visibleColumns, t])
|
||||
}
|
||||
Reference in New Issue
Block a user