feat(dashboard): reusable config datatable (#13389)
* feat: add a reusable configurable data table * fix: cleanup * fix: cleanup * fix: cache invalidation * fix: test --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
c066fe993f
commit
23d5a902b1
19 files changed
+1407
-545
No files matched your search
+447
@@ -0,0 +1,447 @@
|
|||||||
|
import React, { 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"
|
||||||
|
|
||||||
|
export interface ConfigurableDataTableProps<TData> {
|
||||||
|
// Use adapter pattern for entity-specific configuration
|
||||||
|
adapter: TableAdapter<TData>
|
||||||
|
|
||||||
|
// Optional overrides
|
||||||
|
heading?: string
|
||||||
|
subHeading?: string
|
||||||
|
pageSize?: number
|
||||||
|
queryPrefix?: string
|
||||||
|
layout?: "fill" | "auto"
|
||||||
|
actions?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy props interface for backward compatibility
|
||||||
|
export interface LegacyConfigurableDataTableProps<TData> {
|
||||||
|
// Entity configuration
|
||||||
|
entity: string
|
||||||
|
entityName?: string
|
||||||
|
|
||||||
|
// Data and columns
|
||||||
|
data: TData[]
|
||||||
|
columns: DataTableColumnDef<TData, any>[]
|
||||||
|
filters?: DataTableFilter[]
|
||||||
|
|
||||||
|
// Table configuration
|
||||||
|
pageSize?: number
|
||||||
|
queryPrefix?: string
|
||||||
|
getRowId: (row: TData) => string
|
||||||
|
rowHref?: (row: TData) => string
|
||||||
|
|
||||||
|
// UI configuration
|
||||||
|
heading?: string
|
||||||
|
subHeading?: string
|
||||||
|
emptyState?: DataTableEmptyStateProps
|
||||||
|
|
||||||
|
// Loading and counts
|
||||||
|
isLoading?: boolean
|
||||||
|
rowCount?: number
|
||||||
|
|
||||||
|
// Additional content
|
||||||
|
actions?: ReactNode
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
layout?: "fill" | "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal component that handles adapter mode
|
||||||
|
function ConfigurableDataTableWithAdapter<TData>({
|
||||||
|
adapter,
|
||||||
|
heading,
|
||||||
|
subHeading,
|
||||||
|
pageSize: pageSizeProp,
|
||||||
|
queryPrefix: queryPrefixProp,
|
||||||
|
layout = "fill",
|
||||||
|
// actions, // Currently unused
|
||||||
|
}: ConfigurableDataTableProps<TData>) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||||
|
const [editingView, setEditingView] = useState<any>(null)
|
||||||
|
|
||||||
|
const entity = adapter.entity
|
||||||
|
const entityName = adapter.entityName
|
||||||
|
const filters = adapter.filters || []
|
||||||
|
const pageSize = pageSizeProp || adapter.pageSize || 20
|
||||||
|
const queryPrefix = queryPrefixProp || adapter.queryPrefix || ""
|
||||||
|
|
||||||
|
// Get table configuration (single source of truth)
|
||||||
|
const {
|
||||||
|
activeView,
|
||||||
|
createView,
|
||||||
|
updateView,
|
||||||
|
isViewConfigEnabled,
|
||||||
|
visibleColumns,
|
||||||
|
columnOrder,
|
||||||
|
currentColumns,
|
||||||
|
setColumnOrder,
|
||||||
|
handleColumnVisibilityChange,
|
||||||
|
currentConfiguration,
|
||||||
|
hasConfigurationChanged,
|
||||||
|
handleClearConfiguration,
|
||||||
|
isLoadingColumns,
|
||||||
|
apiColumns,
|
||||||
|
requiredFields,
|
||||||
|
} = useTableConfiguration({
|
||||||
|
entity,
|
||||||
|
pageSize,
|
||||||
|
queryPrefix,
|
||||||
|
filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get query params for data fetching
|
||||||
|
const { searchParams } = useOrderTableQuery({
|
||||||
|
pageSize,
|
||||||
|
prefix: queryPrefix,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch data using adapter
|
||||||
|
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) {
|
||||||
|
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={fetchResult.data || []}
|
||||||
|
columns={columns}
|
||||||
|
filters={filters}
|
||||||
|
getRowId={adapter.getRowId || ((row: any) => row.id)}
|
||||||
|
rowCount={fetchResult.count}
|
||||||
|
enablePagination
|
||||||
|
enableSearch
|
||||||
|
pageSize={pageSize}
|
||||||
|
isLoading={fetchResult.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={adapter.getRowHref as ((row: any) => string) | undefined}
|
||||||
|
emptyState={adapter.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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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} />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { ConfigurableDataTable } from "./configurable-data-table"
|
||||||
|
export type { ConfigurableDataTableProps } from "./configurable-data-table"
|
||||||
|
export { SaveViewDropdown } from "./save-view-dropdown"
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Button, DropdownMenu, usePrompt } from "@medusajs/ui"
|
||||||
|
import { ChevronDownMini } from "@medusajs/icons"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
interface SaveViewDropdownProps {
|
||||||
|
isDefaultView: boolean
|
||||||
|
currentViewId?: string
|
||||||
|
currentViewName?: string
|
||||||
|
onSaveAsDefault: () => void
|
||||||
|
onUpdateExisting: () => void
|
||||||
|
onSaveAsNew: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveViewDropdown: React.FC<SaveViewDropdownProps> = ({
|
||||||
|
isDefaultView,
|
||||||
|
currentViewId,
|
||||||
|
currentViewName,
|
||||||
|
onSaveAsDefault,
|
||||||
|
onUpdateExisting,
|
||||||
|
onSaveAsNew,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleSaveAsDefault = async () => {
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("views.prompts.updateDefault.title"),
|
||||||
|
description: t("views.prompts.updateDefault.description"),
|
||||||
|
confirmText: t("views.prompts.updateDefault.confirmText"),
|
||||||
|
cancelText: t("views.prompts.updateDefault.cancelText"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
onSaveAsDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpdateExisting = async () => {
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("views.prompts.updateView.title"),
|
||||||
|
description: t("views.prompts.updateView.description", { name: currentViewName }),
|
||||||
|
confirmText: t("views.prompts.updateView.confirmText"),
|
||||||
|
cancelText: t("views.prompts.updateView.cancelText"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
onUpdateExisting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t("views.save")}
|
||||||
|
<ChevronDownMini />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end">
|
||||||
|
{isDefaultView ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Item onClick={handleSaveAsDefault}>
|
||||||
|
{t("views.updateDefaultForEveryone")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={onSaveAsNew}>
|
||||||
|
{t("views.saveAsNew")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Item onClick={handleUpdateExisting}>
|
||||||
|
{t("views.updateViewName")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={onSaveAsNew}>
|
||||||
|
{t("views.saveAsNew")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,11 @@ _viewsKeys.active = function(entity: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_viewsKeys.configurations = function(entity: string, query?: any) {
|
_viewsKeys.configurations = function(entity: string, query?: any) {
|
||||||
return [this.all, "configurations", entity, query]
|
const key = [this.all, "configurations", entity]
|
||||||
|
if (query !== undefined) {
|
||||||
|
key.push(query)
|
||||||
|
}
|
||||||
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
export const viewsQueryKeys = _viewsKeys
|
export const viewsQueryKeys = _viewsKeys
|
||||||
@@ -176,6 +180,8 @@ export const useUpdateViewConfiguration = (
|
|||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
||||||
|
// Also invalidate active configuration if this view is currently active
|
||||||
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.active(entity) })
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
|
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
|
||||||
// import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
|
|
||||||
|
type ViewConfiguration =
|
||||||
|
HttpTypes.AdminViewConfigurationResponse["view_configuration"]
|
||||||
|
|
||||||
interface ColumnState {
|
interface ColumnState {
|
||||||
visibility: Record<string, boolean>
|
visibility: Record<string, boolean>
|
||||||
@@ -20,13 +22,13 @@ interface UseColumnStateReturn {
|
|||||||
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||||
handleViewChange: (
|
handleViewChange: (
|
||||||
view: ViewConfiguration | null,
|
view: ViewConfiguration | null,
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminColumn[]
|
||||||
) => void
|
) => void
|
||||||
initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void
|
initializeColumns: (apiColumns: HttpTypes.AdminColumn[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useColumnState(
|
export function useColumnState(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
|
apiColumns: HttpTypes.AdminColumn[] | undefined,
|
||||||
activeView?: ViewConfiguration | null
|
activeView?: ViewConfiguration | null
|
||||||
): UseColumnStateReturn {
|
): UseColumnStateReturn {
|
||||||
// Initialize state lazily to avoid unnecessary re-renders
|
// Initialize state lazily to avoid unnecessary re-renders
|
||||||
@@ -85,10 +87,7 @@ export function useColumnState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleViewChange = useCallback(
|
const handleViewChange = useCallback(
|
||||||
(
|
(view: ViewConfiguration | null, apiColumns: HttpTypes.AdminColumn[]) => {
|
||||||
view: ViewConfiguration | null,
|
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
|
||||||
) => {
|
|
||||||
if (view?.configuration) {
|
if (view?.configuration) {
|
||||||
// Apply view configuration
|
// Apply view configuration
|
||||||
const newVisibility: Record<string, boolean> = {}
|
const newVisibility: Record<string, boolean> = {}
|
||||||
@@ -108,7 +107,7 @@ export function useColumnState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const initializeColumns = useCallback(
|
const initializeColumns = useCallback(
|
||||||
(apiColumns: HttpTypes.AdminViewColumn[]) => {
|
(apiColumns: HttpTypes.AdminColumn[]) => {
|
||||||
// Only initialize if we don't already have column state
|
// Only initialize if we don't already have column state
|
||||||
if (Object.keys(visibleColumns).length === 0) {
|
if (Object.keys(visibleColumns).length === 0) {
|
||||||
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
||||||
@@ -177,7 +176,7 @@ const DEFAULT_COLUMN_ORDER = 500
|
|||||||
* Gets the initial column visibility state from API columns
|
* Gets the initial column visibility state from API columns
|
||||||
*/
|
*/
|
||||||
function getInitialColumnVisibility(
|
function getInitialColumnVisibility(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminColumn[]
|
||||||
): Record<string, boolean> {
|
): Record<string, boolean> {
|
||||||
if (!apiColumns || apiColumns.length === 0) {
|
if (!apiColumns || apiColumns.length === 0) {
|
||||||
return {}
|
return {}
|
||||||
@@ -195,9 +194,7 @@ function getInitialColumnVisibility(
|
|||||||
/**
|
/**
|
||||||
* Gets the initial column order from API columns
|
* Gets the initial column order from API columns
|
||||||
*/
|
*/
|
||||||
function getInitialColumnOrder(
|
function getInitialColumnOrder(apiColumns: HttpTypes.AdminColumn[]): string[] {
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
|
||||||
): string[] {
|
|
||||||
if (!apiColumns || apiColumns.length === 0) {
|
if (!apiColumns || apiColumns.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
import React, { useMemo } from "react"
|
|
||||||
import { createDataTableColumnHelper } from "@medusajs/ui"
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
import { getDisplayStrategy, getEntityAccessor } from "../../../lib/table-display-utils"
|
|
||||||
import { getColumnAlignment } from "../../../routes/orders/order-list/components/order-list-table/utils/column-utils"
|
|
||||||
|
|
||||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminOrder>()
|
|
||||||
|
|
||||||
export function useConfigurableOrderTableColumns(apiColumns: any[] | undefined) {
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!apiColumns?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiColumns.map(apiColumn => {
|
|
||||||
// Get the display strategy for this column
|
|
||||||
const displayStrategy = getDisplayStrategy(apiColumn)
|
|
||||||
|
|
||||||
// Get the entity-specific accessor or use default
|
|
||||||
const accessor = getEntityAccessor('orders', apiColumn.field, apiColumn)
|
|
||||||
|
|
||||||
// Determine header alignment
|
|
||||||
const headerAlign = getColumnAlignment(apiColumn)
|
|
||||||
|
|
||||||
return columnHelper.accessor(accessor, {
|
|
||||||
id: apiColumn.field,
|
|
||||||
header: () => apiColumn.name,
|
|
||||||
cell: ({ getValue, row }) => {
|
|
||||||
const value = getValue()
|
|
||||||
|
|
||||||
// If the value is already a React element (from computed columns), return it directly
|
|
||||||
if (React.isValidElement(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, use the display strategy to format the value
|
|
||||||
return displayStrategy(value, row.original)
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
name: apiColumn.name,
|
|
||||||
column: apiColumn, // Store column metadata for future use
|
|
||||||
},
|
|
||||||
enableHiding: apiColumn.hideable,
|
|
||||||
enableSorting: false, // Disable sorting for all columns
|
|
||||||
headerAlign, // Pass the header alignment to the DataTable
|
|
||||||
} as any)
|
|
||||||
})
|
|
||||||
}, [apiColumns])
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { useMemo } from "react"
|
||||||
|
import { createDataTableColumnHelper } from "@medusajs/ui"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { getDisplayStrategy, getEntityAccessor } from "../../../lib/table-display-utils"
|
||||||
|
|
||||||
|
export interface ColumnAdapter<TData> {
|
||||||
|
getColumnAlignment?: (column: HttpTypes.AdminViewColumn) => "left" | "center" | "right"
|
||||||
|
getCustomAccessor?: (field: string, column: HttpTypes.AdminViewColumn) => any
|
||||||
|
transformCellValue?: (value: any, row: TData, column: HttpTypes.AdminViewColumn) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigurableTableColumns<TData = any>(
|
||||||
|
entity: string,
|
||||||
|
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
|
||||||
|
adapter?: ColumnAdapter<TData>
|
||||||
|
) {
|
||||||
|
const columnHelper = createDataTableColumnHelper<TData>()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!apiColumns?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiColumns.map(apiColumn => {
|
||||||
|
// Get the display strategy for this column
|
||||||
|
const displayStrategy = getDisplayStrategy(apiColumn)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Determine header alignment
|
||||||
|
const headerAlign = adapter?.getColumnAlignment
|
||||||
|
? adapter.getColumnAlignment(apiColumn)
|
||||||
|
: getDefaultColumnAlignment(apiColumn)
|
||||||
|
|
||||||
|
return columnHelper.accessor(accessor, {
|
||||||
|
id: apiColumn.field,
|
||||||
|
header: () => apiColumn.name,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the display strategy to format the value
|
||||||
|
return displayStrategy(value, row.original)
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
name: apiColumn.name,
|
||||||
|
column: apiColumn, // Store column metadata for future use
|
||||||
|
},
|
||||||
|
enableHiding: apiColumn.hideable,
|
||||||
|
enableSorting: false, // Disable sorting for all columns by default
|
||||||
|
headerAlign, // Pass the header alignment to the DataTable
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
}, [entity, apiColumns, adapter])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultColumnAlignment(column: HttpTypes.AdminViewColumn): "left" | "center" | "right" {
|
||||||
|
// Currency columns should be right-aligned
|
||||||
|
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") ||
|
||||||
|
column.field.includes("price") ||
|
||||||
|
column.field.includes("quantity") ||
|
||||||
|
column.field.includes("count")
|
||||||
|
) {
|
||||||
|
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")) {
|
||||||
|
return "center"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to left alignment
|
||||||
|
return "left"
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { useState, useMemo, useCallback, useEffect } from "react"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { useViewConfigurations, useViewConfiguration } from "../use-view-configurations"
|
||||||
|
import { useEntityColumns } from "../api/views"
|
||||||
|
import { useFeatureFlag } from "../../providers/feature-flag-provider"
|
||||||
|
import { useColumnState } from "./columns/use-column-state"
|
||||||
|
import { useQueryParams } from "../use-query-params"
|
||||||
|
import { calculateRequiredFields } from "../../lib/table/field-utils"
|
||||||
|
|
||||||
|
export interface TableConfiguration {
|
||||||
|
filters: Record<string, any>
|
||||||
|
sorting: { id: string; desc: boolean } | null
|
||||||
|
search: string
|
||||||
|
visible_columns?: string[]
|
||||||
|
column_order?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseTableConfigurationOptions {
|
||||||
|
entity: string
|
||||||
|
pageSize?: number
|
||||||
|
queryPrefix?: string
|
||||||
|
filters?: Array<{ id: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseTableConfigurationReturn {
|
||||||
|
// View configuration
|
||||||
|
activeView: any
|
||||||
|
createView: any
|
||||||
|
updateView: any
|
||||||
|
isViewConfigEnabled: boolean
|
||||||
|
|
||||||
|
// Column state
|
||||||
|
visibleColumns: Record<string, boolean>
|
||||||
|
columnOrder: string[]
|
||||||
|
currentColumns: {
|
||||||
|
visible: string[]
|
||||||
|
order: string[]
|
||||||
|
}
|
||||||
|
setColumnOrder: (order: string[]) => void
|
||||||
|
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||||
|
|
||||||
|
// Configuration state
|
||||||
|
currentConfiguration: TableConfiguration
|
||||||
|
hasConfigurationChanged: boolean
|
||||||
|
handleClearConfiguration: () => void
|
||||||
|
|
||||||
|
// API columns
|
||||||
|
apiColumns: HttpTypes.AdminViewColumn[] | undefined
|
||||||
|
isLoadingColumns: boolean
|
||||||
|
|
||||||
|
// Query params
|
||||||
|
queryParams: Record<string, any>
|
||||||
|
|
||||||
|
// Required fields for API calls
|
||||||
|
requiredFields: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSortingState(value: string) {
|
||||||
|
return value.startsWith("-")
|
||||||
|
? { id: value.slice(1), desc: true }
|
||||||
|
: { id: value, desc: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// Column state
|
||||||
|
const {
|
||||||
|
visibleColumns,
|
||||||
|
columnOrder,
|
||||||
|
currentColumns,
|
||||||
|
setColumnOrder,
|
||||||
|
handleColumnVisibilityChange,
|
||||||
|
handleViewChange: originalHandleViewChange,
|
||||||
|
} = useColumnState(apiColumns, currentActiveView)
|
||||||
|
|
||||||
|
// Sync view configuration with URL and column state
|
||||||
|
useEffect(() => {
|
||||||
|
if (!apiColumns) return
|
||||||
|
originalHandleViewChange(currentActiveView, apiColumns)
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
// Clear existing query params
|
||||||
|
const keysToDelete = Array.from(prev.keys()).filter(key =>
|
||||||
|
key.startsWith(queryPrefix + "_") || key === queryPrefix + "_q" || key === queryPrefix + "_order"
|
||||||
|
)
|
||||||
|
keysToDelete.forEach(key => prev.delete(key))
|
||||||
|
|
||||||
|
// Apply view configuration
|
||||||
|
if (currentActiveView) {
|
||||||
|
const viewConfig = currentActiveView.configuration
|
||||||
|
|
||||||
|
if (viewConfig.filters) {
|
||||||
|
Object.entries(viewConfig.filters).forEach(([key, value]) => {
|
||||||
|
prev.set(`${queryPrefix}_${key}`, JSON.stringify(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewConfig.sorting) {
|
||||||
|
const sortValue = viewConfig.sorting.desc
|
||||||
|
? `-${viewConfig.sorting.id}`
|
||||||
|
: viewConfig.sorting.id
|
||||||
|
prev.set(`${queryPrefix}_order`, sortValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewConfig.search) {
|
||||||
|
prev.set(`${queryPrefix}_q`, viewConfig.search)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}, [currentActiveView, apiColumns])
|
||||||
|
|
||||||
|
// Current configuration from URL
|
||||||
|
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])
|
||||||
|
|
||||||
|
// Check if configuration has changed from view
|
||||||
|
const [debouncedHasConfigChanged, setDebouncedHasConfigChanged] = useState(false)
|
||||||
|
|
||||||
|
const hasConfigurationChanged = useMemo(() => {
|
||||||
|
const currentFilters = currentConfiguration.filters
|
||||||
|
const currentSorting = currentConfiguration.sorting
|
||||||
|
const currentSearch = currentConfiguration.search
|
||||||
|
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 || []
|
||||||
|
|
||||||
|
// Check filters
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check sorting
|
||||||
|
const normalizedCurrentSorting = currentSorting || undefined
|
||||||
|
const normalizedViewSorting = viewSorting || undefined
|
||||||
|
if (JSON.stringify(normalizedCurrentSorting) !== JSON.stringify(normalizedViewSorting)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check search
|
||||||
|
if (currentSearch !== viewSearch) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check visible columns
|
||||||
|
if (JSON.stringify(currentVisibleColumns) !== JSON.stringify(viewVisibleColumns)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check column order
|
||||||
|
if (JSON.stringify(columnOrder) !== JSON.stringify(viewColumnOrder)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check against defaults
|
||||||
|
if (Object.keys(currentFilters).length > 0) return true
|
||||||
|
if (currentSorting !== null) return true
|
||||||
|
if (currentSearch !== "") return true
|
||||||
|
|
||||||
|
if (apiColumns) {
|
||||||
|
const currentVisibleSet = new Set(currentVisibleColumns)
|
||||||
|
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, currentConfiguration, apiColumns])
|
||||||
|
|
||||||
|
// Debounce configuration change detection
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedHasConfigChanged(hasConfigurationChanged)
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [hasConfigurationChanged])
|
||||||
|
|
||||||
|
// Clear configuration handler
|
||||||
|
const handleClearConfiguration = useCallback(() => {
|
||||||
|
if (apiColumns) {
|
||||||
|
originalHandleViewChange(currentActiveView, apiColumns)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const keysToDelete = Array.from(prev.keys()).filter(key =>
|
||||||
|
key.startsWith(queryPrefix + "_") || key === queryPrefix + "_q" || key === queryPrefix + "_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(`${queryPrefix}_${key}`, JSON.stringify(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewConfig.sorting) {
|
||||||
|
const sortValue = viewConfig.sorting.desc
|
||||||
|
? `-${viewConfig.sorting.id}`
|
||||||
|
: viewConfig.sorting.id
|
||||||
|
prev.set(`${queryPrefix}_order`, sortValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewConfig.search) {
|
||||||
|
prev.set(`${queryPrefix}_q`, viewConfig.search)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}, [currentActiveView, apiColumns, queryPrefix])
|
||||||
|
|
||||||
|
// Calculate required fields based on visible columns
|
||||||
|
const requiredFields = useMemo(() => {
|
||||||
|
return calculateRequiredFields(entity, apiColumns, visibleColumns)
|
||||||
|
}, [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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11505,6 +11505,70 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"views": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"save": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"saveAsNew": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updateDefaultForEveryone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updateViewName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"updateDefault": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confirmText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cancelText": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "description", "confirmText", "cancelText"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"updateView": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"confirmText": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cancelText": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "description", "confirmText", "cancelText"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["updateDefault", "updateView"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["save", "saveAsNew", "updateDefaultForEveryone", "updateViewName", "prompts"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"dateTime": {
|
"dateTime": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -11623,6 +11687,7 @@
|
|||||||
"statuses",
|
"statuses",
|
||||||
"labels",
|
"labels",
|
||||||
"fields",
|
"fields",
|
||||||
|
"views",
|
||||||
"dateTime"
|
"dateTime"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -3098,5 +3098,25 @@
|
|||||||
"minutes_other": "Minutes",
|
"minutes_other": "Minutes",
|
||||||
"seconds_one": "Second",
|
"seconds_one": "Second",
|
||||||
"seconds_other": "Seconds"
|
"seconds_other": "Seconds"
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"save": "Save",
|
||||||
|
"saveAsNew": "Save as new view",
|
||||||
|
"updateDefaultForEveryone": "Update default for everyone",
|
||||||
|
"updateViewName": "Update view",
|
||||||
|
"prompts": {
|
||||||
|
"updateDefault": {
|
||||||
|
"title": "Update default view",
|
||||||
|
"description": "This will update the default view for all users. Are you sure?",
|
||||||
|
"confirmText": "Update for everyone",
|
||||||
|
"cancelText": "Cancel"
|
||||||
|
},
|
||||||
|
"updateView": {
|
||||||
|
"title": "Update view",
|
||||||
|
"description": "Are you sure you want to update \"{{name}}\"?",
|
||||||
|
"confirmText": "Update",
|
||||||
|
"cancelText": "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
||||||
|
|
||||||
|
// Order-specific column adapter
|
||||||
|
export const orderColumnAdapter: ColumnAdapter<HttpTypes.AdminOrder> = {
|
||||||
|
getColumnAlignment: (column) => {
|
||||||
|
// Custom alignment for order columns
|
||||||
|
if (column.field === "display_id") return "center"
|
||||||
|
if (column.semantic_type === "currency") return "right"
|
||||||
|
if (column.semantic_type === "status") return "center"
|
||||||
|
if (column.computed?.type === "country_code") return "center"
|
||||||
|
return "left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product-specific column adapter
|
||||||
|
export const productColumnAdapter: ColumnAdapter<HttpTypes.AdminProduct> = {
|
||||||
|
getColumnAlignment: (column) => {
|
||||||
|
// Custom alignment for product columns
|
||||||
|
if (column.field === "sku") return "center"
|
||||||
|
if (column.field === "stock") return "right"
|
||||||
|
if (column.semantic_type === "currency") return "right"
|
||||||
|
if (column.semantic_type === "status") return "center"
|
||||||
|
return "left"
|
||||||
|
},
|
||||||
|
|
||||||
|
transformCellValue: (value, row, column) => {
|
||||||
|
// Custom transformation for product-specific fields
|
||||||
|
if (column.field === "variants_count") {
|
||||||
|
return `${value || 0} variants`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.field === "status" && value === "draft") {
|
||||||
|
return <span className="text-ui-fg-muted">Draft</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to standard display
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer-specific column adapter
|
||||||
|
export const customerColumnAdapter: ColumnAdapter<HttpTypes.AdminCustomer> = {
|
||||||
|
getColumnAlignment: (column) => {
|
||||||
|
if (column.field === "orders_count") return "right"
|
||||||
|
if (column.semantic_type === "currency") return "right"
|
||||||
|
if (column.semantic_type === "status") return "center"
|
||||||
|
return "left"
|
||||||
|
},
|
||||||
|
|
||||||
|
transformCellValue: (value, row, column) => {
|
||||||
|
// Format customer name
|
||||||
|
if (column.field === "name") {
|
||||||
|
const { first_name, last_name } = row
|
||||||
|
if (first_name || last_name) {
|
||||||
|
return `${first_name || ""} ${last_name || ""}`.trim()
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory-specific column adapter
|
||||||
|
export const inventoryColumnAdapter: ColumnAdapter<HttpTypes.AdminInventoryItem> = {
|
||||||
|
getColumnAlignment: (column) => {
|
||||||
|
if (column.field === "stocked_quantity") return "right"
|
||||||
|
if (column.field === "reserved_quantity") return "right"
|
||||||
|
if (column.field === "available_quantity") return "right"
|
||||||
|
if (column.semantic_type === "status") return "center"
|
||||||
|
return "left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry of entity adapters
|
||||||
|
export const entityAdapters = {
|
||||||
|
orders: orderColumnAdapter,
|
||||||
|
products: productColumnAdapter,
|
||||||
|
customers: customerColumnAdapter,
|
||||||
|
inventory: inventoryColumnAdapter,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type EntityType = keyof typeof entityAdapters
|
||||||
|
|
||||||
|
// Helper function to get adapter for an entity
|
||||||
|
export function getEntityAdapter<TData = any>(entity: string): ColumnAdapter<TData> | undefined {
|
||||||
|
return entityAdapters[entity as EntityType] as ColumnAdapter<TData>
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Default fields configuration for each entity type
|
||||||
|
* These fields are always fetched to ensure basic functionality
|
||||||
|
*/
|
||||||
|
export const ENTITY_DEFAULT_FIELDS = {
|
||||||
|
orders: {
|
||||||
|
properties: [
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
"email",
|
||||||
|
"display_id",
|
||||||
|
"payment_status",
|
||||||
|
"fulfillment_status",
|
||||||
|
"total",
|
||||||
|
"currency_code",
|
||||||
|
],
|
||||||
|
relations: ["*customer", "*sales_channel"]
|
||||||
|
},
|
||||||
|
|
||||||
|
products: {
|
||||||
|
properties: [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"handle",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"thumbnail",
|
||||||
|
],
|
||||||
|
relations: ["*variants", "*categories", "*collections"]
|
||||||
|
},
|
||||||
|
|
||||||
|
customers: {
|
||||||
|
properties: [
|
||||||
|
"id",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"has_account",
|
||||||
|
],
|
||||||
|
relations: ["*groups"]
|
||||||
|
},
|
||||||
|
|
||||||
|
inventory: {
|
||||||
|
properties: [
|
||||||
|
"id",
|
||||||
|
"sku",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"stocked_quantity",
|
||||||
|
"reserved_quantity",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
],
|
||||||
|
relations: ["*location_levels"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default configuration for entities without specific defaults
|
||||||
|
default: {
|
||||||
|
properties: ["id", "created_at", "updated_at"],
|
||||||
|
relations: []
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
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
|
||||||
|
return {
|
||||||
|
properties: config.properties,
|
||||||
|
relations: config.relations,
|
||||||
|
formatted: [...config.properties, ...config.relations].join(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-10
@@ -1,15 +1,20 @@
|
|||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { DEFAULT_FIELDS, DEFAULT_PROPERTIES, DEFAULT_RELATIONS } from "../../../const"
|
import { getEntityDefaultFields } from "./entity-defaults"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the required fields based on visible columns
|
* Calculates the required fields based on visible columns and entity defaults
|
||||||
*/
|
*/
|
||||||
export function calculateRequiredFields(
|
export function calculateRequiredFields(
|
||||||
apiColumns: any[] | undefined,
|
entity: string,
|
||||||
|
apiColumns: HttpTypes.AdminViewColumn[] | undefined,
|
||||||
visibleColumns: Record<string, boolean>
|
visibleColumns: Record<string, boolean>
|
||||||
): string {
|
): string {
|
||||||
|
// Get entity-specific default fields
|
||||||
|
const defaults = getEntityDefaultFields(entity)
|
||||||
|
const defaultFields = defaults.formatted
|
||||||
|
|
||||||
if (!apiColumns?.length) {
|
if (!apiColumns?.length) {
|
||||||
return DEFAULT_FIELDS
|
return defaultFields
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all visible columns
|
// Get all visible columns
|
||||||
@@ -27,8 +32,8 @@ export function calculateRequiredFields(
|
|||||||
visibleColumnObjects.forEach(column => {
|
visibleColumnObjects.forEach(column => {
|
||||||
if (column.computed) {
|
if (column.computed) {
|
||||||
// For computed columns, add all required and optional fields
|
// For computed columns, add all required and optional fields
|
||||||
column.computed.required_fields?.forEach(field => requiredFieldsSet.add(field))
|
column.computed.required_fields?.forEach((field: string) => requiredFieldsSet.add(field))
|
||||||
column.computed.optional_fields?.forEach(field => requiredFieldsSet.add(field))
|
column.computed.optional_fields?.forEach((field: string) => requiredFieldsSet.add(field))
|
||||||
} else if (!column.field.includes('.')) {
|
} else if (!column.field.includes('.')) {
|
||||||
// Direct field
|
// Direct field
|
||||||
requiredFieldsSet.add(column.field)
|
requiredFieldsSet.add(column.field)
|
||||||
@@ -46,7 +51,7 @@ export function calculateRequiredFields(
|
|||||||
// Check which relationship fields need to be added
|
// Check which relationship fields need to be added
|
||||||
const additionalRelationshipFields = visibleRelationshipFields.filter(field => {
|
const additionalRelationshipFields = visibleRelationshipFields.filter(field => {
|
||||||
const [relationName] = field.split('.')
|
const [relationName] = field.split('.')
|
||||||
const isAlreadyCovered = DEFAULT_RELATIONS.some(rel =>
|
const isAlreadyCovered = defaults.relations.some(rel =>
|
||||||
rel === `*${relationName}` || rel === relationName
|
rel === `*${relationName}` || rel === relationName
|
||||||
)
|
)
|
||||||
return !isAlreadyCovered
|
return !isAlreadyCovered
|
||||||
@@ -54,7 +59,7 @@ export function calculateRequiredFields(
|
|||||||
|
|
||||||
// Check which direct fields need to be added
|
// Check which direct fields need to be added
|
||||||
const additionalDirectFields = visibleDirectFields.filter(field => {
|
const additionalDirectFields = visibleDirectFields.filter(field => {
|
||||||
const isAlreadyIncluded = DEFAULT_PROPERTIES.includes(field)
|
const isAlreadyIncluded = defaults.properties.includes(field)
|
||||||
return !isAlreadyIncluded
|
return !isAlreadyIncluded
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -63,8 +68,8 @@ export function calculateRequiredFields(
|
|||||||
|
|
||||||
// Combine default fields with additional needed fields
|
// Combine default fields with additional needed fields
|
||||||
if (additionalFields.length > 0) {
|
if (additionalFields.length > 0) {
|
||||||
return `${DEFAULT_FIELDS},${additionalFields.join(',')}`
|
return `${defaultFields},${additionalFields.join(',')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return DEFAULT_FIELDS
|
return defaultFields
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { DataTableColumnDef, DataTableEmptyStateProps, DataTableFilter } from "@medusajs/ui"
|
||||||
|
import { ColumnAdapter } from "../../hooks/table/columns/use-configurable-table-columns"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter interface for configurable tables.
|
||||||
|
* Defines how to fetch and display data for a specific entity type.
|
||||||
|
*/
|
||||||
|
export interface TableAdapter<TData> {
|
||||||
|
/**
|
||||||
|
* The entity type (e.g., "orders", "products", "customers")
|
||||||
|
*/
|
||||||
|
entity: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch data with the calculated required fields.
|
||||||
|
* Called inside ConfigurableDataTable with the fields and search params.
|
||||||
|
*/
|
||||||
|
useData: (fields: string, params: any) => {
|
||||||
|
data: TData[] | undefined
|
||||||
|
count: number
|
||||||
|
isLoading: boolean
|
||||||
|
isError: boolean
|
||||||
|
error: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract unique ID from a row. Defaults to row.id if not provided.
|
||||||
|
*/
|
||||||
|
getRowId?: (row: TData) => string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate href for row navigation. Return undefined for non-clickable rows.
|
||||||
|
*/
|
||||||
|
getRowHref?: (row: TData) => string | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table filters configuration
|
||||||
|
*/
|
||||||
|
filters?: DataTableFilter[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API columns to table columns.
|
||||||
|
* If not provided, will use default column generation.
|
||||||
|
*/
|
||||||
|
getColumns?: (apiColumns: any[]) => DataTableColumnDef<TData, any>[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column adapter for customizing column behavior (alignment, formatting, etc.)
|
||||||
|
* If not provided, will use entity's default column adapter if available.
|
||||||
|
*/
|
||||||
|
columnAdapter?: ColumnAdapter<TData>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty state configuration
|
||||||
|
*/
|
||||||
|
emptyState?: DataTableEmptyStateProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default page size
|
||||||
|
*/
|
||||||
|
pageSize?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query parameter prefix for URL state management
|
||||||
|
*/
|
||||||
|
queryPrefix?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional entity display name for headings
|
||||||
|
*/
|
||||||
|
entityName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create a type-safe table adapter
|
||||||
|
*/
|
||||||
|
export function createTableAdapter<TData>(
|
||||||
|
adapter: TableAdapter<TData>
|
||||||
|
): TableAdapter<TData> {
|
||||||
|
return {
|
||||||
|
// Provide smart defaults
|
||||||
|
getRowId: (row: any) => row.id,
|
||||||
|
pageSize: 20,
|
||||||
|
queryPrefix: "",
|
||||||
|
...adapter,
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-387
@@ -1,395 +1,16 @@
|
|||||||
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 { useTranslation } from "react-i18next"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { ConfigurableDataTable } from "../../../../../components/table/configurable-data-table"
|
||||||
|
import { useOrderTableAdapter } from "./order-table-adapter"
|
||||||
import { DataTable } from "../../../../../components/data-table"
|
|
||||||
import { useOrders } from "../../../../../hooks/api/orders"
|
|
||||||
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 { 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 = () => {
|
export const ConfigurableOrderListTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
const orderAdapter = useOrderTableAdapter()
|
||||||
|
|
||||||
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: requiredFields,
|
|
||||||
...searchParams,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const columns = useConfigurableOrderTableColumns(apiColumns)
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<ConfigurableDataTable
|
||||||
<DataTable
|
adapter={orderAdapter}
|
||||||
data={orders ?? []}
|
heading={t("orders.domain")}
|
||||||
columns={columns}
|
layout="fill"
|
||||||
filters={filters}
|
/>
|
||||||
getRowId={(row) => row.id}
|
|
||||||
rowCount={count}
|
|
||||||
enablePagination
|
|
||||||
enableSearch
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
isLoading={isLoading || isLoadingColumns}
|
|
||||||
layout="fill"
|
|
||||||
heading={t("orders.domain")}
|
|
||||||
enableColumnVisibility={isViewConfigEnabled}
|
|
||||||
initialColumnVisibility={visibleColumns}
|
|
||||||
onColumnVisibilityChange={handleColumnVisibilityChange}
|
|
||||||
columnOrder={columnOrder}
|
|
||||||
onColumnOrderChange={setColumnOrder}
|
|
||||||
enableViewSelector={isViewConfigEnabled}
|
|
||||||
entity="orders"
|
|
||||||
currentColumns={currentColumns}
|
|
||||||
filterBarContent={filterBarContent}
|
|
||||||
rowHref={(row) => `/orders/${row.id}`}
|
|
||||||
emptyState={{
|
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
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])
|
|
||||||
}
|
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { createTableAdapter, TableAdapter } from "../../../../../lib/table/table-adapters"
|
||||||
|
import { useOrders } from "../../../../../hooks/api/orders"
|
||||||
|
import { useOrderTableFilters } from "./use-order-table-filters"
|
||||||
|
import { orderColumnAdapter } from "../../../../../lib/table/entity-adapters"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the order table adapter with all order-specific logic
|
||||||
|
*/
|
||||||
|
export function createOrderTableAdapter(): TableAdapter<HttpTypes.AdminOrder> {
|
||||||
|
return createTableAdapter<HttpTypes.AdminOrder>({
|
||||||
|
entity: "orders",
|
||||||
|
queryPrefix: "o",
|
||||||
|
pageSize: 20,
|
||||||
|
columnAdapter: orderColumnAdapter,
|
||||||
|
|
||||||
|
useData: (fields, params) => {
|
||||||
|
const { orders, count, isError, error, isLoading } = useOrders(
|
||||||
|
{
|
||||||
|
fields,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: orders,
|
||||||
|
count,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getRowHref: (row) => `/orders/${row.id}`,
|
||||||
|
|
||||||
|
emptyState: {
|
||||||
|
empty: {
|
||||||
|
heading: "No orders found",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get the order table adapter with filters
|
||||||
|
*/
|
||||||
|
export function useOrderTableAdapter(): TableAdapter<HttpTypes.AdminOrder> {
|
||||||
|
const filters = useOrderTableFilters()
|
||||||
|
const adapter = createOrderTableAdapter()
|
||||||
|
|
||||||
|
// Add dynamic filters to the adapter
|
||||||
|
return {
|
||||||
|
...adapter,
|
||||||
|
filters,
|
||||||
|
}
|
||||||
|
}
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -77,4 +77,5 @@ export type AdminViewConfigurationListResponse = PaginatedResponse<{
|
|||||||
view_configurations: AdminViewConfiguration[]
|
view_configurations: AdminViewConfiguration[]
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type AdminViewConfigurationDeleteResponse = DeleteResponse<"view_configuration">
|
export type AdminViewConfigurationDeleteResponse =
|
||||||
|
DeleteResponse<"view_configuration">
|
||||||
Reference in new issue
Block a user