From 41047b38540d121e2abceb5fba65fbc618af1f90 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 18 Sep 2025 18:27:17 +0200 Subject: [PATCH] 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 --- .../__tests__/views/admin/columns.spec.ts | 4 +- .../src/components/data-table/data-table.tsx | 20 +- .../configurable-data-table.tsx | 281 +++------------- .../use-configurable-table-columns.tsx | 82 ++--- .../filters/use-product-table-filters.tsx | 3 + .../hooks/table/use-table-configuration.tsx | 32 +- .../src/i18n/translations/$schema.json | 49 +++ .../dashboard/src/i18n/translations/en.json | 12 + .../src/lib/table/cell-renderers.tsx | 305 ++++++++++++++++++ .../src/lib/table/entity-adapters.tsx | 125 ++++--- .../src/lib/table/entity-defaults.ts | 39 +-- .../dashboard/src/lib/table/table-adapters.ts | 17 +- .../configurable-product-list-table.tsx | 26 ++ .../product-list-table/product-list-table.tsx | 8 + .../product-table-adapter.tsx | 51 +++ .../use-product-table-filters.tsx | 104 ++++++ .../admin/dashboard/src/utils/column-utils.ts | 6 +- packages/core/js-sdk/src/client.ts | 5 +- .../data-table-non-sortable-header-cell.tsx | 1 - .../data-table-sortable-header-cell.tsx | 6 +- .../views/[entity]/columns/entity-mappings.ts | 47 ++- .../admin/views/[entity]/columns/helpers.ts | 75 +++-- 22 files changed, 865 insertions(+), 433 deletions(-) create mode 100644 packages/admin/dashboard/src/lib/table/cell-renderers.tsx create mode 100644 packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/configurable-product-list-table.tsx create mode 100644 packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-table-adapter.tsx create mode 100644 packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/use-product-table-filters.tsx diff --git a/integration-tests/http/__tests__/views/admin/columns.spec.ts b/integration-tests/http/__tests__/views/admin/columns.spec.ts index 5956d13bff..df628de059 100644 --- a/integration-tests/http/__tests__/views/admin/columns.spec.ts +++ b/integration-tests/http/__tests__/views/admin/columns.spec.ts @@ -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, }) }) }) diff --git a/packages/admin/dashboard/src/components/data-table/data-table.tsx b/packages/admin/dashboard/src/components/data-table/data-table.tsx index 4feb5b508c..90ceffab6c 100644 --- a/packages/admin/dashboard/src/components/data-table/data-table.tsx +++ b/packages/admin/dashboard/src/components/data-table/data-table.tsx @@ -66,12 +66,14 @@ interface DataTableProps { 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 = ({ 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 = ({ 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 = ({ )}
- {enableFiltering && } + {showFilterMenu && } {enableSorting && } {enableSearch && (
@@ -392,7 +397,8 @@ export const DataTable = ({
)} {actionMenu && } - {action && } + {actions && actions.length > 0 && } + {!actions && action && }
@@ -505,3 +511,13 @@ const DataTableAction = ({ ) } +const DataTableActions = ({ actions }: { actions: DataTableActionProps[] }) => { + return ( +
+ {actions.map((action, index) => ( + + ))} +
+ ) +} + diff --git a/packages/admin/dashboard/src/components/table/configurable-data-table/configurable-data-table.tsx b/packages/admin/dashboard/src/components/table/configurable-data-table/configurable-data-table.tsx index f8d9a117c2..60344f1277 100644 --- a/packages/admin/dashboard/src/components/table/configurable-data-table/configurable-data-table.tsx +++ b/packages/admin/dashboard/src/components/table/configurable-data-table/configurable-data-table.tsx @@ -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 { - // Use adapter pattern for entity-specific configuration adapter: TableAdapter - - // 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 { - // Entity configuration - entity: string - entityName?: string - - // Data and columns - data: TData[] - columns: DataTableColumnDef[] - 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({ +export function ConfigurableDataTable({ adapter, heading, subHeading, pageSize: pageSizeProp, queryPrefix: queryPrefixProp, layout = "fill", - // actions, // Currently unused + actions, }: ConfigurableDataTableProps) { const { t } = useTranslation() const [saveDialogOpen, setSaveDialogOpen] = useState(false) @@ -77,7 +51,6 @@ function ConfigurableDataTableWithAdapter({ 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({ isLoadingColumns, apiColumns, requiredFields, + queryParams, } = useTableConfiguration({ entity, pageSize, @@ -101,29 +75,37 @@ function ConfigurableDataTableWithAdapter({ 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({ } }} prefix={queryPrefix} + actions={actions} + enableFilterMenu={false} /> {saveDialogOpen && ( @@ -254,194 +238,3 @@ function ConfigurableDataTableWithAdapter({ ) } - -// Internal component that handles legacy mode -function ConfigurableDataTableLegacy(props: LegacyConfigurableDataTableProps) { - const { t } = useTranslation() - const [saveDialogOpen, setSaveDialogOpen] = useState(false) - const [editingView, setEditingView] = useState(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 ? ( - <> - - - - ) : null - - return ( - - - - {saveDialogOpen && ( - { - setSaveDialogOpen(false) - setEditingView(null) - }} - onSaved={() => { - setSaveDialogOpen(false) - setEditingView(null) - }} - /> - )} - - ) -} - -// Main export that delegates to the appropriate component -export function ConfigurableDataTable( - props: ConfigurableDataTableProps | LegacyConfigurableDataTableProps -) { - // Check if using new adapter pattern or legacy props - if ('adapter' in props) { - return {...props} /> - } else { - return {...props} /> - } -} diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-configurable-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-configurable-table-columns.tsx index c8f9900729..bb955adb4b 100644 --- a/packages/admin/dashboard/src/hooks/table/columns/use-configurable-table-columns.tsx +++ b/packages/admin/dashboard/src/hooks/table/columns/use-configurable-table-columns.tsx @@ -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 { - 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( entity: string, - apiColumns: HttpTypes.AdminViewColumn[] | undefined, + apiColumns: HttpTypes.AdminColumn[] | undefined, adapter?: ColumnAdapter ) { const columnHelper = createDataTableColumnHelper() + const { t } = useTranslation() return useMemo(() => { if (!apiColumns?.length) { @@ -22,37 +24,45 @@ export function useConfigurableTableColumns( } 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( 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" -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/hooks/table/filters/use-product-table-filters.tsx b/packages/admin/dashboard/src/hooks/table/filters/use-product-table-filters.tsx index 1fddaca378..db7f87bbe9 100644 --- a/packages/admin/dashboard/src/hooks/table/filters/use-product-table-filters.tsx +++ b/packages/admin/dashboard/src/hooks/table/filters/use-product-table-filters.tsx @@ -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, diff --git a/packages/admin/dashboard/src/hooks/table/use-table-configuration.tsx b/packages/admin/dashboard/src/hooks/table/use-table-configuration.tsx index 941ef57640..07e5f3e71e 100644 --- a/packages/admin/dashboard/src/hooks/table/use-table-configuration.tsx +++ b/packages/admin/dashboard/src/hooks/table/use-table-configuration.tsx @@ -24,13 +24,10 @@ export interface UseTableConfigurationOptions { } export interface UseTableConfigurationReturn { - // View configuration activeView: any createView: any updateView: any isViewConfigEnabled: boolean - - // Column state visibleColumns: Record columnOrder: string[] currentColumns: { @@ -39,20 +36,12 @@ export interface UseTableConfigurationReturn { } setColumnOrder: (order: string[]) => void handleColumnVisibilityChange: (visibility: Record) => 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 - - // 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, } -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index cda8d4adf4..8a3b0b2417 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -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", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 658e41109a..3088c16fc7 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -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", diff --git a/packages/admin/dashboard/src/lib/table/cell-renderers.tsx b/packages/admin/dashboard/src/lib/table/cell-renderers.tsx new file mode 100644 index 0000000000..d5cdc36fbf --- /dev/null +++ b/packages/admin/dashboard/src/lib/table/cell-renderers.tsx @@ -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 = ( + value: any, + row: TData, + column: HttpTypes.AdminColumn, + t: TFunction +) => React.ReactNode + +export type RendererRegistry = Map + +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 + } + + // 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 ( + + {translatedValue} + + ) +} + +const BadgeListRenderer: CellRenderer = (value, row, column, t) => { + // For sales channels + if (column.field === 'sales_channels_display' || column.field === 'sales_channels') { + return + } + + // Generic badge list + if (!Array.isArray(value)) return '-' + + const items = value.slice(0, 2) + const remaining = value.length - 2 + + return ( +
+ {items.map((item, index) => ( + + {typeof item === 'string' ? item : item.name || item.title || '-'} + + ))} + {remaining > 0 && ( + + {t ? t('general.plusCountMore', '+ {{count}} more', { count: remaining }) : `+${remaining}`} + + )} +
+ ) +} + +const ProductInfoRenderer: CellRenderer = (_, row, _column, _t) => { + return +} + +const CollectionRenderer: CellRenderer = (_, row, _column, _t) => { + return +} + +const VariantsRenderer: CellRenderer = (_, row, _column, _t) => { + return +} + +// 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
-
+ + const country = getCountryByIso2(countryCode) + const displayName = country?.display_name || countryCode.toUpperCase() + + return ( +
+ +
+ +
+
+
+ ) +} + +const DateRenderer: CellRenderer = (value, _row, _column, _t) => { + return +} + +const DisplayIdRenderer: CellRenderer = (value, _row, _column, _t) => { + return +} + +const CurrencyRenderer: CellRenderer = (value, row, _column, _t) => { + const currencyCode = row.currency_code || 'USD' + return +} + +const TotalRenderer: CellRenderer = (value, row, _column, _t) => { + const currencyCode = row.currency_code || 'USD' + return +} + +// 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) +} diff --git a/packages/admin/dashboard/src/lib/table/entity-adapters.tsx b/packages/admin/dashboard/src/lib/table/entity-adapters.tsx index 41eee26d6f..b0a4cd0395 100644 --- a/packages/admin/dashboard/src/lib/table/entity-adapters.tsx +++ b/packages/admin/dashboard/src/lib/table/entity-adapters.tsx @@ -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 = { 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 = { 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 Draft + + 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 = { 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 = { } return "-" } - + return null } } -// Inventory-specific column adapter export const inventoryColumnAdapter: ColumnAdapter = { 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(entity: string): ColumnAdapter | undefined { return entityAdapters[entity as EntityType] as ColumnAdapter -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/lib/table/entity-defaults.ts b/packages/admin/dashboard/src/lib/table/entity-defaults.ts index 682b3ac777..ad72115aef 100644 --- a/packages/admin/dashboard/src/lib/table/entity-defaults.ts +++ b/packages/admin/dashboard/src/lib/table/entity-defaults.ts @@ -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(","), } -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/lib/table/table-adapters.ts b/packages/admin/dashboard/src/lib/table/table-adapters.ts index cb69d50ad8..8210718ee7 100644 --- a/packages/admin/dashboard/src/lib/table/table-adapters.ts +++ b/packages/admin/dashboard/src/lib/table/table-adapters.ts @@ -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 { * 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 { 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[] @@ -84,4 +91,4 @@ export function createTableAdapter( queryPrefix: "", ...adapter, } -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/configurable-product-list-table.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/configurable-product-list-table.tsx new file mode 100644 index 0000000000..a1411d550d --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/configurable-product-list-table.tsx @@ -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 ( + <> + + + + ) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx index 533b6763a5..b7f789742f 100644 --- a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -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 + } const initialData = useLoaderData() as Awaited< ReturnType> diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-table-adapter.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-table-adapter.tsx new file mode 100644 index 0000000000..3d6f5cfbba --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-table-adapter.tsx @@ -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 { + return createTableAdapter({ + 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 { + const filters = useProductTableFilters() + const adapter = createProductTableAdapter() + + // Add dynamic filters to the adapter + return { + ...adapter, + filters, + } +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/use-product-table-filters.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/use-product-table-filters.tsx new file mode 100644 index 0000000000..009eeafa83 --- /dev/null +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/use-product-table-filters.tsx @@ -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() + +/** + * 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]) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/utils/column-utils.ts b/packages/admin/dashboard/src/utils/column-utils.ts index cfa54dc8ef..c8aae77f21 100644 --- a/packages/admin/dashboard/src/utils/column-utils.ts +++ b/packages/admin/dashboard/src/utils/column-utils.ts @@ -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 { const visibility: Record = {} @@ -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 diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index dd44da3c40..537b71916b 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -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 } } diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-non-sortable-header-cell.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-non-sortable-header-cell.tsx index 0ca323c1ff..2ad6065a5b 100644 --- a/packages/design-system/ui/src/blocks/data-table/components/data-table-non-sortable-header-cell.tsx +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-non-sortable-header-cell.tsx @@ -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) => { diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-sortable-header-cell.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-sortable-header-cell.tsx index e194a5f620..81a108addb 100644 --- a/packages/design-system/ui/src/blocks/data-table/components/data-table-sortable-header-cell.tsx +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-sortable-header-cell.tsx @@ -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< = { - 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> = { + 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 = { */ 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 || [], },