feat(dashboard): configurable product views (#13408)
* feat: add a reusable configurable data table * fix: cleanup * fix: cleanup * fix: cache invalidation * fix: test * fix: add configurable products * feat: add configurable product table * fix: build errors+table style * fix: sticky header column * add translations * fix: cleanup counterenderer * fix: formatting * fix: client still skips nulls * fix: test * fix: cleanup * fix: revert client bracket format * fix: better typing * fix: add placeholder data to product list
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { createDataTableColumnHelper } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { getDisplayStrategy, getEntityAccessor } from "../../../lib/table-display-utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getCellRenderer, getColumnValue } from "../../../lib/table/cell-renderers"
|
||||
|
||||
export interface ColumnAdapter<TData> {
|
||||
getColumnAlignment?: (column: HttpTypes.AdminViewColumn) => "left" | "center" | "right"
|
||||
getCustomAccessor?: (field: string, column: HttpTypes.AdminViewColumn) => any
|
||||
transformCellValue?: (value: any, row: TData, column: HttpTypes.AdminViewColumn) => React.ReactNode
|
||||
getColumnAlignment?: (column: HttpTypes.AdminColumn) => "left" | "center" | "right"
|
||||
getCustomAccessor?: (field: string, column: HttpTypes.AdminColumn) => any
|
||||
transformCellValue?: (value: any, row: TData, column: HttpTypes.AdminColumn) => React.ReactNode
|
||||
}
|
||||
|
||||
export function useConfigurableTableColumns<TData = any>(
|
||||
entity: string,
|
||||
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
|
||||
apiColumns: HttpTypes.AdminColumn[] | undefined,
|
||||
adapter?: ColumnAdapter<TData>
|
||||
) {
|
||||
const columnHelper = createDataTableColumnHelper<TData>()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!apiColumns?.length) {
|
||||
@@ -22,37 +24,45 @@ export function useConfigurableTableColumns<TData = any>(
|
||||
}
|
||||
|
||||
return apiColumns.map(apiColumn => {
|
||||
// Get the display strategy for this column
|
||||
const displayStrategy = getDisplayStrategy(apiColumn)
|
||||
let renderType = apiColumn.computed?.type
|
||||
|
||||
// Get the entity-specific accessor or use adapter's custom accessor
|
||||
const accessor = adapter?.getCustomAccessor
|
||||
? adapter.getCustomAccessor(apiColumn.field, apiColumn)
|
||||
: getEntityAccessor(entity, apiColumn.field, apiColumn)
|
||||
if (!renderType) {
|
||||
if (apiColumn.semantic_type === 'timestamp') {
|
||||
renderType = 'timestamp'
|
||||
} else if (apiColumn.field === 'display_id') {
|
||||
renderType = 'display_id'
|
||||
} else if (apiColumn.field === 'total') {
|
||||
renderType = 'total'
|
||||
} else if (apiColumn.semantic_type === 'currency') {
|
||||
renderType = 'currency'
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = getCellRenderer(
|
||||
renderType,
|
||||
apiColumn.data_type
|
||||
)
|
||||
|
||||
// Determine header alignment
|
||||
const headerAlign = adapter?.getColumnAlignment
|
||||
? adapter.getColumnAlignment(apiColumn)
|
||||
: getDefaultColumnAlignment(apiColumn)
|
||||
|
||||
const accessor = (row: TData) => getColumnValue(row, apiColumn)
|
||||
|
||||
return columnHelper.accessor(accessor, {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue, row }) => {
|
||||
cell: ({ getValue, row }: { getValue: any, row: any }) => {
|
||||
const value = getValue()
|
||||
|
||||
// If the value is already a React element (from computed columns), return it directly
|
||||
if (React.isValidElement(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Allow adapter to transform the value
|
||||
if (adapter?.transformCellValue) {
|
||||
return adapter.transformCellValue(value, row.original, apiColumn)
|
||||
const transformed = adapter.transformCellValue(value, row.original, apiColumn)
|
||||
if (transformed !== null) {
|
||||
return transformed
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, use the display strategy to format the value
|
||||
return displayStrategy(value, row.original)
|
||||
return renderer(value, row.original, apiColumn, t)
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
@@ -63,21 +73,18 @@ export function useConfigurableTableColumns<TData = any>(
|
||||
headerAlign, // Pass the header alignment to the DataTable
|
||||
} as any)
|
||||
})
|
||||
}, [entity, apiColumns, adapter])
|
||||
}, [entity, apiColumns, adapter, t])
|
||||
}
|
||||
|
||||
function getDefaultColumnAlignment(column: HttpTypes.AdminViewColumn): "left" | "center" | "right" {
|
||||
// Currency columns should be right-aligned
|
||||
function getDefaultColumnAlignment(column: HttpTypes.AdminColumn): "left" | "center" | "right" {
|
||||
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
||||
return "right"
|
||||
}
|
||||
|
||||
// Number columns should be right-aligned (except identifiers)
|
||||
|
||||
if (column.data_type === "number" && column.context !== "identifier") {
|
||||
return "right"
|
||||
}
|
||||
|
||||
// Total/amount/price columns should be right-aligned
|
||||
|
||||
if (
|
||||
column.field.includes("total") ||
|
||||
column.field.includes("amount") ||
|
||||
@@ -87,19 +94,16 @@ function getDefaultColumnAlignment(column: HttpTypes.AdminViewColumn): "left" |
|
||||
) {
|
||||
return "right"
|
||||
}
|
||||
|
||||
// Status columns should be center-aligned
|
||||
|
||||
if (column.semantic_type === "status") {
|
||||
return "center"
|
||||
}
|
||||
|
||||
// Country columns should be center-aligned
|
||||
if (column.computed?.type === "country_code" ||
|
||||
column.field === "country" ||
|
||||
column.field.includes("country_code")) {
|
||||
|
||||
if (column.computed?.type === "country_code" ||
|
||||
column.field === "country" ||
|
||||
column.field.includes("country_code")) {
|
||||
return "center"
|
||||
}
|
||||
|
||||
// Default to left alignment
|
||||
|
||||
return "left"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export const useProductTableFilters = (
|
||||
label: t("fields.type"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
searchable: true,
|
||||
options: product_types.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
@@ -99,6 +100,7 @@ export const useProductTableFilters = (
|
||||
label: t("fields.tag"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
searchable: true,
|
||||
options: product_tags.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
@@ -114,6 +116,7 @@ export const useProductTableFilters = (
|
||||
label: t("fields.salesChannel"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
searchable: true,
|
||||
options: sales_channels.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
|
||||
@@ -24,13 +24,10 @@ export interface UseTableConfigurationOptions {
|
||||
}
|
||||
|
||||
export interface UseTableConfigurationReturn {
|
||||
// View configuration
|
||||
activeView: any
|
||||
createView: any
|
||||
updateView: any
|
||||
isViewConfigEnabled: boolean
|
||||
|
||||
// Column state
|
||||
visibleColumns: Record<string, boolean>
|
||||
columnOrder: string[]
|
||||
currentColumns: {
|
||||
@@ -39,20 +36,12 @@ export interface UseTableConfigurationReturn {
|
||||
}
|
||||
setColumnOrder: (order: string[]) => void
|
||||
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||
|
||||
// Configuration state
|
||||
currentConfiguration: TableConfiguration
|
||||
hasConfigurationChanged: boolean
|
||||
handleClearConfiguration: () => void
|
||||
|
||||
// API columns
|
||||
apiColumns: HttpTypes.AdminViewColumn[] | undefined
|
||||
apiColumns: HttpTypes.AdminColumn[] | undefined
|
||||
isLoadingColumns: boolean
|
||||
|
||||
// Query params
|
||||
queryParams: Record<string, any>
|
||||
|
||||
// Required fields for API calls
|
||||
requiredFields: string
|
||||
}
|
||||
|
||||
@@ -64,24 +53,20 @@ function parseSortingState(value: string) {
|
||||
|
||||
export function useTableConfiguration({
|
||||
entity,
|
||||
pageSize = 20,
|
||||
queryPrefix = "",
|
||||
filters = [],
|
||||
}: UseTableConfigurationOptions): UseTableConfigurationReturn {
|
||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||
const [_, setSearchParams] = useSearchParams()
|
||||
|
||||
// View configurations
|
||||
const { activeView, createView } = useViewConfigurations(entity)
|
||||
const currentActiveView = activeView?.view_configuration || null
|
||||
const { updateView } = useViewConfiguration(entity, currentActiveView?.id || "")
|
||||
|
||||
// Entity columns
|
||||
const { columns: apiColumns, isLoading: isLoadingColumns } = useEntityColumns(entity, {
|
||||
enabled: isViewConfigEnabled,
|
||||
})
|
||||
|
||||
// Query params
|
||||
const queryParams = useQueryParams(
|
||||
["q", "order", ...filters.map(f => f.id)],
|
||||
queryPrefix
|
||||
@@ -152,7 +137,7 @@ export function useTableConfiguration({
|
||||
|
||||
// Check if configuration has changed from view
|
||||
const [debouncedHasConfigChanged, setDebouncedHasConfigChanged] = useState(false)
|
||||
|
||||
|
||||
const hasConfigurationChanged = useMemo(() => {
|
||||
const currentFilters = currentConfiguration.filters
|
||||
const currentSorting = currentConfiguration.sorting
|
||||
@@ -282,32 +267,21 @@ export function useTableConfiguration({
|
||||
}, [entity, apiColumns, visibleColumns])
|
||||
|
||||
return {
|
||||
// View configuration
|
||||
activeView: currentActiveView,
|
||||
createView,
|
||||
updateView,
|
||||
isViewConfigEnabled,
|
||||
|
||||
// Column state
|
||||
visibleColumns,
|
||||
columnOrder,
|
||||
currentColumns,
|
||||
setColumnOrder,
|
||||
handleColumnVisibilityChange,
|
||||
|
||||
// Configuration state
|
||||
currentConfiguration,
|
||||
hasConfigurationChanged: debouncedHasConfigChanged,
|
||||
handleClearConfiguration,
|
||||
|
||||
// API columns
|
||||
apiColumns,
|
||||
isLoadingColumns,
|
||||
|
||||
// Query params
|
||||
queryParams,
|
||||
|
||||
// Required fields
|
||||
requiredFields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user