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:
@@ -224,7 +224,7 @@ medusaIntegrationTestRunner({
|
||||
id: "title",
|
||||
name: "Title",
|
||||
field: "title",
|
||||
default_visible: true,
|
||||
default_visible: false,
|
||||
})
|
||||
|
||||
const handleColumn = response.data.columns.find(
|
||||
@@ -235,7 +235,7 @@ medusaIntegrationTestRunner({
|
||||
id: "handle",
|
||||
name: "Handle",
|
||||
field: "handle",
|
||||
default_visible: true,
|
||||
default_visible: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -66,12 +66,14 @@ interface DataTableProps<TData> {
|
||||
filters?: DataTableFilter[]
|
||||
commands?: DataTableCommand[]
|
||||
action?: DataTableActionProps
|
||||
actions?: DataTableActionProps[]
|
||||
actionMenu?: DataTableActionMenuProps
|
||||
rowCount?: number
|
||||
getRowId: (row: TData) => string
|
||||
enablePagination?: boolean
|
||||
enableSearch?: boolean
|
||||
autoFocusSearch?: boolean
|
||||
enableFilterMenu?: boolean
|
||||
rowHref?: (row: TData) => string
|
||||
emptyState?: DataTableEmptyStateProps
|
||||
heading?: string
|
||||
@@ -105,12 +107,14 @@ export const DataTable = <TData,>({
|
||||
filters,
|
||||
commands,
|
||||
action,
|
||||
actions,
|
||||
actionMenu,
|
||||
getRowId,
|
||||
rowCount = 0,
|
||||
enablePagination = true,
|
||||
enableSearch = true,
|
||||
autoFocusSearch = false,
|
||||
enableFilterMenu,
|
||||
rowHref,
|
||||
heading,
|
||||
subHeading,
|
||||
@@ -138,6 +142,7 @@ export const DataTable = <TData,>({
|
||||
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
||||
|
||||
const enableFiltering = filters && filters.length > 0
|
||||
const showFilterMenu = enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
||||
const enableCommands = commands && commands.length > 0
|
||||
const enableSorting = columns.some((column) => column.enableSorting)
|
||||
|
||||
@@ -381,7 +386,7 @@ export const DataTable = <TData,>({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{enableFiltering && <UiDataTable.FilterMenu />}
|
||||
{showFilterMenu && <UiDataTable.FilterMenu />}
|
||||
{enableSorting && <UiDataTable.SortingMenu />}
|
||||
{enableSearch && (
|
||||
<div className="w-full md:w-auto">
|
||||
@@ -392,7 +397,8 @@ export const DataTable = <TData,>({
|
||||
</div>
|
||||
)}
|
||||
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
||||
{action && <DataTableAction {...action} />}
|
||||
{actions && actions.length > 0 && <DataTableActions actions={actions} />}
|
||||
{!actions && action && <DataTableAction {...action} />}
|
||||
</div>
|
||||
</div>
|
||||
</UiDataTable.Toolbar>
|
||||
@@ -505,3 +511,13 @@ const DataTableAction = ({
|
||||
)
|
||||
}
|
||||
|
||||
const DataTableActions = ({ actions }: { actions: DataTableActionProps[] }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
{actions.map((action, index) => (
|
||||
<DataTableAction key={index} {...action} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +1,45 @@
|
||||
import React, { useState, ReactNode } from "react"
|
||||
import { useState, ReactNode } from "react"
|
||||
import { Container, Button } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DataTable } from "../../data-table"
|
||||
import { SaveViewDialog } from "../save-view-dialog"
|
||||
import { SaveViewDropdown } from "./save-view-dropdown"
|
||||
import { useTableConfiguration } from "../../../hooks/table/use-table-configuration"
|
||||
import { useOrderTableQuery } from "../../../hooks/table/query/use-order-table-query"
|
||||
import { useConfigurableTableColumns } from "../../../hooks/table/columns/use-configurable-table-columns"
|
||||
import { getEntityAdapter } from "../../../lib/table/entity-adapters"
|
||||
import { DataTableColumnDef, DataTableEmptyStateProps, DataTableFilter } from "@medusajs/ui"
|
||||
import { TableAdapter } from "../../../lib/table/table-adapters"
|
||||
|
||||
type DataTableActionProps = {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
} & (
|
||||
| {
|
||||
to: string
|
||||
}
|
||||
| {
|
||||
onClick: () => void
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
export interface ConfigurableDataTableProps<TData> {
|
||||
// Use adapter pattern for entity-specific configuration
|
||||
adapter: TableAdapter<TData>
|
||||
|
||||
// Optional overrides
|
||||
heading?: string
|
||||
subHeading?: string
|
||||
pageSize?: number
|
||||
queryPrefix?: string
|
||||
layout?: "fill" | "auto"
|
||||
actions?: ReactNode
|
||||
actions?: DataTableActionProps[]
|
||||
}
|
||||
|
||||
// Legacy props interface for backward compatibility
|
||||
export interface LegacyConfigurableDataTableProps<TData> {
|
||||
// Entity configuration
|
||||
entity: string
|
||||
entityName?: string
|
||||
|
||||
// Data and columns
|
||||
data: TData[]
|
||||
columns: DataTableColumnDef<TData, any>[]
|
||||
filters?: DataTableFilter[]
|
||||
|
||||
// Table configuration
|
||||
pageSize?: number
|
||||
queryPrefix?: string
|
||||
getRowId: (row: TData) => string
|
||||
rowHref?: (row: TData) => string
|
||||
|
||||
// UI configuration
|
||||
heading?: string
|
||||
subHeading?: string
|
||||
emptyState?: DataTableEmptyStateProps
|
||||
|
||||
// Loading and counts
|
||||
isLoading?: boolean
|
||||
rowCount?: number
|
||||
|
||||
// Additional content
|
||||
actions?: ReactNode
|
||||
|
||||
// Layout
|
||||
layout?: "fill" | "auto"
|
||||
}
|
||||
|
||||
// Internal component that handles adapter mode
|
||||
function ConfigurableDataTableWithAdapter<TData>({
|
||||
export function ConfigurableDataTable<TData>({
|
||||
adapter,
|
||||
heading,
|
||||
subHeading,
|
||||
pageSize: pageSizeProp,
|
||||
queryPrefix: queryPrefixProp,
|
||||
layout = "fill",
|
||||
// actions, // Currently unused
|
||||
actions,
|
||||
}: ConfigurableDataTableProps<TData>) {
|
||||
const { t } = useTranslation()
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||
@@ -77,7 +51,6 @@ function ConfigurableDataTableWithAdapter<TData>({
|
||||
const pageSize = pageSizeProp || adapter.pageSize || 20
|
||||
const queryPrefix = queryPrefixProp || adapter.queryPrefix || ""
|
||||
|
||||
// Get table configuration (single source of truth)
|
||||
const {
|
||||
activeView,
|
||||
createView,
|
||||
@@ -94,6 +67,7 @@ function ConfigurableDataTableWithAdapter<TData>({
|
||||
isLoadingColumns,
|
||||
apiColumns,
|
||||
requiredFields,
|
||||
queryParams,
|
||||
} = useTableConfiguration({
|
||||
entity,
|
||||
pageSize,
|
||||
@@ -101,29 +75,37 @@ function ConfigurableDataTableWithAdapter<TData>({
|
||||
filters,
|
||||
})
|
||||
|
||||
// Get query params for data fetching
|
||||
const { searchParams } = useOrderTableQuery({
|
||||
pageSize,
|
||||
prefix: queryPrefix,
|
||||
const parsedQueryParams = { ...queryParams }
|
||||
filters.forEach(filter => {
|
||||
const filterKey = filter.id
|
||||
if (parsedQueryParams[filterKey] !== undefined) {
|
||||
try {
|
||||
parsedQueryParams[filterKey] = JSON.parse(parsedQueryParams[filterKey])
|
||||
} catch {
|
||||
// If parsing fails, keep the original value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch data using adapter
|
||||
const searchParams = {
|
||||
...parsedQueryParams,
|
||||
fields: requiredFields,
|
||||
limit: pageSize,
|
||||
offset: parsedQueryParams.offset ? Number(parsedQueryParams.offset) : 0,
|
||||
}
|
||||
|
||||
const fetchResult = adapter.useData(requiredFields, searchParams)
|
||||
|
||||
// Generate columns
|
||||
// Use adapter's column adapter if provided, otherwise fall back to entity adapter
|
||||
const columnAdapter = adapter.columnAdapter || getEntityAdapter(entity)
|
||||
const generatedColumns = useConfigurableTableColumns(entity, apiColumns || [], columnAdapter)
|
||||
const columns = (adapter.getColumns && apiColumns)
|
||||
? adapter.getColumns(apiColumns)
|
||||
: generatedColumns
|
||||
|
||||
// Handle errors
|
||||
if (fetchResult.isError) {
|
||||
throw fetchResult.error
|
||||
}
|
||||
|
||||
// View save handlers
|
||||
const handleSaveAsDefault = async () => {
|
||||
try {
|
||||
if (activeView?.is_system_default) {
|
||||
@@ -233,6 +215,8 @@ function ConfigurableDataTableWithAdapter<TData>({
|
||||
}
|
||||
}}
|
||||
prefix={queryPrefix}
|
||||
actions={actions}
|
||||
enableFilterMenu={false}
|
||||
/>
|
||||
|
||||
{saveDialogOpen && (
|
||||
@@ -254,194 +238,3 @@ function ConfigurableDataTableWithAdapter<TData>({
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Internal component that handles legacy mode
|
||||
function ConfigurableDataTableLegacy<TData>(props: LegacyConfigurableDataTableProps<TData>) {
|
||||
const { t } = useTranslation()
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||
const [editingView, setEditingView] = useState<any>(null)
|
||||
|
||||
const {
|
||||
entity,
|
||||
entityName,
|
||||
data,
|
||||
columns,
|
||||
filters = [],
|
||||
pageSize = 20,
|
||||
queryPrefix = "",
|
||||
getRowId,
|
||||
rowHref,
|
||||
heading,
|
||||
subHeading,
|
||||
emptyState,
|
||||
isLoading = false,
|
||||
rowCount = 0,
|
||||
// actions, // Currently unused
|
||||
layout = "fill",
|
||||
} = props
|
||||
|
||||
// Get table configuration
|
||||
const {
|
||||
activeView,
|
||||
createView,
|
||||
updateView,
|
||||
isViewConfigEnabled,
|
||||
visibleColumns,
|
||||
columnOrder,
|
||||
currentColumns,
|
||||
setColumnOrder,
|
||||
handleColumnVisibilityChange,
|
||||
currentConfiguration,
|
||||
hasConfigurationChanged,
|
||||
handleClearConfiguration,
|
||||
isLoadingColumns,
|
||||
} = useTableConfiguration({
|
||||
entity,
|
||||
pageSize,
|
||||
queryPrefix,
|
||||
filters,
|
||||
})
|
||||
|
||||
// View save handlers
|
||||
const handleSaveAsDefault = async () => {
|
||||
try {
|
||||
if (activeView?.is_system_default) {
|
||||
await updateView.mutateAsync({
|
||||
name: activeView.name || null,
|
||||
configuration: {
|
||||
visible_columns: currentColumns.visible,
|
||||
column_order: currentColumns.order,
|
||||
filters: currentConfiguration.filters || {},
|
||||
sorting: currentConfiguration.sorting || null,
|
||||
search: currentConfiguration.search || "",
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await createView.mutateAsync({
|
||||
name: "Default",
|
||||
is_system_default: true,
|
||||
set_active: true,
|
||||
configuration: {
|
||||
visible_columns: currentColumns.visible,
|
||||
column_order: currentColumns.order,
|
||||
filters: currentConfiguration.filters || {},
|
||||
sorting: currentConfiguration.sorting || null,
|
||||
search: currentConfiguration.search || "",
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (_) {
|
||||
// Error is handled by the hook
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateExisting = async () => {
|
||||
if (!activeView) return
|
||||
|
||||
try {
|
||||
await updateView.mutateAsync({
|
||||
name: activeView.name,
|
||||
configuration: {
|
||||
visible_columns: currentColumns.visible,
|
||||
column_order: currentColumns.order,
|
||||
filters: currentConfiguration.filters || {},
|
||||
sorting: currentConfiguration.sorting || null,
|
||||
search: currentConfiguration.search || "",
|
||||
}
|
||||
})
|
||||
} catch (_) {
|
||||
// Error is handled by the hook
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveAsNew = () => {
|
||||
setSaveDialogOpen(true)
|
||||
setEditingView(null)
|
||||
}
|
||||
|
||||
// Filter bar content with save controls
|
||||
const filterBarContent = hasConfigurationChanged ? (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleClearConfiguration}
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Button>
|
||||
<SaveViewDropdown
|
||||
isDefaultView={activeView?.is_system_default || !activeView}
|
||||
currentViewId={activeView?.id}
|
||||
currentViewName={activeView?.name}
|
||||
onSaveAsDefault={handleSaveAsDefault}
|
||||
onUpdateExisting={handleUpdateExisting}
|
||||
onSaveAsNew={handleSaveAsNew}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
getRowId={getRowId}
|
||||
rowCount={rowCount}
|
||||
enablePagination
|
||||
enableSearch
|
||||
pageSize={pageSize}
|
||||
isLoading={isLoading || isLoadingColumns}
|
||||
layout={layout}
|
||||
heading={heading || entityName || (entity ? t(`${entity}.domain` as any) : "")}
|
||||
subHeading={subHeading}
|
||||
enableColumnVisibility={isViewConfigEnabled}
|
||||
initialColumnVisibility={visibleColumns}
|
||||
onColumnVisibilityChange={handleColumnVisibilityChange}
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrderChange={setColumnOrder}
|
||||
enableViewSelector={isViewConfigEnabled}
|
||||
entity={entity}
|
||||
currentColumns={currentColumns}
|
||||
filterBarContent={filterBarContent}
|
||||
rowHref={rowHref}
|
||||
emptyState={emptyState || {
|
||||
empty: {
|
||||
heading: t(`${entity}.list.noRecordsMessage` as any),
|
||||
}
|
||||
}}
|
||||
prefix={queryPrefix}
|
||||
/>
|
||||
|
||||
{saveDialogOpen && (
|
||||
<SaveViewDialog
|
||||
entity={entity}
|
||||
currentColumns={currentColumns}
|
||||
currentConfiguration={currentConfiguration}
|
||||
editingView={editingView}
|
||||
onClose={() => {
|
||||
setSaveDialogOpen(false)
|
||||
setEditingView(null)
|
||||
}}
|
||||
onSaved={() => {
|
||||
setSaveDialogOpen(false)
|
||||
setEditingView(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Main export that delegates to the appropriate component
|
||||
export function ConfigurableDataTable<TData>(
|
||||
props: ConfigurableDataTableProps<TData> | LegacyConfigurableDataTableProps<TData>
|
||||
) {
|
||||
// Check if using new adapter pattern or legacy props
|
||||
if ('adapter' in props) {
|
||||
return <ConfigurableDataTableWithAdapter<TData> {...props} />
|
||||
} else {
|
||||
return <ConfigurableDataTableLegacy<TData> {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2022,6 +2022,54 @@
|
||||
"required": ["draft", "published", "proposed", "rejected"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"columns": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"product_display": {
|
||||
"type": "string"
|
||||
},
|
||||
"variants_count": {
|
||||
"type": "string"
|
||||
},
|
||||
"sales_channels_display": {
|
||||
"type": "string"
|
||||
},
|
||||
"collection": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"handle": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"product_display",
|
||||
"variants_count",
|
||||
"sales_channels_display",
|
||||
"collection",
|
||||
"status",
|
||||
"thumbnail",
|
||||
"title",
|
||||
"handle",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2690,6 +2738,7 @@
|
||||
"variantCount_other",
|
||||
"deleteVariantWarning",
|
||||
"productStatus",
|
||||
"columns",
|
||||
"fields",
|
||||
"variant",
|
||||
"options",
|
||||
|
||||
@@ -536,6 +536,18 @@
|
||||
"proposed": "Proposed",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"columns": {
|
||||
"product_display": "Product",
|
||||
"variants_count": "Variants",
|
||||
"sales_channels_display": "Sales Channels",
|
||||
"collection": "Collection",
|
||||
"status": "Status",
|
||||
"thumbnail": "Thumbnail",
|
||||
"title": "Title",
|
||||
"handle": "Handle",
|
||||
"created_at": "Created",
|
||||
"updated_at": "Updated"
|
||||
},
|
||||
"fields": {
|
||||
"title": {
|
||||
"label": "Title",
|
||||
|
||||
305
packages/admin/dashboard/src/lib/table/cell-renderers.tsx
Normal file
305
packages/admin/dashboard/src/lib/table/cell-renderers.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import React from "react"
|
||||
import { Badge, StatusBadge, Tooltip } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import ReactCountryFlag from "react-country-flag"
|
||||
import { getCountryByIso2 } from "../data/countries"
|
||||
import { ProductCell } from "../../components/table/table-cells/product/product-cell"
|
||||
import { CollectionCell } from "../../components/table/table-cells/product/collection-cell"
|
||||
import { SalesChannelsCell } from "../../components/table/table-cells/product/sales-channels-cell"
|
||||
import { VariantCell } from "../../components/table/table-cells/product/variant-cell"
|
||||
import { ProductStatusCell } from "../../components/table/table-cells/product/product-status-cell"
|
||||
import { DateCell } from "../../components/table/table-cells/common/date-cell"
|
||||
import { DisplayIdCell } from "../../components/table/table-cells/order/display-id-cell"
|
||||
import { TotalCell } from "../../components/table/table-cells/order/total-cell"
|
||||
import { MoneyAmountCell } from "../../components/table/table-cells/common/money-amount-cell"
|
||||
import { TFunction } from "i18next"
|
||||
|
||||
export type CellRenderer<TData = any> = (
|
||||
value: any,
|
||||
row: TData,
|
||||
column: HttpTypes.AdminColumn,
|
||||
t: TFunction
|
||||
) => React.ReactNode
|
||||
|
||||
export type RendererRegistry = Map<string, CellRenderer>
|
||||
|
||||
const cellRenderers: RendererRegistry = new Map()
|
||||
|
||||
const getNestedValue = (obj: any, path: string) => {
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj)
|
||||
}
|
||||
|
||||
const TextRenderer: CellRenderer = (value, _row, _column, _t) => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const CountRenderer: CellRenderer = (value, _row, _column, t) => {
|
||||
const items = value || []
|
||||
const count = Array.isArray(items) ? items.length : 0
|
||||
return t('general.items', { count })
|
||||
}
|
||||
|
||||
const StatusRenderer: CellRenderer = (value, row, column, t) => {
|
||||
if (!value) return '-'
|
||||
|
||||
if (column.field === 'status' && row.status && (row.handle || row.is_giftcard !== undefined)) {
|
||||
return <ProductStatusCell status={row.status} />
|
||||
}
|
||||
|
||||
// Generic status badge
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
case 'published':
|
||||
case 'fulfilled':
|
||||
case 'paid':
|
||||
return 'green'
|
||||
case 'pending':
|
||||
case 'proposed':
|
||||
case 'processing':
|
||||
return 'orange'
|
||||
case 'draft':
|
||||
return 'grey'
|
||||
case 'rejected':
|
||||
case 'failed':
|
||||
case 'canceled':
|
||||
return 'red'
|
||||
default:
|
||||
return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing translation keys where available
|
||||
const getTranslatedStatus = (status: string): string => {
|
||||
if (!t) return status
|
||||
|
||||
const lowerStatus = status.toLowerCase()
|
||||
switch (lowerStatus) {
|
||||
case 'active':
|
||||
return t('general.active', 'Active') as string
|
||||
case 'published':
|
||||
return t('products.productStatus.published', 'Published') as string
|
||||
case 'draft':
|
||||
return t('orders.status.draft', 'Draft') as string
|
||||
case 'pending':
|
||||
return t('orders.status.pending', 'Pending') as string
|
||||
case 'canceled':
|
||||
return t('orders.status.canceled', 'Canceled') as string
|
||||
default:
|
||||
// Try generic status translation with fallback
|
||||
return t(`status.${lowerStatus}`, status) as string
|
||||
}
|
||||
}
|
||||
|
||||
const translatedValue = getTranslatedStatus(value)
|
||||
|
||||
return (
|
||||
<StatusBadge color={getStatusColor(value)}>
|
||||
{translatedValue}
|
||||
</StatusBadge>
|
||||
)
|
||||
}
|
||||
|
||||
const BadgeListRenderer: CellRenderer = (value, row, column, t) => {
|
||||
// For sales channels
|
||||
if (column.field === 'sales_channels_display' || column.field === 'sales_channels') {
|
||||
return <SalesChannelsCell salesChannels={row.sales_channels} />
|
||||
}
|
||||
|
||||
// Generic badge list
|
||||
if (!Array.isArray(value)) return '-'
|
||||
|
||||
const items = value.slice(0, 2)
|
||||
const remaining = value.length - 2
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{items.map((item, index) => (
|
||||
<Badge key={index} size="xsmall">
|
||||
{typeof item === 'string' ? item : item.name || item.title || '-'}
|
||||
</Badge>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<Badge size="xsmall" color="grey">
|
||||
{t ? t('general.plusCountMore', '+ {{count}} more', { count: remaining }) : `+${remaining}`}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductInfoRenderer: CellRenderer = (_, row, _column, _t) => {
|
||||
return <ProductCell product={row} />
|
||||
}
|
||||
|
||||
const CollectionRenderer: CellRenderer = (_, row, _column, _t) => {
|
||||
return <CollectionCell collection={row.collection} />
|
||||
}
|
||||
|
||||
const VariantsRenderer: CellRenderer = (_, row, _column, _t) => {
|
||||
return <VariantCell variants={row.variants} />
|
||||
}
|
||||
|
||||
// Order-specific renderers
|
||||
const CustomerNameRenderer: CellRenderer = (_, row, _column, t) => {
|
||||
if (row.customer?.first_name || row.customer?.last_name) {
|
||||
const fullName = `${row.customer.first_name || ''} ${row.customer.last_name || ''}`.trim()
|
||||
if (fullName) return fullName
|
||||
}
|
||||
|
||||
// Fall back to email
|
||||
if (row.customer?.email) {
|
||||
return row.customer.email
|
||||
}
|
||||
|
||||
// Fall back to phone
|
||||
if (row.customer?.phone) {
|
||||
return row.customer.phone
|
||||
}
|
||||
|
||||
return t ? t('customers.guest', 'Guest') : 'Guest'
|
||||
}
|
||||
|
||||
const AddressSummaryRenderer: CellRenderer = (_, row, column, _t) => {
|
||||
let address = null
|
||||
if (column.field === 'shipping_address_display') {
|
||||
address = row.shipping_address
|
||||
} else if (column.field === 'billing_address_display') {
|
||||
address = row.billing_address
|
||||
} else {
|
||||
address = row.shipping_address || row.billing_address
|
||||
}
|
||||
|
||||
if (!address) return '-'
|
||||
|
||||
const parts = []
|
||||
|
||||
if (address.address_1) {
|
||||
parts.push(address.address_1)
|
||||
}
|
||||
|
||||
const locationParts = []
|
||||
if (address.city) locationParts.push(address.city)
|
||||
if (address.province) locationParts.push(address.province)
|
||||
if (address.postal_code) locationParts.push(address.postal_code)
|
||||
|
||||
if (locationParts.length > 0) {
|
||||
parts.push(locationParts.join(', '))
|
||||
}
|
||||
|
||||
if (address.country_code) {
|
||||
parts.push(address.country_code.toUpperCase())
|
||||
}
|
||||
|
||||
return parts.join(' • ') || '-'
|
||||
}
|
||||
|
||||
const CountryCodeRenderer: CellRenderer = (_, row, _column, _t) => {
|
||||
const countryCode = row.shipping_address?.country_code
|
||||
|
||||
if (!countryCode) return <div className="flex w-full justify-center">-</div>
|
||||
|
||||
const country = getCountryByIso2(countryCode)
|
||||
const displayName = country?.display_name || countryCode.toUpperCase()
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Tooltip content={displayName}>
|
||||
<div className="flex size-4 items-center justify-center overflow-hidden rounded-sm">
|
||||
<ReactCountryFlag
|
||||
countryCode={countryCode.toUpperCase()}
|
||||
svg
|
||||
style={{
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
}}
|
||||
aria-label={displayName}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DateRenderer: CellRenderer = (value, _row, _column, _t) => {
|
||||
return <DateCell date={value} />
|
||||
}
|
||||
|
||||
const DisplayIdRenderer: CellRenderer = (value, _row, _column, _t) => {
|
||||
return <DisplayIdCell displayId={value} />
|
||||
}
|
||||
|
||||
const CurrencyRenderer: CellRenderer = (value, row, _column, _t) => {
|
||||
const currencyCode = row.currency_code || 'USD'
|
||||
return <MoneyAmountCell currencyCode={currencyCode} amount={value} align="right" />
|
||||
}
|
||||
|
||||
const TotalRenderer: CellRenderer = (value, row, _column, _t) => {
|
||||
const currencyCode = row.currency_code || 'USD'
|
||||
return <TotalCell currencyCode={currencyCode} total={value} />
|
||||
}
|
||||
|
||||
// Register built-in renderers
|
||||
cellRenderers.set('text', TextRenderer)
|
||||
cellRenderers.set('count', CountRenderer)
|
||||
cellRenderers.set('status', StatusRenderer)
|
||||
cellRenderers.set('badge_list', BadgeListRenderer)
|
||||
cellRenderers.set('date', DateRenderer)
|
||||
cellRenderers.set('timestamp', DateRenderer)
|
||||
cellRenderers.set('currency', CurrencyRenderer)
|
||||
cellRenderers.set('total', TotalRenderer)
|
||||
|
||||
// Register product-specific renderers
|
||||
cellRenderers.set('product_info', ProductInfoRenderer)
|
||||
cellRenderers.set('collection', CollectionRenderer)
|
||||
cellRenderers.set('variants', VariantsRenderer)
|
||||
cellRenderers.set('sales_channels_list', BadgeListRenderer)
|
||||
|
||||
// Register order-specific renderers
|
||||
cellRenderers.set('customer_name', CustomerNameRenderer)
|
||||
cellRenderers.set('address_summary', AddressSummaryRenderer)
|
||||
cellRenderers.set('country_code', CountryCodeRenderer)
|
||||
cellRenderers.set('display_id', DisplayIdRenderer)
|
||||
|
||||
export function getCellRenderer(
|
||||
renderType?: string,
|
||||
dataType?: string
|
||||
): CellRenderer {
|
||||
if (renderType && cellRenderers.has(renderType)) {
|
||||
return cellRenderers.get(renderType)!
|
||||
}
|
||||
|
||||
switch (dataType) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
return TextRenderer
|
||||
case 'date':
|
||||
return DateRenderer
|
||||
case 'boolean':
|
||||
return (value, _row, _column, t) => {
|
||||
if (t) {
|
||||
return value ? t('fields.yes', 'Yes') : t('fields.no', 'No')
|
||||
}
|
||||
return value ? 'Yes' : 'No'
|
||||
}
|
||||
case 'enum':
|
||||
return StatusRenderer
|
||||
case 'currency':
|
||||
return CurrencyRenderer
|
||||
default:
|
||||
return TextRenderer
|
||||
}
|
||||
}
|
||||
|
||||
export function registerCellRenderer(type: string, renderer: CellRenderer) {
|
||||
cellRenderers.set(type, renderer)
|
||||
}
|
||||
|
||||
export function getColumnValue(row: any, column: HttpTypes.AdminColumn): any {
|
||||
if (column.computed) {
|
||||
return row
|
||||
}
|
||||
|
||||
return getNestedValue(row, column.field)
|
||||
}
|
||||
@@ -1,55 +1,98 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
||||
|
||||
// Order-specific column adapter
|
||||
export const orderColumnAdapter: ColumnAdapter<HttpTypes.AdminOrder> = {
|
||||
getColumnAlignment: (column) => {
|
||||
// Custom alignment for order columns
|
||||
if (column.field === "display_id") return "center"
|
||||
if (column.semantic_type === "currency") return "right"
|
||||
if (column.semantic_type === "status") return "center"
|
||||
if (column.computed?.type === "country_code") return "center"
|
||||
if (column.semantic_type === "currency") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "status") {
|
||||
return "center"
|
||||
}
|
||||
if (column.computed?.type === "country_code") {
|
||||
return "center"
|
||||
}
|
||||
return "left"
|
||||
}
|
||||
}
|
||||
|
||||
// Product-specific column adapter
|
||||
export const productColumnAdapter: ColumnAdapter<HttpTypes.AdminProduct> = {
|
||||
getColumnAlignment: (column) => {
|
||||
// Custom alignment for product columns
|
||||
if (column.field === "sku") return "center"
|
||||
if (column.field === "stock") return "right"
|
||||
if (column.semantic_type === "currency") return "right"
|
||||
if (column.semantic_type === "status") return "center"
|
||||
if (column.field === "product_display") {
|
||||
return "left"
|
||||
}
|
||||
if (column.field === "collection.title") {
|
||||
return "left"
|
||||
}
|
||||
if (column.field === "sales_channels_display") {
|
||||
return "left"
|
||||
}
|
||||
if (column.field === "variants_count") {
|
||||
return "left"
|
||||
}
|
||||
if (column.field === "sku") {
|
||||
return "center"
|
||||
}
|
||||
if (column.field === "stock") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "currency") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "status") {
|
||||
return "left"
|
||||
}
|
||||
if (column.computed?.type === "product_info") {
|
||||
return "left"
|
||||
}
|
||||
if (column.computed?.type === "count") {
|
||||
return "left"
|
||||
}
|
||||
if (column.computed?.type === "sales_channels_list") {
|
||||
return "left"
|
||||
}
|
||||
|
||||
return "left"
|
||||
},
|
||||
|
||||
transformCellValue: (value, row, column) => {
|
||||
// Custom transformation for product-specific fields
|
||||
if (column.field === "variants_count") {
|
||||
return `${value || 0} variants`
|
||||
|
||||
transformCellValue: (_value, row, column) => {
|
||||
if (column.field === "variants_count" || column.computed?.type === "count") {
|
||||
const count = Array.isArray(row.variants) ? row.variants.length : 0
|
||||
return `${count} ${count === 1 ? 'variant' : 'variants'}`
|
||||
}
|
||||
|
||||
if (column.field === "status" && value === "draft") {
|
||||
return <span className="text-ui-fg-muted">Draft</span>
|
||||
|
||||
if (column.field === "product_display" || column.computed?.type === "product_info") {
|
||||
return null
|
||||
}
|
||||
|
||||
// Default to standard display
|
||||
|
||||
if (column.field === "sales_channels_display" || column.computed?.type === "sales_channels_list") {
|
||||
return null
|
||||
}
|
||||
|
||||
if (column.field === "status") {
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Customer-specific column adapter
|
||||
export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
||||
getColumnAlignment: (column) => {
|
||||
if (column.field === "orders_count") return "right"
|
||||
if (column.semantic_type === "currency") return "right"
|
||||
if (column.semantic_type === "status") return "center"
|
||||
if (column.field === "orders_count") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "currency") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "status") {
|
||||
return "center"
|
||||
}
|
||||
|
||||
return "left"
|
||||
},
|
||||
|
||||
transformCellValue: (value, row, column) => {
|
||||
// Format customer name
|
||||
|
||||
transformCellValue: (_value, row, column) => {
|
||||
if (column.field === "name") {
|
||||
const { first_name, last_name } = row
|
||||
if (first_name || last_name) {
|
||||
@@ -57,23 +100,30 @@ export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory-specific column adapter
|
||||
export const inventoryColumnAdapter: ColumnAdapter<HttpTypes.AdminInventoryItem> = {
|
||||
getColumnAlignment: (column) => {
|
||||
if (column.field === "stocked_quantity") return "right"
|
||||
if (column.field === "reserved_quantity") return "right"
|
||||
if (column.field === "available_quantity") return "right"
|
||||
if (column.semantic_type === "status") return "center"
|
||||
if (column.field === "stocked_quantity") {
|
||||
return "right"
|
||||
}
|
||||
if (column.field === "reserved_quantity") {
|
||||
return "right"
|
||||
}
|
||||
if (column.field === "available_quantity") {
|
||||
return "right"
|
||||
}
|
||||
if (column.semantic_type === "status") {
|
||||
return "center"
|
||||
}
|
||||
|
||||
return "left"
|
||||
}
|
||||
}
|
||||
|
||||
// Registry of entity adapters
|
||||
export const entityAdapters = {
|
||||
orders: orderColumnAdapter,
|
||||
products: productColumnAdapter,
|
||||
@@ -83,7 +133,6 @@ export const entityAdapters = {
|
||||
|
||||
export type EntityType = keyof typeof entityAdapters
|
||||
|
||||
// Helper function to get adapter for an entity
|
||||
export function getEntityAdapter<TData = any>(entity: string): ColumnAdapter<TData> | undefined {
|
||||
return entityAdapters[entity as EntityType] as ColumnAdapter<TData>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ENTITY_DEFAULT_FIELDS = {
|
||||
orders: {
|
||||
properties: [
|
||||
"id",
|
||||
"status",
|
||||
"status",
|
||||
"created_at",
|
||||
"email",
|
||||
"display_id",
|
||||
@@ -15,22 +15,14 @@ export const ENTITY_DEFAULT_FIELDS = {
|
||||
"total",
|
||||
"currency_code",
|
||||
],
|
||||
relations: ["*customer", "*sales_channel"]
|
||||
relations: ["*customer", "*sales_channel"],
|
||||
},
|
||||
|
||||
|
||||
products: {
|
||||
properties: [
|
||||
"id",
|
||||
"title",
|
||||
"handle",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"thumbnail",
|
||||
],
|
||||
relations: ["*variants", "*categories", "*collections"]
|
||||
properties: ["id", "title", "handle", "status", "thumbnail"],
|
||||
relations: ["collection.title", "*sales_channels", "*variants"],
|
||||
},
|
||||
|
||||
|
||||
customers: {
|
||||
properties: [
|
||||
"id",
|
||||
@@ -41,9 +33,9 @@ export const ENTITY_DEFAULT_FIELDS = {
|
||||
"updated_at",
|
||||
"has_account",
|
||||
],
|
||||
relations: ["*groups"]
|
||||
relations: ["*groups"],
|
||||
},
|
||||
|
||||
|
||||
inventory: {
|
||||
properties: [
|
||||
"id",
|
||||
@@ -55,14 +47,14 @@ export const ENTITY_DEFAULT_FIELDS = {
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
relations: ["*location_levels"]
|
||||
relations: ["*location_levels"],
|
||||
},
|
||||
|
||||
|
||||
// Default configuration for entities without specific defaults
|
||||
default: {
|
||||
properties: ["id", "created_at", "updated_at"],
|
||||
relations: []
|
||||
}
|
||||
relations: [],
|
||||
},
|
||||
} as const
|
||||
|
||||
export type EntityType = keyof typeof ENTITY_DEFAULT_FIELDS
|
||||
@@ -71,10 +63,11 @@ export type EntityType = keyof typeof ENTITY_DEFAULT_FIELDS
|
||||
* Get default fields for an entity
|
||||
*/
|
||||
export function getEntityDefaultFields(entity: string) {
|
||||
const config = ENTITY_DEFAULT_FIELDS[entity as EntityType] || ENTITY_DEFAULT_FIELDS.default
|
||||
const config =
|
||||
ENTITY_DEFAULT_FIELDS[entity as EntityType] || ENTITY_DEFAULT_FIELDS.default
|
||||
return {
|
||||
properties: config.properties,
|
||||
relations: config.relations,
|
||||
formatted: [...config.properties, ...config.relations].join(",")
|
||||
formatted: [...config.properties, ...config.relations].join(","),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DataTableColumnDef, DataTableEmptyStateProps, DataTableFilter } from "@medusajs/ui"
|
||||
import {
|
||||
DataTableColumnDef,
|
||||
DataTableEmptyStateProps,
|
||||
DataTableFilter,
|
||||
} from "@medusajs/ui"
|
||||
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
||||
|
||||
/**
|
||||
@@ -15,9 +19,12 @@ export interface TableAdapter<TData> {
|
||||
* Hook to fetch data with the calculated required fields.
|
||||
* Called inside ConfigurableDataTable with the fields and search params.
|
||||
*/
|
||||
useData: (fields: string, params: any) => {
|
||||
useData: (
|
||||
fields: string,
|
||||
params: any
|
||||
) => {
|
||||
data: TData[] | undefined
|
||||
count: number
|
||||
count: number | undefined
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
error: any
|
||||
@@ -39,7 +46,7 @@ export interface TableAdapter<TData> {
|
||||
filters?: DataTableFilter[]
|
||||
|
||||
/**
|
||||
* Transform API columns to table columns.
|
||||
* Transform API columns to table columns.
|
||||
* If not provided, will use default column generation.
|
||||
*/
|
||||
getColumns?: (apiColumns: any[]) => DataTableColumnDef<TData, any>[]
|
||||
@@ -84,4 +91,4 @@ export function createTableAdapter<TData>(
|
||||
queryPrefix: "",
|
||||
...adapter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Outlet, useLocation } from "react-router-dom"
|
||||
|
||||
import { ConfigurableDataTable } from "../../../../../components/table/configurable-data-table"
|
||||
import { useProductTableAdapter } from "./product-table-adapter"
|
||||
|
||||
export const ConfigurableProductListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const adapter = useProductTableAdapter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigurableDataTable
|
||||
adapter={adapter}
|
||||
heading={t("products.domain")}
|
||||
actions={[
|
||||
{ label: t("actions.export"), to: `export${location.search}` },
|
||||
{ label: t("actions.import"), to: "import" },
|
||||
{ label: t("actions.create"), to: "create" }
|
||||
]}
|
||||
/>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -18,12 +18,20 @@ import { useProductTableFilters } from "../../../../../hooks/table/filters/use-p
|
||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { productsLoader } from "../../loader"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { ConfigurableProductListTable } from "./configurable-product-list-table"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ProductListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||
|
||||
// If feature flag is enabled, use the new configurable table
|
||||
if (isViewConfigEnabled) {
|
||||
return <ConfigurableProductListTable />
|
||||
}
|
||||
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<ReturnType<typeof productsLoader>>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useProducts } from "../../../../../hooks/api/products"
|
||||
import { productColumnAdapter } from "../../../../../lib/table/entity-adapters"
|
||||
import { createTableAdapter, TableAdapter } from "../../../../../lib/table/table-adapters"
|
||||
import { useProductTableFilters } from "./use-product-table-filters"
|
||||
|
||||
export function createProductTableAdapter(): TableAdapter<HttpTypes.AdminProduct> {
|
||||
return createTableAdapter<HttpTypes.AdminProduct>({
|
||||
entity: "products",
|
||||
queryPrefix: "p",
|
||||
pageSize: 20,
|
||||
columnAdapter: productColumnAdapter,
|
||||
useData: (fields, params) => {
|
||||
const { products, count, isError, error, isLoading } = useProducts(
|
||||
{
|
||||
fields,
|
||||
...params,
|
||||
is_giftcard: false, // Exclude gift cards from product list
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData, previousQuery) => {
|
||||
// Only keep placeholder data if the fields haven't changed
|
||||
const prevFields = previousQuery?.[previousQuery.length - 1]?.query?.fields
|
||||
if (prevFields && prevFields !== fields) {
|
||||
// Fields changed, don't use placeholder data
|
||||
return undefined
|
||||
}
|
||||
// Fields are the same, keep previous data for smooth transitions
|
||||
return previousData
|
||||
},
|
||||
}
|
||||
)
|
||||
return { data: products, count, isLoading, isError, error }
|
||||
},
|
||||
getRowHref: (row) => `/products/${row.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the product table adapter with filters
|
||||
*/
|
||||
export function useProductTableAdapter(): TableAdapter<HttpTypes.AdminProduct> {
|
||||
const filters = useProductTableFilters()
|
||||
const adapter = createProductTableAdapter()
|
||||
|
||||
// Add dynamic filters to the adapter
|
||||
return {
|
||||
...adapter,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { createDataTableFilterHelper } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
|
||||
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
||||
import { useProductTags } from "../../../../../hooks/api"
|
||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||
|
||||
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()
|
||||
|
||||
/**
|
||||
* Hook to create filters in the format expected by @medusajs/ui DataTable
|
||||
*/
|
||||
export const useProductTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
const dateFilters = useDataTableDateFilters()
|
||||
|
||||
const { product_types } = useProductTypes({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { product_tags } = useProductTags({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { sales_channels } = useSalesChannels({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
const filters = [...dateFilters]
|
||||
|
||||
if (product_types?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("type_id", {
|
||||
label: t("fields.type"),
|
||||
type: "multiselect",
|
||||
options: product_types.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (product_tags?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("tag_id", {
|
||||
label: t("fields.tag"),
|
||||
type: "multiselect",
|
||||
options: product_tags.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (sales_channels?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("sales_channel_id", {
|
||||
label: t("fields.salesChannel"),
|
||||
type: "multiselect",
|
||||
options: sales_channels.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Status filter
|
||||
filters.push(
|
||||
filterHelper.accessor("status", {
|
||||
label: t("fields.status"),
|
||||
type: "multiselect",
|
||||
options: [
|
||||
{
|
||||
label: t("products.productStatus.draft"),
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.proposed"),
|
||||
value: "proposed",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.published"),
|
||||
value: "published",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.rejected"),
|
||||
value: "rejected",
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
return filters
|
||||
}, [product_types, product_tags, sales_channels, dateFilters, t])
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const DEFAULT_COLUMN_ORDER = 500
|
||||
/**
|
||||
* Determines the appropriate column alignment based on the column metadata
|
||||
*/
|
||||
export function getColumnAlignment(column: HttpTypes.AdminViewColumn): ColumnAlignment {
|
||||
export function getColumnAlignment(column: HttpTypes.AdminColumn): ColumnAlignment {
|
||||
// Currency columns should be right-aligned
|
||||
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
||||
return ColumnAlignment.RIGHT
|
||||
@@ -44,7 +44,7 @@ export function getColumnAlignment(column: HttpTypes.AdminViewColumn): ColumnAli
|
||||
* Gets the initial column visibility state from API columns
|
||||
*/
|
||||
export function getInitialColumnVisibility(
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
apiColumns: HttpTypes.AdminColumn[]
|
||||
): Record<string, boolean> {
|
||||
const visibility: Record<string, boolean> = {}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function getInitialColumnVisibility(
|
||||
* Gets the initial column order from API columns
|
||||
*/
|
||||
export function getInitialColumnOrder(
|
||||
apiColumns: HttpTypes.AdminViewColumn[]
|
||||
apiColumns: HttpTypes.AdminColumn[]
|
||||
): string[] {
|
||||
const sortedColumns = [...apiColumns].sort((a, b) => {
|
||||
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
||||
|
||||
@@ -250,7 +250,10 @@ export class Client {
|
||||
const params = Object.fromEntries(
|
||||
normalizedInput.searchParams.entries()
|
||||
)
|
||||
const stringifiedQuery = stringify({ ...params, ...init.query }, { skipNulls: true })
|
||||
const stringifiedQuery = stringify(
|
||||
{ ...params, ...init.query },
|
||||
{ skipNulls: true }
|
||||
)
|
||||
normalizedInput.search = stringifiedQuery
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ export const DataTableNonSortableHeaderCell = React.forwardRef<
|
||||
...propStyle,
|
||||
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
||||
transition,
|
||||
position: 'relative' as const,
|
||||
}
|
||||
|
||||
const combineRefs = (element: HTMLTableCellElement | null) => {
|
||||
|
||||
@@ -38,9 +38,7 @@ export const DataTableSortableHeaderCell = React.forwardRef<
|
||||
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
||||
transition,
|
||||
opacity: isDragging ? 0.8 : 1,
|
||||
zIndex: isDragging ? 50 : undefined,
|
||||
backgroundColor: "white",
|
||||
position: 'relative' as const,
|
||||
zIndex: isDragging ? 50 : isFirstColumn ? 1 : undefined,
|
||||
}
|
||||
|
||||
const combineRefs = (element: HTMLTableCellElement | null) => {
|
||||
@@ -58,7 +56,7 @@ export const DataTableSortableHeaderCell = React.forwardRef<
|
||||
<Table.HeaderCell
|
||||
ref={combineRefs}
|
||||
style={style}
|
||||
className={clx(className, "group/header-cell relative")}
|
||||
className={clx(className, "group/header-cell bg-ui-bg-base")}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
{...props}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ENTITY_MAPPINGS = {
|
||||
computedColumns: {
|
||||
customer_display: {
|
||||
name: "Customer",
|
||||
computation_type: "customer_name",
|
||||
render_type: "customer_name",
|
||||
required_fields: [
|
||||
"customer.first_name",
|
||||
"customer.last_name",
|
||||
@@ -34,7 +34,7 @@ export const ENTITY_MAPPINGS = {
|
||||
},
|
||||
shipping_address_display: {
|
||||
name: "Shipping Address",
|
||||
computation_type: "address_summary",
|
||||
render_type: "address_summary",
|
||||
required_fields: [
|
||||
"shipping_address.city",
|
||||
"shipping_address.country_code",
|
||||
@@ -48,7 +48,7 @@ export const ENTITY_MAPPINGS = {
|
||||
},
|
||||
billing_address_display: {
|
||||
name: "Billing Address",
|
||||
computation_type: "address_summary",
|
||||
render_type: "address_summary",
|
||||
required_fields: [
|
||||
"billing_address.city",
|
||||
"billing_address.country_code",
|
||||
@@ -62,7 +62,7 @@ export const ENTITY_MAPPINGS = {
|
||||
},
|
||||
country: {
|
||||
name: "Country",
|
||||
computation_type: "country_code",
|
||||
render_type: "country_code",
|
||||
required_fields: ["shipping_address.country_code"],
|
||||
optional_fields: [],
|
||||
default_visible: true,
|
||||
@@ -73,18 +73,47 @@ export const ENTITY_MAPPINGS = {
|
||||
serviceName: "product",
|
||||
graphqlType: "Product",
|
||||
defaultVisibleFields: [
|
||||
"title",
|
||||
"handle",
|
||||
"product_display",
|
||||
"collection.title",
|
||||
"sales_channels_display",
|
||||
"variants_count",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
fieldFilters: {
|
||||
excludeSuffixes: ["_link"],
|
||||
excludePrefixes: ["raw_"],
|
||||
excludeFields: [],
|
||||
},
|
||||
computedColumns: {},
|
||||
computedColumns: {
|
||||
product_display: {
|
||||
name: "Product",
|
||||
render_type: "product_info",
|
||||
required_fields: [
|
||||
"title",
|
||||
"thumbnail",
|
||||
],
|
||||
optional_fields: ["handle"],
|
||||
default_visible: true,
|
||||
},
|
||||
variants_count: {
|
||||
name: "Variants",
|
||||
render_type: "count",
|
||||
required_fields: [
|
||||
"variants",
|
||||
],
|
||||
optional_fields: [],
|
||||
default_visible: true,
|
||||
},
|
||||
sales_channels_display: {
|
||||
name: "Sales Channels",
|
||||
render_type: "sales_channels_list",
|
||||
required_fields: [
|
||||
"sales_channels",
|
||||
],
|
||||
optional_fields: [],
|
||||
default_visible: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
customers: {
|
||||
serviceName: "customer",
|
||||
|
||||
@@ -255,15 +255,31 @@ export const getTypeInfoFromGraphQLType = (
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_COLUMN_ORDERS: Record<string, number> = {
|
||||
display_id: 100,
|
||||
created_at: 200,
|
||||
customer_display: 300,
|
||||
"sales_channel.name": 400,
|
||||
fulfillment_status: 500,
|
||||
payment_status: 600,
|
||||
total: 700,
|
||||
country: 800,
|
||||
type Entities = keyof typeof ENTITY_MAPPINGS
|
||||
|
||||
export const DEFAULT_COLUMN_ORDERS: Record<Entities, Record<string, number>> = {
|
||||
orders: {
|
||||
display_id: 100,
|
||||
created_at: 200,
|
||||
customer_display: 300,
|
||||
"sales_channel.name": 400,
|
||||
fulfillment_status: 500,
|
||||
payment_status: 600,
|
||||
total: 700,
|
||||
country: 800,
|
||||
},
|
||||
products: {
|
||||
product_display: 100,
|
||||
"collection.title": 200,
|
||||
sales_channels_display: 300,
|
||||
variants_count: 400,
|
||||
status: 500,
|
||||
},
|
||||
// Add other entities as needed
|
||||
customers: {},
|
||||
users: {},
|
||||
regions: {},
|
||||
"sales-channels": {},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +290,7 @@ export const DEFAULT_COLUMN_ORDERS: Record<string, number> = {
|
||||
*/
|
||||
export const generateEntityColumns = (
|
||||
entity: string,
|
||||
entityMapping: typeof ENTITY_MAPPINGS[keyof typeof ENTITY_MAPPINGS]
|
||||
entityMapping: (typeof ENTITY_MAPPINGS)[keyof typeof ENTITY_MAPPINGS]
|
||||
): HttpTypes.AdminColumn[] | null => {
|
||||
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
|
||||
|
||||
@@ -304,8 +320,7 @@ export const generateEntityColumns = (
|
||||
const mergedSchemaAST = mergeTypeDefs(allSchemas)
|
||||
const mergedSchemaString = print(mergedSchemaAST)
|
||||
|
||||
const { schema: cleanedSchemaString } =
|
||||
cleanGraphQLSchema(mergedSchemaString)
|
||||
const { schema: cleanedSchemaString } = cleanGraphQLSchema(mergedSchemaString)
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs: cleanedSchemaString,
|
||||
@@ -393,9 +408,7 @@ export const generateEntityColumns = (
|
||||
const directColumns = directFields.map((fieldName) => {
|
||||
const displayName = formatFieldName(fieldName)
|
||||
|
||||
const type = schemaTypeMap[
|
||||
entityMapping.graphqlType
|
||||
] as GraphQLObjectType
|
||||
const type = schemaTypeMap[entityMapping.graphqlType] as GraphQLObjectType
|
||||
const fieldDef = type?.getFields()?.[fieldName]
|
||||
const typeInfo = fieldDef
|
||||
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
||||
@@ -406,8 +419,8 @@ export const generateEntityColumns = (
|
||||
|
||||
const isDefaultField =
|
||||
entityMapping.defaultVisibleFields.includes(fieldName)
|
||||
const defaultOrder =
|
||||
DEFAULT_COLUMN_ORDERS[fieldName] || (isDefaultField ? 500 : 850)
|
||||
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||
const defaultOrder = entityOrders[fieldName] || (isDefaultField ? 500 : 850)
|
||||
const category = getColumnCategory(
|
||||
fieldName,
|
||||
typeInfo.data_type,
|
||||
@@ -421,8 +434,7 @@ export const generateEntityColumns = (
|
||||
field: fieldName,
|
||||
sortable,
|
||||
hideable: true,
|
||||
default_visible:
|
||||
entityMapping.defaultVisibleFields.includes(fieldName),
|
||||
default_visible: entityMapping.defaultVisibleFields.includes(fieldName),
|
||||
data_type: typeInfo.data_type,
|
||||
semantic_type: typeInfo.semantic_type,
|
||||
context: typeInfo.context,
|
||||
@@ -442,9 +454,7 @@ export const generateEntityColumns = (
|
||||
)
|
||||
|
||||
// Filter out problematic fields from related type
|
||||
const relatedType = schemaTypeMap[
|
||||
relatedTypeName
|
||||
] as GraphQLObjectType
|
||||
const relatedType = schemaTypeMap[relatedTypeName] as GraphQLObjectType
|
||||
const relatedFields = allRelatedFields.filter((fieldName) => {
|
||||
const field = relatedType?.getFields()[fieldName]
|
||||
if (!field) return true
|
||||
@@ -466,13 +476,11 @@ export const generateEntityColumns = (
|
||||
|
||||
limitedFields.forEach((fieldName) => {
|
||||
const fieldPath = `${relationName}.${fieldName}`
|
||||
const displayName = `${formatFieldName(
|
||||
relationName
|
||||
)} ${formatFieldName(fieldName)}`
|
||||
const displayName = `${formatFieldName(relationName)} ${formatFieldName(
|
||||
fieldName
|
||||
)}`
|
||||
|
||||
const relatedType = schemaTypeMap[
|
||||
relatedTypeName
|
||||
] as GraphQLObjectType
|
||||
const relatedType = schemaTypeMap[relatedTypeName] as GraphQLObjectType
|
||||
const fieldDef = relatedType?.getFields()?.[fieldName]
|
||||
const typeInfo = fieldDef
|
||||
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
||||
@@ -493,8 +501,9 @@ export const generateEntityColumns = (
|
||||
// If field is not in default visible fields, place it after country (850)
|
||||
const isDefaultField =
|
||||
entityMapping.defaultVisibleFields.includes(fieldPath)
|
||||
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||
const defaultOrder =
|
||||
DEFAULT_COLUMN_ORDERS[fieldPath] || (isDefaultField ? 700 : 850)
|
||||
entityOrders[fieldPath] || (isDefaultField ? 700 : 850)
|
||||
const category = getColumnCategory(
|
||||
fieldPath,
|
||||
typeInfo.data_type,
|
||||
@@ -534,8 +543,9 @@ export const generateEntityColumns = (
|
||||
// If field is not in default visible fields, place it after country (850)
|
||||
const isDefaultField =
|
||||
entityMapping.defaultVisibleFields.includes(columnId)
|
||||
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||
const defaultOrder =
|
||||
DEFAULT_COLUMN_ORDERS[columnId] || (isDefaultField ? 600 : 850)
|
||||
entityOrders[columnId] || (isDefaultField ? 600 : 850)
|
||||
const category = getColumnCategory(columnId, "string", "computed")
|
||||
|
||||
computedColumns.push({
|
||||
@@ -545,13 +555,12 @@ export const generateEntityColumns = (
|
||||
field: columnId,
|
||||
sortable: false, // Computed columns can't be sorted server-side
|
||||
hideable: true,
|
||||
default_visible:
|
||||
entityMapping.defaultVisibleFields.includes(columnId),
|
||||
default_visible: entityMapping.defaultVisibleFields.includes(columnId),
|
||||
data_type: "string", // Computed columns typically output strings
|
||||
semantic_type: "computed",
|
||||
context: "display",
|
||||
computed: {
|
||||
type: columnConfig.computation_type,
|
||||
type: columnConfig.render_type,
|
||||
required_fields: columnConfig.required_fields,
|
||||
optional_fields: columnConfig.optional_fields || [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user