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:
1 parent
f8d8eeace1
commit
c717535ca2
22 files changed
+1677
-326
No files matched your search
@@ -13,7 +13,6 @@ import {
|
|||||||
DataTableFilteringState,
|
DataTableFilteringState,
|
||||||
DataTablePaginationState,
|
DataTablePaginationState,
|
||||||
DataTableSortingState,
|
DataTableSortingState,
|
||||||
clx,
|
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import React, { ReactNode, useCallback, useMemo } from "react"
|
import React, { ReactNode, useCallback, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -93,7 +92,6 @@ interface DataTableProps<TData> {
|
|||||||
onColumnOrderChange?: (order: ColumnOrderState) => void
|
onColumnOrderChange?: (order: ColumnOrderState) => void
|
||||||
enableViewSelector?: boolean
|
enableViewSelector?: boolean
|
||||||
entity?: string
|
entity?: string
|
||||||
onViewChange?: (view: any) => void
|
|
||||||
currentColumns?: {
|
currentColumns?: {
|
||||||
visible: string[]
|
visible: string[]
|
||||||
order: string[]
|
order: string[]
|
||||||
@@ -129,7 +127,6 @@ export const DataTable = <TData,>({
|
|||||||
onColumnOrderChange,
|
onColumnOrderChange,
|
||||||
enableViewSelector = false,
|
enableViewSelector = false,
|
||||||
entity,
|
entity,
|
||||||
onViewChange,
|
|
||||||
currentColumns,
|
currentColumns,
|
||||||
filterBarContent,
|
filterBarContent,
|
||||||
}: DataTableProps<TData>) => {
|
}: DataTableProps<TData>) => {
|
||||||
@@ -378,13 +375,14 @@ export const DataTable = <TData,>({
|
|||||||
{effectiveEnableViewSelector && entity && (
|
{effectiveEnableViewSelector && entity && (
|
||||||
<ViewPills
|
<ViewPills
|
||||||
entity={entity}
|
entity={entity}
|
||||||
onViewChange={onViewChange}
|
|
||||||
currentColumns={currentColumns}
|
currentColumns={currentColumns}
|
||||||
currentConfiguration={currentConfiguration}
|
currentConfiguration={currentConfiguration}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
|
{enableFiltering && <UiDataTable.FilterMenu />}
|
||||||
|
{enableSorting && <UiDataTable.SortingMenu />}
|
||||||
{enableSearch && (
|
{enableSearch && (
|
||||||
<div className="w-full md:w-auto">
|
<div className="w-full md:w-auto">
|
||||||
<UiDataTable.Search
|
<UiDataTable.Search
|
||||||
|
|||||||
+59
-142
@@ -3,19 +3,17 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
Switch,
|
Drawer,
|
||||||
FocusModal,
|
Heading,
|
||||||
Hint,
|
Text,
|
||||||
toast,
|
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { useForm, Controller } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useViewConfigurations, useViewConfiguration } from "../../../hooks/use-view-configurations"
|
import { useViewConfigurations, useViewConfiguration } from "../../../hooks/use-view-configurations"
|
||||||
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
|
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
|
||||||
|
|
||||||
|
|
||||||
type SaveViewFormData = {
|
type SaveViewFormData = {
|
||||||
name: string
|
name: string
|
||||||
isSystemDefault: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SaveViewDialogProps {
|
interface SaveViewDialogProps {
|
||||||
@@ -50,35 +48,14 @@ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
watch,
|
|
||||||
control,
|
|
||||||
} = useForm<SaveViewFormData>({
|
} = useForm<SaveViewFormData>({
|
||||||
mode: "onChange",
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: editingView?.name || "",
|
name: editingView?.name || "",
|
||||||
isSystemDefault: editingView?.is_system_default || false,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isSystemDefault = watch("isSystemDefault")
|
|
||||||
|
|
||||||
const isAdmin = true // TODO: Get from auth context
|
|
||||||
|
|
||||||
const onSubmit = async (data: SaveViewFormData) => {
|
const onSubmit = async (data: SaveViewFormData) => {
|
||||||
// Manual validation for new views
|
if (!data.name.trim()) {
|
||||||
if (!editingView && !data.isSystemDefault && (!data.name || !data.name.trim())) {
|
|
||||||
toast.error("Name is required unless setting as system default")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation for editing views - if converting to system default without a name, that's ok
|
|
||||||
if (editingView && !data.isSystemDefault && !editingView.name && (!data.name || !data.name.trim())) {
|
|
||||||
toast.error("Name is required for personal views")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentColumns && !editingView) {
|
|
||||||
toast.error("No column configuration to save")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,56 +63,30 @@ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
|
|||||||
try {
|
try {
|
||||||
if (editingView) {
|
if (editingView) {
|
||||||
// Update existing view
|
// Update existing view
|
||||||
const updateData: any = {
|
const result = await updateView.mutateAsync({
|
||||||
is_system_default: data.isSystemDefault,
|
name: data.name.trim(),
|
||||||
set_active: true, // Always set updated view as active
|
configuration: {
|
||||||
}
|
visible_columns: currentColumns?.visible || editingView.configuration.visible_columns,
|
||||||
|
column_order: currentColumns?.order || editingView.configuration.column_order,
|
||||||
// Only include name if it was provided and changed (empty string means keep current)
|
filters: currentConfiguration?.filters || editingView.configuration.filters || {},
|
||||||
if (data.name && data.name.trim() !== "" && data.name !== editingView.name) {
|
sorting: currentConfiguration?.sorting || editingView.configuration.sorting || null,
|
||||||
updateData.name = data.name
|
search: currentConfiguration?.search || editingView.configuration.search || "",
|
||||||
}
|
},
|
||||||
|
})
|
||||||
// Only update configuration if currentColumns is provided
|
|
||||||
if (currentColumns) {
|
|
||||||
updateData.configuration = {
|
|
||||||
visible_columns: currentColumns.visible,
|
|
||||||
column_order: currentColumns.order,
|
|
||||||
filters: currentConfiguration?.filters || {},
|
|
||||||
sorting: currentConfiguration?.sorting || null,
|
|
||||||
search: currentConfiguration?.search || "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await updateView.mutateAsync(updateData)
|
|
||||||
onSaved(result.view_configuration)
|
onSaved(result.view_configuration)
|
||||||
} else {
|
} else {
|
||||||
// Create new view
|
// Create new view
|
||||||
if (!currentColumns) {
|
const result = await createView.mutateAsync({
|
||||||
toast.error("No column configuration to save")
|
name: data.name.trim(),
|
||||||
return
|
set_active: true,
|
||||||
}
|
|
||||||
|
|
||||||
// Only include name if provided (not required for system defaults)
|
|
||||||
const createData: any = {
|
|
||||||
entity,
|
|
||||||
is_system_default: data.isSystemDefault,
|
|
||||||
set_active: true, // Always set newly created view as active
|
|
||||||
configuration: {
|
configuration: {
|
||||||
visible_columns: currentColumns.visible,
|
visible_columns: currentColumns?.visible || [],
|
||||||
column_order: currentColumns.order,
|
column_order: currentColumns?.order || [],
|
||||||
filters: currentConfiguration?.filters || {},
|
filters: currentConfiguration?.filters || {},
|
||||||
sorting: currentConfiguration?.sorting || null,
|
sorting: currentConfiguration?.sorting || null,
|
||||||
search: currentConfiguration?.search || "",
|
search: currentConfiguration?.search || "",
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
// Only add name if it's provided and not empty
|
|
||||||
if (data.name && data.name.trim()) {
|
|
||||||
createData.name = data.name.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createView.mutateAsync(createData)
|
|
||||||
onSaved(result.view_configuration)
|
onSaved(result.view_configuration)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -146,101 +97,67 @@ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusModal open onOpenChange={onClose}>
|
<Drawer open onOpenChange={onClose}>
|
||||||
<FocusModal.Content>
|
<Drawer.Content className="flex flex-col">
|
||||||
<FocusModal.Header>
|
<Drawer.Header>
|
||||||
<div className="flex items-center justify-between">
|
<Drawer.Title asChild>
|
||||||
<FocusModal.Title>
|
<Heading>
|
||||||
{editingView ? "Edit View" : "Save View"}
|
{editingView ? "Edit View Name" : "Save as New View"}
|
||||||
</FocusModal.Title>
|
</Heading>
|
||||||
</div>
|
</Drawer.Title>
|
||||||
</FocusModal.Header>
|
<Drawer.Description asChild>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} noValidate>
|
<Text>
|
||||||
<FocusModal.Body className="flex flex-col gap-y-4">
|
{editingView
|
||||||
|
? "Change the name of your saved view"
|
||||||
|
: "Save your current configuration as a new view"}
|
||||||
|
</Text>
|
||||||
|
</Drawer.Description>
|
||||||
|
</Drawer.Header>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-1 flex-col">
|
||||||
|
<Drawer.Body className="flex-1">
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<Label htmlFor="name" weight="plus">
|
<Label htmlFor="name" weight="plus">
|
||||||
View Name {(editingView || isSystemDefault) && <span className="text-ui-fg-muted font-normal">(optional)</span>}
|
View Name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
{...register("name", {
|
||||||
{...register("name")}
|
required: "Name is required",
|
||||||
placeholder={
|
validate: value => value.trim().length > 0 || "Name cannot be empty"
|
||||||
editingView
|
})}
|
||||||
? editingView.name
|
type="text"
|
||||||
: isSystemDefault
|
placeholder="Enter view name"
|
||||||
? "Leave empty for no name"
|
autoFocus
|
||||||
: "e.g., My Custom View"
|
|
||||||
}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
{errors.name && (
|
||||||
|
<span className="text-sm text-ui-fg-error">
|
||||||
{isAdmin && (
|
{errors.name.message}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex flex-col gap-y-1">
|
|
||||||
<Label htmlFor="isSystemDefault" weight="plus">
|
|
||||||
Set as System Default
|
|
||||||
</Label>
|
|
||||||
<Hint>
|
|
||||||
This view will be the default for all users
|
|
||||||
</Hint>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="isSystemDefault"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Switch
|
|
||||||
id="isSystemDefault"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editingView && (
|
|
||||||
<div className="rounded-md bg-ui-bg-subtle p-3">
|
|
||||||
<p className="text-ui-fg-subtle text-sm">
|
|
||||||
You are editing the view "{editingView.name}".
|
|
||||||
{editingView.is_system_default && (
|
|
||||||
<span className="block mt-1 text-ui-fg-warning">
|
|
||||||
This is a system default view.
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Drawer.Body>
|
||||||
|
|
||||||
{!isAdmin && isSystemDefault && (
|
<Drawer.Footer>
|
||||||
<Hint variant="error">
|
<Drawer.Close asChild>
|
||||||
Only administrators can create system default views
|
|
||||||
</Hint>
|
|
||||||
)}
|
|
||||||
</FocusModal.Body>
|
|
||||||
<FocusModal.Footer>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
</Drawer.Close>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={!isAdmin && isSystemDefault}
|
|
||||||
>
|
>
|
||||||
{editingView ? "Update" : "Save"} View
|
{editingView ? "Update" : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Drawer.Footer>
|
||||||
</FocusModal.Footer>
|
|
||||||
</form>
|
</form>
|
||||||
</FocusModal.Content>
|
</Drawer.Content>
|
||||||
</FocusModal>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ import { SaveViewDialog } from "../save-view-dialog"
|
|||||||
|
|
||||||
interface ViewPillsProps {
|
interface ViewPillsProps {
|
||||||
entity: string
|
entity: string
|
||||||
onViewChange?: (view: ViewConfiguration | null) => void
|
|
||||||
currentColumns?: {
|
currentColumns?: {
|
||||||
visible: string[]
|
visible: string[]
|
||||||
order: string[]
|
order: string[]
|
||||||
@@ -30,7 +29,6 @@ interface ViewPillsProps {
|
|||||||
|
|
||||||
export const ViewPills: React.FC<ViewPillsProps> = ({
|
export const ViewPills: React.FC<ViewPillsProps> = ({
|
||||||
entity,
|
entity,
|
||||||
onViewChange,
|
|
||||||
currentColumns,
|
currentColumns,
|
||||||
currentConfiguration,
|
currentConfiguration,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -41,7 +39,7 @@ export const ViewPills: React.FC<ViewPillsProps> = ({
|
|||||||
isDefaultViewActive,
|
isDefaultViewActive,
|
||||||
} = useViewConfigurations(entity)
|
} = useViewConfigurations(entity)
|
||||||
|
|
||||||
const views = listViews.data?.view_configurations || []
|
const views = listViews?.view_configurations || []
|
||||||
|
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||||
const [editingView, setEditingView] = useState<ViewConfiguration | null>(null)
|
const [editingView, setEditingView] = useState<ViewConfiguration | null>(null)
|
||||||
@@ -50,43 +48,25 @@ export const ViewPills: React.FC<ViewPillsProps> = ({
|
|||||||
const [deletingViewId, setDeletingViewId] = useState<string | null>(null)
|
const [deletingViewId, setDeletingViewId] = useState<string | null>(null)
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const currentActiveView = activeView.data?.view_configuration || null
|
const currentActiveView = activeView?.view_configuration || null
|
||||||
|
|
||||||
// Track if we've notified parent of initial view
|
|
||||||
const hasNotifiedInitialView = useRef(false)
|
|
||||||
|
|
||||||
// Get delete mutation for the current deleting view
|
// Get delete mutation for the current deleting view
|
||||||
const { deleteView } = useViewConfiguration(entity, deletingViewId || '')
|
const { deleteView } = useViewConfiguration(entity, deletingViewId || '')
|
||||||
|
|
||||||
// Notify parent of initial view once
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasNotifiedInitialView.current && activeView.isSuccess) {
|
|
||||||
hasNotifiedInitialView.current = true
|
|
||||||
// Use setTimeout to ensure this happens after render
|
|
||||||
setTimeout(() => {
|
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange(currentActiveView)
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}, [activeView.isSuccess, currentActiveView]) // Remove onViewChange from dependencies
|
|
||||||
|
|
||||||
const handleViewSelect = async (viewId: string | null) => {
|
const handleViewSelect = async (viewId: string | null) => {
|
||||||
|
try {
|
||||||
if (viewId === null) {
|
if (viewId === null) {
|
||||||
// Select default view - clear the active view
|
// Select default view - clear the active view
|
||||||
await setActiveView.mutateAsync(null)
|
await setActiveView.mutateAsync(null)
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange(null)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = views.find(v => v.id === viewId)
|
const view = views.find(v => v.id === viewId)
|
||||||
if (view) {
|
if (view) {
|
||||||
await setActiveView.mutateAsync(viewId)
|
await setActiveView.mutateAsync(viewId)
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange(view)
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in handleViewSelect:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,18 +88,13 @@ export const ViewPills: React.FC<ViewPillsProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deletingViewId && deleteView.mutateAsync) {
|
if (deletingViewId && deleteView.mutateAsync) {
|
||||||
deleteView.mutateAsync().then(() => {
|
deleteView.mutateAsync().then(() => {
|
||||||
if (currentActiveView?.id === deletingViewId) {
|
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setDeletingViewId(null)
|
setDeletingViewId(null)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setDeletingViewId(null)
|
setDeletingViewId(null)
|
||||||
// Error is handled by the hook
|
// Error is handled by the hook
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [deletingViewId, deleteView.mutateAsync, currentActiveView?.id, onViewChange])
|
}, [deletingViewId, deleteView.mutateAsync])
|
||||||
|
|
||||||
const handleEditView = (view: ViewConfiguration) => {
|
const handleEditView = (view: ViewConfiguration) => {
|
||||||
setEditingView(view)
|
setEditingView(view)
|
||||||
@@ -282,10 +257,6 @@ export const ViewPills: React.FC<ViewPillsProps> = ({
|
|||||||
setEditingView(null)
|
setEditingView(null)
|
||||||
toast.success(`View "${newView.name}" saved successfully`)
|
toast.success(`View "${newView.name}" saved successfully`)
|
||||||
// The view is already set as active in SaveViewDialog
|
// The view is already set as active in SaveViewDialog
|
||||||
// Notify parent of the new active view
|
|
||||||
if (onViewChange) {
|
|
||||||
onViewChange(newView)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,10 +10,28 @@ import {
|
|||||||
|
|
||||||
import { sdk } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory, TQueryKey } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
const VIEWS_QUERY_KEY = "views" as const
|
const VIEWS_QUERY_KEY = "views" as const
|
||||||
export const viewsQueryKeys = queryKeysFactory(VIEWS_QUERY_KEY)
|
const _viewsKeys = queryKeysFactory(VIEWS_QUERY_KEY) as TQueryKey<"views"> & {
|
||||||
|
columns: (entity: string) => any
|
||||||
|
active: (entity: string) => any
|
||||||
|
configurations: (entity: string, query?: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewsKeys.columns = function(entity: string) {
|
||||||
|
return [this.all, "columns", entity]
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewsKeys.active = function(entity: string) {
|
||||||
|
return [this.detail(entity), "active"]
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewsKeys.configurations = function(entity: string, query?: any) {
|
||||||
|
return [this.all, "configurations", entity, query]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viewsQueryKeys = _viewsKeys
|
||||||
|
|
||||||
// Generic hook to get columns for any entity
|
// Generic hook to get columns for any entity
|
||||||
export const useEntityColumns = (entity: string, options?: Omit<
|
export const useEntityColumns = (entity: string, options?: Omit<
|
||||||
@@ -27,7 +45,7 @@ export const useEntityColumns = (entity: string, options?: Omit<
|
|||||||
>) => {
|
>) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => sdk.admin.views.columns(entity),
|
queryFn: () => sdk.admin.views.columns(entity),
|
||||||
queryKey: viewsQueryKeys.list(entity),
|
queryKey: viewsQueryKeys.columns(entity),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,7 +70,7 @@ export const useViewConfigurations = (
|
|||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const { data, ...rest } = useQuery({
|
||||||
queryFn: () => sdk.admin.views.listConfigurations(entity, query),
|
queryFn: () => sdk.admin.views.listConfigurations(entity, query),
|
||||||
queryKey: viewsQueryKeys.list(entity, query),
|
queryKey: viewsQueryKeys.configurations(entity, query),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -80,12 +98,14 @@ export const useActiveViewConfiguration = (
|
|||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { data, ...rest } = useQuery({
|
const query = useQuery({
|
||||||
queryFn: () => sdk.admin.views.retrieveActiveConfiguration(entity),
|
queryFn: () => sdk.admin.views.retrieveActiveConfiguration(entity),
|
||||||
queryKey: [viewsQueryKeys.detail(entity), "active"],
|
queryKey: viewsQueryKeys.active(entity),
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data, ...rest } = query
|
||||||
|
|
||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +133,7 @@ export const useViewConfiguration = (
|
|||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create view configuration with toast notifications
|
||||||
export const useCreateViewConfiguration = (
|
export const useCreateViewConfiguration = (
|
||||||
entity: string,
|
entity: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
@@ -122,22 +143,23 @@ export const useCreateViewConfiguration = (
|
|||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: HttpTypes.AdminCreateViewConfiguration) =>
|
mutationFn: (payload: Omit<HttpTypes.AdminCreateViewConfiguration, "entity">) =>
|
||||||
sdk.admin.views.createConfiguration(entity, payload),
|
sdk.admin.views.createConfiguration(entity, payload),
|
||||||
|
...options,
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
||||||
// If set_active was true, also invalidate the active configuration
|
// If set_active was true, also invalidate the active configuration
|
||||||
if ((variables as any).set_active) {
|
if ((variables as any).set_active) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [...viewsQueryKeys.detail(entity, "active")]
|
queryKey: viewsQueryKeys.active(entity)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update view configuration
|
||||||
export const useUpdateViewConfiguration = (
|
export const useUpdateViewConfiguration = (
|
||||||
entity: string,
|
entity: string,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -150,12 +172,12 @@ export const useUpdateViewConfiguration = (
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: HttpTypes.AdminUpdateViewConfiguration) =>
|
mutationFn: (payload: HttpTypes.AdminUpdateViewConfiguration) =>
|
||||||
sdk.admin.views.updateConfiguration(entity, id, payload),
|
sdk.admin.views.updateConfiguration(entity, id, payload),
|
||||||
|
...options,
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +193,20 @@ export const useDeleteViewConfiguration = (
|
|||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => sdk.admin.views.deleteConfiguration(entity, id),
|
mutationFn: () => sdk.admin.views.deleteConfiguration(entity, id),
|
||||||
|
...options,
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
|
||||||
// Also invalidate active configuration as it might have changed
|
// Also invalidate active configuration as it might have changed
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [...viewsQueryKeys.detail(entity, "active")]
|
queryKey: viewsQueryKeys.active(entity)
|
||||||
})
|
})
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set active view configuration
|
||||||
export const useSetActiveViewConfiguration = (
|
export const useSetActiveViewConfiguration = (
|
||||||
entity: string,
|
entity: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
@@ -193,17 +216,23 @@ export const useSetActiveViewConfiguration = (
|
|||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (viewConfigurationId: string | null) =>
|
mutationFn: (viewConfigurationId: string | null) => {
|
||||||
sdk.admin.views.setActiveConfiguration(entity, {
|
return sdk.admin.views.setActiveConfiguration(entity, {
|
||||||
view_configuration_id: viewConfigurationId
|
view_configuration_id: viewConfigurationId
|
||||||
}),
|
|
||||||
onSuccess: (data, variables, context) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: [...viewsQueryKeys.detail(entity, "active")]
|
|
||||||
})
|
})
|
||||||
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
onSuccess: async (data, variables, context) => {
|
||||||
|
// Invalidate active configuration
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: viewsQueryKeys.active(entity)
|
||||||
|
})
|
||||||
|
// Also invalidate the list as the active status might be shown there
|
||||||
|
await queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) })
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
onError: (error, variables, context) => {
|
||||||
|
options?.onError?.(error, variables, context)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
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"
|
import type { ViewConfiguration } from "../../../hooks/use-view-configurations"
|
||||||
|
|
||||||
|
interface ColumnState {
|
||||||
|
visibility: Record<string, boolean>
|
||||||
|
order: string[]
|
||||||
|
}
|
||||||
|
|
||||||
interface UseColumnStateReturn {
|
interface UseColumnStateReturn {
|
||||||
visibleColumns: Record<string, boolean>
|
visibleColumns: Record<string, boolean>
|
||||||
columnOrder: string[]
|
columnOrder: string[]
|
||||||
|
columnState: ColumnState
|
||||||
currentColumns: {
|
currentColumns: {
|
||||||
visible: string[]
|
visible: string[]
|
||||||
order: string[]
|
order: string[]
|
||||||
@@ -12,7 +18,10 @@ interface UseColumnStateReturn {
|
|||||||
setVisibleColumns: (visibility: Record<string, boolean>) => void
|
setVisibleColumns: (visibility: Record<string, boolean>) => void
|
||||||
setColumnOrder: (order: string[]) => void
|
setColumnOrder: (order: string[]) => void
|
||||||
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
handleColumnVisibilityChange: (visibility: Record<string, boolean>) => void
|
||||||
handleViewChange: (view: ViewConfiguration | null, apiColumns: HttpTypes.AdminViewColumn[]) => void
|
handleViewChange: (
|
||||||
|
view: ViewConfiguration | null,
|
||||||
|
apiColumns: HttpTypes.AdminViewColumn[]
|
||||||
|
) => void
|
||||||
initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void
|
initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,30 +30,42 @@ export function useColumnState(
|
|||||||
activeView?: ViewConfiguration | null
|
activeView?: ViewConfiguration | null
|
||||||
): UseColumnStateReturn {
|
): UseColumnStateReturn {
|
||||||
// Initialize state lazily to avoid unnecessary re-renders
|
// Initialize state lazily to avoid unnecessary re-renders
|
||||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(() => {
|
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||||
if (apiColumns?.length && activeView) {
|
() => {
|
||||||
|
if (apiColumns?.length && activeView?.configuration) {
|
||||||
// If there's an active view, initialize with its configuration
|
// If there's an active view, initialize with its configuration
|
||||||
const visibility: Record<string, boolean> = {}
|
const visibility: Record<string, boolean> = {}
|
||||||
apiColumns.forEach(column => {
|
apiColumns.forEach((column) => {
|
||||||
visibility[column.field] = activeView.configuration.visible_columns.includes(column.field)
|
visibility[column.field] =
|
||||||
|
activeView.configuration.visible_columns?.includes(column.field) ||
|
||||||
|
false
|
||||||
})
|
})
|
||||||
return visibility
|
return visibility
|
||||||
} else if (apiColumns?.length) {
|
} else if (apiColumns?.length) {
|
||||||
return getInitialColumnVisibility(apiColumns)
|
return getInitialColumnVisibility(apiColumns)
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
|
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
|
||||||
if (activeView) {
|
if (activeView?.configuration?.column_order) {
|
||||||
// If there's an active view, use its column order
|
// If there's an active view, use its column order
|
||||||
return activeView.configuration.column_order || []
|
return activeView.configuration.column_order
|
||||||
} else if (apiColumns?.length) {
|
} else if (apiColumns?.length) {
|
||||||
return getInitialColumnOrder(apiColumns)
|
return getInitialColumnOrder(apiColumns)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const columnState = useMemo<ColumnState>(
|
||||||
|
() => ({
|
||||||
|
visibility: visibleColumns,
|
||||||
|
order: columnOrder,
|
||||||
|
}),
|
||||||
|
[visibleColumns, columnOrder]
|
||||||
|
)
|
||||||
|
|
||||||
const currentColumns = useMemo(() => {
|
const currentColumns = useMemo(() => {
|
||||||
const visible = Object.entries(visibleColumns)
|
const visible = Object.entries(visibleColumns)
|
||||||
.filter(([_, isVisible]) => isVisible)
|
.filter(([_, isVisible]) => isVisible)
|
||||||
@@ -56,30 +77,38 @@ export function useColumnState(
|
|||||||
}
|
}
|
||||||
}, [visibleColumns, columnOrder])
|
}, [visibleColumns, columnOrder])
|
||||||
|
|
||||||
const handleColumnVisibilityChange = useCallback((visibility: Record<string, boolean>) => {
|
const handleColumnVisibilityChange = useCallback(
|
||||||
|
(visibility: Record<string, boolean>) => {
|
||||||
setVisibleColumns(visibility)
|
setVisibleColumns(visibility)
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const handleViewChange = useCallback((
|
const handleViewChange = useCallback(
|
||||||
|
(
|
||||||
view: ViewConfiguration | null,
|
view: ViewConfiguration | null,
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminViewColumn[]
|
||||||
) => {
|
) => {
|
||||||
if (view) {
|
if (view?.configuration) {
|
||||||
// Apply view configuration
|
// Apply view configuration
|
||||||
const newVisibility: Record<string, boolean> = {}
|
const newVisibility: Record<string, boolean> = {}
|
||||||
apiColumns.forEach(column => {
|
apiColumns.forEach((column) => {
|
||||||
newVisibility[column.field] = view.configuration.visible_columns.includes(column.field)
|
newVisibility[column.field] =
|
||||||
|
view.configuration.visible_columns?.includes(column.field) || false
|
||||||
})
|
})
|
||||||
setVisibleColumns(newVisibility)
|
setVisibleColumns(newVisibility)
|
||||||
setColumnOrder(view.configuration.column_order)
|
setColumnOrder(view.configuration.column_order || [])
|
||||||
} else {
|
} else {
|
||||||
// Reset to default visibility when no view is selected
|
// Reset to default visibility when no view is selected
|
||||||
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
||||||
setColumnOrder(getInitialColumnOrder(apiColumns))
|
setColumnOrder(getInitialColumnOrder(apiColumns))
|
||||||
}
|
}
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const initializeColumns = useCallback((apiColumns: HttpTypes.AdminViewColumn[]) => {
|
const initializeColumns = useCallback(
|
||||||
|
(apiColumns: HttpTypes.AdminViewColumn[]) => {
|
||||||
// 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))
|
||||||
@@ -87,26 +116,40 @@ export function useColumnState(
|
|||||||
if (columnOrder.length === 0) {
|
if (columnOrder.length === 0) {
|
||||||
setColumnOrder(getInitialColumnOrder(apiColumns))
|
setColumnOrder(getInitialColumnOrder(apiColumns))
|
||||||
}
|
}
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// Track previous active view to detect changes
|
// Track previous active view to detect changes
|
||||||
const prevActiveViewRef = useRef<ViewConfiguration | null | undefined>()
|
const prevActiveViewRef = useRef<ViewConfiguration | null | undefined>()
|
||||||
|
|
||||||
// Sync local state when active view updates (e.g., after saving)
|
// Sync local state when active view changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apiColumns?.length && activeView && prevActiveViewRef.current) {
|
if (apiColumns?.length) {
|
||||||
// Check if the active view has been updated (same ID but different updated_at)
|
// Check if this is a different view or an update to the same view
|
||||||
if (
|
const viewChanged = prevActiveViewRef.current?.id !== activeView?.id
|
||||||
prevActiveViewRef.current.id === activeView.id &&
|
const viewUpdated =
|
||||||
|
activeView &&
|
||||||
|
prevActiveViewRef.current?.id === activeView.id &&
|
||||||
prevActiveViewRef.current.updated_at !== activeView.updated_at
|
prevActiveViewRef.current.updated_at !== activeView.updated_at
|
||||||
) {
|
|
||||||
// Sync local state with the updated view configuration
|
if (viewChanged || viewUpdated) {
|
||||||
|
if (activeView?.configuration) {
|
||||||
|
// Apply the active view's configuration
|
||||||
const newVisibility: Record<string, boolean> = {}
|
const newVisibility: Record<string, boolean> = {}
|
||||||
apiColumns.forEach(column => {
|
apiColumns.forEach((column) => {
|
||||||
newVisibility[column.field] = activeView.configuration.visible_columns.includes(column.field)
|
newVisibility[column.field] =
|
||||||
|
activeView.configuration?.visible_columns?.includes(
|
||||||
|
column.field
|
||||||
|
) || false
|
||||||
})
|
})
|
||||||
setVisibleColumns(newVisibility)
|
setVisibleColumns(newVisibility)
|
||||||
setColumnOrder(activeView.configuration.column_order)
|
setColumnOrder(activeView.configuration?.column_order || [])
|
||||||
|
} else {
|
||||||
|
// No active view - reset to defaults
|
||||||
|
setVisibleColumns(getInitialColumnVisibility(apiColumns))
|
||||||
|
setColumnOrder(getInitialColumnOrder(apiColumns))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +159,7 @@ export function useColumnState(
|
|||||||
return {
|
return {
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
columnOrder,
|
columnOrder,
|
||||||
|
columnState,
|
||||||
currentColumns,
|
currentColumns,
|
||||||
setVisibleColumns,
|
setVisibleColumns,
|
||||||
setColumnOrder,
|
setColumnOrder,
|
||||||
@@ -135,10 +179,14 @@ const DEFAULT_COLUMN_ORDER = 500
|
|||||||
function getInitialColumnVisibility(
|
function getInitialColumnVisibility(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminViewColumn[]
|
||||||
): Record<string, boolean> {
|
): Record<string, boolean> {
|
||||||
|
if (!apiColumns || apiColumns.length === 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const visibility: Record<string, boolean> = {}
|
const visibility: Record<string, boolean> = {}
|
||||||
|
|
||||||
apiColumns.forEach(column => {
|
apiColumns.forEach((column) => {
|
||||||
visibility[column.field] = column.default_visible
|
visibility[column.field] = column.default_visible ?? true
|
||||||
})
|
})
|
||||||
|
|
||||||
return visibility
|
return visibility
|
||||||
@@ -150,11 +198,15 @@ function getInitialColumnVisibility(
|
|||||||
function getInitialColumnOrder(
|
function getInitialColumnOrder(
|
||||||
apiColumns: HttpTypes.AdminViewColumn[]
|
apiColumns: HttpTypes.AdminViewColumn[]
|
||||||
): string[] {
|
): string[] {
|
||||||
|
if (!apiColumns || apiColumns.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const sortedColumns = [...apiColumns].sort((a, b) => {
|
const sortedColumns = [...apiColumns].sort((a, b) => {
|
||||||
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
const orderA = a.default_order ?? DEFAULT_COLUMN_ORDER
|
||||||
const orderB = b.default_order ?? DEFAULT_COLUMN_ORDER
|
const orderB = b.default_order ?? DEFAULT_COLUMN_ORDER
|
||||||
return orderA - orderB
|
return orderA - orderB
|
||||||
})
|
})
|
||||||
|
|
||||||
return sortedColumns.map(col => col.field)
|
return sortedColumns.map((col) => col.field)
|
||||||
}
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
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,234 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DateCell,
|
||||||
|
DateHeader,
|
||||||
|
} from "../../../components/table/table-cells/common/date-cell"
|
||||||
|
import { CountryCell } from "../../../components/table/table-cells/order/country-cell"
|
||||||
|
import {
|
||||||
|
CustomerCell,
|
||||||
|
CustomerHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/customer-cell"
|
||||||
|
import {
|
||||||
|
DisplayIdCell,
|
||||||
|
DisplayIdHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/display-id-cell"
|
||||||
|
import {
|
||||||
|
FulfillmentStatusCell,
|
||||||
|
FulfillmentStatusHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/fulfillment-status-cell"
|
||||||
|
import {
|
||||||
|
PaymentStatusCell,
|
||||||
|
PaymentStatusHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/payment-status-cell"
|
||||||
|
import {
|
||||||
|
SalesChannelCell,
|
||||||
|
SalesChannelHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/sales-channel-cell"
|
||||||
|
import {
|
||||||
|
TotalCell,
|
||||||
|
TotalHeader,
|
||||||
|
} from "../../../components/table/table-cells/order/total-cell"
|
||||||
|
import { TextCell, TextHeader } from "../../../components/table/table-cells/common/text-cell"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<HttpTypes.AdminOrder>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to build columns dynamically based on API columns response
|
||||||
|
*/
|
||||||
|
export const useOrderDataTableColumns = (
|
||||||
|
apiColumns: HttpTypes.AdminOrderColumn[] | undefined,
|
||||||
|
visibleColumns: string[]
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!apiColumns || apiColumns.length === 0) {
|
||||||
|
// Return default columns if no API columns
|
||||||
|
return [
|
||||||
|
columnHelper.accessor("display_id", {
|
||||||
|
header: () => <DisplayIdHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const id = getValue()
|
||||||
|
return <DisplayIdCell displayId={id!} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("created_at", {
|
||||||
|
header: () => <DateHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const date = new Date(getValue())
|
||||||
|
return <DateCell date={date} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("customer", {
|
||||||
|
header: () => <CustomerHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const customer = getValue()
|
||||||
|
return <CustomerCell customer={customer} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("sales_channel", {
|
||||||
|
header: () => <SalesChannelHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const channel = getValue()
|
||||||
|
return <SalesChannelCell channel={channel} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("payment_status", {
|
||||||
|
header: () => <PaymentStatusHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const status = getValue()
|
||||||
|
return <PaymentStatusCell status={status} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("fulfillment_status", {
|
||||||
|
header: () => <FulfillmentStatusHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const status = getValue()
|
||||||
|
return <FulfillmentStatusCell status={status} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("total", {
|
||||||
|
header: () => <TotalHeader />,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const total = getValue()
|
||||||
|
const currencyCode = row.original.currency_code
|
||||||
|
return <TotalCell currencyCode={currencyCode} total={total} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "country",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const country = row.original.shipping_address?.country
|
||||||
|
return <CountryCell country={country} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build columns from API response
|
||||||
|
return apiColumns
|
||||||
|
.filter((col) => visibleColumns.includes(col.id))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aIndex = visibleColumns.indexOf(a.id)
|
||||||
|
const bIndex = visibleColumns.indexOf(b.id)
|
||||||
|
return aIndex - bIndex
|
||||||
|
})
|
||||||
|
.map((col) => {
|
||||||
|
// Handle special columns with custom cells
|
||||||
|
switch (col.id) {
|
||||||
|
case "display_id":
|
||||||
|
return columnHelper.accessor("display_id", {
|
||||||
|
header: () => <DisplayIdHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const id = getValue()
|
||||||
|
return <DisplayIdCell displayId={id!} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "created_at":
|
||||||
|
case "updated_at":
|
||||||
|
return columnHelper.accessor(col.field as any, {
|
||||||
|
header: () => <DateHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const date = getValue() ? new Date(getValue() as string) : null
|
||||||
|
return date ? <DateCell date={date} /> : null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "email":
|
||||||
|
return columnHelper.accessor("email", {
|
||||||
|
header: () => <TextHeader text={col.name} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const email = getValue()
|
||||||
|
return <TextCell text={email || ""} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "customer_display":
|
||||||
|
return columnHelper.accessor("customer", {
|
||||||
|
header: () => <CustomerHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const customer = getValue()
|
||||||
|
return <CustomerCell customer={customer} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "sales_channel.name":
|
||||||
|
return columnHelper.accessor("sales_channel", {
|
||||||
|
header: () => <SalesChannelHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const channel = getValue()
|
||||||
|
return <SalesChannelCell channel={channel} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "payment_status":
|
||||||
|
return columnHelper.accessor("payment_status", {
|
||||||
|
header: () => <PaymentStatusHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const status = getValue()
|
||||||
|
return <PaymentStatusCell status={status} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "fulfillment_status":
|
||||||
|
return columnHelper.accessor("fulfillment_status", {
|
||||||
|
header: () => <FulfillmentStatusHeader />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const status = getValue()
|
||||||
|
return <FulfillmentStatusCell status={status} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "total":
|
||||||
|
return columnHelper.accessor("total", {
|
||||||
|
header: () => <TotalHeader />,
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const total = getValue()
|
||||||
|
const currencyCode = row.original.currency_code
|
||||||
|
return <TotalCell currencyCode={currencyCode} total={total} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case "country":
|
||||||
|
return columnHelper.display({
|
||||||
|
id: "country",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const country = row.original.shipping_address?.country
|
||||||
|
return <CountryCell country={country} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle relationship fields (e.g., customer.email)
|
||||||
|
if (col.field.includes(".")) {
|
||||||
|
const [relation, field] = col.field.split(".")
|
||||||
|
return columnHelper.accessor((row: any) => {
|
||||||
|
const relationData = row[relation]
|
||||||
|
return relationData?.[field] || ""
|
||||||
|
}, {
|
||||||
|
id: col.id,
|
||||||
|
header: () => <TextHeader text={col.name} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
return <TextCell text={value || ""} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default text column
|
||||||
|
return columnHelper.accessor(col.field as any, {
|
||||||
|
header: () => <TextHeader text={col.name} />,
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
return <TextCell text={value || ""} />
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [apiColumns, visibleColumns, t])
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
import { toast } from "@medusajs/ui"
|
import { toast } from "@medusajs/ui"
|
||||||
import { FetchError } from "@medusajs/js-sdk"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
import { useFeatureFlag } from "../providers/feature-flag-provider"
|
import { useFeatureFlag } from "../providers/feature-flag-provider"
|
||||||
@@ -12,13 +11,8 @@ import {
|
|||||||
useSetActiveViewConfiguration as useSetActiveViewConfigurationBase,
|
useSetActiveViewConfiguration as useSetActiveViewConfigurationBase,
|
||||||
} from "./api/views"
|
} from "./api/views"
|
||||||
|
|
||||||
// Re-export the type for convenience
|
|
||||||
export type ViewConfiguration = HttpTypes.AdminViewConfigurationResponse
|
|
||||||
|
|
||||||
// Common error handler
|
// Common error handler
|
||||||
const handleError = (error: Error, message?: string) => {
|
const handleError = (error: Error, message?: string) => {
|
||||||
console.error("View configuration error:", error)
|
|
||||||
|
|
||||||
let errorMessage = message
|
let errorMessage = message
|
||||||
if (!errorMessage) {
|
if (!errorMessage) {
|
||||||
if (error instanceof FetchError) {
|
if (error instanceof FetchError) {
|
||||||
@@ -36,11 +30,12 @@ const handleError = (error: Error, message?: string) => {
|
|||||||
export const useViewConfigurations = (entity: string) => {
|
export const useViewConfigurations = (entity: string) => {
|
||||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||||
|
|
||||||
|
// List views
|
||||||
const listViews = useViewConfigurationsBase(entity, { limit: 100 }, {
|
const listViews = useViewConfigurationsBase(entity, { limit: 100 }, {
|
||||||
enabled: isViewConfigEnabled && !!entity,
|
enabled: isViewConfigEnabled && !!entity,
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Active view
|
||||||
const activeView = useActiveViewConfigurationBase(entity, {
|
const activeView = useActiveViewConfigurationBase(entity, {
|
||||||
enabled: isViewConfigEnabled && !!entity,
|
enabled: isViewConfigEnabled && !!entity,
|
||||||
})
|
})
|
||||||
@@ -57,7 +52,8 @@ export const useViewConfigurations = (entity: string) => {
|
|||||||
|
|
||||||
// Set active view mutation
|
// Set active view mutation
|
||||||
const setActiveView = useSetActiveViewConfigurationBase(entity, {
|
const setActiveView = useSetActiveViewConfigurationBase(entity, {
|
||||||
onSuccess: () => { },
|
onSuccess: () => {
|
||||||
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
handleError(error, "Failed to update active view")
|
handleError(error, "Failed to update active view")
|
||||||
},
|
},
|
||||||
@@ -69,7 +65,7 @@ export const useViewConfigurations = (entity: string) => {
|
|||||||
activeView,
|
activeView,
|
||||||
createView,
|
createView,
|
||||||
setActiveView,
|
setActiveView,
|
||||||
isDefaultViewActive: activeView.data?.is_default_active ?? true,
|
isDefaultViewActive: activeView?.is_default_active ?? true,
|
||||||
}), [
|
}), [
|
||||||
isViewConfigEnabled,
|
isViewConfigEnabled,
|
||||||
listViews,
|
listViews,
|
||||||
@@ -79,6 +75,7 @@ export const useViewConfigurations = (entity: string) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook for update/delete operations on a specific view
|
||||||
export const useViewConfiguration = (entity: string, viewId: string) => {
|
export const useViewConfiguration = (entity: string, viewId: string) => {
|
||||||
const updateView = useUpdateViewConfigurationBase(entity, viewId, {
|
const updateView = useUpdateViewConfigurationBase(entity, viewId, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -91,7 +88,7 @@ export const useViewConfiguration = (entity: string, viewId: string) => {
|
|||||||
|
|
||||||
const deleteView = useDeleteViewConfigurationBase(entity, viewId, {
|
const deleteView = useDeleteViewConfigurationBase(entity, viewId, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("View deleted")
|
toast.success("View deleted successfully")
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
handleError(error, "Failed to delete view")
|
handleError(error, "Failed to delete view")
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Badge, StatusBadge, Tooltip } from "@medusajs/ui"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import ReactCountryFlag from "react-country-flag"
|
||||||
|
import { getCountryByIso2 } from "./data/countries"
|
||||||
|
import { getStylizedAmount } from "./money-amount-helpers"
|
||||||
|
|
||||||
|
// Helper function to get nested value from object using dot notation
|
||||||
|
const getNestedValue = (obj: any, path: string) => {
|
||||||
|
return path.split('.').reduce((current, key) => current?.[key], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format date
|
||||||
|
const formatDate = (date: string | Date, format: 'short' | 'long' | 'relative' = 'short') => {
|
||||||
|
const dateObj = new Date(date)
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'short':
|
||||||
|
return dateObj.toLocaleDateString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
case 'long':
|
||||||
|
return dateObj.toLocaleDateString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
case 'relative':
|
||||||
|
const now = new Date()
|
||||||
|
const diffInMs = now.getTime() - dateObj.getTime()
|
||||||
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (diffInDays === 0) return 'Today'
|
||||||
|
if (diffInDays === 1) return 'Yesterday'
|
||||||
|
if (diffInDays < 7) return `${diffInDays} days ago`
|
||||||
|
|
||||||
|
return dateObj.toLocaleDateString('en-GB', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short'
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return dateObj.toLocaleDateString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payment status display
|
||||||
|
const PaymentStatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'paid':
|
||||||
|
case 'captured':
|
||||||
|
return 'green'
|
||||||
|
case 'pending':
|
||||||
|
case 'awaiting':
|
||||||
|
return 'orange'
|
||||||
|
case 'failed':
|
||||||
|
case 'canceled':
|
||||||
|
return 'red'
|
||||||
|
default:
|
||||||
|
return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={getStatusColor(status)}>
|
||||||
|
{status}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fulfillment status display
|
||||||
|
const FulfillmentStatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'fulfilled':
|
||||||
|
case 'shipped':
|
||||||
|
return 'green'
|
||||||
|
case 'partially_fulfilled':
|
||||||
|
case 'preparing':
|
||||||
|
return 'orange'
|
||||||
|
case 'canceled':
|
||||||
|
case 'returned':
|
||||||
|
return 'red'
|
||||||
|
case 'pending':
|
||||||
|
case 'not_fulfilled':
|
||||||
|
return 'grey'
|
||||||
|
default:
|
||||||
|
return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={getStatusColor(status)}>
|
||||||
|
{status}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic status badge
|
||||||
|
const GenericStatusBadge = ({ status }: { status: string }) => {
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className="capitalize">
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display strategies registry
|
||||||
|
export const DISPLAY_STRATEGIES = {
|
||||||
|
// Known semantic types with pixel-perfect display
|
||||||
|
status: {
|
||||||
|
payment: (value: any) => <PaymentStatusBadge status={value} />,
|
||||||
|
fulfillment: (value: any) => <FulfillmentStatusBadge status={value} />,
|
||||||
|
default: (value: any) => <GenericStatusBadge status={value} />
|
||||||
|
},
|
||||||
|
|
||||||
|
currency: {
|
||||||
|
default: (value: any, row: any) => {
|
||||||
|
if (value === null || value === undefined) return '-'
|
||||||
|
const currencyCode = row.currency_code || 'USD'
|
||||||
|
const formatted = getStylizedAmount(value, currencyCode)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-end text-right">
|
||||||
|
<span className="truncate">{formatted}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
timestamp: {
|
||||||
|
creation: (value: any) => value ? formatDate(value, 'short') : '-',
|
||||||
|
update: (value: any) => value ? formatDate(value, 'relative') : '-',
|
||||||
|
default: (value: any) => value ? formatDate(value, 'short') : '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
identifier: {
|
||||||
|
order: (value: any) => `#${value}`,
|
||||||
|
default: (value: any) => value
|
||||||
|
},
|
||||||
|
|
||||||
|
email: {
|
||||||
|
default: (value: any) => value || '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generic fallbacks for custom fields
|
||||||
|
enum: {
|
||||||
|
default: (value: any) => <GenericStatusBadge status={value} />
|
||||||
|
},
|
||||||
|
|
||||||
|
// Base type fallbacks
|
||||||
|
string: {
|
||||||
|
default: (value: any) => value || '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
number: {
|
||||||
|
default: (value: any) => value?.toLocaleString() || '0'
|
||||||
|
},
|
||||||
|
|
||||||
|
boolean: {
|
||||||
|
default: (value: any) => (
|
||||||
|
<Badge variant={value ? 'solid' : 'outline'}>
|
||||||
|
{value ? 'Yes' : 'No'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
object: {
|
||||||
|
relationship: (value: any) => {
|
||||||
|
if (!value || typeof value !== 'object') return '-'
|
||||||
|
|
||||||
|
// Try common display fields
|
||||||
|
if (value.name) return value.name
|
||||||
|
if (value.title) return value.title
|
||||||
|
if (value.email) return value.email
|
||||||
|
if (value.display_name) return value.display_name
|
||||||
|
|
||||||
|
return JSON.stringify(value)
|
||||||
|
},
|
||||||
|
default: (value: any) => {
|
||||||
|
if (!value || typeof value !== 'object') return '-'
|
||||||
|
|
||||||
|
// Try common display fields
|
||||||
|
if (value.name) return value.name
|
||||||
|
if (value.title) return value.title
|
||||||
|
if (value.email) return value.email
|
||||||
|
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Date types (in addition to timestamp)
|
||||||
|
date: {
|
||||||
|
default: (value: any) => value ? formatDate(value, 'short') : '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
datetime: {
|
||||||
|
default: (value: any) => value ? formatDate(value, 'long') : '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Computed columns
|
||||||
|
computed: {
|
||||||
|
display: (value: any) => value || '-',
|
||||||
|
default: (value: any) => value || '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy selection function
|
||||||
|
export const getDisplayStrategy = (column: any) => {
|
||||||
|
const semanticStrategies = DISPLAY_STRATEGIES[column.semantic_type as keyof typeof DISPLAY_STRATEGIES]
|
||||||
|
if (semanticStrategies) {
|
||||||
|
const contextStrategy = semanticStrategies[column.context as keyof typeof semanticStrategies]
|
||||||
|
if (contextStrategy) return contextStrategy
|
||||||
|
|
||||||
|
const defaultStrategy = semanticStrategies.default
|
||||||
|
if (defaultStrategy) return defaultStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to data type
|
||||||
|
// Map 'text' data type to 'string' strategy
|
||||||
|
const dataType = column.data_type === 'text' ? 'string' : column.data_type
|
||||||
|
const dataTypeStrategies = DISPLAY_STRATEGIES[dataType as keyof typeof DISPLAY_STRATEGIES]
|
||||||
|
if (dataTypeStrategies) {
|
||||||
|
const defaultStrategy = dataTypeStrategies.default
|
||||||
|
if (defaultStrategy) return defaultStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
return (value: any) => String(value || '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed column computation functions
|
||||||
|
export const COMPUTED_COLUMN_FUNCTIONS = {
|
||||||
|
customer_name: (row: any) => {
|
||||||
|
// Try customer object first
|
||||||
|
if (row.customer?.first_name || row.customer?.last_name) {
|
||||||
|
const fullName = `${row.customer.first_name || ''} ${row.customer.last_name || ''}`.trim()
|
||||||
|
if (fullName) return fullName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to email
|
||||||
|
if (row.customer?.email) {
|
||||||
|
return row.customer.email
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to phone
|
||||||
|
if (row.customer?.phone) {
|
||||||
|
return row.customer.phone
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Guest'
|
||||||
|
},
|
||||||
|
|
||||||
|
address_summary: (row: any, column?: any) => {
|
||||||
|
// Determine which address to use based on the column field
|
||||||
|
let address = null
|
||||||
|
if (column?.field === 'shipping_address_display') {
|
||||||
|
address = row.shipping_address
|
||||||
|
} else if (column?.field === 'billing_address_display') {
|
||||||
|
address = row.billing_address
|
||||||
|
} else {
|
||||||
|
// Fallback to shipping address if no specific field
|
||||||
|
address = row.shipping_address || row.billing_address
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!address) return '-'
|
||||||
|
|
||||||
|
// Build address parts in a meaningful order
|
||||||
|
const parts = []
|
||||||
|
|
||||||
|
// Include street address if available
|
||||||
|
if (address.address_1) {
|
||||||
|
parts.push(address.address_1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// City, Province/State, Postal Code
|
||||||
|
const locationParts = []
|
||||||
|
if (address.city) locationParts.push(address.city)
|
||||||
|
if (address.province) locationParts.push(address.province)
|
||||||
|
if (address.postal_code) locationParts.push(address.postal_code)
|
||||||
|
|
||||||
|
if (locationParts.length > 0) {
|
||||||
|
parts.push(locationParts.join(', '))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Country
|
||||||
|
if (address.country_code) {
|
||||||
|
parts.push(address.country_code.toUpperCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' • ') || '-'
|
||||||
|
},
|
||||||
|
|
||||||
|
country_code: (row: any) => {
|
||||||
|
// Get country code from shipping address
|
||||||
|
const countryCode = row.shipping_address?.country_code
|
||||||
|
|
||||||
|
if (!countryCode) return <div className="flex w-full justify-center">-</div>
|
||||||
|
|
||||||
|
// Get country information
|
||||||
|
const country = getCountryByIso2(countryCode)
|
||||||
|
const displayName = country?.display_name || countryCode.toUpperCase()
|
||||||
|
|
||||||
|
// Display country flag with tooltip - centered in the cell
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
<Tooltip content={displayName}>
|
||||||
|
<div className="flex size-4 items-center justify-center overflow-hidden rounded-sm">
|
||||||
|
<ReactCountryFlag
|
||||||
|
countryCode={countryCode.toUpperCase()}
|
||||||
|
svg
|
||||||
|
style={{
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
}}
|
||||||
|
aria-label={displayName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity-specific column overrides
|
||||||
|
export const ENTITY_COLUMN_OVERRIDES = {
|
||||||
|
orders: {
|
||||||
|
// Override for customer column that combines multiple fields
|
||||||
|
customer: {
|
||||||
|
accessor: (row: any) => {
|
||||||
|
// Complex logic for combining fields
|
||||||
|
const shipping = row.shipping_address
|
||||||
|
const customer = row.customer
|
||||||
|
|
||||||
|
if (shipping?.first_name || shipping?.last_name) {
|
||||||
|
return `${shipping.first_name || ''} ${shipping.last_name || ''}`.trim()
|
||||||
|
}
|
||||||
|
if (customer?.first_name || customer?.last_name) {
|
||||||
|
return `${customer.first_name || ''} ${customer.last_name || ''}`.trim()
|
||||||
|
}
|
||||||
|
return customer?.email || 'Guest'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get entity-specific accessor
|
||||||
|
export const getEntityAccessor = (entity: string, fieldName: string, column?: any) => {
|
||||||
|
// Check if this is a computed column
|
||||||
|
if (column?.computed) {
|
||||||
|
const computationFn = COMPUTED_COLUMN_FUNCTIONS[column.computed.type as keyof typeof COMPUTED_COLUMN_FUNCTIONS]
|
||||||
|
if (computationFn) {
|
||||||
|
// Return a wrapper function that passes the column info
|
||||||
|
return (row: any) => computationFn(row, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityOverrides = ENTITY_COLUMN_OVERRIDES[entity as keyof typeof ENTITY_COLUMN_OVERRIDES]
|
||||||
|
if (entityOverrides) {
|
||||||
|
const fieldOverride = entityOverrides[fieldName as keyof typeof entityOverrides]
|
||||||
|
if (fieldOverride?.accessor) {
|
||||||
|
return fieldOverride.accessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default accessor using dot notation
|
||||||
|
return (row: any) => getNestedValue(row, fieldName)
|
||||||
|
}
|
||||||
+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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+342
-55
@@ -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 { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useState, useEffect } from "react"
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
import { DataTable } from "../../../../../components/data-table"
|
import { DataTable } from "../../../../../components/data-table"
|
||||||
import { useOrders } from "../../../../../hooks/api/orders"
|
import { useOrders } from "../../../../../hooks/api/orders"
|
||||||
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
|
import { useConfigurableOrderTableColumns } from "../../../../../hooks/table/columns/use-configurable-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 { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||||
|
import { useViewConfigurations, useViewConfiguration } from "../../../../../hooks/use-view-configurations"
|
||||||
import { DEFAULT_FIELDS } from "../../const"
|
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 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 [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>({})
|
const {
|
||||||
const [columnOrder, setColumnOrder] = useState<string[]>([])
|
activeView,
|
||||||
|
createView,
|
||||||
|
} = useViewConfigurations("orders")
|
||||||
|
|
||||||
const { searchParams, raw } = useOrderTableQuery({
|
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,
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: QUERY_PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { orders, count, isError, error, isLoading } = useOrders(
|
const { orders, count, isError, error, isLoading } = useOrders(
|
||||||
{
|
{
|
||||||
fields: DEFAULT_FIELDS,
|
fields: requiredFields,
|
||||||
...searchParams,
|
...searchParams,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,38 +336,7 @@ export const ConfigurableOrderListTable = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const filters = useOrderTableFilters()
|
const columns = useConfigurableOrderTableColumns(apiColumns)
|
||||||
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([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
@@ -81,28 +353,43 @@ export const ConfigurableOrderListTable = () => {
|
|||||||
enablePagination
|
enablePagination
|
||||||
enableSearch
|
enableSearch
|
||||||
pageSize={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || isLoadingColumns}
|
||||||
layout="fill"
|
layout="fill"
|
||||||
heading={t("orders.domain")}
|
heading={t("orders.domain")}
|
||||||
enableColumnVisibility={true}
|
enableColumnVisibility={isViewConfigEnabled}
|
||||||
initialColumnVisibility={columnVisibility}
|
initialColumnVisibility={visibleColumns}
|
||||||
onColumnVisibilityChange={setColumnVisibility}
|
onColumnVisibilityChange={handleColumnVisibilityChange}
|
||||||
columnOrder={columnOrder}
|
columnOrder={columnOrder}
|
||||||
onColumnOrderChange={setColumnOrder}
|
onColumnOrderChange={setColumnOrder}
|
||||||
enableViewSelector={true}
|
enableViewSelector={isViewConfigEnabled}
|
||||||
entity="orders"
|
entity="orders"
|
||||||
onViewChange={handleViewChange}
|
currentColumns={currentColumns}
|
||||||
currentColumns={{
|
filterBarContent={filterBarContent}
|
||||||
visible: Object.entries(columnVisibility)
|
|
||||||
.filter(([_, visible]) => visible !== false)
|
|
||||||
.map(([col]) => col),
|
|
||||||
order: columnOrder.length > 0 ? columnOrder : columns.map(c => c.id!).filter(Boolean)
|
|
||||||
}}
|
|
||||||
rowHref={(row) => `/orders/${row.id}`}
|
rowHref={(row) => `/orders/${row.id}`}
|
||||||
emptyState={{
|
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>
|
</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 { _DataTable } from "../../../../../components/table/data-table/data-table"
|
||||||
import { useOrders } from "../../../../../hooks/api/orders"
|
import { useOrders } from "../../../../../hooks/api/orders"
|
||||||
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
|
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 { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
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",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
"created_at",
|
"created_at",
|
||||||
@@ -10,7 +10,7 @@ const DEFAULT_PROPERTIES = [
|
|||||||
"currency_code",
|
"currency_code",
|
||||||
]
|
]
|
||||||
|
|
||||||
const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
|
export const DEFAULT_RELATIONS = ["*customer", "*sales_channel"]
|
||||||
|
|
||||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||||
","
|
","
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
export interface AdminCreateViewConfiguration {
|
export interface AdminCreateViewConfiguration {
|
||||||
/**
|
|
||||||
* The entity this configuration is for (e.g., "order", "product").
|
|
||||||
*/
|
|
||||||
entity: string
|
|
||||||
/**
|
/**
|
||||||
* The name of the view configuration.
|
* The name of the view configuration.
|
||||||
*/
|
*/
|
||||||
name?: string
|
name?: string | null
|
||||||
/**
|
/**
|
||||||
* Whether this is a system default configuration.
|
* Whether this is a system default configuration.
|
||||||
*/
|
*/
|
||||||
@@ -53,7 +49,7 @@ export interface AdminUpdateViewConfiguration {
|
|||||||
/**
|
/**
|
||||||
* The name of the view configuration.
|
* The name of the view configuration.
|
||||||
*/
|
*/
|
||||||
name?: string
|
name?: string | null
|
||||||
/**
|
/**
|
||||||
* Whether this is a system default configuration.
|
* Whether this is a system default configuration.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
DragStartEvent,
|
|
||||||
} from "@dnd-kit/core"
|
} from "@dnd-kit/core"
|
||||||
import {
|
import {
|
||||||
arrayMove,
|
arrayMove,
|
||||||
|
|||||||
@@ -40,23 +40,10 @@ export const GET = async (
|
|||||||
default_type: "code",
|
default_type: "code",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Check if the user has an explicit preference
|
|
||||||
const activeViewPref = await settingsService.getUserPreference(
|
|
||||||
req.auth_context.actor_id,
|
|
||||||
`active_view.${req.params.entity}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// If there's no preference and the view is a system default, it means we're falling back to system default
|
|
||||||
const isDefaultActive =
|
|
||||||
!activeViewPref && viewConfiguration.is_system_default
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
view_configuration: viewConfiguration,
|
view_configuration: viewConfiguration,
|
||||||
is_default_active: isDefaultActive,
|
is_default_active: viewConfiguration.is_system_default,
|
||||||
default_type:
|
default_type: viewConfiguration.is_system_default ? "system" : undefined,
|
||||||
isDefaultActive && viewConfiguration.is_system_default
|
|
||||||
? "system"
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user