feat(admin): add view configuration client infrastructure (#13186)

This is part of stacked PRs to add a view configuration feature which will allow users to customize the columns seen in tables in the Medusa Admin dashboard.

**What**
- Adds client providers, sdk methods and hooks for interacting with the views api.
This commit is contained in:
Sebastian Rindom
2025-09-01 11:20:37 +02:00
committed by GitHub
parent 72675c59ec
commit fa10c78ed3
6 changed files with 446 additions and 1 deletions

View File

@@ -34,4 +34,5 @@ export * from "./tags"
export * from "./tax-rates"
export * from "./tax-regions"
export * from "./users"
export * from "./views"
export * from "./workflow-executions"

View File

@@ -0,0 +1,209 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
const VIEWS_QUERY_KEY = "views" as const
export const viewsQueryKeys = queryKeysFactory(VIEWS_QUERY_KEY)
// Generic hook to get columns for any entity
export const useEntityColumns = (entity: string, options?: Omit<
UseQueryOptions<
HttpTypes.AdminViewsEntityColumnsResponse,
FetchError,
HttpTypes.AdminViewsEntityColumnsResponse,
QueryKey
>,
"queryFn" | "queryKey"
>) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.views.columns(entity),
queryKey: viewsQueryKeys.list(entity),
...options,
})
return { ...data, ...rest }
}
// View Configuration hooks
// List view configurations for an entity
export const useViewConfigurations = (
entity: string,
query?: HttpTypes.AdminGetViewConfigurationsParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminViewConfigurationListResponse,
FetchError,
HttpTypes.AdminViewConfigurationListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.views.listConfigurations(entity, query),
queryKey: viewsQueryKeys.list(entity, query),
...options,
})
return { ...data, ...rest }
}
// Get active view configuration for an entity
export const useActiveViewConfiguration = (
entity: string,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminViewConfigurationResponse & {
active_view_configuration_id?: string | null
is_default_active?: boolean
default_type?: "system" | "code"
},
FetchError,
HttpTypes.AdminViewConfigurationResponse & {
active_view_configuration_id?: string | null
is_default_active?: boolean
default_type?: "system" | "code"
},
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.views.retrieveActiveConfiguration(entity),
queryKey: [viewsQueryKeys.detail(entity), "active"],
...options,
})
return { ...data, ...rest }
}
// Get a specific view configuration
export const useViewConfiguration = (
entity: string,
id: string,
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminViewConfigurationResponse,
FetchError,
HttpTypes.AdminViewConfigurationResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.views.retrieveConfiguration(entity, id, query),
queryKey: viewsQueryKeys.detail(id, query),
...options,
})
return { ...data, ...rest }
}
export const useCreateViewConfiguration = (
entity: string,
options?: UseMutationOptions<
HttpTypes.AdminViewConfigurationResponse,
FetchError,
HttpTypes.AdminCreateViewConfiguration
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminCreateViewConfiguration) =>
sdk.admin.views.createConfiguration(entity, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
// If set_active was true, also invalidate the active configuration
if ((variables as any).set_active) {
queryClient.invalidateQueries({
queryKey: [...viewsQueryKeys.detail(entity, "active")]
})
}
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateViewConfiguration = (
entity: string,
id: string,
options?: UseMutationOptions<
HttpTypes.AdminViewConfigurationResponse,
FetchError,
HttpTypes.AdminUpdateViewConfiguration
>
) => {
return useMutation({
mutationFn: (payload: HttpTypes.AdminUpdateViewConfiguration) =>
sdk.admin.views.updateConfiguration(entity, id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
// Delete view configuration
export const useDeleteViewConfiguration = (
entity: string,
id: string,
options?: UseMutationOptions<
HttpTypes.AdminViewConfigurationDeleteResponse,
FetchError,
void
>
) => {
return useMutation({
mutationFn: () => sdk.admin.views.deleteConfiguration(entity, id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.list(entity) })
queryClient.invalidateQueries({ queryKey: viewsQueryKeys.detail(id) })
// Also invalidate active configuration as it might have changed
queryClient.invalidateQueries({
queryKey: [...viewsQueryKeys.detail(entity, "active")]
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useSetActiveViewConfiguration = (
entity: string,
options?: UseMutationOptions<
{ success: boolean },
FetchError,
string | null
>
) => {
return useMutation({
mutationFn: (viewConfigurationId: string | null) =>
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,
})
}

View File

@@ -0,0 +1,105 @@
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"
import {
useViewConfigurations as useViewConfigurationsBase,
useActiveViewConfiguration as useActiveViewConfigurationBase,
useCreateViewConfiguration as useCreateViewConfigurationBase,
useUpdateViewConfiguration as useUpdateViewConfigurationBase,
useDeleteViewConfiguration as useDeleteViewConfigurationBase,
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) {
errorMessage = error.message
} else if (error.message) {
errorMessage = error.message
} else {
errorMessage = "An error occurred"
}
}
toast.error(errorMessage)
}
export const useViewConfigurations = (entity: string) => {
const isViewConfigEnabled = useFeatureFlag("view_configurations")
const listViews = useViewConfigurationsBase(entity, { limit: 100 }, {
enabled: isViewConfigEnabled && !!entity,
})
const activeView = useActiveViewConfigurationBase(entity, {
enabled: isViewConfigEnabled && !!entity,
})
// Create view mutation
const createView = useCreateViewConfigurationBase(entity, {
onSuccess: () => {
toast.success(`View created`)
},
onError: (error) => {
handleError(error, "Failed to create view")
},
})
// Set active view mutation
const setActiveView = useSetActiveViewConfigurationBase(entity, {
onSuccess: () => { },
onError: (error) => {
handleError(error, "Failed to update active view")
},
})
return useMemo(() => ({
isViewConfigEnabled,
listViews,
activeView,
createView,
setActiveView,
isDefaultViewActive: activeView.data?.is_default_active ?? true,
}), [
isViewConfigEnabled,
listViews,
activeView,
createView,
setActiveView,
])
}
export const useViewConfiguration = (entity: string, viewId: string) => {
const updateView = useUpdateViewConfigurationBase(entity, viewId, {
onSuccess: () => {
toast.success(`View updated`)
},
onError: (error) => {
handleError(error, "Failed to update view")
},
})
const deleteView = useDeleteViewConfigurationBase(entity, viewId, {
onSuccess: () => {
toast.success("View deleted")
},
onError: (error) => {
handleError(error, "Failed to delete view")
},
})
return {
updateView,
deleteView,
}
}

View File

@@ -9,7 +9,6 @@ import { ExtensionProvider } from "./extension-provider"
import { I18nProvider } from "./i18n-provider"
import { ThemeProvider } from "./theme-provider"
import { FeatureFlagProvider } from "./feature-flag-provider"
type ProvidersProps = PropsWithChildren<{
api: DashboardApp["api"]
}>