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:
@@ -34,4 +34,5 @@ export * from "./tags"
|
|||||||
export * from "./tax-rates"
|
export * from "./tax-rates"
|
||||||
export * from "./tax-regions"
|
export * from "./tax-regions"
|
||||||
export * from "./users"
|
export * from "./users"
|
||||||
|
export * from "./views"
|
||||||
export * from "./workflow-executions"
|
export * from "./workflow-executions"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import { ExtensionProvider } from "./extension-provider"
|
|||||||
import { I18nProvider } from "./i18n-provider"
|
import { I18nProvider } from "./i18n-provider"
|
||||||
import { ThemeProvider } from "./theme-provider"
|
import { ThemeProvider } from "./theme-provider"
|
||||||
import { FeatureFlagProvider } from "./feature-flag-provider"
|
import { FeatureFlagProvider } from "./feature-flag-provider"
|
||||||
|
|
||||||
type ProvidersProps = PropsWithChildren<{
|
type ProvidersProps = PropsWithChildren<{
|
||||||
api: DashboardApp["api"]
|
api: DashboardApp["api"]
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { TaxRate } from "./tax-rate"
|
|||||||
import { TaxRegion } from "./tax-region"
|
import { TaxRegion } from "./tax-region"
|
||||||
import { Upload } from "./upload"
|
import { Upload } from "./upload"
|
||||||
import { User } from "./user"
|
import { User } from "./user"
|
||||||
|
import { Views } from "./views"
|
||||||
import { WorkflowExecution } from "./workflow-execution"
|
import { WorkflowExecution } from "./workflow-execution"
|
||||||
import { ShippingOptionType } from "./shipping-option-type"
|
import { ShippingOptionType } from "./shipping-option-type"
|
||||||
|
|
||||||
@@ -226,6 +227,10 @@ export class Admin {
|
|||||||
* @tags plugin
|
* @tags plugin
|
||||||
*/
|
*/
|
||||||
public plugin: Plugin
|
public plugin: Plugin
|
||||||
|
/**
|
||||||
|
* @tags views
|
||||||
|
*/
|
||||||
|
public views: Views
|
||||||
|
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
this.invite = new Invite(client)
|
this.invite = new Invite(client)
|
||||||
@@ -273,5 +278,6 @@ export class Admin {
|
|||||||
this.campaign = new Campaign(client)
|
this.campaign = new Campaign(client)
|
||||||
this.plugin = new Plugin(client)
|
this.plugin = new Plugin(client)
|
||||||
this.taxProvider = new TaxProvider(client)
|
this.taxProvider = new TaxProvider(client)
|
||||||
|
this.views = new Views(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { HttpTypes, SelectParams } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { Client } from "../client"
|
||||||
|
import { ClientHeaders } from "../types"
|
||||||
|
|
||||||
|
export class Views {
|
||||||
|
constructor(private client: Client) {}
|
||||||
|
|
||||||
|
// Generic method to get columns for any entity
|
||||||
|
async columns(
|
||||||
|
entity: string,
|
||||||
|
query?: SelectParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewsEntityColumnsResponse> {
|
||||||
|
return await this.client.fetch(`/admin/views/${entity}/columns`, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// View configurations
|
||||||
|
async listConfigurations(
|
||||||
|
entity: string,
|
||||||
|
query?: HttpTypes.AdminGetViewConfigurationsParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewConfigurationListResponse> {
|
||||||
|
return await this.client.fetch(`/admin/views/${entity}/configurations`, {
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConfiguration(
|
||||||
|
entity: string,
|
||||||
|
body: HttpTypes.AdminCreateViewConfiguration,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewConfigurationResponse> {
|
||||||
|
return await this.client.fetch(`/admin/views/${entity}/configurations`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveConfiguration(
|
||||||
|
entity: string,
|
||||||
|
id: string,
|
||||||
|
query?: SelectParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewConfigurationResponse> {
|
||||||
|
return await this.client.fetch(
|
||||||
|
`/admin/views/${entity}/configurations/${id}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateConfiguration(
|
||||||
|
entity: string,
|
||||||
|
id: string,
|
||||||
|
body: HttpTypes.AdminUpdateViewConfiguration,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewConfigurationResponse> {
|
||||||
|
return await this.client.fetch(
|
||||||
|
`/admin/views/${entity}/configurations/${id}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteConfiguration(
|
||||||
|
entity: string,
|
||||||
|
id: string,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<HttpTypes.AdminViewConfigurationDeleteResponse> {
|
||||||
|
return await this.client.fetch(
|
||||||
|
`/admin/views/${entity}/configurations/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveActiveConfiguration(
|
||||||
|
entity: string,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<
|
||||||
|
HttpTypes.AdminViewConfigurationResponse & {
|
||||||
|
active_view_configuration_id?: string | null
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return await this.client.fetch(
|
||||||
|
`/admin/views/${entity}/configurations/active`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setActiveConfiguration(
|
||||||
|
entity: string,
|
||||||
|
body: { view_configuration_id: string | null },
|
||||||
|
headers?: ClientHeaders
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
return await this.client.fetch(
|
||||||
|
`/admin/views/${entity}/configurations/active`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user