From c717535ca20ecf80546f85638670d4d09d561221 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 1 Sep 2025 19:04:18 +0200 Subject: [PATCH] feat(admin): add configurable order views (#13211) Adds support for configurable order views. https://github.com/user-attachments/assets/ed4a5f61-1667-4ed7-9478-423894f3eba6 --- .../src/components/data-table/data-table.tsx | 6 +- .../save-view-dialog/save-view-dialog.tsx | 221 +++------- .../table/view-selector/view-pills.tsx | 59 +-- .../admin/dashboard/src/hooks/api/views.tsx | 75 +++- .../hooks/table/columns/use-column-state.ts | 188 +++++--- .../use-configurable-order-table-columns.tsx | 49 +++ .../columns/use-order-data-table-columns.tsx | 234 ++++++++++ .../src/hooks/use-view-configurations.tsx | 17 +- .../dashboard/src/lib/table-display-utils.tsx | 372 ++++++++++++++++ .../components/save-view-dropdown.tsx | 81 ++++ .../configurable-order-list-table.tsx | 401 +++++++++++++++--- .../components/order-list-table/constants.ts | 14 + .../hooks/use-order-data-table-columns.tsx | 153 +++++++ .../hooks/use-required-fields.ts | 12 + .../order-list-table/order-list-table.tsx | 2 +- .../use-order-table-filters.tsx | 62 +++ .../order-list-table/utils/column-utils.ts | 71 ++++ .../order-list-table/utils/field-utils.ts | 70 +++ .../src/routes/orders/order-list/const.ts | 4 +- .../http/view-configuration/admin/payloads.ts | 10 +- .../components/data-table-table.tsx | 1 - .../[entity]/configurations/active/route.ts | 17 +- 22 files changed, 1735 insertions(+), 384 deletions(-) create mode 100644 packages/admin/dashboard/src/hooks/table/columns/use-configurable-order-table-columns.tsx create mode 100644 packages/admin/dashboard/src/hooks/table/columns/use-order-data-table-columns.tsx create mode 100644 packages/admin/dashboard/src/lib/table-display-utils.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/components/save-view-dropdown.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/constants.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-order-data-table-columns.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-required-fields.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/use-order-table-filters.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/column-utils.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/field-utils.ts diff --git a/packages/admin/dashboard/src/components/data-table/data-table.tsx b/packages/admin/dashboard/src/components/data-table/data-table.tsx index 96751861be..4feb5b508c 100644 --- a/packages/admin/dashboard/src/components/data-table/data-table.tsx +++ b/packages/admin/dashboard/src/components/data-table/data-table.tsx @@ -13,7 +13,6 @@ import { DataTableFilteringState, DataTablePaginationState, DataTableSortingState, - clx, } from "@medusajs/ui" import React, { ReactNode, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" @@ -93,7 +92,6 @@ interface DataTableProps { onColumnOrderChange?: (order: ColumnOrderState) => void enableViewSelector?: boolean entity?: string - onViewChange?: (view: any) => void currentColumns?: { visible: string[] order: string[] @@ -129,7 +127,6 @@ export const DataTable = ({ onColumnOrderChange, enableViewSelector = false, entity, - onViewChange, currentColumns, filterBarContent, }: DataTableProps) => { @@ -378,13 +375,14 @@ export const DataTable = ({ {effectiveEnableViewSelector && entity && ( )}
+ {enableFiltering && } + {enableSorting && } {enableSearch && (
= ({ register, handleSubmit, formState: { errors }, - watch, - control, } = useForm({ - mode: "onChange", defaultValues: { 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) => { - // Manual validation for new views - 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") + if (!data.name.trim()) { return } @@ -86,56 +63,30 @@ export const SaveViewDialog: React.FC = ({ try { if (editingView) { // Update existing view - const updateData: any = { - is_system_default: data.isSystemDefault, - set_active: true, // Always set updated view as active - } - - // Only include name if it was provided and changed (empty string means keep current) - if (data.name && data.name.trim() !== "" && data.name !== editingView.name) { - updateData.name = data.name - } - - // 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) + const result = await updateView.mutateAsync({ + name: data.name.trim(), + configuration: { + visible_columns: currentColumns?.visible || editingView.configuration.visible_columns, + column_order: currentColumns?.order || editingView.configuration.column_order, + filters: currentConfiguration?.filters || editingView.configuration.filters || {}, + sorting: currentConfiguration?.sorting || editingView.configuration.sorting || null, + search: currentConfiguration?.search || editingView.configuration.search || "", + }, + }) onSaved(result.view_configuration) } else { // Create new view - if (!currentColumns) { - toast.error("No column configuration to save") - return - } - - // 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 + const result = await createView.mutateAsync({ + name: data.name.trim(), + set_active: true, configuration: { - visible_columns: currentColumns.visible, - column_order: currentColumns.order, + visible_columns: currentColumns?.visible || [], + column_order: currentColumns?.order || [], filters: currentConfiguration?.filters || {}, sorting: currentConfiguration?.sorting || null, 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) } } catch (error) { @@ -146,101 +97,67 @@ export const SaveViewDialog: React.FC = ({ } return ( - - - -
- - {editingView ? "Edit View" : "Save View"} - -
-
-
- + + + + + + {editingView ? "Edit View Name" : "Save as New View"} + + + + + {editingView + ? "Change the name of your saved view" + : "Save your current configuration as a new view"} + + + + + +
value.trim().length > 0 || "Name cannot be empty" + })} + type="text" + placeholder="Enter view name" + autoFocus /> + {errors.name && ( + + {errors.name.message} + + )}
+
- {isAdmin && ( -
-
- - - This view will be the default for all users - -
- ( - - )} - /> -
- )} - - {editingView && ( -
-

- You are editing the view "{editingView.name}". - {editingView.is_system_default && ( - - This is a system default view. - - )} -

-
- )} - - {!isAdmin && isSystemDefault && ( - - Only administrators can create system default views - - )} -
- -
+ + - -
-
+ + +
-
-
+ + ) -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/components/table/view-selector/view-pills.tsx b/packages/admin/dashboard/src/components/table/view-selector/view-pills.tsx index 2932e97550..ff66cf02cb 100644 --- a/packages/admin/dashboard/src/components/table/view-selector/view-pills.tsx +++ b/packages/admin/dashboard/src/components/table/view-selector/view-pills.tsx @@ -16,7 +16,6 @@ import { SaveViewDialog } from "../save-view-dialog" interface ViewPillsProps { entity: string - onViewChange?: (view: ViewConfiguration | null) => void currentColumns?: { visible: string[] order: string[] @@ -30,7 +29,6 @@ interface ViewPillsProps { export const ViewPills: React.FC = ({ entity, - onViewChange, currentColumns, currentConfiguration, }) => { @@ -40,8 +38,8 @@ export const ViewPills: React.FC = ({ setActiveView, isDefaultViewActive, } = useViewConfigurations(entity) - - const views = listViews.data?.view_configurations || [] + + const views = listViews?.view_configurations || [] const [saveDialogOpen, setSaveDialogOpen] = useState(false) const [editingView, setEditingView] = useState(null) @@ -50,43 +48,25 @@ export const ViewPills: React.FC = ({ const [deletingViewId, setDeletingViewId] = useState(null) const prompt = usePrompt() - const currentActiveView = activeView.data?.view_configuration || null - - // Track if we've notified parent of initial view - const hasNotifiedInitialView = useRef(false) + const currentActiveView = activeView?.view_configuration || null // Get delete mutation for the current deleting view 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) => { - if (viewId === null) { - // Select default view - clear the active view - await setActiveView.mutateAsync(null) - if (onViewChange) { - onViewChange(null) + try { + if (viewId === null) { + // Select default view - clear the active view + await setActiveView.mutateAsync(null) + return } - return - } - const view = views.find(v => v.id === viewId) - if (view) { - await setActiveView.mutateAsync(viewId) - if (onViewChange) { - onViewChange(view) + const view = views.find(v => v.id === viewId) + if (view) { + await setActiveView.mutateAsync(viewId) } + } catch (error) { + console.error("Error in handleViewSelect:", error) } } @@ -108,18 +88,13 @@ export const ViewPills: React.FC = ({ useEffect(() => { if (deletingViewId && deleteView.mutateAsync) { deleteView.mutateAsync().then(() => { - if (currentActiveView?.id === deletingViewId) { - if (onViewChange) { - onViewChange(null) - } - } setDeletingViewId(null) }).catch(() => { setDeletingViewId(null) // Error is handled by the hook }) } - }, [deletingViewId, deleteView.mutateAsync, currentActiveView?.id, onViewChange]) + }, [deletingViewId, deleteView.mutateAsync]) const handleEditView = (view: ViewConfiguration) => { setEditingView(view) @@ -282,13 +257,9 @@ export const ViewPills: React.FC = ({ setEditingView(null) toast.success(`View "${newView.name}" saved successfully`) // The view is already set as active in SaveViewDialog - // Notify parent of the new active view - if (onViewChange) { - onViewChange(newView) - } }} /> )} ) -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/hooks/api/views.tsx b/packages/admin/dashboard/src/hooks/api/views.tsx index 9a82c7585e..62c27e6ed2 100644 --- a/packages/admin/dashboard/src/hooks/api/views.tsx +++ b/packages/admin/dashboard/src/hooks/api/views.tsx @@ -10,10 +10,28 @@ import { import { sdk } from "../../lib/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 -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 export const useEntityColumns = (entity: string, options?: Omit< @@ -27,7 +45,7 @@ export const useEntityColumns = (entity: string, options?: Omit< >) => { const { data, ...rest } = useQuery({ queryFn: () => sdk.admin.views.columns(entity), - queryKey: viewsQueryKeys.list(entity), + queryKey: viewsQueryKeys.columns(entity), ...options, }) @@ -52,7 +70,7 @@ export const useViewConfigurations = ( ) => { const { data, ...rest } = useQuery({ queryFn: () => sdk.admin.views.listConfigurations(entity, query), - queryKey: viewsQueryKeys.list(entity, query), + queryKey: viewsQueryKeys.configurations(entity, query), ...options, }) @@ -80,12 +98,14 @@ export const useActiveViewConfiguration = ( "queryFn" | "queryKey" > ) => { - const { data, ...rest } = useQuery({ + const query = useQuery({ queryFn: () => sdk.admin.views.retrieveActiveConfiguration(entity), - queryKey: [viewsQueryKeys.detail(entity), "active"], + queryKey: viewsQueryKeys.active(entity), ...options, }) + const { data, ...rest } = query + return { ...data, ...rest } } @@ -113,6 +133,7 @@ export const useViewConfiguration = ( return { ...data, ...rest } } +// Create view configuration with toast notifications export const useCreateViewConfiguration = ( entity: string, options?: UseMutationOptions< @@ -122,22 +143,23 @@ export const useCreateViewConfiguration = ( > ) => { return useMutation({ - mutationFn: (payload: HttpTypes.AdminCreateViewConfiguration) => + mutationFn: (payload: Omit) => sdk.admin.views.createConfiguration(entity, payload), + ...options, 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 ((variables as any).set_active) { queryClient.invalidateQueries({ - queryKey: [...viewsQueryKeys.detail(entity, "active")] + queryKey: viewsQueryKeys.active(entity) }) } options?.onSuccess?.(data, variables, context) }, - ...options, }) } +// Update view configuration export const useUpdateViewConfiguration = ( entity: string, id: string, @@ -150,12 +172,12 @@ export const useUpdateViewConfiguration = ( return useMutation({ mutationFn: (payload: HttpTypes.AdminUpdateViewConfiguration) => sdk.admin.views.updateConfiguration(entity, id, payload), + ...options, onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) }) + queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) }) queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) }) options?.onSuccess?.(data, variables, context) }, - ...options, }) } @@ -171,19 +193,20 @@ export const useDeleteViewConfiguration = ( ) => { return useMutation({ mutationFn: () => sdk.admin.views.deleteConfiguration(entity, id), + ...options, onSuccess: (data, variables, context) => { - queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) }) + queryClient.invalidateQueries({ queryKey: viewsQueryKeys.configurations(entity) }) queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) }) // Also invalidate active configuration as it might have changed queryClient.invalidateQueries({ - queryKey: [...viewsQueryKeys.detail(entity, "active")] + queryKey: viewsQueryKeys.active(entity) }) options?.onSuccess?.(data, variables, context) }, - ...options, }) } +// Set active view configuration export const useSetActiveViewConfiguration = ( entity: string, options?: UseMutationOptions< @@ -193,17 +216,23 @@ export const useSetActiveViewConfiguration = ( > ) => { return useMutation({ - mutationFn: (viewConfigurationId: string | null) => - sdk.admin.views.setActiveConfiguration(entity, { + mutationFn: (viewConfigurationId: string | null) => { + return sdk.admin.views.setActiveConfiguration(entity, { 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, + 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) + }, }) } diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-column-state.ts b/packages/admin/dashboard/src/hooks/table/columns/use-column-state.ts index b1bf871513..fd3bb8e23b 100644 --- a/packages/admin/dashboard/src/hooks/table/columns/use-column-state.ts +++ b/packages/admin/dashboard/src/hooks/table/columns/use-column-state.ts @@ -1,10 +1,16 @@ 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" +interface ColumnState { + visibility: Record + order: string[] +} + interface UseColumnStateReturn { visibleColumns: Record columnOrder: string[] + columnState: ColumnState currentColumns: { visible: string[] order: string[] @@ -12,7 +18,10 @@ interface UseColumnStateReturn { setVisibleColumns: (visibility: Record) => void setColumnOrder: (order: string[]) => void handleColumnVisibilityChange: (visibility: Record) => void - handleViewChange: (view: ViewConfiguration | null, apiColumns: HttpTypes.AdminViewColumn[]) => void + handleViewChange: ( + view: ViewConfiguration | null, + apiColumns: HttpTypes.AdminViewColumn[] + ) => void initializeColumns: (apiColumns: HttpTypes.AdminViewColumn[]) => void } @@ -21,30 +30,42 @@ export function useColumnState( activeView?: ViewConfiguration | null ): UseColumnStateReturn { // Initialize state lazily to avoid unnecessary re-renders - const [visibleColumns, setVisibleColumns] = useState>(() => { - if (apiColumns?.length && activeView) { - // If there's an active view, initialize with its configuration - const visibility: Record = {} - apiColumns.forEach(column => { - visibility[column.field] = activeView.configuration.visible_columns.includes(column.field) - }) - return visibility - } else if (apiColumns?.length) { - return getInitialColumnVisibility(apiColumns) + const [visibleColumns, setVisibleColumns] = useState>( + () => { + if (apiColumns?.length && activeView?.configuration) { + // If there's an active view, initialize with its configuration + const visibility: Record = {} + apiColumns.forEach((column) => { + visibility[column.field] = + activeView.configuration.visible_columns?.includes(column.field) || + false + }) + return visibility + } else if (apiColumns?.length) { + return getInitialColumnVisibility(apiColumns) + } + return {} } - return {} - }) + ) const [columnOrder, setColumnOrder] = useState(() => { - if (activeView) { + if (activeView?.configuration?.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) { return getInitialColumnOrder(apiColumns) } return [] }) + const columnState = useMemo( + () => ({ + visibility: visibleColumns, + order: columnOrder, + }), + [visibleColumns, columnOrder] + ) + const currentColumns = useMemo(() => { const visible = Object.entries(visibleColumns) .filter(([_, isVisible]) => isVisible) @@ -56,66 +77,89 @@ export function useColumnState( } }, [visibleColumns, columnOrder]) - const handleColumnVisibilityChange = useCallback((visibility: Record) => { - setVisibleColumns(visibility) - }, []) + const handleColumnVisibilityChange = useCallback( + (visibility: Record) => { + setVisibleColumns(visibility) + }, + [] + ) - const handleViewChange = useCallback(( - view: ViewConfiguration | null, - apiColumns: HttpTypes.AdminViewColumn[] - ) => { - if (view) { - // Apply view configuration - const newVisibility: Record = {} - apiColumns.forEach(column => { - newVisibility[column.field] = view.configuration.visible_columns.includes(column.field) - }) - setVisibleColumns(newVisibility) - setColumnOrder(view.configuration.column_order) - } else { - // Reset to default visibility when no view is selected - setVisibleColumns(getInitialColumnVisibility(apiColumns)) - setColumnOrder(getInitialColumnOrder(apiColumns)) - } - }, []) + const handleViewChange = useCallback( + ( + view: ViewConfiguration | null, + apiColumns: HttpTypes.AdminViewColumn[] + ) => { + if (view?.configuration) { + // Apply view configuration + const newVisibility: Record = {} + apiColumns.forEach((column) => { + newVisibility[column.field] = + view.configuration.visible_columns?.includes(column.field) || false + }) + setVisibleColumns(newVisibility) + setColumnOrder(view.configuration.column_order || []) + } else { + // Reset to default visibility when no view is selected + setVisibleColumns(getInitialColumnVisibility(apiColumns)) + setColumnOrder(getInitialColumnOrder(apiColumns)) + } + }, + [] + ) - const initializeColumns = useCallback((apiColumns: HttpTypes.AdminViewColumn[]) => { - // Only initialize if we don't already have column state - if (Object.keys(visibleColumns).length === 0) { - setVisibleColumns(getInitialColumnVisibility(apiColumns)) - } - if (columnOrder.length === 0) { - setColumnOrder(getInitialColumnOrder(apiColumns)) - } - }, []) + const initializeColumns = useCallback( + (apiColumns: HttpTypes.AdminViewColumn[]) => { + // Only initialize if we don't already have column state + if (Object.keys(visibleColumns).length === 0) { + setVisibleColumns(getInitialColumnVisibility(apiColumns)) + } + if (columnOrder.length === 0) { + setColumnOrder(getInitialColumnOrder(apiColumns)) + } + }, + [] + ) // Track previous active view to detect changes const prevActiveViewRef = useRef() - - // Sync local state when active view updates (e.g., after saving) + + // Sync local state when active view changes useEffect(() => { - if (apiColumns?.length && activeView && prevActiveViewRef.current) { - // Check if the active view has been updated (same ID but different updated_at) - if ( - prevActiveViewRef.current.id === activeView.id && + if (apiColumns?.length) { + // Check if this is a different view or an update to the same view + const viewChanged = prevActiveViewRef.current?.id !== activeView?.id + const viewUpdated = + activeView && + prevActiveViewRef.current?.id === activeView.id && prevActiveViewRef.current.updated_at !== activeView.updated_at - ) { - // Sync local state with the updated view configuration - const newVisibility: Record = {} - apiColumns.forEach(column => { - newVisibility[column.field] = activeView.configuration.visible_columns.includes(column.field) - }) - setVisibleColumns(newVisibility) - setColumnOrder(activeView.configuration.column_order) + + if (viewChanged || viewUpdated) { + if (activeView?.configuration) { + // Apply the active view's configuration + const newVisibility: Record = {} + apiColumns.forEach((column) => { + newVisibility[column.field] = + activeView.configuration?.visible_columns?.includes( + column.field + ) || false + }) + setVisibleColumns(newVisibility) + setColumnOrder(activeView.configuration?.column_order || []) + } else { + // No active view - reset to defaults + setVisibleColumns(getInitialColumnVisibility(apiColumns)) + setColumnOrder(getInitialColumnOrder(apiColumns)) + } } } - + prevActiveViewRef.current = activeView }, [activeView, apiColumns]) return { visibleColumns, columnOrder, + columnState, currentColumns, setVisibleColumns, setColumnOrder, @@ -135,12 +179,16 @@ const DEFAULT_COLUMN_ORDER = 500 function getInitialColumnVisibility( apiColumns: HttpTypes.AdminViewColumn[] ): Record { + if (!apiColumns || apiColumns.length === 0) { + return {} + } + const visibility: Record = {} - - apiColumns.forEach(column => { - visibility[column.field] = column.default_visible + + apiColumns.forEach((column) => { + visibility[column.field] = column.default_visible ?? true }) - + return visibility } @@ -150,11 +198,15 @@ function getInitialColumnVisibility( function getInitialColumnOrder( apiColumns: HttpTypes.AdminViewColumn[] ): string[] { + if (!apiColumns || apiColumns.length === 0) { + return [] + } + 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) -} \ No newline at end of file + + return sortedColumns.map((col) => col.field) +} diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-configurable-order-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-configurable-order-table-columns.tsx new file mode 100644 index 0000000000..0b3ca6423f --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/columns/use-configurable-order-table-columns.tsx @@ -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() + +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]) +} diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-order-data-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-order-data-table-columns.tsx new file mode 100644 index 0000000000..1cf79cb40a --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/columns/use-order-data-table-columns.tsx @@ -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() + +/** + * 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: () => , + cell: ({ getValue }) => { + const id = getValue() + return + }, + }), + columnHelper.accessor("created_at", { + header: () => , + cell: ({ getValue }) => { + const date = new Date(getValue()) + return + }, + }), + columnHelper.accessor("customer", { + header: () => , + cell: ({ getValue }) => { + const customer = getValue() + return + }, + }), + columnHelper.accessor("sales_channel", { + header: () => , + cell: ({ getValue }) => { + const channel = getValue() + return + }, + }), + columnHelper.accessor("payment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + return + }, + }), + columnHelper.accessor("fulfillment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + return + }, + }), + columnHelper.accessor("total", { + header: () => , + cell: ({ getValue, row }) => { + const total = getValue() + const currencyCode = row.original.currency_code + return + }, + }), + columnHelper.display({ + id: "country", + cell: ({ row }) => { + const country = row.original.shipping_address?.country + return + }, + }), + ] + } + + // 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: () => , + cell: ({ getValue }) => { + const id = getValue() + return + }, + }) + + case "created_at": + case "updated_at": + return columnHelper.accessor(col.field as any, { + header: () => , + cell: ({ getValue }) => { + const date = getValue() ? new Date(getValue() as string) : null + return date ? : null + }, + }) + + case "email": + return columnHelper.accessor("email", { + header: () => , + cell: ({ getValue }) => { + const email = getValue() + return + }, + }) + + case "customer_display": + return columnHelper.accessor("customer", { + header: () => , + cell: ({ getValue }) => { + const customer = getValue() + return + }, + }) + + case "sales_channel.name": + return columnHelper.accessor("sales_channel", { + header: () => , + cell: ({ getValue }) => { + const channel = getValue() + return + }, + }) + + case "payment_status": + return columnHelper.accessor("payment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + return + }, + }) + + case "fulfillment_status": + return columnHelper.accessor("fulfillment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + return + }, + }) + + case "total": + return columnHelper.accessor("total", { + header: () => , + cell: ({ getValue, row }) => { + const total = getValue() + const currencyCode = row.original.currency_code + return + }, + }) + + case "country": + return columnHelper.display({ + id: "country", + cell: ({ row }) => { + const country = row.original.shipping_address?.country + return + }, + }) + + 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: () => , + cell: ({ getValue }) => { + const value = getValue() + return + }, + }) + } + + // Default text column + return columnHelper.accessor(col.field as any, { + header: () => , + cell: ({ getValue }) => { + const value = getValue() + return + }, + }) + } + }) + }, [apiColumns, visibleColumns, t]) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/hooks/use-view-configurations.tsx b/packages/admin/dashboard/src/hooks/use-view-configurations.tsx index c56e339f32..23e9f0046f 100644 --- a/packages/admin/dashboard/src/hooks/use-view-configurations.tsx +++ b/packages/admin/dashboard/src/hooks/use-view-configurations.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react" -import { HttpTypes } from "@medusajs/types" import { toast } from "@medusajs/ui" import { FetchError } from "@medusajs/js-sdk" import { useFeatureFlag } from "../providers/feature-flag-provider" @@ -12,13 +11,8 @@ import { useSetActiveViewConfiguration as useSetActiveViewConfigurationBase, } from "./api/views" -// Re-export the type for convenience -export type ViewConfiguration = HttpTypes.AdminViewConfigurationResponse - // Common error handler const handleError = (error: Error, message?: string) => { - console.error("View configuration error:", error) - let errorMessage = message if (!errorMessage) { if (error instanceof FetchError) { @@ -36,11 +30,12 @@ const handleError = (error: Error, message?: string) => { export const useViewConfigurations = (entity: string) => { const isViewConfigEnabled = useFeatureFlag("view_configurations") + // List views const listViews = useViewConfigurationsBase(entity, { limit: 100 }, { enabled: isViewConfigEnabled && !!entity, - }) + // Active view const activeView = useActiveViewConfigurationBase(entity, { enabled: isViewConfigEnabled && !!entity, }) @@ -57,7 +52,8 @@ export const useViewConfigurations = (entity: string) => { // Set active view mutation const setActiveView = useSetActiveViewConfigurationBase(entity, { - onSuccess: () => { }, + onSuccess: () => { + }, onError: (error) => { handleError(error, "Failed to update active view") }, @@ -69,7 +65,7 @@ export const useViewConfigurations = (entity: string) => { activeView, createView, setActiveView, - isDefaultViewActive: activeView.data?.is_default_active ?? true, + isDefaultViewActive: activeView?.is_default_active ?? true, }), [ isViewConfigEnabled, 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) => { const updateView = useUpdateViewConfigurationBase(entity, viewId, { onSuccess: () => { @@ -91,7 +88,7 @@ export const useViewConfiguration = (entity: string, viewId: string) => { const deleteView = useDeleteViewConfigurationBase(entity, viewId, { onSuccess: () => { - toast.success("View deleted") + toast.success("View deleted successfully") }, onError: (error) => { handleError(error, "Failed to delete view") diff --git a/packages/admin/dashboard/src/lib/table-display-utils.tsx b/packages/admin/dashboard/src/lib/table-display-utils.tsx new file mode 100644 index 0000000000..d85947645d --- /dev/null +++ b/packages/admin/dashboard/src/lib/table-display-utils.tsx @@ -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 ( + + {status} + + ) +} + +// 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 ( + + {status} + + ) +} + +// Generic status badge +const GenericStatusBadge = ({ status }: { status: string }) => { + return ( + + {status} + + ) +} + +// Display strategies registry +export const DISPLAY_STRATEGIES = { + // Known semantic types with pixel-perfect display + status: { + payment: (value: any) => , + fulfillment: (value: any) => , + default: (value: any) => + }, + + currency: { + default: (value: any, row: any) => { + if (value === null || value === undefined) return '-' + const currencyCode = row.currency_code || 'USD' + const formatted = getStylizedAmount(value, currencyCode) + + return ( +
+ {formatted} +
+ ) + } + }, + + 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) => + }, + + // Base type fallbacks + string: { + default: (value: any) => value || '-' + }, + + number: { + default: (value: any) => value?.toLocaleString() || '0' + }, + + boolean: { + default: (value: any) => ( + + {value ? 'Yes' : 'No'} + + ) + }, + + 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
-
+ + // Get country information + const country = getCountryByIso2(countryCode) + const displayName = country?.display_name || countryCode.toUpperCase() + + // Display country flag with tooltip - centered in the cell + return ( +
+ +
+ +
+
+
+ ) + } +} + +// 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) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/components/save-view-dropdown.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/components/save-view-dropdown.tsx new file mode 100644 index 0000000000..021e132b57 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/components/save-view-dropdown.tsx @@ -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 = ({ + 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 ( + + + + + + {isDefaultView ? ( + <> + + Update default for everyone + + + Save as new view + + + ) : ( + <> + + Update "{currentViewName}" + + + Save as new view + + + )} + + + ) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx index dfa0b1fd29..cc3e604c22 100644 --- a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx @@ -1,31 +1,334 @@ -import { Container, Heading } from "@medusajs/ui" +import { useState, useMemo, useCallback, useEffect } from "react" +import { Container, Button } from "@medusajs/ui" import { keepPreviousData } from "@tanstack/react-query" import { useTranslation } from "react-i18next" -import { useState, useEffect } from "react" +import { useSearchParams } from "react-router-dom" import { DataTable } from "../../../../../components/data-table" import { useOrders } from "../../../../../hooks/api/orders" -import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" -import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useConfigurableOrderTableColumns } from "../../../../../hooks/table/columns/use-configurable-order-table-columns" +import { useOrderTableFilters } from "./use-order-table-filters" import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" - -import { DEFAULT_FIELDS } from "../../const" +import { useViewConfigurations, useViewConfiguration } from "../../../../../hooks/use-view-configurations" +import { useEntityColumns } from "../../../../../hooks/api/views" +import { useFeatureFlag } from "../../../../../providers/feature-flag-provider" +import { useColumnState } from "../../../../../hooks/table/columns/use-column-state" +import { useQueryParams } from "../../../../../hooks/use-query-params" +import { SaveViewDropdown } from "./components/save-view-dropdown" +import { SaveViewDialog } from "../../../../../components/table/save-view-dialog" +import { useRequiredFields } from "./hooks/use-required-fields" const PAGE_SIZE = 20 +const QUERY_PREFIX = "o" + +function parseSortingState(value: string) { + return value.startsWith("-") + ? { id: value.slice(1), desc: true } + : { id: value, desc: false } +} export const ConfigurableOrderListTable = () => { const { t } = useTranslation() - - const [columnVisibility, setColumnVisibility] = useState>({}) - const [columnOrder, setColumnOrder] = useState([]) + const isViewConfigEnabled = useFeatureFlag("view_configurations") - const { searchParams, raw } = useOrderTableQuery({ + const { + activeView, + createView, + } = useViewConfigurations("orders") + + const currentActiveView = activeView?.view_configuration || null + + const { updateView } = useViewConfiguration("orders", currentActiveView?.id || "") + + const { columns: apiColumns, isLoading: isLoadingColumns } = useEntityColumns("orders", { + enabled: isViewConfigEnabled, + }) + + const filters = useOrderTableFilters() + + const queryParams = useQueryParams( + ["q", "order", ...filters.map(f => f.id)], + QUERY_PREFIX + ) + + const [_, setSearchParams] = useSearchParams() + + const { + visibleColumns, + columnOrder, + currentColumns, + setColumnOrder, + handleColumnVisibilityChange, + handleViewChange: originalHandleViewChange, + } = useColumnState(apiColumns, currentActiveView) + + useEffect(() => { + if (!apiColumns) return + originalHandleViewChange(currentActiveView, apiColumns) + setSearchParams((prev) => { + const keysToDelete = Array.from(prev.keys()).filter(key => + key.startsWith(QUERY_PREFIX + "_") || key === QUERY_PREFIX + "_q" || key === QUERY_PREFIX + "_order" + ) + keysToDelete.forEach(key => prev.delete(key)) + + if (currentActiveView) { + const viewConfig = currentActiveView.configuration + + if (viewConfig.filters) { + Object.entries(viewConfig.filters).forEach(([key, value]) => { + prev.set(`${QUERY_PREFIX}_${key}`, JSON.stringify(value)) + }) + } + + if (viewConfig.sorting) { + const sortValue = viewConfig.sorting.desc + ? `-${viewConfig.sorting.id}` + : viewConfig.sorting.id + prev.set(`${QUERY_PREFIX}_order`, sortValue) + } + + if (viewConfig.search) { + prev.set(`${QUERY_PREFIX}_q`, viewConfig.search) + } + } + + return prev + }) + }, [currentActiveView, apiColumns]) + + const [debouncedHasConfigChanged, setDebouncedHasConfigChanged] = useState(false) + + const hasConfigurationChanged = useMemo(() => { + const currentFilters: Record = {} + 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 = {} + 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(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 ? ( + <> + + + + ) : null + + const { searchParams } = useOrderTableQuery({ pageSize: PAGE_SIZE, + prefix: QUERY_PREFIX, }) const { orders, count, isError, error, isLoading } = useOrders( { - fields: DEFAULT_FIELDS, + fields: requiredFields, ...searchParams, }, { @@ -33,38 +336,7 @@ export const ConfigurableOrderListTable = () => { } ) - const filters = useOrderTableFilters() - const columns = useOrderTableColumns({}) - - const handleViewChange = (view: any) => { - if (view) { - // Apply view configuration - const visibilityState: Record = {} - const allColumns = columns.map(c => c.id!) - - // Set all columns to hidden first - allColumns.forEach(col => { - visibilityState[col] = false - }) - - // Then show only the visible columns from the view - if (view.configuration?.visible_columns) { - view.configuration.visible_columns.forEach((col: string) => { - visibilityState[col] = true - }) - } - - setColumnVisibility(visibilityState) - - if (view.configuration?.column_order) { - setColumnOrder(view.configuration.column_order) - } - } else { - // Reset to default (all visible) - setColumnVisibility({}) - setColumnOrder([]) - } - } + const columns = useConfigurableOrderTableColumns(apiColumns) if (isError) { throw error @@ -81,28 +353,43 @@ export const ConfigurableOrderListTable = () => { enablePagination enableSearch pageSize={PAGE_SIZE} - isLoading={isLoading} + isLoading={isLoading || isLoadingColumns} layout="fill" heading={t("orders.domain")} - enableColumnVisibility={true} - initialColumnVisibility={columnVisibility} - onColumnVisibilityChange={setColumnVisibility} + enableColumnVisibility={isViewConfigEnabled} + initialColumnVisibility={visibleColumns} + onColumnVisibilityChange={handleColumnVisibilityChange} columnOrder={columnOrder} onColumnOrderChange={setColumnOrder} - enableViewSelector={true} + enableViewSelector={isViewConfigEnabled} entity="orders" - onViewChange={handleViewChange} - currentColumns={{ - visible: Object.entries(columnVisibility) - .filter(([_, visible]) => visible !== false) - .map(([col]) => col), - order: columnOrder.length > 0 ? columnOrder : columns.map(c => c.id!).filter(Boolean) - }} + currentColumns={currentColumns} + filterBarContent={filterBarContent} rowHref={(row) => `/orders/${row.id}`} emptyState={{ - message: t("orders.list.noRecordsMessage"), + empty: { + heading: t("orders.list.noRecordsMessage"), + } }} + prefix={QUERY_PREFIX} /> + + {saveDialogOpen && ( + { + setSaveDialogOpen(false) + setEditingView(null) + }} + onSaved={() => { + setSaveDialogOpen(false) + setEditingView(null) + }} + /> + )} ) -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/constants.ts b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/constants.ts new file mode 100644 index 0000000000..17379178c6 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/constants.ts @@ -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 + order: string[] +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-order-data-table-columns.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-order-data-table-columns.tsx new file mode 100644 index 0000000000..971d089935 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-order-data-table-columns.tsx @@ -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() + +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 ( +
+ # + {value} +
+ ) + }, + 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 ? ( + {value} + ) : 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 ? ( + {value} + ) : 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]) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-required-fields.ts b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-required-fields.ts new file mode 100644 index 0000000000..963b9837c5 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/hooks/use-required-fields.ts @@ -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 { + return useMemo(() => { + return calculateRequiredFields(apiColumns, visibleColumns) + }, [apiColumns, visibleColumns]) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx index 16be9f4ecb..cb10250a67 100644 --- a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next" import { _DataTable } from "../../../../../components/table/data-table/data-table" import { useOrders } from "../../../../../hooks/api/orders" import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" -import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useOrderTableFilters } from "./use-order-table-filters" import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" import { useFeatureFlag } from "../../../../../providers/feature-flag-provider" diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/use-order-table-filters.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/use-order-table-filters.tsx new file mode 100644 index 0000000000..75ca566fed --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/use-order-table-filters.tsx @@ -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() + +/** + * 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]) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/column-utils.ts b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/column-utils.ts new file mode 100644 index 0000000000..7596973632 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/column-utils.ts @@ -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 { + const visibility: Record = {} + + 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) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/field-utils.ts b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/field-utils.ts new file mode 100644 index 0000000000..b5b6c1868a --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/utils/field-utils.ts @@ -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 { + 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() + + 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 +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/const.ts b/packages/admin/dashboard/src/routes/orders/order-list/const.ts index a79f925158..6046bfd5fe 100644 --- a/packages/admin/dashboard/src/routes/orders/order-list/const.ts +++ b/packages/admin/dashboard/src/routes/orders/order-list/const.ts @@ -1,4 +1,4 @@ -const DEFAULT_PROPERTIES = [ +export const DEFAULT_PROPERTIES = [ "id", "status", "created_at", @@ -10,7 +10,7 @@ const DEFAULT_PROPERTIES = [ "currency_code", ] -const DEFAULT_RELATIONS = ["*customer", "*sales_channel"] +export const DEFAULT_RELATIONS = ["*customer", "*sales_channel"] export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join( "," diff --git a/packages/core/types/src/http/view-configuration/admin/payloads.ts b/packages/core/types/src/http/view-configuration/admin/payloads.ts index 53a2360539..6612f628a3 100644 --- a/packages/core/types/src/http/view-configuration/admin/payloads.ts +++ b/packages/core/types/src/http/view-configuration/admin/payloads.ts @@ -1,12 +1,8 @@ export interface AdminCreateViewConfiguration { - /** - * The entity this configuration is for (e.g., "order", "product"). - */ - entity: string /** * The name of the view configuration. */ - name?: string + name?: string | null /** * Whether this is a system default configuration. */ @@ -53,7 +49,7 @@ export interface AdminUpdateViewConfiguration { /** * The name of the view configuration. */ - name?: string + name?: string | null /** * Whether this is a system default configuration. */ @@ -105,4 +101,4 @@ export interface AdminSetActiveViewConfiguration { * The ID of the view configuration to set as active, or null to clear the active view. */ view_configuration_id: string | null -} \ No newline at end of file +} diff --git a/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx b/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx index f40d3f56f6..9c700245ca 100644 --- a/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx +++ b/packages/design-system/ui/src/blocks/data-table/components/data-table-table.tsx @@ -12,7 +12,6 @@ import { useSensor, useSensors, DragEndEvent, - DragStartEvent, } from "@dnd-kit/core" import { arrayMove, diff --git a/packages/medusa/src/api/admin/views/[entity]/configurations/active/route.ts b/packages/medusa/src/api/admin/views/[entity]/configurations/active/route.ts index fb0623cbff..f77e2ce839 100644 --- a/packages/medusa/src/api/admin/views/[entity]/configurations/active/route.ts +++ b/packages/medusa/src/api/admin/views/[entity]/configurations/active/route.ts @@ -40,23 +40,10 @@ export const GET = async ( default_type: "code", }) } 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({ view_configuration: viewConfiguration, - is_default_active: isDefaultActive, - default_type: - isDefaultActive && viewConfiguration.is_system_default - ? "system" - : undefined, + is_default_active: viewConfiguration.is_system_default, + default_type: viewConfiguration.is_system_default ? "system" : undefined, }) } }