feat(admin): add configurable order views (#13211)
Adds support for configurable order views. https://github.com/user-attachments/assets/ed4a5f61-1667-4ed7-9478-423894f3eba6
This commit is contained in:
+81
@@ -0,0 +1,81 @@
|
||||
import React from "react"
|
||||
import { Button, DropdownMenu, usePrompt } from "@medusajs/ui"
|
||||
import { ChevronDownMini } from "@medusajs/icons"
|
||||
|
||||
interface SaveViewDropdownProps {
|
||||
isDefaultView: boolean
|
||||
currentViewId?: string | null
|
||||
currentViewName?: string | null
|
||||
onSaveAsDefault: () => void
|
||||
onUpdateExisting: () => void
|
||||
onSaveAsNew: () => void
|
||||
}
|
||||
|
||||
export const SaveViewDropdown: React.FC<SaveViewDropdownProps> = ({
|
||||
isDefaultView,
|
||||
currentViewId,
|
||||
currentViewName,
|
||||
onSaveAsDefault,
|
||||
onUpdateExisting,
|
||||
onSaveAsNew,
|
||||
}) => {
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleSaveAsDefault = async () => {
|
||||
const result = await prompt({
|
||||
title: "Update default view",
|
||||
description: "This will update the default view for all users. Are you sure?",
|
||||
confirmText: "Update for everyone",
|
||||
cancelText: "Cancel",
|
||||
})
|
||||
|
||||
if (result) {
|
||||
onSaveAsDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateExisting = async () => {
|
||||
const result = await prompt({
|
||||
title: "Update view",
|
||||
description: `Are you sure you want to update "${currentViewName}"?`,
|
||||
confirmText: "Update",
|
||||
cancelText: "Cancel",
|
||||
})
|
||||
|
||||
if (result) {
|
||||
onUpdateExisting()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
Save
|
||||
<ChevronDownMini />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
{isDefaultView ? (
|
||||
<>
|
||||
<DropdownMenu.Item onClick={handleSaveAsDefault}>
|
||||
Update default for everyone
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onClick={onSaveAsNew}>
|
||||
Save as new view
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenu.Item onClick={handleUpdateExisting}>
|
||||
Update "{currentViewName}"
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onClick={onSaveAsNew}>
|
||||
Save as new view
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
+344
-57
@@ -1,31 +1,334 @@
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useState, useMemo, useCallback, useEffect } from "react"
|
||||
import { Container, Button } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
import { DataTable } from "../../../../../components/data-table"
|
||||
import { useOrders } from "../../../../../hooks/api/orders"
|
||||
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
|
||||
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
|
||||
import { useConfigurableOrderTableColumns } from "../../../../../hooks/table/columns/use-configurable-order-table-columns"
|
||||
import { useOrderTableFilters } from "./use-order-table-filters"
|
||||
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||
|
||||
import { DEFAULT_FIELDS } from "../../const"
|
||||
import { useViewConfigurations, useViewConfiguration } from "../../../../../hooks/use-view-configurations"
|
||||
import { useEntityColumns } from "../../../../../hooks/api/views"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { useColumnState } from "../../../../../hooks/table/columns/use-column-state"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
import { SaveViewDropdown } from "./components/save-view-dropdown"
|
||||
import { SaveViewDialog } from "../../../../../components/table/save-view-dialog"
|
||||
import { useRequiredFields } from "./hooks/use-required-fields"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const QUERY_PREFIX = "o"
|
||||
|
||||
function parseSortingState(value: string) {
|
||||
return value.startsWith("-")
|
||||
? { id: value.slice(1), desc: true }
|
||||
: { id: value, desc: false }
|
||||
}
|
||||
|
||||
export const ConfigurableOrderListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({})
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>([])
|
||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||
|
||||
const { searchParams, raw } = useOrderTableQuery({
|
||||
const {
|
||||
activeView,
|
||||
createView,
|
||||
} = useViewConfigurations("orders")
|
||||
|
||||
const currentActiveView = activeView?.view_configuration || null
|
||||
|
||||
const { updateView } = useViewConfiguration("orders", currentActiveView?.id || "")
|
||||
|
||||
const { columns: apiColumns, isLoading: isLoadingColumns } = useEntityColumns("orders", {
|
||||
enabled: isViewConfigEnabled,
|
||||
})
|
||||
|
||||
const filters = useOrderTableFilters()
|
||||
|
||||
const queryParams = useQueryParams(
|
||||
["q", "order", ...filters.map(f => f.id)],
|
||||
QUERY_PREFIX
|
||||
)
|
||||
|
||||
const [_, setSearchParams] = useSearchParams()
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
columnOrder,
|
||||
currentColumns,
|
||||
setColumnOrder,
|
||||
handleColumnVisibilityChange,
|
||||
handleViewChange: originalHandleViewChange,
|
||||
} = useColumnState(apiColumns, currentActiveView)
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiColumns) return
|
||||
originalHandleViewChange(currentActiveView, apiColumns)
|
||||
setSearchParams((prev) => {
|
||||
const keysToDelete = Array.from(prev.keys()).filter(key =>
|
||||
key.startsWith(QUERY_PREFIX + "_") || key === QUERY_PREFIX + "_q" || key === QUERY_PREFIX + "_order"
|
||||
)
|
||||
keysToDelete.forEach(key => prev.delete(key))
|
||||
|
||||
if (currentActiveView) {
|
||||
const viewConfig = currentActiveView.configuration
|
||||
|
||||
if (viewConfig.filters) {
|
||||
Object.entries(viewConfig.filters).forEach(([key, value]) => {
|
||||
prev.set(`${QUERY_PREFIX}_${key}`, JSON.stringify(value))
|
||||
})
|
||||
}
|
||||
|
||||
if (viewConfig.sorting) {
|
||||
const sortValue = viewConfig.sorting.desc
|
||||
? `-${viewConfig.sorting.id}`
|
||||
: viewConfig.sorting.id
|
||||
prev.set(`${QUERY_PREFIX}_order`, sortValue)
|
||||
}
|
||||
|
||||
if (viewConfig.search) {
|
||||
prev.set(`${QUERY_PREFIX}_q`, viewConfig.search)
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}, [currentActiveView, apiColumns])
|
||||
|
||||
const [debouncedHasConfigChanged, setDebouncedHasConfigChanged] = useState(false)
|
||||
|
||||
const hasConfigurationChanged = useMemo(() => {
|
||||
const currentFilters: Record<string, any> = {}
|
||||
filters.forEach(filter => {
|
||||
if (queryParams[filter.id] !== undefined) {
|
||||
currentFilters[filter.id] = JSON.parse(queryParams[filter.id] || "")
|
||||
}
|
||||
})
|
||||
|
||||
const currentSorting = queryParams.order ? parseSortingState(queryParams.order) : null
|
||||
const currentSearch = queryParams.q || ""
|
||||
const currentVisibleColumns = Object.entries(visibleColumns)
|
||||
.filter(([_, isVisible]) => isVisible)
|
||||
.map(([field]) => field)
|
||||
.sort()
|
||||
|
||||
if (currentActiveView) {
|
||||
const viewFilters = currentActiveView.configuration.filters || {}
|
||||
const viewSorting = currentActiveView.configuration.sorting
|
||||
const viewSearch = currentActiveView.configuration.search || ""
|
||||
const viewVisibleColumns = [...(currentActiveView.configuration.visible_columns || [])].sort()
|
||||
const viewColumnOrder = currentActiveView.configuration.column_order || []
|
||||
|
||||
const filterKeys = new Set([...Object.keys(currentFilters), ...Object.keys(viewFilters)])
|
||||
for (const key of filterKeys) {
|
||||
if (JSON.stringify(currentFilters[key]) !== JSON.stringify(viewFilters[key])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedCurrentSorting = currentSorting || undefined
|
||||
const normalizedViewSorting = viewSorting || undefined
|
||||
if (JSON.stringify(normalizedCurrentSorting) !== JSON.stringify(normalizedViewSorting)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (currentSearch !== viewSearch) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (JSON.stringify(currentVisibleColumns) !== JSON.stringify(viewVisibleColumns)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (JSON.stringify(columnOrder) !== JSON.stringify(viewColumnOrder)) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if (Object.keys(currentFilters).length > 0) return true
|
||||
if (currentSorting !== null) return true
|
||||
if (currentSearch !== "") return true
|
||||
|
||||
if (apiColumns) {
|
||||
const currentVisibleSet = new Set(
|
||||
Object.entries(visibleColumns)
|
||||
.filter(([_, isVisible]) => isVisible)
|
||||
.map(([field]) => field)
|
||||
)
|
||||
|
||||
const defaultVisibleSet = new Set(
|
||||
apiColumns
|
||||
.filter(col => col.default_visible)
|
||||
.map(col => col.field)
|
||||
)
|
||||
|
||||
if (currentVisibleSet.size !== defaultVisibleSet.size ||
|
||||
[...currentVisibleSet].some(field => !defaultVisibleSet.has(field))) {
|
||||
return true
|
||||
}
|
||||
|
||||
const defaultOrder = apiColumns
|
||||
.sort((a, b) => (a.default_order ?? 500) - (b.default_order ?? 500))
|
||||
.map(col => col.field)
|
||||
|
||||
if (JSON.stringify(columnOrder) !== JSON.stringify(defaultOrder)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, [currentActiveView, visibleColumns, columnOrder, filters, queryParams, apiColumns])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedHasConfigChanged(hasConfigurationChanged)
|
||||
}, 50)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [hasConfigurationChanged])
|
||||
|
||||
const handleClearConfiguration = useCallback(() => {
|
||||
if (apiColumns) {
|
||||
originalHandleViewChange(currentActiveView, apiColumns)
|
||||
}
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const keysToDelete = Array.from(prev.keys()).filter(key =>
|
||||
key.startsWith(QUERY_PREFIX + "_") || key === QUERY_PREFIX + "_q" || key === QUERY_PREFIX + "_order"
|
||||
)
|
||||
keysToDelete.forEach(key => prev.delete(key))
|
||||
|
||||
if (currentActiveView?.configuration) {
|
||||
const viewConfig = currentActiveView.configuration
|
||||
|
||||
if (viewConfig.filters) {
|
||||
Object.entries(viewConfig.filters).forEach(([key, value]) => {
|
||||
prev.set(`${QUERY_PREFIX}_${key}`, JSON.stringify(value))
|
||||
})
|
||||
}
|
||||
|
||||
if (viewConfig.sorting) {
|
||||
const sortValue = viewConfig.sorting.desc
|
||||
? `-${viewConfig.sorting.id}`
|
||||
: viewConfig.sorting.id
|
||||
prev.set(`${QUERY_PREFIX}_order`, sortValue)
|
||||
}
|
||||
|
||||
if (viewConfig.search) {
|
||||
prev.set(`${QUERY_PREFIX}_q`, viewConfig.search)
|
||||
}
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}, [currentActiveView, apiColumns])
|
||||
|
||||
const currentConfiguration = useMemo(() => {
|
||||
const currentFilters: Record<string, any> = {}
|
||||
filters.forEach(filter => {
|
||||
if (queryParams[filter.id] !== undefined) {
|
||||
currentFilters[filter.id] = JSON.parse(queryParams[filter.id] || "")
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
filters: currentFilters,
|
||||
sorting: queryParams.order ? parseSortingState(queryParams.order) : null,
|
||||
search: queryParams.q || "",
|
||||
}
|
||||
}, [filters, queryParams])
|
||||
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||
const [editingView, setEditingView] = useState<any>(null)
|
||||
|
||||
const handleSaveAsDefault = async () => {
|
||||
try {
|
||||
if (currentActiveView?.is_system_default) {
|
||||
await updateView.mutateAsync({
|
||||
name: currentActiveView.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 (!currentActiveView) return
|
||||
|
||||
try {
|
||||
await updateView.mutateAsync({
|
||||
name: currentActiveView.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)
|
||||
}
|
||||
|
||||
const requiredFields = useRequiredFields(apiColumns, visibleColumns)
|
||||
|
||||
const filterBarContent = debouncedHasConfigChanged ? (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleClearConfiguration}
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Button>
|
||||
<SaveViewDropdown
|
||||
isDefaultView={currentActiveView?.is_system_default || !currentActiveView}
|
||||
currentViewId={currentActiveView?.id}
|
||||
currentViewName={currentActiveView?.name}
|
||||
onSaveAsDefault={handleSaveAsDefault}
|
||||
onUpdateExisting={handleUpdateExisting}
|
||||
onSaveAsNew={handleSaveAsNew}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
|
||||
const { searchParams } = useOrderTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: QUERY_PREFIX,
|
||||
})
|
||||
|
||||
const { orders, count, isError, error, isLoading } = useOrders(
|
||||
{
|
||||
fields: DEFAULT_FIELDS,
|
||||
fields: requiredFields,
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
@@ -33,38 +336,7 @@ export const ConfigurableOrderListTable = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const filters = useOrderTableFilters()
|
||||
const columns = useOrderTableColumns({})
|
||||
|
||||
const handleViewChange = (view: any) => {
|
||||
if (view) {
|
||||
// Apply view configuration
|
||||
const visibilityState: Record<string, boolean> = {}
|
||||
const allColumns = columns.map(c => c.id!)
|
||||
|
||||
// Set all columns to hidden first
|
||||
allColumns.forEach(col => {
|
||||
visibilityState[col] = false
|
||||
})
|
||||
|
||||
// Then show only the visible columns from the view
|
||||
if (view.configuration?.visible_columns) {
|
||||
view.configuration.visible_columns.forEach((col: string) => {
|
||||
visibilityState[col] = true
|
||||
})
|
||||
}
|
||||
|
||||
setColumnVisibility(visibilityState)
|
||||
|
||||
if (view.configuration?.column_order) {
|
||||
setColumnOrder(view.configuration.column_order)
|
||||
}
|
||||
} else {
|
||||
// Reset to default (all visible)
|
||||
setColumnVisibility({})
|
||||
setColumnOrder([])
|
||||
}
|
||||
}
|
||||
const columns = useConfigurableOrderTableColumns(apiColumns)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
@@ -81,28 +353,43 @@ export const ConfigurableOrderListTable = () => {
|
||||
enablePagination
|
||||
enableSearch
|
||||
pageSize={PAGE_SIZE}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoading || isLoadingColumns}
|
||||
layout="fill"
|
||||
heading={t("orders.domain")}
|
||||
enableColumnVisibility={true}
|
||||
initialColumnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
enableColumnVisibility={isViewConfigEnabled}
|
||||
initialColumnVisibility={visibleColumns}
|
||||
onColumnVisibilityChange={handleColumnVisibilityChange}
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrderChange={setColumnOrder}
|
||||
enableViewSelector={true}
|
||||
enableViewSelector={isViewConfigEnabled}
|
||||
entity="orders"
|
||||
onViewChange={handleViewChange}
|
||||
currentColumns={{
|
||||
visible: Object.entries(columnVisibility)
|
||||
.filter(([_, visible]) => visible !== false)
|
||||
.map(([col]) => col),
|
||||
order: columnOrder.length > 0 ? columnOrder : columns.map(c => c.id!).filter(Boolean)
|
||||
}}
|
||||
currentColumns={currentColumns}
|
||||
filterBarContent={filterBarContent}
|
||||
rowHref={(row) => `/orders/${row.id}`}
|
||||
emptyState={{
|
||||
message: t("orders.list.noRecordsMessage"),
|
||||
empty: {
|
||||
heading: t("orders.list.noRecordsMessage"),
|
||||
}
|
||||
}}
|
||||
prefix={QUERY_PREFIX}
|
||||
/>
|
||||
|
||||
{saveDialogOpen && (
|
||||
<SaveViewDialog
|
||||
entity="orders"
|
||||
currentColumns={currentColumns}
|
||||
currentConfiguration={currentConfiguration}
|
||||
editingView={editingView}
|
||||
onClose={() => {
|
||||
setSaveDialogOpen(false)
|
||||
setEditingView(null)
|
||||
}}
|
||||
onSaved={() => {
|
||||
setSaveDialogOpen(false)
|
||||
setEditingView(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export const PAGE_SIZE = 20
|
||||
export const DEFAULT_COLUMN_ORDER = 500
|
||||
export const QUERY_PREFIX = "o"
|
||||
|
||||
export enum ColumnAlignment {
|
||||
LEFT = "left",
|
||||
CENTER = "center",
|
||||
RIGHT = "right",
|
||||
}
|
||||
|
||||
export interface ColumnState {
|
||||
visibility: Record<string, boolean>
|
||||
order: string[]
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { createDataTableColumnHelper, StatusBadge } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useDate } from "../../../../../../hooks/use-date"
|
||||
|
||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminOrder>()
|
||||
|
||||
export function useOrderDataTableColumns(apiColumns: any[] | undefined) {
|
||||
const { getFullDate } = useDate()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!apiColumns?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return apiColumns.map(apiColumn => {
|
||||
// Special handling for specific columns
|
||||
if (apiColumn.field === "display_id") {
|
||||
return columnHelper.accessor("display_id", {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="text-ui-fg-subtle">#</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (apiColumn.field === "created_at" || apiColumn.field === "updated_at") {
|
||||
return columnHelper.accessor(apiColumn.field as any, {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
if (!value) return null
|
||||
return getFullDate({ date: value })
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (apiColumn.field === "payment_status") {
|
||||
return columnHelper.accessor("payment_status", {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return value ? (
|
||||
<StatusBadge variant="default">{value}</StatusBadge>
|
||||
) : null
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (apiColumn.field === "fulfillment_status") {
|
||||
return columnHelper.accessor("fulfillment_status", {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return value ? (
|
||||
<StatusBadge variant="default">{value}</StatusBadge>
|
||||
) : null
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (apiColumn.field === "total") {
|
||||
return columnHelper.accessor("total", {
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
// Format as currency if we have the value
|
||||
return value !== null && value !== undefined ? `$${(value / 100).toFixed(2)}` : null
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle nested fields with dot notation
|
||||
const fieldParts = apiColumn.field.split(".")
|
||||
|
||||
return columnHelper.accessor(
|
||||
(row) => {
|
||||
let value: any = row
|
||||
for (const part of fieldParts) {
|
||||
value = value?.[part]
|
||||
}
|
||||
return value
|
||||
},
|
||||
{
|
||||
id: apiColumn.field,
|
||||
header: () => apiColumn.name,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
if (value === null || value === undefined) return null
|
||||
|
||||
// Handle objects by trying to display sensible values
|
||||
if (typeof value === "object") {
|
||||
if (value.name) return value.name
|
||||
if (value.title) return value.title
|
||||
if (value.code) return value.code
|
||||
if (value.label) return value.label
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
return String(value)
|
||||
},
|
||||
meta: {
|
||||
name: apiColumn.name,
|
||||
column: apiColumn,
|
||||
},
|
||||
enableHiding: apiColumn.hideable,
|
||||
enableSorting: false,
|
||||
}
|
||||
)
|
||||
})
|
||||
}, [apiColumns, getFullDate])
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { useMemo } from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { calculateRequiredFields } from "../utils/field-utils"
|
||||
|
||||
export function useRequiredFields(
|
||||
apiColumns: any[] | undefined,
|
||||
visibleColumns: Record<string, boolean>
|
||||
): string {
|
||||
return useMemo(() => {
|
||||
return calculateRequiredFields(apiColumns, visibleColumns)
|
||||
}, [apiColumns, visibleColumns])
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"
|
||||
import { _DataTable } from "../../../../../components/table/data-table/data-table"
|
||||
import { useOrders } from "../../../../../hooks/api/orders"
|
||||
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
|
||||
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
|
||||
import { useOrderTableFilters } from "./use-order-table-filters"
|
||||
import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
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 { useRegions } from "../../../../../hooks/api/regions"
|
||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||
|
||||
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminOrder>()
|
||||
|
||||
/**
|
||||
* Hook to create filters in the format expected by @medusajs/ui DataTable
|
||||
*/
|
||||
export const useOrderTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
const dateFilters = useDataTableDateFilters()
|
||||
|
||||
const { regions } = useRegions({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
})
|
||||
|
||||
const { sales_channels } = useSalesChannels({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
const filters = [...dateFilters]
|
||||
|
||||
if (regions?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("region_id", {
|
||||
label: t("fields.region"),
|
||||
type: "multiselect",
|
||||
options: regions.map((r) => ({
|
||||
label: r.name,
|
||||
value: r.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,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Add payment and fulfillment status filters when they are properly linked to orders
|
||||
// Note: These filters are commented out in the legacy implementation as well
|
||||
|
||||
return filters
|
||||
}, [regions, sales_channels, dateFilters, t])
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export enum ColumnAlignment {
|
||||
LEFT = "left",
|
||||
CENTER = "center",
|
||||
RIGHT = "right",
|
||||
}
|
||||
|
||||
export const DEFAULT_COLUMN_ORDER = 500
|
||||
|
||||
/**
|
||||
* Determines the appropriate column alignment based on the column metadata
|
||||
*/
|
||||
export function getColumnAlignment(column: any): ColumnAlignment {
|
||||
// Currency columns should be right-aligned
|
||||
if (column.semantic_type === "currency" || column.data_type === "currency") {
|
||||
return ColumnAlignment.RIGHT
|
||||
}
|
||||
|
||||
// Number columns should be right-aligned (except identifiers)
|
||||
if (column.data_type === "number" && column.context !== "identifier") {
|
||||
return ColumnAlignment.RIGHT
|
||||
}
|
||||
|
||||
// Total/amount/price columns should be right-aligned
|
||||
if (
|
||||
column.field.includes("total") ||
|
||||
column.field.includes("amount") ||
|
||||
column.field.includes("price")
|
||||
) {
|
||||
return ColumnAlignment.RIGHT
|
||||
}
|
||||
|
||||
// Country columns should be center-aligned
|
||||
if (column.field === "country" || column.field.includes("country_code")) {
|
||||
return ColumnAlignment.CENTER
|
||||
}
|
||||
|
||||
// Default to left alignment
|
||||
return ColumnAlignment.LEFT
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial column visibility state from API columns
|
||||
*/
|
||||
export function getInitialColumnVisibility(
|
||||
apiColumns: any[]
|
||||
): Record<string, boolean> {
|
||||
const visibility: Record<string, boolean> = {}
|
||||
|
||||
apiColumns.forEach(column => {
|
||||
visibility[column.field] = column.default_visible
|
||||
})
|
||||
|
||||
return visibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial column order from API columns
|
||||
*/
|
||||
export function getInitialColumnOrder(
|
||||
apiColumns: any[]
|
||||
): string[] {
|
||||
const sortedColumns = [...apiColumns].sort((a, b) => {
|
||||
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
||||
const orderB = b.default_order ?? DEFAULT_COLUMN_ORDER
|
||||
return orderA - orderB
|
||||
})
|
||||
|
||||
return sortedColumns.map(col => col.field)
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { DEFAULT_FIELDS, DEFAULT_PROPERTIES, DEFAULT_RELATIONS } from "../../../const"
|
||||
|
||||
/**
|
||||
* Calculates the required fields based on visible columns
|
||||
*/
|
||||
export function calculateRequiredFields(
|
||||
apiColumns: any[] | undefined,
|
||||
visibleColumns: Record<string, boolean>
|
||||
): string {
|
||||
if (!apiColumns?.length) {
|
||||
return DEFAULT_FIELDS
|
||||
}
|
||||
|
||||
// Get all visible columns
|
||||
const visibleColumnObjects = apiColumns.filter(column => {
|
||||
// If visibleColumns has data, use it; otherwise use default_visible
|
||||
if (Object.keys(visibleColumns).length > 0) {
|
||||
return visibleColumns[column.field] === true
|
||||
}
|
||||
return column.default_visible
|
||||
})
|
||||
|
||||
// Collect all required fields from visible columns
|
||||
const requiredFieldsSet = new Set<string>()
|
||||
|
||||
visibleColumnObjects.forEach(column => {
|
||||
if (column.computed) {
|
||||
// For computed columns, add all required and optional fields
|
||||
column.computed.required_fields?.forEach(field => requiredFieldsSet.add(field))
|
||||
column.computed.optional_fields?.forEach(field => requiredFieldsSet.add(field))
|
||||
} else if (!column.field.includes('.')) {
|
||||
// Direct field
|
||||
requiredFieldsSet.add(column.field)
|
||||
} else {
|
||||
// Relationship field
|
||||
requiredFieldsSet.add(column.field)
|
||||
}
|
||||
})
|
||||
|
||||
// Separate relationship fields from direct fields
|
||||
const allRequiredFields = Array.from(requiredFieldsSet)
|
||||
const visibleRelationshipFields = allRequiredFields.filter(field => field.includes('.'))
|
||||
const visibleDirectFields = allRequiredFields.filter(field => !field.includes('.'))
|
||||
|
||||
// Check which relationship fields need to be added
|
||||
const additionalRelationshipFields = visibleRelationshipFields.filter(field => {
|
||||
const [relationName] = field.split('.')
|
||||
const isAlreadyCovered = DEFAULT_RELATIONS.some(rel =>
|
||||
rel === `*${relationName}` || rel === relationName
|
||||
)
|
||||
return !isAlreadyCovered
|
||||
})
|
||||
|
||||
// Check which direct fields need to be added
|
||||
const additionalDirectFields = visibleDirectFields.filter(field => {
|
||||
const isAlreadyIncluded = DEFAULT_PROPERTIES.includes(field)
|
||||
return !isAlreadyIncluded
|
||||
})
|
||||
|
||||
// Combine all additional fields
|
||||
const additionalFields = [...additionalRelationshipFields, ...additionalDirectFields]
|
||||
|
||||
// Combine default fields with additional needed fields
|
||||
if (additionalFields.length > 0) {
|
||||
return `${DEFAULT_FIELDS},${additionalFields.join(',')}`
|
||||
}
|
||||
|
||||
return DEFAULT_FIELDS
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const DEFAULT_PROPERTIES = [
|
||||
export const DEFAULT_PROPERTIES = [
|
||||
"id",
|
||||
"status",
|
||||
"created_at",
|
||||
@@ -10,7 +10,7 @@ const DEFAULT_PROPERTIES = [
|
||||
"currency_code",
|
||||
]
|
||||
|
||||
const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
|
||||
export const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
|
||||
|
||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||
","
|
||||
|
||||
Reference in New Issue
Block a user