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 parent
9563ee446f
commit
41047b3854
22 files changed
+832
-400
No files matched your search
@@ -224,7 +224,7 @@ medusaIntegrationTestRunner({
|
|||||||
id: "title",
|
id: "title",
|
||||||
name: "Title",
|
name: "Title",
|
||||||
field: "title",
|
field: "title",
|
||||||
default_visible: true,
|
default_visible: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleColumn = response.data.columns.find(
|
const handleColumn = response.data.columns.find(
|
||||||
@@ -235,7 +235,7 @@ medusaIntegrationTestRunner({
|
|||||||
id: "handle",
|
id: "handle",
|
||||||
name: "Handle",
|
name: "Handle",
|
||||||
field: "handle",
|
field: "handle",
|
||||||
default_visible: true,
|
default_visible: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,12 +66,14 @@ interface DataTableProps<TData> {
|
|||||||
filters?: DataTableFilter[]
|
filters?: DataTableFilter[]
|
||||||
commands?: DataTableCommand[]
|
commands?: DataTableCommand[]
|
||||||
action?: DataTableActionProps
|
action?: DataTableActionProps
|
||||||
|
actions?: DataTableActionProps[]
|
||||||
actionMenu?: DataTableActionMenuProps
|
actionMenu?: DataTableActionMenuProps
|
||||||
rowCount?: number
|
rowCount?: number
|
||||||
getRowId: (row: TData) => string
|
getRowId: (row: TData) => string
|
||||||
enablePagination?: boolean
|
enablePagination?: boolean
|
||||||
enableSearch?: boolean
|
enableSearch?: boolean
|
||||||
autoFocusSearch?: boolean
|
autoFocusSearch?: boolean
|
||||||
|
enableFilterMenu?: boolean
|
||||||
rowHref?: (row: TData) => string
|
rowHref?: (row: TData) => string
|
||||||
emptyState?: DataTableEmptyStateProps
|
emptyState?: DataTableEmptyStateProps
|
||||||
heading?: string
|
heading?: string
|
||||||
@@ -105,12 +107,14 @@ export const DataTable = <TData,>({
|
|||||||
filters,
|
filters,
|
||||||
commands,
|
commands,
|
||||||
action,
|
action,
|
||||||
|
actions,
|
||||||
actionMenu,
|
actionMenu,
|
||||||
getRowId,
|
getRowId,
|
||||||
rowCount = 0,
|
rowCount = 0,
|
||||||
enablePagination = true,
|
enablePagination = true,
|
||||||
enableSearch = true,
|
enableSearch = true,
|
||||||
autoFocusSearch = false,
|
autoFocusSearch = false,
|
||||||
|
enableFilterMenu,
|
||||||
rowHref,
|
rowHref,
|
||||||
heading,
|
heading,
|
||||||
subHeading,
|
subHeading,
|
||||||
@@ -138,6 +142,7 @@ export const DataTable = <TData,>({
|
|||||||
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
||||||
|
|
||||||
const enableFiltering = filters && filters.length > 0
|
const enableFiltering = filters && filters.length > 0
|
||||||
|
const showFilterMenu = enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
||||||
const enableCommands = commands && commands.length > 0
|
const enableCommands = commands && commands.length > 0
|
||||||
const enableSorting = columns.some((column) => column.enableSorting)
|
const enableSorting = columns.some((column) => column.enableSorting)
|
||||||
|
|
||||||
@@ -381,7 +386,7 @@ export const DataTable = <TData,>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
{enableFiltering && <UiDataTable.FilterMenu />}
|
{showFilterMenu && <UiDataTable.FilterMenu />}
|
||||||
{enableSorting && <UiDataTable.SortingMenu />}
|
{enableSorting && <UiDataTable.SortingMenu />}
|
||||||
{enableSearch && (
|
{enableSearch && (
|
||||||
<div className="w-full md:w-auto">
|
<div className="w-full md:w-auto">
|
||||||
@@ -392,7 +397,8 @@ export const DataTable = <TData,>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
||||||
{action && <DataTableAction {...action} />}
|
{actions && actions.length > 0 && <DataTableActions actions={actions} />}
|
||||||
|
{!actions && action && <DataTableAction {...action} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UiDataTable.Toolbar>
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
+37
-244
@@ -1,71 +1,45 @@
|
|||||||
import React, { useState, ReactNode } from "react"
|
import { useState, ReactNode } from "react"
|
||||||
import { Container, Button } from "@medusajs/ui"
|
import { Container, Button } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { DataTable } from "../../data-table"
|
import { DataTable } from "../../data-table"
|
||||||
import { SaveViewDialog } from "../save-view-dialog"
|
import { SaveViewDialog } from "../save-view-dialog"
|
||||||
import { SaveViewDropdown } from "./save-view-dropdown"
|
import { SaveViewDropdown } from "./save-view-dropdown"
|
||||||
import { useTableConfiguration } from "../../../hooks/table/use-table-configuration"
|
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 { useConfigurableTableColumns } from "../../../hooks/table/columns/use-configurable-table-columns"
|
||||||
import { getEntityAdapter } from "../../../lib/table/entity-adapters"
|
import { getEntityAdapter } from "../../../lib/table/entity-adapters"
|
||||||
import { DataTableColumnDef, DataTableEmptyStateProps, DataTableFilter } from "@medusajs/ui"
|
|
||||||
import { TableAdapter } from "../../../lib/table/table-adapters"
|
import { TableAdapter } from "../../../lib/table/table-adapters"
|
||||||
|
|
||||||
|
type DataTableActionProps = {
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
export interface ConfigurableDataTableProps<TData> {
|
export interface ConfigurableDataTableProps<TData> {
|
||||||
// Use adapter pattern for entity-specific configuration
|
|
||||||
adapter: TableAdapter<TData>
|
adapter: TableAdapter<TData>
|
||||||
|
|
||||||
// Optional overrides
|
|
||||||
heading?: string
|
heading?: string
|
||||||
subHeading?: string
|
subHeading?: string
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
queryPrefix?: string
|
queryPrefix?: string
|
||||||
layout?: "fill" | "auto"
|
layout?: "fill" | "auto"
|
||||||
actions?: ReactNode
|
actions?: DataTableActionProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy props interface for backward compatibility
|
export function ConfigurableDataTable<TData>({
|
||||||
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>({
|
|
||||||
adapter,
|
adapter,
|
||||||
heading,
|
heading,
|
||||||
subHeading,
|
subHeading,
|
||||||
pageSize: pageSizeProp,
|
pageSize: pageSizeProp,
|
||||||
queryPrefix: queryPrefixProp,
|
queryPrefix: queryPrefixProp,
|
||||||
layout = "fill",
|
layout = "fill",
|
||||||
// actions, // Currently unused
|
actions,
|
||||||
}: ConfigurableDataTableProps<TData>) {
|
}: ConfigurableDataTableProps<TData>) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||||
@@ -77,7 +51,6 @@ function ConfigurableDataTableWithAdapter<TData>({
|
|||||||
const pageSize = pageSizeProp || adapter.pageSize || 20
|
const pageSize = pageSizeProp || adapter.pageSize || 20
|
||||||
const queryPrefix = queryPrefixProp || adapter.queryPrefix || ""
|
const queryPrefix = queryPrefixProp || adapter.queryPrefix || ""
|
||||||
|
|
||||||
// Get table configuration (single source of truth)
|
|
||||||
const {
|
const {
|
||||||
activeView,
|
activeView,
|
||||||
createView,
|
createView,
|
||||||
@@ -94,6 +67,7 @@ function ConfigurableDataTableWithAdapter<TData>({
|
|||||||
isLoadingColumns,
|
isLoadingColumns,
|
||||||
apiColumns,
|
apiColumns,
|
||||||
requiredFields,
|
requiredFields,
|
||||||
|
queryParams,
|
||||||
} = useTableConfiguration({
|
} = useTableConfiguration({
|
||||||
entity,
|
entity,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -101,29 +75,37 @@ function ConfigurableDataTableWithAdapter<TData>({
|
|||||||
filters,
|
filters,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get query params for data fetching
|
const parsedQueryParams = { ...queryParams }
|
||||||
const { searchParams } = useOrderTableQuery({
|
filters.forEach(filter => {
|
||||||
pageSize,
|
const filterKey = filter.id
|
||||||
prefix: queryPrefix,
|
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)
|
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 columnAdapter = adapter.columnAdapter || getEntityAdapter(entity)
|
||||||
const generatedColumns = useConfigurableTableColumns(entity, apiColumns || [], columnAdapter)
|
const generatedColumns = useConfigurableTableColumns(entity, apiColumns || [], columnAdapter)
|
||||||
const columns = (adapter.getColumns && apiColumns)
|
const columns = (adapter.getColumns && apiColumns)
|
||||||
? adapter.getColumns(apiColumns)
|
? adapter.getColumns(apiColumns)
|
||||||
: generatedColumns
|
: generatedColumns
|
||||||
|
|
||||||
// Handle errors
|
|
||||||
if (fetchResult.isError) {
|
if (fetchResult.isError) {
|
||||||
throw fetchResult.error
|
throw fetchResult.error
|
||||||
}
|
}
|
||||||
|
|
||||||
// View save handlers
|
|
||||||
const handleSaveAsDefault = async () => {
|
const handleSaveAsDefault = async () => {
|
||||||
try {
|
try {
|
||||||
if (activeView?.is_system_default) {
|
if (activeView?.is_system_default) {
|
||||||
@@ -233,6 +215,8 @@ function ConfigurableDataTableWithAdapter<TData>({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
prefix={queryPrefix}
|
prefix={queryPrefix}
|
||||||
|
actions={actions}
|
||||||
|
enableFilterMenu={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{saveDialogOpen && (
|
{saveDialogOpen && (
|
||||||
@@ -254,194 +238,3 @@ function ConfigurableDataTableWithAdapter<TData>({
|
|||||||
</Container>
|
</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} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+34
-30
@@ -1,20 +1,22 @@
|
|||||||
import React, { useMemo } from "react"
|
import React, { useMemo } from "react"
|
||||||
import { createDataTableColumnHelper } from "@medusajs/ui"
|
import { createDataTableColumnHelper } from "@medusajs/ui"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
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> {
|
export interface ColumnAdapter<TData> {
|
||||||
getColumnAlignment?: (column: HttpTypes.AdminViewColumn) => "left" | "center" | "right"
|
getColumnAlignment?: (column: HttpTypes.AdminColumn) => "left" | "center" | "right"
|
||||||
getCustomAccessor?: (field: string, column: HttpTypes.AdminViewColumn) => any
|
getCustomAccessor?: (field: string, column: HttpTypes.AdminColumn) => any
|
||||||
transformCellValue?: (value: any, row: TData, column: HttpTypes.AdminViewColumn) => React.ReactNode
|
transformCellValue?: (value: any, row: TData, column: HttpTypes.AdminColumn) => React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useConfigurableTableColumns<TData = any>(
|
export function useConfigurableTableColumns<TData = any>(
|
||||||
entity: string,
|
entity: string,
|
||||||
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
|
apiColumns: HttpTypes.AdminColumn[] | undefined,
|
||||||
adapter?: ColumnAdapter<TData>
|
adapter?: ColumnAdapter<TData>
|
||||||
) {
|
) {
|
||||||
const columnHelper = createDataTableColumnHelper<TData>()
|
const columnHelper = createDataTableColumnHelper<TData>()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!apiColumns?.length) {
|
if (!apiColumns?.length) {
|
||||||
@@ -22,37 +24,45 @@ export function useConfigurableTableColumns<TData = any>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return apiColumns.map(apiColumn => {
|
return apiColumns.map(apiColumn => {
|
||||||
// Get the display strategy for this column
|
let renderType = apiColumn.computed?.type
|
||||||
const displayStrategy = getDisplayStrategy(apiColumn)
|
|
||||||
|
|
||||||
// Get the entity-specific accessor or use adapter's custom accessor
|
if (!renderType) {
|
||||||
const accessor = adapter?.getCustomAccessor
|
if (apiColumn.semantic_type === 'timestamp') {
|
||||||
? adapter.getCustomAccessor(apiColumn.field, apiColumn)
|
renderType = 'timestamp'
|
||||||
: getEntityAccessor(entity, apiColumn.field, apiColumn)
|
} 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
|
const headerAlign = adapter?.getColumnAlignment
|
||||||
? adapter.getColumnAlignment(apiColumn)
|
? adapter.getColumnAlignment(apiColumn)
|
||||||
: getDefaultColumnAlignment(apiColumn)
|
: getDefaultColumnAlignment(apiColumn)
|
||||||
|
|
||||||
|
const accessor = (row: TData) => getColumnValue(row, apiColumn)
|
||||||
|
|
||||||
return columnHelper.accessor(accessor, {
|
return columnHelper.accessor(accessor, {
|
||||||
id: apiColumn.field,
|
id: apiColumn.field,
|
||||||
header: () => apiColumn.name,
|
header: () => apiColumn.name,
|
||||||
cell: ({ getValue, row }) => {
|
cell: ({ getValue, row }: { getValue: any, row: any }) => {
|
||||||
const value = getValue()
|
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) {
|
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 renderer(value, row.original, apiColumn, t)
|
||||||
return displayStrategy(value, row.original)
|
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
name: apiColumn.name,
|
name: apiColumn.name,
|
||||||
@@ -63,21 +73,18 @@ export function useConfigurableTableColumns<TData = any>(
|
|||||||
headerAlign, // Pass the header alignment to the DataTable
|
headerAlign, // Pass the header alignment to the DataTable
|
||||||
} as any)
|
} as any)
|
||||||
})
|
})
|
||||||
}, [entity, apiColumns, adapter])
|
}, [entity, apiColumns, adapter, t])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultColumnAlignment(column: HttpTypes.AdminViewColumn): "left" | "center" | "right" {
|
function getDefaultColumnAlignment(column: HttpTypes.AdminColumn): "left" | "center" | "right" {
|
||||||
// Currency columns should be right-aligned
|
|
||||||
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
||||||
return "right"
|
return "right"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number columns should be right-aligned (except identifiers)
|
|
||||||
if (column.data_type === "number" && column.context !== "identifier") {
|
if (column.data_type === "number" && column.context !== "identifier") {
|
||||||
return "right"
|
return "right"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total/amount/price columns should be right-aligned
|
|
||||||
if (
|
if (
|
||||||
column.field.includes("total") ||
|
column.field.includes("total") ||
|
||||||
column.field.includes("amount") ||
|
column.field.includes("amount") ||
|
||||||
@@ -88,18 +95,15 @@ function getDefaultColumnAlignment(column: HttpTypes.AdminViewColumn): "left" |
|
|||||||
return "right"
|
return "right"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status columns should be center-aligned
|
|
||||||
if (column.semantic_type === "status") {
|
if (column.semantic_type === "status") {
|
||||||
return "center"
|
return "center"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Country columns should be center-aligned
|
|
||||||
if (column.computed?.type === "country_code" ||
|
if (column.computed?.type === "country_code" ||
|
||||||
column.field === "country" ||
|
column.field === "country" ||
|
||||||
column.field.includes("country_code")) {
|
column.field.includes("country_code")) {
|
||||||
return "center"
|
return "center"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to left alignment
|
|
||||||
return "left"
|
return "left"
|
||||||
}
|
}
|
||||||
@@ -84,6 +84,7 @@ export const useProductTableFilters = (
|
|||||||
label: t("fields.type"),
|
label: t("fields.type"),
|
||||||
type: "select",
|
type: "select",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
searchable: true,
|
||||||
options: product_types.map((t) => ({
|
options: product_types.map((t) => ({
|
||||||
label: t.value,
|
label: t.value,
|
||||||
value: t.id,
|
value: t.id,
|
||||||
@@ -99,6 +100,7 @@ export const useProductTableFilters = (
|
|||||||
label: t("fields.tag"),
|
label: t("fields.tag"),
|
||||||
type: "select",
|
type: "select",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
searchable: true,
|
||||||
options: product_tags.map((t) => ({
|
options: product_tags.map((t) => ({
|
||||||
label: t.value,
|
label: t.value,
|
||||||
value: t.id,
|
value: t.id,
|
||||||
@@ -114,6 +116,7 @@ export const useProductTableFilters = (
|
|||||||
label: t("fields.salesChannel"),
|
label: t("fields.salesChannel"),
|
||||||
type: "select",
|
type: "select",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
searchable: true,
|
||||||
options: sales_channels.map((s) => ({
|
options: sales_channels.map((s) => ({
|
||||||
label: s.name,
|
label: s.name,
|
||||||
value: s.id,
|
value: s.id,
|
||||||
|
|||||||
@@ -24,13 +24,10 @@ export interface UseTableConfigurationOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UseTableConfigurationReturn {
|
export interface UseTableConfigurationReturn {
|
||||||
// View configuration
|
|
||||||
activeView: any
|
activeView: any
|
||||||
createView: any
|
createView: any
|
||||||
updateView: any
|
updateView: any
|
||||||
isViewConfigEnabled: boolean
|
isViewConfigEnabled: boolean
|
||||||
|
|
||||||
// Column state
|
|
||||||
visibleColumns: Record<string, boolean>
|
visibleColumns: Record<string, boolean>
|
||||||
columnOrder: string[]
|
columnOrder: string[]
|
||||||
currentColumns: {
|
currentColumns: {
|
||||||
@@ -39,20 +36,12 @@ export interface UseTableConfigurationReturn {
|
|||||||
}
|
}
|
||||||
setColumnOrder: (order: string[]) => void
|
setColumnOrder: (order: string[]) => void
|
||||||
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||||
|
|
||||||
// Configuration state
|
|
||||||
currentConfiguration: TableConfiguration
|
currentConfiguration: TableConfiguration
|
||||||
hasConfigurationChanged: boolean
|
hasConfigurationChanged: boolean
|
||||||
handleClearConfiguration: () => void
|
handleClearConfiguration: () => void
|
||||||
|
apiColumns: HttpTypes.AdminColumn[] | undefined
|
||||||
// API columns
|
|
||||||
apiColumns: HttpTypes.AdminViewColumn[] | undefined
|
|
||||||
isLoadingColumns: boolean
|
isLoadingColumns: boolean
|
||||||
|
|
||||||
// Query params
|
|
||||||
queryParams: Record<string, any>
|
queryParams: Record<string, any>
|
||||||
|
|
||||||
// Required fields for API calls
|
|
||||||
requiredFields: string
|
requiredFields: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,24 +53,20 @@ function parseSortingState(value: string) {
|
|||||||
|
|
||||||
export function useTableConfiguration({
|
export function useTableConfiguration({
|
||||||
entity,
|
entity,
|
||||||
pageSize = 20,
|
|
||||||
queryPrefix = "",
|
queryPrefix = "",
|
||||||
filters = [],
|
filters = [],
|
||||||
}: UseTableConfigurationOptions): UseTableConfigurationReturn {
|
}: UseTableConfigurationOptions): UseTableConfigurationReturn {
|
||||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||||
const [_, setSearchParams] = useSearchParams()
|
const [_, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
// View configurations
|
|
||||||
const { activeView, createView } = useViewConfigurations(entity)
|
const { activeView, createView } = useViewConfigurations(entity)
|
||||||
const currentActiveView = activeView?.view_configuration || null
|
const currentActiveView = activeView?.view_configuration || null
|
||||||
const { updateView } = useViewConfiguration(entity, currentActiveView?.id || "")
|
const { updateView } = useViewConfiguration(entity, currentActiveView?.id || "")
|
||||||
|
|
||||||
// Entity columns
|
|
||||||
const { columns: apiColumns, isLoading: isLoadingColumns } = useEntityColumns(entity, {
|
const { columns: apiColumns, isLoading: isLoadingColumns } = useEntityColumns(entity, {
|
||||||
enabled: isViewConfigEnabled,
|
enabled: isViewConfigEnabled,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Query params
|
|
||||||
const queryParams = useQueryParams(
|
const queryParams = useQueryParams(
|
||||||
["q", "order", ...filters.map(f => f.id)],
|
["q", "order", ...filters.map(f => f.id)],
|
||||||
queryPrefix
|
queryPrefix
|
||||||
@@ -282,32 +267,21 @@ export function useTableConfiguration({
|
|||||||
}, [entity, apiColumns, visibleColumns])
|
}, [entity, apiColumns, visibleColumns])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// View configuration
|
|
||||||
activeView: currentActiveView,
|
activeView: currentActiveView,
|
||||||
createView,
|
createView,
|
||||||
updateView,
|
updateView,
|
||||||
isViewConfigEnabled,
|
isViewConfigEnabled,
|
||||||
|
|
||||||
// Column state
|
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
columnOrder,
|
columnOrder,
|
||||||
currentColumns,
|
currentColumns,
|
||||||
setColumnOrder,
|
setColumnOrder,
|
||||||
handleColumnVisibilityChange,
|
handleColumnVisibilityChange,
|
||||||
|
|
||||||
// Configuration state
|
|
||||||
currentConfiguration,
|
currentConfiguration,
|
||||||
hasConfigurationChanged: debouncedHasConfigChanged,
|
hasConfigurationChanged: debouncedHasConfigChanged,
|
||||||
handleClearConfiguration,
|
handleClearConfiguration,
|
||||||
|
|
||||||
// API columns
|
|
||||||
apiColumns,
|
apiColumns,
|
||||||
isLoadingColumns,
|
isLoadingColumns,
|
||||||
|
|
||||||
// Query params
|
|
||||||
queryParams,
|
queryParams,
|
||||||
|
|
||||||
// Required fields
|
|
||||||
requiredFields,
|
requiredFields,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2022,6 +2022,54 @@
|
|||||||
"required": ["draft", "published", "proposed", "rejected"],
|
"required": ["draft", "published", "proposed", "rejected"],
|
||||||
"additionalProperties": false
|
"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": {
|
"fields": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2690,6 +2738,7 @@
|
|||||||
"variantCount_other",
|
"variantCount_other",
|
||||||
"deleteVariantWarning",
|
"deleteVariantWarning",
|
||||||
"productStatus",
|
"productStatus",
|
||||||
|
"columns",
|
||||||
"fields",
|
"fields",
|
||||||
"variant",
|
"variant",
|
||||||
"options",
|
"options",
|
||||||
|
|||||||
@@ -536,6 +536,18 @@
|
|||||||
"proposed": "Proposed",
|
"proposed": "Proposed",
|
||||||
"rejected": "Rejected"
|
"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": {
|
"fields": {
|
||||||
"title": {
|
"title": {
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
|
|||||||
@@ -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 { HttpTypes } from "@medusajs/types"
|
||||||
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
||||||
|
|
||||||
// Order-specific column adapter
|
|
||||||
export const orderColumnAdapter: ColumnAdapter<HttpTypes.AdminOrder> = {
|
export const orderColumnAdapter: ColumnAdapter<HttpTypes.AdminOrder> = {
|
||||||
getColumnAlignment: (column) => {
|
getColumnAlignment: (column) => {
|
||||||
// Custom alignment for order columns
|
if (column.semantic_type === "currency") {
|
||||||
if (column.field === "display_id") return "center"
|
return "right"
|
||||||
if (column.semantic_type === "currency") return "right"
|
}
|
||||||
if (column.semantic_type === "status") return "center"
|
if (column.semantic_type === "status") {
|
||||||
if (column.computed?.type === "country_code") return "center"
|
return "center"
|
||||||
|
}
|
||||||
|
if (column.computed?.type === "country_code") {
|
||||||
|
return "center"
|
||||||
|
}
|
||||||
return "left"
|
return "left"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product-specific column adapter
|
|
||||||
export const productColumnAdapter: ColumnAdapter<HttpTypes.AdminProduct> = {
|
export const productColumnAdapter: ColumnAdapter<HttpTypes.AdminProduct> = {
|
||||||
getColumnAlignment: (column) => {
|
getColumnAlignment: (column) => {
|
||||||
// Custom alignment for product columns
|
if (column.field === "product_display") {
|
||||||
if (column.field === "sku") return "center"
|
return "left"
|
||||||
if (column.field === "stock") return "right"
|
}
|
||||||
if (column.semantic_type === "currency") return "right"
|
if (column.field === "collection.title") {
|
||||||
if (column.semantic_type === "status") return "center"
|
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"
|
return "left"
|
||||||
},
|
},
|
||||||
|
|
||||||
transformCellValue: (value, row, column) => {
|
transformCellValue: (_value, row, column) => {
|
||||||
// Custom transformation for product-specific fields
|
if (column.field === "variants_count" || column.computed?.type === "count") {
|
||||||
if (column.field === "variants_count") {
|
const count = Array.isArray(row.variants) ? row.variants.length : 0
|
||||||
return `${value || 0} variants`
|
return `${count} ${count === 1 ? 'variant' : 'variants'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (column.field === "status" && value === "draft") {
|
if (column.field === "product_display" || column.computed?.type === "product_info") {
|
||||||
return <span className="text-ui-fg-muted">Draft</span>
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.field === "sales_channels_display" || column.computed?.type === "sales_channels_list") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.field === "status") {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to standard display
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customer-specific column adapter
|
|
||||||
export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
||||||
getColumnAlignment: (column) => {
|
getColumnAlignment: (column) => {
|
||||||
if (column.field === "orders_count") return "right"
|
if (column.field === "orders_count") {
|
||||||
if (column.semantic_type === "currency") return "right"
|
return "right"
|
||||||
if (column.semantic_type === "status") return "center"
|
}
|
||||||
|
if (column.semantic_type === "currency") {
|
||||||
|
return "right"
|
||||||
|
}
|
||||||
|
if (column.semantic_type === "status") {
|
||||||
|
return "center"
|
||||||
|
}
|
||||||
|
|
||||||
return "left"
|
return "left"
|
||||||
},
|
},
|
||||||
|
|
||||||
transformCellValue: (value, row, column) => {
|
transformCellValue: (_value, row, column) => {
|
||||||
// Format customer name
|
|
||||||
if (column.field === "name") {
|
if (column.field === "name") {
|
||||||
const { first_name, last_name } = row
|
const { first_name, last_name } = row
|
||||||
if (first_name || last_name) {
|
if (first_name || last_name) {
|
||||||
@@ -62,18 +105,25 @@ export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inventory-specific column adapter
|
|
||||||
export const inventoryColumnAdapter: ColumnAdapter<HttpTypes.AdminInventoryItem> = {
|
export const inventoryColumnAdapter: ColumnAdapter<HttpTypes.AdminInventoryItem> = {
|
||||||
getColumnAlignment: (column) => {
|
getColumnAlignment: (column) => {
|
||||||
if (column.field === "stocked_quantity") return "right"
|
if (column.field === "stocked_quantity") {
|
||||||
if (column.field === "reserved_quantity") return "right"
|
return "right"
|
||||||
if (column.field === "available_quantity") return "right"
|
}
|
||||||
if (column.semantic_type === "status") return "center"
|
if (column.field === "reserved_quantity") {
|
||||||
|
return "right"
|
||||||
|
}
|
||||||
|
if (column.field === "available_quantity") {
|
||||||
|
return "right"
|
||||||
|
}
|
||||||
|
if (column.semantic_type === "status") {
|
||||||
|
return "center"
|
||||||
|
}
|
||||||
|
|
||||||
return "left"
|
return "left"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry of entity adapters
|
|
||||||
export const entityAdapters = {
|
export const entityAdapters = {
|
||||||
orders: orderColumnAdapter,
|
orders: orderColumnAdapter,
|
||||||
products: productColumnAdapter,
|
products: productColumnAdapter,
|
||||||
@@ -83,7 +133,6 @@ export const entityAdapters = {
|
|||||||
|
|
||||||
export type EntityType = keyof typeof 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 {
|
export function getEntityAdapter<TData = any>(entity: string): ColumnAdapter<TData> | undefined {
|
||||||
return entityAdapters[entity as EntityType] as ColumnAdapter<TData>
|
return entityAdapters[entity as EntityType] as ColumnAdapter<TData>
|
||||||
}
|
}
|
||||||
@@ -15,20 +15,12 @@ export const ENTITY_DEFAULT_FIELDS = {
|
|||||||
"total",
|
"total",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
],
|
],
|
||||||
relations: ["*customer", "*sales_channel"]
|
relations: ["*customer", "*sales_channel"],
|
||||||
},
|
},
|
||||||
|
|
||||||
products: {
|
products: {
|
||||||
properties: [
|
properties: ["id", "title", "handle", "status", "thumbnail"],
|
||||||
"id",
|
relations: ["collection.title", "*sales_channels", "*variants"],
|
||||||
"title",
|
|
||||||
"handle",
|
|
||||||
"status",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"thumbnail",
|
|
||||||
],
|
|
||||||
relations: ["*variants", "*categories", "*collections"]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
customers: {
|
customers: {
|
||||||
@@ -41,7 +33,7 @@ export const ENTITY_DEFAULT_FIELDS = {
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
"has_account",
|
"has_account",
|
||||||
],
|
],
|
||||||
relations: ["*groups"]
|
relations: ["*groups"],
|
||||||
},
|
},
|
||||||
|
|
||||||
inventory: {
|
inventory: {
|
||||||
@@ -55,14 +47,14 @@ export const ENTITY_DEFAULT_FIELDS = {
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
],
|
],
|
||||||
relations: ["*location_levels"]
|
relations: ["*location_levels"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Default configuration for entities without specific defaults
|
// Default configuration for entities without specific defaults
|
||||||
default: {
|
default: {
|
||||||
properties: ["id", "created_at", "updated_at"],
|
properties: ["id", "created_at", "updated_at"],
|
||||||
relations: []
|
relations: [],
|
||||||
}
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type EntityType = keyof typeof ENTITY_DEFAULT_FIELDS
|
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
|
* Get default fields for an entity
|
||||||
*/
|
*/
|
||||||
export function getEntityDefaultFields(entity: string) {
|
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 {
|
return {
|
||||||
properties: config.properties,
|
properties: config.properties,
|
||||||
relations: config.relations,
|
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"
|
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.
|
* Hook to fetch data with the calculated required fields.
|
||||||
* Called inside ConfigurableDataTable with the fields and search params.
|
* Called inside ConfigurableDataTable with the fields and search params.
|
||||||
*/
|
*/
|
||||||
useData: (fields: string, params: any) => {
|
useData: (
|
||||||
|
fields: string,
|
||||||
|
params: any
|
||||||
|
) => {
|
||||||
data: TData[] | undefined
|
data: TData[] | undefined
|
||||||
count: number
|
count: number | undefined
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
isError: boolean
|
isError: boolean
|
||||||
error: any
|
error: any
|
||||||
|
|||||||
+26
@@ -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 />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+8
@@ -18,12 +18,20 @@ import { useProductTableFilters } from "../../../../../hooks/table/filters/use-p
|
|||||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { productsLoader } from "../../loader"
|
import { productsLoader } from "../../loader"
|
||||||
|
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||||
|
import { ConfigurableProductListTable } from "./configurable-product-list-table"
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const ProductListTable = () => {
|
export const ProductListTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const location = useLocation()
|
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<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<ReturnType<typeof productsLoader>>
|
ReturnType<ReturnType<typeof productsLoader>>
|
||||||
|
|||||||
+51
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -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
|
* 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
|
// Currency columns should be right-aligned
|
||||||
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
||||||
return ColumnAlignment.RIGHT
|
return ColumnAlignment.RIGHT
|
||||||
@@ -44,7 +44,7 @@ export function getColumnAlignment(column: HttpTypes.AdminViewColumn): ColumnAli
|
|||||||
* Gets the initial column visibility state from API columns
|
* Gets the initial column visibility state from API columns
|
||||||
*/
|
*/
|
||||||
export function getInitialColumnVisibility(
|
export function getInitialColumnVisibility(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminColumn[]
|
||||||
): Record<string, boolean> {
|
): Record<string, boolean> {
|
||||||
const visibility: Record<string, boolean> = {}
|
const visibility: Record<string, boolean> = {}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export function getInitialColumnVisibility(
|
|||||||
* Gets the initial column order from API columns
|
* Gets the initial column order from API columns
|
||||||
*/
|
*/
|
||||||
export function getInitialColumnOrder(
|
export function getInitialColumnOrder(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminColumn[]
|
||||||
): string[] {
|
): string[] {
|
||||||
const sortedColumns = [...apiColumns].sort((a, b) => {
|
const sortedColumns = [...apiColumns].sort((a, b) => {
|
||||||
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
||||||
|
|||||||
@@ -250,7 +250,10 @@ export class Client {
|
|||||||
const params = Object.fromEntries(
|
const params = Object.fromEntries(
|
||||||
normalizedInput.searchParams.entries()
|
normalizedInput.searchParams.entries()
|
||||||
)
|
)
|
||||||
const stringifiedQuery = stringify({ ...params, ...init.query }, { skipNulls: true })
|
const stringifiedQuery = stringify(
|
||||||
|
{ ...params, ...init.query },
|
||||||
|
{ skipNulls: true }
|
||||||
|
)
|
||||||
normalizedInput.search = stringifiedQuery
|
normalizedInput.search = stringifiedQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -35,7 +35,6 @@ export const DataTableNonSortableHeaderCell = React.forwardRef<
|
|||||||
...propStyle,
|
...propStyle,
|
||||||
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
||||||
transition,
|
transition,
|
||||||
position: 'relative' as const,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineRefs = (element: HTMLTableCellElement | null) => {
|
const combineRefs = (element: HTMLTableCellElement | null) => {
|
||||||
|
|||||||
+2
-4
@@ -38,9 +38,7 @@ export const DataTableSortableHeaderCell = React.forwardRef<
|
|||||||
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
transform: transformStyle ? CSS.Transform.toString(transformStyle) : undefined,
|
||||||
transition,
|
transition,
|
||||||
opacity: isDragging ? 0.8 : 1,
|
opacity: isDragging ? 0.8 : 1,
|
||||||
zIndex: isDragging ? 50 : undefined,
|
zIndex: isDragging ? 50 : isFirstColumn ? 1 : undefined,
|
||||||
backgroundColor: "white",
|
|
||||||
position: 'relative' as const,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineRefs = (element: HTMLTableCellElement | null) => {
|
const combineRefs = (element: HTMLTableCellElement | null) => {
|
||||||
@@ -58,7 +56,7 @@ export const DataTableSortableHeaderCell = React.forwardRef<
|
|||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
ref={combineRefs}
|
ref={combineRefs}
|
||||||
style={style}
|
style={style}
|
||||||
className={clx(className, "group/header-cell relative")}
|
className={clx(className, "group/header-cell bg-ui-bg-base")}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const ENTITY_MAPPINGS = {
|
|||||||
computedColumns: {
|
computedColumns: {
|
||||||
customer_display: {
|
customer_display: {
|
||||||
name: "Customer",
|
name: "Customer",
|
||||||
computation_type: "customer_name",
|
render_type: "customer_name",
|
||||||
required_fields: [
|
required_fields: [
|
||||||
"customer.first_name",
|
"customer.first_name",
|
||||||
"customer.last_name",
|
"customer.last_name",
|
||||||
@@ -34,7 +34,7 @@ export const ENTITY_MAPPINGS = {
|
|||||||
},
|
},
|
||||||
shipping_address_display: {
|
shipping_address_display: {
|
||||||
name: "Shipping Address",
|
name: "Shipping Address",
|
||||||
computation_type: "address_summary",
|
render_type: "address_summary",
|
||||||
required_fields: [
|
required_fields: [
|
||||||
"shipping_address.city",
|
"shipping_address.city",
|
||||||
"shipping_address.country_code",
|
"shipping_address.country_code",
|
||||||
@@ -48,7 +48,7 @@ export const ENTITY_MAPPINGS = {
|
|||||||
},
|
},
|
||||||
billing_address_display: {
|
billing_address_display: {
|
||||||
name: "Billing Address",
|
name: "Billing Address",
|
||||||
computation_type: "address_summary",
|
render_type: "address_summary",
|
||||||
required_fields: [
|
required_fields: [
|
||||||
"billing_address.city",
|
"billing_address.city",
|
||||||
"billing_address.country_code",
|
"billing_address.country_code",
|
||||||
@@ -62,7 +62,7 @@ export const ENTITY_MAPPINGS = {
|
|||||||
},
|
},
|
||||||
country: {
|
country: {
|
||||||
name: "Country",
|
name: "Country",
|
||||||
computation_type: "country_code",
|
render_type: "country_code",
|
||||||
required_fields: ["shipping_address.country_code"],
|
required_fields: ["shipping_address.country_code"],
|
||||||
optional_fields: [],
|
optional_fields: [],
|
||||||
default_visible: true,
|
default_visible: true,
|
||||||
@@ -73,18 +73,47 @@ export const ENTITY_MAPPINGS = {
|
|||||||
serviceName: "product",
|
serviceName: "product",
|
||||||
graphqlType: "Product",
|
graphqlType: "Product",
|
||||||
defaultVisibleFields: [
|
defaultVisibleFields: [
|
||||||
"title",
|
"product_display",
|
||||||
"handle",
|
"collection.title",
|
||||||
|
"sales_channels_display",
|
||||||
|
"variants_count",
|
||||||
"status",
|
"status",
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
],
|
],
|
||||||
fieldFilters: {
|
fieldFilters: {
|
||||||
excludeSuffixes: ["_link"],
|
excludeSuffixes: ["_link"],
|
||||||
excludePrefixes: ["raw_"],
|
excludePrefixes: ["raw_"],
|
||||||
excludeFields: [],
|
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: {
|
customers: {
|
||||||
serviceName: "customer",
|
serviceName: "customer",
|
||||||
|
|||||||
@@ -255,7 +255,10 @@ export const getTypeInfoFromGraphQLType = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_COLUMN_ORDERS: Record<string, number> = {
|
type Entities = keyof typeof ENTITY_MAPPINGS
|
||||||
|
|
||||||
|
export const DEFAULT_COLUMN_ORDERS: Record<Entities, Record<string, number>> = {
|
||||||
|
orders: {
|
||||||
display_id: 100,
|
display_id: 100,
|
||||||
created_at: 200,
|
created_at: 200,
|
||||||
customer_display: 300,
|
customer_display: 300,
|
||||||
@@ -264,6 +267,19 @@ export const DEFAULT_COLUMN_ORDERS: Record<string, number> = {
|
|||||||
payment_status: 600,
|
payment_status: 600,
|
||||||
total: 700,
|
total: 700,
|
||||||
country: 800,
|
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 = (
|
export const generateEntityColumns = (
|
||||||
entity: string,
|
entity: string,
|
||||||
entityMapping: typeof ENTITY_MAPPINGS[keyof typeof ENTITY_MAPPINGS]
|
entityMapping: (typeof ENTITY_MAPPINGS)[keyof typeof ENTITY_MAPPINGS]
|
||||||
): HttpTypes.AdminColumn[] | null => {
|
): HttpTypes.AdminColumn[] | null => {
|
||||||
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
|
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
|
||||||
|
|
||||||
@@ -304,8 +320,7 @@ export const generateEntityColumns = (
|
|||||||
const mergedSchemaAST = mergeTypeDefs(allSchemas)
|
const mergedSchemaAST = mergeTypeDefs(allSchemas)
|
||||||
const mergedSchemaString = print(mergedSchemaAST)
|
const mergedSchemaString = print(mergedSchemaAST)
|
||||||
|
|
||||||
const { schema: cleanedSchemaString } =
|
const { schema: cleanedSchemaString } = cleanGraphQLSchema(mergedSchemaString)
|
||||||
cleanGraphQLSchema(mergedSchemaString)
|
|
||||||
|
|
||||||
const schema = makeExecutableSchema({
|
const schema = makeExecutableSchema({
|
||||||
typeDefs: cleanedSchemaString,
|
typeDefs: cleanedSchemaString,
|
||||||
@@ -393,9 +408,7 @@ export const generateEntityColumns = (
|
|||||||
const directColumns = directFields.map((fieldName) => {
|
const directColumns = directFields.map((fieldName) => {
|
||||||
const displayName = formatFieldName(fieldName)
|
const displayName = formatFieldName(fieldName)
|
||||||
|
|
||||||
const type = schemaTypeMap[
|
const type = schemaTypeMap[entityMapping.graphqlType] as GraphQLObjectType
|
||||||
entityMapping.graphqlType
|
|
||||||
] as GraphQLObjectType
|
|
||||||
const fieldDef = type?.getFields()?.[fieldName]
|
const fieldDef = type?.getFields()?.[fieldName]
|
||||||
const typeInfo = fieldDef
|
const typeInfo = fieldDef
|
||||||
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
||||||
@@ -406,8 +419,8 @@ export const generateEntityColumns = (
|
|||||||
|
|
||||||
const isDefaultField =
|
const isDefaultField =
|
||||||
entityMapping.defaultVisibleFields.includes(fieldName)
|
entityMapping.defaultVisibleFields.includes(fieldName)
|
||||||
const defaultOrder =
|
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||||
DEFAULT_COLUMN_ORDERS[fieldName] || (isDefaultField ? 500 : 850)
|
const defaultOrder = entityOrders[fieldName] || (isDefaultField ? 500 : 850)
|
||||||
const category = getColumnCategory(
|
const category = getColumnCategory(
|
||||||
fieldName,
|
fieldName,
|
||||||
typeInfo.data_type,
|
typeInfo.data_type,
|
||||||
@@ -421,8 +434,7 @@ export const generateEntityColumns = (
|
|||||||
field: fieldName,
|
field: fieldName,
|
||||||
sortable,
|
sortable,
|
||||||
hideable: true,
|
hideable: true,
|
||||||
default_visible:
|
default_visible: entityMapping.defaultVisibleFields.includes(fieldName),
|
||||||
entityMapping.defaultVisibleFields.includes(fieldName),
|
|
||||||
data_type: typeInfo.data_type,
|
data_type: typeInfo.data_type,
|
||||||
semantic_type: typeInfo.semantic_type,
|
semantic_type: typeInfo.semantic_type,
|
||||||
context: typeInfo.context,
|
context: typeInfo.context,
|
||||||
@@ -442,9 +454,7 @@ export const generateEntityColumns = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Filter out problematic fields from related type
|
// Filter out problematic fields from related type
|
||||||
const relatedType = schemaTypeMap[
|
const relatedType = schemaTypeMap[relatedTypeName] as GraphQLObjectType
|
||||||
relatedTypeName
|
|
||||||
] as GraphQLObjectType
|
|
||||||
const relatedFields = allRelatedFields.filter((fieldName) => {
|
const relatedFields = allRelatedFields.filter((fieldName) => {
|
||||||
const field = relatedType?.getFields()[fieldName]
|
const field = relatedType?.getFields()[fieldName]
|
||||||
if (!field) return true
|
if (!field) return true
|
||||||
@@ -466,13 +476,11 @@ export const generateEntityColumns = (
|
|||||||
|
|
||||||
limitedFields.forEach((fieldName) => {
|
limitedFields.forEach((fieldName) => {
|
||||||
const fieldPath = `${relationName}.${fieldName}`
|
const fieldPath = `${relationName}.${fieldName}`
|
||||||
const displayName = `${formatFieldName(
|
const displayName = `${formatFieldName(relationName)} ${formatFieldName(
|
||||||
relationName
|
fieldName
|
||||||
)} ${formatFieldName(fieldName)}`
|
)}`
|
||||||
|
|
||||||
const relatedType = schemaTypeMap[
|
const relatedType = schemaTypeMap[relatedTypeName] as GraphQLObjectType
|
||||||
relatedTypeName
|
|
||||||
] as GraphQLObjectType
|
|
||||||
const fieldDef = relatedType?.getFields()?.[fieldName]
|
const fieldDef = relatedType?.getFields()?.[fieldName]
|
||||||
const typeInfo = fieldDef
|
const typeInfo = fieldDef
|
||||||
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
? getTypeInfoFromGraphQLType(fieldDef.type, fieldName)
|
||||||
@@ -493,8 +501,9 @@ export const generateEntityColumns = (
|
|||||||
// If field is not in default visible fields, place it after country (850)
|
// If field is not in default visible fields, place it after country (850)
|
||||||
const isDefaultField =
|
const isDefaultField =
|
||||||
entityMapping.defaultVisibleFields.includes(fieldPath)
|
entityMapping.defaultVisibleFields.includes(fieldPath)
|
||||||
|
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||||
const defaultOrder =
|
const defaultOrder =
|
||||||
DEFAULT_COLUMN_ORDERS[fieldPath] || (isDefaultField ? 700 : 850)
|
entityOrders[fieldPath] || (isDefaultField ? 700 : 850)
|
||||||
const category = getColumnCategory(
|
const category = getColumnCategory(
|
||||||
fieldPath,
|
fieldPath,
|
||||||
typeInfo.data_type,
|
typeInfo.data_type,
|
||||||
@@ -534,8 +543,9 @@ export const generateEntityColumns = (
|
|||||||
// If field is not in default visible fields, place it after country (850)
|
// If field is not in default visible fields, place it after country (850)
|
||||||
const isDefaultField =
|
const isDefaultField =
|
||||||
entityMapping.defaultVisibleFields.includes(columnId)
|
entityMapping.defaultVisibleFields.includes(columnId)
|
||||||
|
const entityOrders = DEFAULT_COLUMN_ORDERS[entity] || {}
|
||||||
const defaultOrder =
|
const defaultOrder =
|
||||||
DEFAULT_COLUMN_ORDERS[columnId] || (isDefaultField ? 600 : 850)
|
entityOrders[columnId] || (isDefaultField ? 600 : 850)
|
||||||
const category = getColumnCategory(columnId, "string", "computed")
|
const category = getColumnCategory(columnId, "string", "computed")
|
||||||
|
|
||||||
computedColumns.push({
|
computedColumns.push({
|
||||||
@@ -545,13 +555,12 @@ export const generateEntityColumns = (
|
|||||||
field: columnId,
|
field: columnId,
|
||||||
sortable: false, // Computed columns can't be sorted server-side
|
sortable: false, // Computed columns can't be sorted server-side
|
||||||
hideable: true,
|
hideable: true,
|
||||||
default_visible:
|
default_visible: entityMapping.defaultVisibleFields.includes(columnId),
|
||||||
entityMapping.defaultVisibleFields.includes(columnId),
|
|
||||||
data_type: "string", // Computed columns typically output strings
|
data_type: "string", // Computed columns typically output strings
|
||||||
semantic_type: "computed",
|
semantic_type: "computed",
|
||||||
context: "display",
|
context: "display",
|
||||||
computed: {
|
computed: {
|
||||||
type: columnConfig.computation_type,
|
type: columnConfig.render_type,
|
||||||
required_fields: columnConfig.required_fields,
|
required_fields: columnConfig.required_fields,
|
||||||
optional_fields: columnConfig.optional_fields || [],
|
optional_fields: columnConfig.optional_fields || [],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user