feat(dashboard): Migrate to new hooks and API client (#6963)
This commit is contained in:
committed by
GitHub
parent
5ba74ec5fc
commit
8a5c6928f7
@@ -23,7 +23,7 @@
|
||||
"@medusajs/ui": "workspace:^",
|
||||
"@radix-ui/react-collapsible": "1.0.3",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@tanstack/react-query": "4.22.0",
|
||||
"@tanstack/react-query": "^5.28.14",
|
||||
"@tanstack/react-table": "8.10.7",
|
||||
"@tanstack/react-virtual": "^3.0.4",
|
||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||
@@ -35,6 +35,7 @@
|
||||
"i18next-http-backend": "2.4.2",
|
||||
"match-sorter": "^6.3.4",
|
||||
"medusa-react": "workspace:^",
|
||||
"qs": "^6.12.0",
|
||||
"react": "18.2.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -648,7 +648,7 @@
|
||||
"inviteUserHint": "Invite a new user to your store.",
|
||||
"sendInvite": "Send invite",
|
||||
"pendingInvites": "Pending Invites",
|
||||
"revokeInviteWarning": "You are about to revoke the invite for {{email}}. This action cannot be undone.",
|
||||
"deleteInviteWarning": "You are about to delete the invite for {{email}}. This action cannot be undone.",
|
||||
"resendInvite": "Resend invite",
|
||||
"copyInviteLink": "Copy invite link",
|
||||
"expiredOnDate": "Expired on {{date}}",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Product,
|
||||
ProductCollection,
|
||||
ProductVariant,
|
||||
SalesChannel,
|
||||
} from "@medusajs/medusa"
|
||||
ProductCollectionDTO,
|
||||
ProductDTO,
|
||||
ProductVariantDTO,
|
||||
} from "@medusajs/types"
|
||||
import { StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Thumbnail } from "../thumbnail"
|
||||
@@ -11,7 +11,7 @@ import { Thumbnail } from "../thumbnail"
|
||||
export const ProductVariantCell = ({
|
||||
variants,
|
||||
}: {
|
||||
variants: ProductVariant[] | null
|
||||
variants: ProductVariantDTO[] | null
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -35,7 +35,7 @@ export const ProductVariantCell = ({
|
||||
export const ProductStatusCell = ({
|
||||
status,
|
||||
}: {
|
||||
status: Product["status"]
|
||||
status: ProductDTO["status"]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ProductAvailabilityCell = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const ProductTitleCell = ({ product }: { product: Product }) => {
|
||||
export const ProductTitleCell = ({ product }: { product: ProductDTO }) => {
|
||||
const thumbnail = product.thumbnail
|
||||
const title = product.title
|
||||
|
||||
@@ -112,7 +112,7 @@ export const ProductTitleCell = ({ product }: { product: Product }) => {
|
||||
export const ProductCollectionCell = ({
|
||||
collection,
|
||||
}: {
|
||||
collection: ProductCollection | null
|
||||
collection: ProductCollectionDTO | null
|
||||
}) => {
|
||||
if (!collection) {
|
||||
return (
|
||||
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
import { Avatar, Text } from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useV2Store } from "../../../lib/api-v2"
|
||||
|
||||
import { Skeleton } from "../../common/skeleton"
|
||||
import { NavItem, NavItemProps } from "../../layout/nav-item"
|
||||
import { Shell } from "../../layout/shell"
|
||||
|
||||
import extensions from "medusa-admin:routes/links"
|
||||
import { useStore } from "../../../hooks/api/store"
|
||||
|
||||
export const MainLayout = () => {
|
||||
return (
|
||||
@@ -46,7 +46,7 @@ const MainSidebar = () => {
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const { store, isError, error } = useV2Store({})
|
||||
const { store, isError, error } = useStore()
|
||||
|
||||
const name = store?.name
|
||||
const fallback = store?.name?.slice(0, 1).toUpperCase()
|
||||
|
||||
@@ -12,25 +12,21 @@ import {
|
||||
User as UserIcon,
|
||||
} from "@medusajs/icons"
|
||||
import { Avatar, DropdownMenu, IconButton, Kbd, Text, clx } from "@medusajs/ui"
|
||||
import { PropsWithChildren } from "react"
|
||||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
UIMatch,
|
||||
useLocation,
|
||||
useMatches,
|
||||
useNavigate,
|
||||
} from "react-router-dom"
|
||||
import { useAdminDeleteSession, useAdminGetSession } from "medusa-react"
|
||||
|
||||
import { PropsWithChildren } from "react"
|
||||
import { Skeleton } from "../../common/skeleton"
|
||||
import { queryClient } from "../../../lib/medusa"
|
||||
|
||||
import { useMe } from "../../../hooks/api/users"
|
||||
import { useSearch } from "../../../providers/search-provider"
|
||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||
import { useTheme } from "../../../providers/theme-provider"
|
||||
import { useV2Session } from "../../../lib/api-v2"
|
||||
|
||||
const V2_ENABLED = import.meta.env.VITE_MEDUSA_V2 || "false"
|
||||
|
||||
export const Shell = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
@@ -119,22 +115,7 @@ const Breadcrumbs = () => {
|
||||
}
|
||||
|
||||
const UserBadge = () => {
|
||||
const isV2Enabled = V2_ENABLED === "true"
|
||||
|
||||
console.warn(isV2Enabled)
|
||||
// Medusa V2 disabled
|
||||
const v1 = useAdminGetSession({
|
||||
enabled: !isV2Enabled,
|
||||
})
|
||||
|
||||
// Medusa V2 enabled
|
||||
const v2 = useV2Session({
|
||||
enabled: isV2Enabled,
|
||||
})
|
||||
|
||||
// Comment: Only place where we switch between the two modes inline.
|
||||
// This is to avoid having to rebuild the shell for the app.
|
||||
const { user, isLoading, isError, error } = !isV2Enabled ? v1 : v2
|
||||
const { user, isLoading, isError, error } = useMe()
|
||||
|
||||
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ")
|
||||
const displayName = name || user?.email
|
||||
@@ -220,20 +201,20 @@ const ThemeToggle = () => {
|
||||
}
|
||||
|
||||
const Logout = () => {
|
||||
const navigate = useNavigate()
|
||||
const { mutateAsync: logoutMutation } = useAdminDeleteSession()
|
||||
// const navigate = useNavigate()
|
||||
// const { mutateAsync: logoutMutation } = useAdminDeleteSession()
|
||||
|
||||
const handleLayout = async () => {
|
||||
await logoutMutation(undefined, {
|
||||
onSuccess: () => {
|
||||
/**
|
||||
* When the user logs out, we want to clear the query cache
|
||||
*/
|
||||
queryClient.clear()
|
||||
|
||||
navigate("/login")
|
||||
},
|
||||
})
|
||||
// await logoutMutation(undefined, {
|
||||
// onSuccess: () => {
|
||||
// /**
|
||||
// * When the user logs out, we want to clear the query cache
|
||||
// */
|
||||
// queryClient.clear()
|
||||
// navigate("/login")
|
||||
// },
|
||||
// })
|
||||
// noop
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ProductCollection } from "@medusajs/medusa"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ProductCollectionDTO } from "@medusajs/types"
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type CollectionCellProps = {
|
||||
collection?: ProductCollection | null
|
||||
collection?: ProductCollectionDTO | null
|
||||
}
|
||||
|
||||
export const CollectionCell = ({ collection }: CollectionCellProps) => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Product } from "@medusajs/medusa"
|
||||
import type { PricedProduct } from "@medusajs/medusa/dist/types/pricing"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ProductDTO } from "@medusajs/types"
|
||||
import { Thumbnail } from "../../../../common/thumbnail"
|
||||
|
||||
type ProductCellProps = {
|
||||
product: Product | PricedProduct
|
||||
product: ProductDTO
|
||||
}
|
||||
|
||||
export const ProductCell = ({ product }: ProductCellProps) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { SalesChannel } from "@medusajs/medusa"
|
||||
import { Tooltip } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { SalesChannelDTO } from "@medusajs/types"
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type SalesChannelsCellProps = {
|
||||
salesChannels?: SalesChannel[] | null
|
||||
salesChannels?: SalesChannelDTO[] | null
|
||||
}
|
||||
|
||||
export const SalesChannelsCell = ({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ProductVariant } from "@medusajs/medusa"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ProductVariantDTO } from "@medusajs/types"
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type VariantCellProps = {
|
||||
variants?: ProductVariant[] | null
|
||||
variants?: ProductVariantDTO[] | null
|
||||
}
|
||||
|
||||
export const VariantCell = ({ variants }: VariantCellProps) => {
|
||||
|
||||
113
packages/admin-next/dashboard/src/hooks/api/api-keys.tsx
Normal file
113
packages/admin-next/dashboard/src/hooks/api/api-keys.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
MutationOptions,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { CreateApiKeyReq, UpdateApiKeyReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ApiKeyDeleteRes,
|
||||
ApiKeyListRes,
|
||||
ApiKeyRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const API_KEYS_QUERY_KEY = "api_keys" as const
|
||||
export const apiKeysQueryKeys = queryKeysFactory(API_KEYS_QUERY_KEY)
|
||||
|
||||
export const useApiKey = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ApiKeyRes, Error, ApiKeyRes, QueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.apiKeys.retrieve(id, query),
|
||||
queryKey: apiKeysQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useApiKeys = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ApiKeyListRes, Error, ApiKeyListRes, QueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.apiKeys.list(query),
|
||||
queryKey: apiKeysQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateApiKey = (
|
||||
options?: UseMutationOptions<ApiKeyRes, Error, CreateApiKeyReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.apiKeys.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateApiKey = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<ApiKeyRes, Error, UpdateApiKeyReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.apiKeys.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRevokeApiKey = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<ApiKeyRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.apiKeys.revoke(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteApiKey = (
|
||||
id: string,
|
||||
options?: MutationOptions<ApiKeyDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.apiKeys.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
})
|
||||
}
|
||||
22
packages/admin-next/dashboard/src/hooks/api/auth.tsx
Normal file
22
packages/admin-next/dashboard/src/hooks/api/auth.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { UseMutationOptions, useMutation } from "@tanstack/react-query"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { EmailPassReq } from "../../types/api-payloads"
|
||||
import { EmailPassRes } from "../../types/api-responses"
|
||||
|
||||
export const useEmailPassLogin = (
|
||||
options?: UseMutationOptions<EmailPassRes, Error, EmailPassReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.auth.authenticate.emailPass(payload),
|
||||
onSuccess: async (data: { token: string }, variables, context) => {
|
||||
const { token } = data
|
||||
|
||||
// Create a new session with the token
|
||||
await client.auth.login(token)
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
123
packages/admin-next/dashboard/src/hooks/api/collections.tsx
Normal file
123
packages/admin-next/dashboard/src/hooks/api/collections.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import {
|
||||
CreateProductCollectionReq,
|
||||
UpdateProductCollectionReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
ProductCollectionDeleteRes,
|
||||
ProductCollectionListRes,
|
||||
ProductCollectionRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const COLLECTION_QUERY_KEY = "collections" as const
|
||||
export const collectionsQueryKeys = queryKeysFactory(COLLECTION_QUERY_KEY)
|
||||
|
||||
export const useCollection = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
ProductCollectionRes,
|
||||
Error,
|
||||
ProductCollectionRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: collectionsQueryKeys.detail(id),
|
||||
queryFn: async () => client.collections.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCollections = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
ProductCollectionListRes,
|
||||
Error,
|
||||
ProductCollectionListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: collectionsQueryKeys.list(query),
|
||||
queryFn: async () => client.collections.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateCollection = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
ProductCollectionRes,
|
||||
Error,
|
||||
UpdateProductCollectionReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: UpdateProductCollectionReq) =>
|
||||
client.collections.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateCollection = (
|
||||
options?: UseMutationOptions<
|
||||
ProductCollectionRes,
|
||||
Error,
|
||||
CreateProductCollectionReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.collections.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionsQueryKeys.lists() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteCollection = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<ProductCollectionDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.collections.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: collectionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: collectionsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
39
packages/admin-next/dashboard/src/hooks/api/currencies.tsx
Normal file
39
packages/admin-next/dashboard/src/hooks/api/currencies.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { CurrencyListRes, CurrencyRes } from "../../types/api-responses"
|
||||
|
||||
const CURRENCIES_QUERY_KEY = "currencies" as const
|
||||
const currenciesQueryKeys = queryKeysFactory(CURRENCIES_QUERY_KEY)
|
||||
|
||||
export const useCurrencies = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<CurrencyListRes, Error, CurrencyListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.currencies.list(query),
|
||||
queryKey: currenciesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCurrency = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<CurrencyRes, Error, CurrencyRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: currenciesQueryKeys.detail(id),
|
||||
queryFn: async () => client.currencies.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
77
packages/admin-next/dashboard/src/hooks/api/customers.tsx
Normal file
77
packages/admin-next/dashboard/src/hooks/api/customers.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
||||
import { CustomerListRes, CustomerRes } from "../../types/api-responses"
|
||||
|
||||
const CUSTOMERS_QUERY_KEY = "customers" as const
|
||||
const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
|
||||
|
||||
export const useCustomer = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<CustomerRes, Error, CustomerRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: customersQueryKeys.detail(id),
|
||||
queryFn: async () => client.customers.retrieve(id, query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCustomers = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<CustomerListRes, Error, CustomerListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.customers.list(query),
|
||||
queryKey: customersQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateCustomer = (
|
||||
options?: UseMutationOptions<CustomerRes, Error, CreateCustomerReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.customers.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateCustomer = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<CustomerRes, Error, UpdateCustomerReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.customers.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: customersQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
94
packages/admin-next/dashboard/src/hooks/api/invites.tsx
Normal file
94
packages/admin-next/dashboard/src/hooks/api/invites.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { CreateInviteReq } from "../../types/api-payloads"
|
||||
import {
|
||||
InviteDeleteRes,
|
||||
InviteListRes,
|
||||
InviteRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const INVITES_QUERY_KEY = "invites" as const
|
||||
const invitesQueryKeys = queryKeysFactory(INVITES_QUERY_KEY)
|
||||
|
||||
export const useInvite = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<InviteRes, Error, InviteRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: invitesQueryKeys.detail(id),
|
||||
queryFn: async () => client.invites.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useInvites = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<InviteListRes, Error, InviteListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.invites.list(query),
|
||||
queryKey: invitesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateInvite = (
|
||||
options?: UseMutationOptions<InviteRes, Error, CreateInviteReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.invites.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: invitesQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useResendInvite = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<InviteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.invites.resend(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: invitesQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: invitesQueryKeys.detail(id) })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteInvite = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<InviteDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.invites.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: invitesQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: invitesQueryKeys.detail(id) })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { ProductTypeListRes, ProductTypeRes } from "../../types/api-responses"
|
||||
|
||||
const PRODUCT_TYPES_QUERY_KEY = "product_types" as const
|
||||
const productTypesQueryKeys = queryKeysFactory(PRODUCT_TYPES_QUERY_KEY)
|
||||
|
||||
export const useProductType = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ProductTypeRes, Error, ProductTypeRes, QueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.productTypes.retrieve(id, query),
|
||||
queryKey: productTypesQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useProductTypes = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ProductTypeListRes, Error, ProductTypeListRes, QueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.productTypes.list(query),
|
||||
queryKey: productTypesQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
40
packages/admin-next/dashboard/src/hooks/api/products.tsx
Normal file
40
packages/admin-next/dashboard/src/hooks/api/products.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { ProductListRes, ProductRes } from "../../types/api-responses"
|
||||
|
||||
const PRODUCTS_QUERY_KEY = "products" as const
|
||||
export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
||||
|
||||
export const useProduct = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ProductRes, Error, ProductRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.products.retrieve(id, query),
|
||||
queryKey: productsQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useProducts = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<ProductListRes, Error, ProductListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.products.list(query),
|
||||
queryKey: productsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
41
packages/admin-next/dashboard/src/hooks/api/promotions.tsx
Normal file
41
packages/admin-next/dashboard/src/hooks/api/promotions.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
||||
|
||||
const PROMOTIONS_QUERY_KEY = "promotions" as const
|
||||
const promotionsQueryKeys = queryKeysFactory(PROMOTIONS_QUERY_KEY)
|
||||
|
||||
export const usePromotion = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<PromotionRes, Error, PromotionRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.detail(id),
|
||||
queryFn: async () => client.promotions.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const usePromotions = (
|
||||
query?: AdminGetPromotionsParams,
|
||||
options?: Omit<
|
||||
UseQueryOptions<PromotionListRes, Error, PromotionListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: promotionsQueryKeys.list(query),
|
||||
queryFn: async () => client.promotions.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
96
packages/admin-next/dashboard/src/hooks/api/regions.tsx
Normal file
96
packages/admin-next/dashboard/src/hooks/api/regions.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { CreateRegionReq } from "../../types/api-payloads"
|
||||
import {
|
||||
RegionDeleteRes,
|
||||
RegionListRes,
|
||||
RegionRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const REGIONS_QUERY_KEY = "regions" as const
|
||||
const regionsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY)
|
||||
|
||||
export const useRegion = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<RegionRes, Error, RegionRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: regionsQueryKeys.detail(id),
|
||||
queryFn: async () => client.regions.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useRegions = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<RegionListRes, Error, RegionListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.regions.list(query),
|
||||
queryKey: regionsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateRegion = (
|
||||
options?: UseMutationOptions<RegionRes, Error, CreateRegionReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.regions.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: regionsQueryKeys.lists() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateRegion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<RegionRes, Error, CreateRegionReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.regions.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: regionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: regionsQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteRegion = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<RegionDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.regions.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: regionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: regionsQueryKeys.detail(id) })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
190
packages/admin-next/dashboard/src/hooks/api/sales-channels.tsx
Normal file
190
packages/admin-next/dashboard/src/hooks/api/sales-channels.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import {
|
||||
AddProductsSalesChannelReq,
|
||||
CreateSalesChannelReq,
|
||||
RemoveProductsSalesChannelReq,
|
||||
UpdateSalesChannelReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
SalesChannelDeleteRes,
|
||||
SalesChannelListRes,
|
||||
SalesChannelRes,
|
||||
} from "../../types/api-responses"
|
||||
import { productsQueryKeys } from "./products"
|
||||
|
||||
const SALES_CHANNELS_QUERY_KEY = "sales-channels" as const
|
||||
const salesChannelsQueryKeys = queryKeysFactory(SALES_CHANNELS_QUERY_KEY)
|
||||
|
||||
export const useSalesChannel = (
|
||||
id: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<SalesChannelRes, Error, SalesChannelRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryKey: salesChannelsQueryKeys.detail(id),
|
||||
queryFn: async () => client.salesChannels.retrieve(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useSalesChannels = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<SalesChannelListRes, Error, SalesChannelListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.salesChannels.list(query),
|
||||
queryKey: salesChannelsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateSalesChannel = (
|
||||
options?: UseMutationOptions<SalesChannelRes, Error, CreateSalesChannelReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.salesChannels.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.lists(),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateSalesChannel = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<SalesChannelRes, Error, UpdateSalesChannelReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.salesChannels.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteSalesChannel = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<SalesChannelDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.salesChannels.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
// Invalidate all products to ensure they are updated if they were linked to the sales channel
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.all,
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useSalesChannelRemoveProducts = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
SalesChannelRes,
|
||||
Error,
|
||||
RemoveProductsSalesChannelReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.salesChannels.removeProducts(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
// Invalidate the products that were removed
|
||||
for (const product of variables?.product_ids || []) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(product),
|
||||
})
|
||||
}
|
||||
|
||||
// Invalidate the products list query
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useSalesChannelAddProducts = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
SalesChannelRes,
|
||||
Error,
|
||||
AddProductsSalesChannelReq
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.salesChannels.addProducts(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: salesChannelsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
// Invalidate the products that were removed
|
||||
for (const product of variables?.product_ids || []) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(product),
|
||||
})
|
||||
}
|
||||
|
||||
// Invalidate the products list query
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
117
packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx
Normal file
117
packages/admin-next/dashboard/src/hooks/api/stock-locations.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import {
|
||||
CreateStockLocationReq,
|
||||
UpdateStockLocationReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
StockLocationDeleteRes,
|
||||
StockLocationListRes,
|
||||
StockLocationRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const
|
||||
const stockLocationsQueryKeys = queryKeysFactory(STOCK_LOCATIONS_QUERY_KEY)
|
||||
|
||||
export const useStockLocation = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<StockLocationRes, Error, StockLocationRes, QueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.stockLocations.retrieve(id, query),
|
||||
queryKey: stockLocationsQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useStockLocations = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
StockLocationListRes,
|
||||
Error,
|
||||
StockLocationListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.stockLocations.list(query),
|
||||
queryKey: stockLocationsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateStockLocation = (
|
||||
options?: UseMutationOptions<StockLocationRes, Error, CreateStockLocationReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.stockLocations.create(payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateStockLocation = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<StockLocationRes, Error, UpdateStockLocationReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.stockLocations.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.detail(id),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteStockLocation = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<StockLocationDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.stockLocations.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.detail(id),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
48
packages/admin-next/dashboard/src/hooks/api/store.tsx
Normal file
48
packages/admin-next/dashboard/src/hooks/api/store.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
MutationOptions,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { queryKeysFactory } from "medusa-react"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { UpdateStoreReq } from "../../types/api-payloads"
|
||||
import { StoreRes } from "../../types/api-responses"
|
||||
|
||||
const STORE_QUERY_KEY = "store" as const
|
||||
const storeQueryKeys = queryKeysFactory(STORE_QUERY_KEY)
|
||||
|
||||
export const useStore = (
|
||||
options?: Omit<
|
||||
UseQueryOptions<StoreRes, Error, StoreRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.stores.retrieve(),
|
||||
queryKey: storeQueryKeys.details(),
|
||||
...options,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateStore = (
|
||||
id: string,
|
||||
options?: MutationOptions<StoreRes, Error, UpdateStoreReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.stores.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: storeQueryKeys.details() })
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
104
packages/admin-next/dashboard/src/hooks/api/users.tsx
Normal file
104
packages/admin-next/dashboard/src/hooks/api/users.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { UpdateUserReq } from "../../types/api-payloads"
|
||||
import { UserDeleteRes, UserListRes, UserRes } from "../../types/api-responses"
|
||||
|
||||
const USERS_QUERY_KEY = "users" as const
|
||||
const usersQueryKeys = {
|
||||
...queryKeysFactory(USERS_QUERY_KEY),
|
||||
me: () => [USERS_QUERY_KEY, "me"],
|
||||
}
|
||||
|
||||
export const useMe = (
|
||||
options?: UseQueryOptions<UserRes, Error, UserRes, QueryKey>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.users.me(),
|
||||
queryKey: usersQueryKeys.me(),
|
||||
...options,
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export const useUser = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<UserRes, Error, UserRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.users.retrieve(id, query),
|
||||
queryKey: usersQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUsers = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<UserListRes, Error, UserListRes, QueryKey>,
|
||||
"queryFn" | "queryKey"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.users.list(query),
|
||||
queryKey: usersQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useUpdateUser = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<UserRes, Error, UpdateUserReq>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) => client.users.update(id, payload),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
||||
|
||||
// We invalidate the me query in case the user updates their own profile
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.me() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteUser = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<UserDeleteRes, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.users.delete(id),
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.detail(id) })
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.lists() })
|
||||
|
||||
// We invalidate the me query in case the user updates their own profile
|
||||
queryClient.invalidateQueries({ queryKey: usersQueryKeys.me() })
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import {
|
||||
WorkflowExecutionListRes,
|
||||
WorkflowExecutionRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
const WORKFLOW_EXECUTIONS_QUERY_KEY = "workflow_executions" as const
|
||||
const workflowExecutionsQueryKeys = queryKeysFactory(
|
||||
WORKFLOW_EXECUTIONS_QUERY_KEY
|
||||
)
|
||||
|
||||
export const useWorkflowExecutions = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
WorkflowExecutionListRes,
|
||||
Error,
|
||||
WorkflowExecutionListRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.workflowExecutions.list(query),
|
||||
queryKey: workflowExecutionsQueryKeys.list(query),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useWorkflowExecution = (
|
||||
id: string,
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
WorkflowExecutionRes,
|
||||
Error,
|
||||
WorkflowExecutionRes,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => client.workflowExecutions.retrieve(id, query),
|
||||
queryKey: workflowExecutionsQueryKeys.detail(id),
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
@@ -22,8 +21,9 @@ import {
|
||||
VariantCell,
|
||||
VariantHeader,
|
||||
} from "../../../components/table/table-cells/product/variant-cell"
|
||||
import { ExtendedProductDTO } from "../../../types/api-responses"
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
const columnHelper = createColumnHelper<ExtendedProductDTO>()
|
||||
|
||||
export const useProductTableColumns = () => {
|
||||
return useMemo(
|
||||
@@ -55,5 +55,5 @@ export const useProductTableColumns = () => {
|
||||
}),
|
||||
],
|
||||
[]
|
||||
) as ColumnDef<Product>[]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
|
||||
import { SalesChannelDTO } from "@medusajs/types"
|
||||
import { useMemo } from "react"
|
||||
import {
|
||||
DescriptionCell,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
NameHeader,
|
||||
} from "../../../components/table/table-cells/sales-channel/name-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<SalesChannel>()
|
||||
const columnHelper = createColumnHelper<SalesChannelDTO>()
|
||||
|
||||
export const useSalesChannelTableColumns = () => {
|
||||
return useMemo(
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
useAdminCollections,
|
||||
useAdminProductCategories,
|
||||
useAdminProductTags,
|
||||
useAdminProductTypes,
|
||||
useAdminSalesChannels,
|
||||
} from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../components/table/data-table"
|
||||
import { useProductTypes } from "../../api/product-types"
|
||||
import { useSalesChannels } from "../../api/sales-channels"
|
||||
|
||||
const excludeableFields = ["sales_channel_id", "collections"] as const
|
||||
|
||||
@@ -15,19 +10,19 @@ export const useProductTableFilters = (
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product_types } = useAdminProductTypes({
|
||||
const { product_types } = useProductTypes({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { product_tags } = useAdminProductTags({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
// const { product_tags } = useAdminProductTags({
|
||||
// limit: 1000,
|
||||
// offset: 0,
|
||||
// })
|
||||
|
||||
const isSalesChannelExcluded = exclude?.includes("sales_channel_id")
|
||||
|
||||
const { sales_channels } = useAdminSalesChannels(
|
||||
const { sales_channels } = useSalesChannels(
|
||||
{
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
@@ -38,24 +33,24 @@ export const useProductTableFilters = (
|
||||
}
|
||||
)
|
||||
|
||||
const { product_categories } = useAdminProductCategories({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
fields: "id,name",
|
||||
expand: "",
|
||||
})
|
||||
// const { product_categories } = useAdminProductCategories({
|
||||
// limit: 1000,
|
||||
// offset: 0,
|
||||
// fields: "id,name",
|
||||
// expand: "",
|
||||
// })
|
||||
|
||||
const isCollectionExcluded = exclude?.includes("collections")
|
||||
|
||||
const { collections } = useAdminCollections(
|
||||
{
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
enabled: !isCollectionExcluded,
|
||||
}
|
||||
)
|
||||
// const { collections } = useAdminCollections(
|
||||
// {
|
||||
// limit: 1000,
|
||||
// offset: 0,
|
||||
// },
|
||||
// {
|
||||
// enabled: !isCollectionExcluded,
|
||||
// }
|
||||
// )
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
@@ -74,20 +69,20 @@ export const useProductTableFilters = (
|
||||
filters = [...filters, typeFilter]
|
||||
}
|
||||
|
||||
if (product_tags) {
|
||||
const tagFilter: Filter = {
|
||||
key: "tags",
|
||||
label: t("fields.tag"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: product_tags.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
}
|
||||
// if (product_tags) {
|
||||
// const tagFilter: Filter = {
|
||||
// key: "tags",
|
||||
// label: t("fields.tag"),
|
||||
// type: "select",
|
||||
// multiple: true,
|
||||
// options: product_tags.map((t) => ({
|
||||
// label: t.value,
|
||||
// value: t.id,
|
||||
// })),
|
||||
// }
|
||||
|
||||
filters = [...filters, tagFilter]
|
||||
}
|
||||
// filters = [...filters, tagFilter]
|
||||
// }
|
||||
|
||||
if (sales_channels) {
|
||||
const salesChannelFilter: Filter = {
|
||||
@@ -104,35 +99,35 @@ export const useProductTableFilters = (
|
||||
filters = [...filters, salesChannelFilter]
|
||||
}
|
||||
|
||||
if (product_categories) {
|
||||
const categoryFilter: Filter = {
|
||||
key: "category_id",
|
||||
label: t("fields.category"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: product_categories.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
})),
|
||||
}
|
||||
// if (product_categories) {
|
||||
// const categoryFilter: Filter = {
|
||||
// key: "category_id",
|
||||
// label: t("fields.category"),
|
||||
// type: "select",
|
||||
// multiple: true,
|
||||
// options: product_categories.map((c) => ({
|
||||
// label: c.name,
|
||||
// value: c.id,
|
||||
// })),
|
||||
// }
|
||||
|
||||
filters = [...filters, categoryFilter]
|
||||
}
|
||||
// filters = [...filters, categoryFilter]
|
||||
// }
|
||||
|
||||
if (collections) {
|
||||
const collectionFilter: Filter = {
|
||||
key: "collection_id",
|
||||
label: t("fields.collection"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: collections.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id,
|
||||
})),
|
||||
}
|
||||
// if (collections) {
|
||||
// const collectionFilter: Filter = {
|
||||
// key: "collection_id",
|
||||
// label: t("fields.collection"),
|
||||
// type: "select",
|
||||
// multiple: true,
|
||||
// options: collections.map((c) => ({
|
||||
// label: c.title,
|
||||
// value: c.id,
|
||||
// })),
|
||||
// }
|
||||
|
||||
filters = [...filters, collectionFilter]
|
||||
}
|
||||
// filters = [...filters, collectionFilter]
|
||||
// }
|
||||
|
||||
const giftCardFilter: Filter = {
|
||||
key: "is_giftcard",
|
||||
|
||||
@@ -21,9 +21,9 @@ export const useSalesChannelTableQuery = ({
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
q,
|
||||
// created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
// updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
// q, // Re-enable when params are fixed
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
40
packages/admin-next/dashboard/src/lib/client/api-keys.ts
Normal file
40
packages/admin-next/dashboard/src/lib/client/api-keys.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CreateApiKeyReq, UpdateApiKeyReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ApiKeyDeleteRes,
|
||||
ApiKeyListRes,
|
||||
ApiKeyRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
const retrieveApiKey = async (id: string, query?: Record<string, any>) => {
|
||||
return getRequest<ApiKeyRes>(`/admin/api-keys/${id}`, query)
|
||||
}
|
||||
|
||||
const listApiKeys = async (query?: Record<string, any>) => {
|
||||
return getRequest<ApiKeyListRes>(`/admin/api-keys`, query)
|
||||
}
|
||||
|
||||
const deleteApiKey = async (id: string) => {
|
||||
return deleteRequest<ApiKeyDeleteRes>(`/admin/api-keys/${id}`)
|
||||
}
|
||||
|
||||
const revokeApiKey = async (id: string) => {
|
||||
return postRequest<ApiKeyRes>(`/admin/api-keys/${id}/revoke`)
|
||||
}
|
||||
|
||||
const createApiKey = async (payload: CreateApiKeyReq) => {
|
||||
return postRequest<ApiKeyRes>(`/admin/api-keys`, payload)
|
||||
}
|
||||
|
||||
const updateApiKey = async (id: string, payload: UpdateApiKeyReq) => {
|
||||
return postRequest<ApiKeyRes>(`/admin/api-keys/${id}`, payload)
|
||||
}
|
||||
|
||||
export const apiKeys = {
|
||||
retrieve: retrieveApiKey,
|
||||
list: listApiKeys,
|
||||
delete: deleteApiKey,
|
||||
create: createApiKey,
|
||||
update: updateApiKey,
|
||||
revoke: revokeApiKey,
|
||||
}
|
||||
22
packages/admin-next/dashboard/src/lib/client/auth.ts
Normal file
22
packages/admin-next/dashboard/src/lib/client/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { EmailPassReq } from "../../types/api-payloads"
|
||||
import { EmailPassRes } from "../../types/api-responses"
|
||||
import { postRequest } from "./common"
|
||||
|
||||
async function emailPass(payload: EmailPassReq) {
|
||||
return postRequest<EmailPassRes>("/auth/admin/emailpass", payload)
|
||||
}
|
||||
|
||||
async function login(token: string) {
|
||||
return postRequest<void>("/auth/session", undefined, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const auth = {
|
||||
authenticate: {
|
||||
emailPass,
|
||||
},
|
||||
login,
|
||||
}
|
||||
33
packages/admin-next/dashboard/src/lib/client/client.ts
Normal file
33
packages/admin-next/dashboard/src/lib/client/client.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { apiKeys } from "./api-keys"
|
||||
import { auth } from "./auth"
|
||||
import { collections } from "./collections"
|
||||
import { currencies } from "./currencies"
|
||||
import { customers } from "./customers"
|
||||
import { invites } from "./invites"
|
||||
import { productTypes } from "./product-types"
|
||||
import { products } from "./products"
|
||||
import { promotions } from "./promotions"
|
||||
import { regions } from "./regions"
|
||||
import { salesChannels } from "./sales-channels"
|
||||
import { stockLocations } from "./stock-locations"
|
||||
import { stores } from "./stores"
|
||||
import { users } from "./users"
|
||||
import { workflowExecutions } from "./workflow-executions"
|
||||
|
||||
export const client = {
|
||||
auth: auth,
|
||||
apiKeys: apiKeys,
|
||||
customers: customers,
|
||||
currencies: currencies,
|
||||
collections: collections,
|
||||
promotions: promotions,
|
||||
stores: stores,
|
||||
salesChannels: salesChannels,
|
||||
users: users,
|
||||
regions: regions,
|
||||
invites: invites,
|
||||
products: products,
|
||||
productTypes: productTypes,
|
||||
stockLocations: stockLocations,
|
||||
workflowExecutions: workflowExecutions,
|
||||
}
|
||||
44
packages/admin-next/dashboard/src/lib/client/collections.ts
Normal file
44
packages/admin-next/dashboard/src/lib/client/collections.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
CreateProductCollectionReq,
|
||||
UpdateProductCollectionReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
ProductCollectionDeleteRes,
|
||||
ProductCollectionListRes,
|
||||
ProductCollectionRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function listProductCollections(query?: Record<string, any>) {
|
||||
return getRequest<ProductCollectionListRes>(`/admin/collections`, query)
|
||||
}
|
||||
|
||||
async function retrieveProductCollection(
|
||||
id: string,
|
||||
query?: Record<string, any>
|
||||
) {
|
||||
return getRequest<ProductCollectionRes>(`/admin/collections/${id}`, query)
|
||||
}
|
||||
|
||||
async function updateProductCollection(
|
||||
id: string,
|
||||
payload: UpdateProductCollectionReq
|
||||
) {
|
||||
return postRequest<ProductCollectionRes>(`/admin/collections/${id}`, payload)
|
||||
}
|
||||
|
||||
async function createProductCollection(payload: CreateProductCollectionReq) {
|
||||
return postRequest<ProductCollectionRes>(`/admin/collections`, payload)
|
||||
}
|
||||
|
||||
async function deleteProductCollection(id: string) {
|
||||
return deleteRequest<ProductCollectionDeleteRes>(`/admin/collections/${id}`)
|
||||
}
|
||||
|
||||
export const collections = {
|
||||
list: listProductCollections,
|
||||
retrieve: retrieveProductCollection,
|
||||
update: updateProductCollection,
|
||||
create: createProductCollection,
|
||||
delete: deleteProductCollection,
|
||||
}
|
||||
107
packages/admin-next/dashboard/src/lib/client/common.ts
Normal file
107
packages/admin-next/dashboard/src/lib/client/common.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { stringify } from "qs"
|
||||
|
||||
const baseUrl = "http://localhost:9000"
|
||||
|
||||
const commonHeaders: HeadersInit = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
function getUrl(path: string, query?: Record<string, any>) {
|
||||
const params = query ? stringify(query) : null
|
||||
|
||||
return `${baseUrl}${path}${params ? `?${params}` : ""}`
|
||||
}
|
||||
|
||||
function getBody(payload?: Record<string, any>) {
|
||||
return payload ? JSON.stringify(payload) : undefined
|
||||
}
|
||||
|
||||
function getOptions(
|
||||
options?: Omit<RequestInit, "body">,
|
||||
payload?: Record<string, any>
|
||||
): RequestInit {
|
||||
const body = getBody(payload)
|
||||
|
||||
return {
|
||||
...options,
|
||||
headers: {
|
||||
...commonHeaders,
|
||||
...options?.headers,
|
||||
},
|
||||
body,
|
||||
credentials: "include",
|
||||
}
|
||||
}
|
||||
|
||||
async function makeRequest<
|
||||
TRes,
|
||||
TPayload extends Record<string, any> | undefined,
|
||||
TQuery extends Record<string, any> | undefined = undefined,
|
||||
>(
|
||||
path: string,
|
||||
payload?: TPayload,
|
||||
query?: TQuery,
|
||||
options?: Omit<RequestInit, "body">
|
||||
): Promise<TRes> {
|
||||
const url = getUrl(path, query)
|
||||
const requestOptions = getOptions(options, payload)
|
||||
|
||||
const response = await fetch(url, requestOptions)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
// Temp: Add a better error type
|
||||
throw new Error(`API error ${response.status}: ${errorData.message}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export async function getRequest<
|
||||
TRes,
|
||||
TQuery extends Record<string, any> | undefined = {},
|
||||
>(
|
||||
path: string,
|
||||
query?: TQuery,
|
||||
options?: Omit<RequestInit, "body" | "method">
|
||||
): Promise<TRes> {
|
||||
return makeRequest<TRes, undefined, Record<string, any>>(
|
||||
path,
|
||||
undefined,
|
||||
query,
|
||||
{
|
||||
...options,
|
||||
method: "GET",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function postRequest<
|
||||
TRes,
|
||||
TPayload extends Record<string, any> | undefined = {},
|
||||
>(
|
||||
path: string,
|
||||
payload?: TPayload,
|
||||
options?: Omit<RequestInit, "body" | "method">
|
||||
): Promise<TRes> {
|
||||
return makeRequest<TRes, Record<string, any>, undefined>(
|
||||
path,
|
||||
payload,
|
||||
undefined,
|
||||
{
|
||||
...options,
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteRequest<TRes>(
|
||||
path: string,
|
||||
options?: Omit<RequestInit, "body" | "method">
|
||||
): Promise<TRes> {
|
||||
return makeRequest<TRes, undefined, undefined>(path, undefined, undefined, {
|
||||
...options,
|
||||
method: "DELETE",
|
||||
})
|
||||
}
|
||||
15
packages/admin-next/dashboard/src/lib/client/currencies.ts
Normal file
15
packages/admin-next/dashboard/src/lib/client/currencies.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { CurrencyListRes, CurrencyRes } from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
|
||||
async function retrieveCurrency(id: string, query?: Record<string, any>) {
|
||||
return getRequest<CurrencyRes>(`/admin/currencies/${id}`, query)
|
||||
}
|
||||
|
||||
async function listCurrencies(query?: Record<string, any>) {
|
||||
return getRequest<CurrencyListRes>("/admin/currencies", query)
|
||||
}
|
||||
|
||||
export const currencies = {
|
||||
retrieve: retrieveCurrency,
|
||||
list: listCurrencies,
|
||||
}
|
||||
26
packages/admin-next/dashboard/src/lib/client/customers.ts
Normal file
26
packages/admin-next/dashboard/src/lib/client/customers.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
|
||||
import { CustomerListRes, CustomerRes } from "../../types/api-responses"
|
||||
import { getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveCustomer(id: string, query?: Record<string, any>) {
|
||||
return getRequest<CustomerRes>(`/admin/customers/${id}`, query)
|
||||
}
|
||||
|
||||
async function listCustomers(query?: Record<string, any>) {
|
||||
return getRequest<CustomerListRes>(`/admin/customers`, query)
|
||||
}
|
||||
|
||||
async function createCustomer(payload: CreateCustomerReq) {
|
||||
return postRequest<CustomerRes>(`/admin/customers`, payload)
|
||||
}
|
||||
|
||||
async function updateCustomer(id: string, payload: UpdateCustomerReq) {
|
||||
return postRequest<CustomerRes>(`/admin/customers/${id}`, payload)
|
||||
}
|
||||
|
||||
export const customers = {
|
||||
retrieve: retrieveCustomer,
|
||||
list: listCustomers,
|
||||
create: createCustomer,
|
||||
update: updateCustomer,
|
||||
}
|
||||
1
packages/admin-next/dashboard/src/lib/client/index.ts
Normal file
1
packages/admin-next/dashboard/src/lib/client/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./client"
|
||||
38
packages/admin-next/dashboard/src/lib/client/invites.ts
Normal file
38
packages/admin-next/dashboard/src/lib/client/invites.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { CreateInviteReq } from "../../types/api-payloads"
|
||||
import {
|
||||
InviteDeleteRes,
|
||||
InviteListRes,
|
||||
InviteRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveInvite(id: string, query?: Record<string, any>) {
|
||||
return getRequest<InviteRes, Record<string, any>>(
|
||||
`/admin/invites/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function listInvites(query?: Record<string, any>) {
|
||||
return getRequest<InviteListRes, Record<string, any>>(`/admin/invites`, query)
|
||||
}
|
||||
|
||||
async function createInvite(payload: CreateInviteReq) {
|
||||
return postRequest<InviteRes>(`/admin/invites`, payload)
|
||||
}
|
||||
|
||||
async function resendInvite(id: string) {
|
||||
return postRequest<InviteRes>(`/admin/invites/${id}/resend`)
|
||||
}
|
||||
|
||||
async function deleteInvite(id: string) {
|
||||
return deleteRequest<InviteDeleteRes>(`/admin/invites/${id}`)
|
||||
}
|
||||
|
||||
export const invites = {
|
||||
retrieve: retrieveInvite,
|
||||
list: listInvites,
|
||||
create: createInvite,
|
||||
resend: resendInvite,
|
||||
delete: deleteInvite,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ProductTypeListRes, ProductTypeRes } from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
|
||||
async function listProductTypes(query?: Record<string, any>) {
|
||||
return getRequest<ProductTypeListRes>(`/admin/product-types`, query)
|
||||
}
|
||||
|
||||
async function retrieveProductType(id: string, query?: Record<string, any>) {
|
||||
return getRequest<ProductTypeRes>(`/admin/product-types/${id}`, query)
|
||||
}
|
||||
|
||||
export const productTypes = {
|
||||
list: listProductTypes,
|
||||
retrieve: retrieveProductType,
|
||||
}
|
||||
15
packages/admin-next/dashboard/src/lib/client/products.ts
Normal file
15
packages/admin-next/dashboard/src/lib/client/products.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ProductListRes, ProductRes } from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
|
||||
async function retrieveProduct(id: string, query?: Record<string, any>) {
|
||||
return getRequest<ProductRes>(`/admin/products/${id}`, query)
|
||||
}
|
||||
|
||||
async function listProducts(query?: Record<string, any>) {
|
||||
return getRequest<ProductListRes>(`/admin/products`, query)
|
||||
}
|
||||
|
||||
export const products = {
|
||||
retrieve: retrieveProduct,
|
||||
list: listProducts,
|
||||
}
|
||||
23
packages/admin-next/dashboard/src/lib/client/promotions.ts
Normal file
23
packages/admin-next/dashboard/src/lib/client/promotions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||
|
||||
import { PromotionListRes, PromotionRes } from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
|
||||
const retrievePromotion = async (
|
||||
id: string,
|
||||
query?: AdminGetPromotionsParams
|
||||
) => {
|
||||
return getRequest<PromotionRes, AdminGetPromotionsParams>(
|
||||
`/admin/promotions/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
const listPromotions = async (query?: AdminGetPromotionsParams) => {
|
||||
return getRequest<PromotionListRes>(`/admin/promotions`, query)
|
||||
}
|
||||
|
||||
export const promotions = {
|
||||
retrieve: retrievePromotion,
|
||||
list: listPromotions,
|
||||
}
|
||||
35
packages/admin-next/dashboard/src/lib/client/regions.ts
Normal file
35
packages/admin-next/dashboard/src/lib/client/regions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { CreateRegionDTO, UpdateRegionDTO } from "@medusajs/types"
|
||||
import {
|
||||
RegionDeleteRes,
|
||||
RegionListRes,
|
||||
RegionRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveRegion(id: string, query?: Record<string, any>) {
|
||||
return getRequest<RegionRes>(`/admin/regions/${id}`, query)
|
||||
}
|
||||
|
||||
async function listRegions(query?: Record<string, any>) {
|
||||
return getRequest<RegionListRes>(`/admin/regions`, query)
|
||||
}
|
||||
|
||||
async function createRegion(payload: CreateRegionDTO) {
|
||||
return postRequest<RegionRes>(`/admin/regions`, payload)
|
||||
}
|
||||
|
||||
async function updateRegion(id: string, payload: UpdateRegionDTO) {
|
||||
return postRequest<RegionRes>(`/admin/regions/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteRegion(id: string) {
|
||||
return deleteRequest<RegionDeleteRes>(`/admin/regions/${id}`)
|
||||
}
|
||||
|
||||
export const regions = {
|
||||
retrieve: retrieveRegion,
|
||||
list: listRegions,
|
||||
create: createRegion,
|
||||
update: updateRegion,
|
||||
delete: deleteRegion,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
AddProductsSalesChannelReq,
|
||||
CreateSalesChannelReq,
|
||||
RemoveProductsSalesChannelReq,
|
||||
UpdateSalesChannelReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
SalesChannelDeleteRes,
|
||||
SalesChannelListRes,
|
||||
SalesChannelRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveSalesChannel(id: string, query?: Record<string, any>) {
|
||||
return getRequest<SalesChannelRes, Record<string, any>>(
|
||||
`/admin/sales-channels/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function listSalesChannels(query?: Record<string, any>) {
|
||||
return getRequest<SalesChannelListRes, Record<string, any>>(
|
||||
`/admin/sales-channels`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function createSalesChannel(payload: CreateSalesChannelReq) {
|
||||
return postRequest<SalesChannelRes>(`/admin/sales-channels`, payload)
|
||||
}
|
||||
|
||||
async function updateSalesChannel(id: string, payload: UpdateSalesChannelReq) {
|
||||
return postRequest<SalesChannelRes>(`/admin/sales-channels/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteSalesChannel(id: string) {
|
||||
return deleteRequest<SalesChannelDeleteRes>(`/admin/sales-channels/${id}`)
|
||||
}
|
||||
|
||||
async function batchRemoveProducts(
|
||||
id: string,
|
||||
payload: RemoveProductsSalesChannelReq
|
||||
) {
|
||||
return postRequest<SalesChannelRes>(
|
||||
`/admin/sales-channels/${id}/products/batch/remove`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function batchAddProducts(
|
||||
id: string,
|
||||
payload: AddProductsSalesChannelReq
|
||||
) {
|
||||
return postRequest<SalesChannelRes>(
|
||||
`/admin/sales-channels/${id}/products/batch/add`,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
export const salesChannels = {
|
||||
retrieve: retrieveSalesChannel,
|
||||
list: listSalesChannels,
|
||||
create: createSalesChannel,
|
||||
update: updateSalesChannel,
|
||||
delete: deleteSalesChannel,
|
||||
removeProducts: batchRemoveProducts,
|
||||
addProducts: batchAddProducts,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
CreateStockLocationReq,
|
||||
UpdateStockLocationReq,
|
||||
} from "../../types/api-payloads"
|
||||
import {
|
||||
StockLocationDeleteRes,
|
||||
StockLocationListRes,
|
||||
StockLocationRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function listStockLocations(query?: Record<string, any>) {
|
||||
return getRequest<StockLocationListRes>(`/admin/stock-locations`, query)
|
||||
}
|
||||
|
||||
async function retrieveStockLocation(id: string, query?: Record<string, any>) {
|
||||
return getRequest<StockLocationRes>(`/admin/stock-locations/${id}`, query)
|
||||
}
|
||||
|
||||
async function createStockLocation(payload: CreateStockLocationReq) {
|
||||
return postRequest<StockLocationRes>(`/admin/stock-locations`, payload)
|
||||
}
|
||||
|
||||
async function updateStockLocation(
|
||||
id: string,
|
||||
payload: UpdateStockLocationReq
|
||||
) {
|
||||
return postRequest<StockLocationRes>(`/admin/stock-locations/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteStockLocation(id: string) {
|
||||
return deleteRequest<StockLocationDeleteRes>(`/admin/stock-locations/${id}`)
|
||||
}
|
||||
|
||||
export const stockLocations = {
|
||||
list: listStockLocations,
|
||||
retrieve: retrieveStockLocation,
|
||||
create: createStockLocation,
|
||||
update: updateStockLocation,
|
||||
delete: deleteStockLocation,
|
||||
}
|
||||
25
packages/admin-next/dashboard/src/lib/client/stores.ts
Normal file
25
packages/admin-next/dashboard/src/lib/client/stores.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { UpdateStoreReq } from "../../types/api-payloads"
|
||||
import { StoreListRes, StoreRes } from "../../types/api-responses"
|
||||
import { getRequest, postRequest } from "./common"
|
||||
|
||||
async function retrieveStore(query?: Record<string, any>): Promise<StoreRes> {
|
||||
const response = await getRequest<StoreListRes>("/admin/stores", query)
|
||||
|
||||
const activeStore = response.stores?.[0]
|
||||
|
||||
if (!activeStore) {
|
||||
// Temp: Add proper error handling
|
||||
throw new Error("No active store found")
|
||||
}
|
||||
|
||||
return { store: activeStore }
|
||||
}
|
||||
|
||||
async function updateStore(id: string, payload: UpdateStoreReq) {
|
||||
return postRequest<StoreRes>(`/admin/stores/${id}`, payload)
|
||||
}
|
||||
|
||||
export const stores = {
|
||||
retrieve: retrieveStore,
|
||||
update: updateStore,
|
||||
}
|
||||
31
packages/admin-next/dashboard/src/lib/client/users.ts
Normal file
31
packages/admin-next/dashboard/src/lib/client/users.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { UpdateUserReq } from "../../types/api-payloads"
|
||||
import { UserDeleteRes, UserListRes, UserRes } from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function me() {
|
||||
return getRequest<UserRes>("/admin/users/me")
|
||||
}
|
||||
|
||||
async function retrieveUser(id: string, query?: Record<string, any>) {
|
||||
return getRequest<UserRes>(`/admin/users/${id}`, query)
|
||||
}
|
||||
|
||||
async function listUsers(query?: Record<string, any>) {
|
||||
return getRequest<UserListRes>(`/admin/users`, query)
|
||||
}
|
||||
|
||||
async function updateUser(id: string, payload: UpdateUserReq) {
|
||||
return postRequest<UserRes>(`/admin/users/${id}`, payload)
|
||||
}
|
||||
|
||||
async function deleteUser(id: string) {
|
||||
return deleteRequest<UserDeleteRes>(`/admin/users/${id}`)
|
||||
}
|
||||
|
||||
export const users = {
|
||||
me,
|
||||
retrieve: retrieveUser,
|
||||
list: listUsers,
|
||||
update: updateUser,
|
||||
delete: deleteUser,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
WorkflowExecutionListRes,
|
||||
WorkflowExecutionRes,
|
||||
} from "../../types/api-responses"
|
||||
import { getRequest } from "./common"
|
||||
|
||||
async function retrieveWorkflowExecution(
|
||||
id: string,
|
||||
query?: Record<string, any>
|
||||
) {
|
||||
return getRequest<WorkflowExecutionRes>(
|
||||
`/admin/workflows-executions/${id}`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
async function listWorkflowExecutions(query?: Record<string, any>) {
|
||||
return getRequest<WorkflowExecutionListRes>(
|
||||
`/admin/workflows-executions`,
|
||||
query
|
||||
)
|
||||
}
|
||||
|
||||
export const workflowExecutions = {
|
||||
retrieve: retrieveWorkflowExecution,
|
||||
list: listWorkflowExecutions,
|
||||
}
|
||||
45
packages/admin-next/dashboard/src/lib/query-key-factory.ts
Normal file
45
packages/admin-next/dashboard/src/lib/query-key-factory.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { QueryKey, UseQueryOptions } from "@tanstack/react-query"
|
||||
|
||||
type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
|
||||
all: readonly [TKey]
|
||||
lists: () => readonly [...TQueryKey<TKey>["all"], "list"]
|
||||
list: (
|
||||
query?: TListQuery
|
||||
) => readonly [
|
||||
...ReturnType<TQueryKey<TKey>["lists"]>,
|
||||
{ query: TListQuery | undefined },
|
||||
]
|
||||
details: () => readonly [...TQueryKey<TKey>["all"], "detail"]
|
||||
detail: (
|
||||
id: TDetailQuery
|
||||
) => readonly [...ReturnType<TQueryKey<TKey>["details"]>, TDetailQuery]
|
||||
}
|
||||
|
||||
export type UseQueryOptionsWrapper<
|
||||
// Return type of queryFn
|
||||
TQueryFn = unknown,
|
||||
// Type thrown in case the queryFn rejects
|
||||
E = Error,
|
||||
// Query key type
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = Omit<
|
||||
UseQueryOptions<TQueryFn, E, TQueryFn, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
|
||||
export const queryKeysFactory = <
|
||||
T,
|
||||
TListQueryType = any,
|
||||
TDetailQueryType = string,
|
||||
>(
|
||||
globalKey: T
|
||||
) => {
|
||||
const queryKeyFactory: TQueryKey<T, TListQueryType, TDetailQueryType> = {
|
||||
all: [globalKey],
|
||||
lists: () => [...queryKeyFactory.all, "list"],
|
||||
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
|
||||
details: () => [...queryKeyFactory.all, "detail"],
|
||||
detail: (id: TDetailQueryType) => [...queryKeyFactory.details(), id],
|
||||
}
|
||||
return queryKeyFactory
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import { StockLocationExpandedDTO } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminRemoveLocationFromSalesChannel } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content/empty-table-content"
|
||||
|
||||
type LocationSalesChannelSectionProps = {
|
||||
location: StockLocationExpandedDTO
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export const LocationSalesChannelSection = ({
|
||||
location,
|
||||
}: LocationSalesChannelSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const salesChannels = location.sales_channels
|
||||
const count = location.sales_channels?.length || 0
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: salesChannels ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
meta: {
|
||||
locationId: location.id,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Sales Channels</Heading>
|
||||
<Link to={"add-sales-channels"}>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("locations.addSalesChannels")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{count ? (
|
||||
<Table>
|
||||
<Table.Header>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate(`/settings/sales-channels/${row.original.id}`)
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
) : (
|
||||
<NoRecords
|
||||
action={{
|
||||
label: t("locations.addSalesChannels"),
|
||||
to: "add-sales-channels",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const SalesChannelActions = ({
|
||||
salesChannel,
|
||||
locationId,
|
||||
}: {
|
||||
salesChannel: SalesChannel
|
||||
locationId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminRemoveLocationFromSalesChannel()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("locations.removeSalesChannelsWarning", { count: 1 }),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
location_id: locationId,
|
||||
sales_channel_id: salesChannel.id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<SalesChannel>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("description", {
|
||||
header: t("fields.description"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<div>
|
||||
<StatusBadge color={value ? "grey" : "green"}>
|
||||
{value ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { locationId } = table.options.meta as {
|
||||
locationId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<SalesChannelActions
|
||||
salesChannel={row.original}
|
||||
locationId={locationId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content/empty-table-content"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import {
|
||||
useAdminDeleteStockLocation,
|
||||
useAdminStockLocations,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { StockLocationExpandedDTO } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const LocationsListTable = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { stock_locations, count, isLoading, isError, error } =
|
||||
useAdminStockLocations({
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
fields: "*address",
|
||||
})
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: stock_locations ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">Locations</Heading>
|
||||
<div>
|
||||
<Link to="create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{(stock_locations?.length ?? 0) > 0 ? (
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate(`/settings/locations/${row.original.id}`)
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<NoLocations />
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const LocationActions = ({
|
||||
location,
|
||||
}: {
|
||||
location: StockLocationExpandedDTO
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminDeleteStockLocation(location.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("locations.deleteLocationWarning", {
|
||||
name: location.name,
|
||||
}),
|
||||
verificationText: location.name,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/locations/${location.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<StockLocationExpandedDTO>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: (cell) => cell.getValue(),
|
||||
}),
|
||||
columnHelper.accessor("address", {
|
||||
header: t("fields.address"),
|
||||
cell: (cell) => {
|
||||
const value = cell.getValue()
|
||||
|
||||
if (!value) {
|
||||
return "-"
|
||||
}
|
||||
|
||||
return `${value.address_1}${value.city ? `, ${value.city}` : ""}`
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <LocationActions location={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const NoLocations = () => {
|
||||
const [params] = useSearchParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const noParams = params.toString().length === 0
|
||||
|
||||
if (noParams) {
|
||||
return (
|
||||
<NoRecords
|
||||
action={{
|
||||
label: t("locations.createLocation"),
|
||||
to: "/settings/locations/create",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <NoResults title={t("locations.noLocationsFound")} />
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminDeleteSalesChannel, useAdminSalesChannels } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../components/common/action-menu"
|
||||
import { OrderBy } from "../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const SalesChannelListTable = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { q, order } = useQueryParams(["q", "order"])
|
||||
|
||||
const { sales_channels, count, isLoading, isError, error } =
|
||||
useAdminSalesChannels(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
q,
|
||||
order,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: sales_channels ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("salesChannels.domain")}</Heading>
|
||||
<Link to="/settings/sales-channels/create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["name", "created_at", "updated_at"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate(`/settings/sales-channels/${row.original.id}`)
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const SalesChannelActions = ({
|
||||
salesChannel,
|
||||
}: {
|
||||
salesChannel: SalesChannel
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminDeleteSalesChannel(salesChannel.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirm = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("salesChannels.deleteSalesChannelWarning", {
|
||||
name: salesChannel.name,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: salesChannel.name,
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<SalesChannel>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("description", {
|
||||
header: t("fields.description"),
|
||||
cell: ({ getValue }) => (
|
||||
<div className="w-[200px] truncate">
|
||||
<span>{getValue()}</span>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
return (
|
||||
<div>
|
||||
<StatusBadge color={value ? "grey" : "green"}>
|
||||
{value ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <SalesChannelActions salesChannel={row.original} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -6,17 +6,12 @@ import type {
|
||||
AdminGiftCardsRes,
|
||||
AdminOrdersRes,
|
||||
AdminProductsRes,
|
||||
AdminPublishableApiKeysRes,
|
||||
AdminRegionsRes,
|
||||
AdminSalesChannelsRes,
|
||||
AdminUserRes,
|
||||
} from "@medusajs/medusa"
|
||||
import { Outlet, RouteObject } from "react-router-dom"
|
||||
|
||||
import { ProtectedRoute } from "../../components/authentication/require-auth"
|
||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||
import { MainLayout } from "../../components/layout/main-layout"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
|
||||
import routes from "medusa-admin:routes/pages"
|
||||
import settings from "medusa-admin:settings/pages"
|
||||
@@ -495,282 +490,6 @@ export const v1Routes: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <SettingsLayout />,
|
||||
handle: {
|
||||
crumb: () => "Settings",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: () => import("../../routes/settings"),
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
lazy: () => import("../../routes/profile/profile-detail"),
|
||||
handle: {
|
||||
crumb: () => "Profile",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/profile/profile-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "store",
|
||||
lazy: () => import("../../routes/store/store-detail"),
|
||||
handle: {
|
||||
crumb: () => "Store",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/store/store-edit"),
|
||||
},
|
||||
{
|
||||
path: "add-currencies",
|
||||
lazy: () => import("../../routes/store/store-add-currencies"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "locations",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Locations",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../routes/locations/location-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../routes/locations/location-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/locations/location-detail"),
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/locations/location-edit"),
|
||||
},
|
||||
{
|
||||
path: "add-sales-channels",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/locations/location-add-sales-channels"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "return-reasons",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Return Reasons",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../routes/return-reasons/return-reason-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/return-reasons/return-reason-create"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
lazy: () =>
|
||||
import("../../routes/return-reasons/return-reason-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "regions",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Regions",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../routes/regions/region-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () => import("../../routes/regions/region-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/regions/region-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminRegionsRes) => data.region.name,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/regions/region-edit"),
|
||||
},
|
||||
{
|
||||
path: "countries/add",
|
||||
lazy: () =>
|
||||
import("../../routes/regions/region-add-countries"),
|
||||
},
|
||||
{
|
||||
path: "shipping-options/:so_id/edit",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/regions/region-edit-shipping-option"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "shipping-options/create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/regions/region-create-shipping-option"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Users",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../routes/users/user-list"),
|
||||
children: [
|
||||
{
|
||||
path: "invite",
|
||||
lazy: () => import("../../routes/users/user-invite"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/users/user-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminUserRes) => data.user.email,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/users/user-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "taxes",
|
||||
handle: {
|
||||
crumb: () => "Taxes",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () => import("../../routes/taxes/tax-list"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/taxes/tax-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminRegionsRes) => data.region.name,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () => import("../../routes/taxes/tax-edit"),
|
||||
},
|
||||
{
|
||||
path: "tax-rates/create",
|
||||
lazy: () => import("../../routes/taxes/tax-rate-create"),
|
||||
},
|
||||
{
|
||||
path: "tax-rates/:rate_id/edit",
|
||||
lazy: () => import("../../routes/taxes/tax-rate-edit"),
|
||||
},
|
||||
{
|
||||
path: "tax-rates/:rate_id/edit-overrides",
|
||||
lazy: () =>
|
||||
import("../../routes/taxes/tax-rate-edit-overrides"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sales-channels",
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Sales Channels",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../routes/sales-channels/sales-channel-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/sales-channels/sales-channel-create"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () =>
|
||||
import("../../routes/sales-channels/sales-channel-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminSalesChannelsRes) =>
|
||||
data.sales_channel.name,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import("../../routes/sales-channels/sales-channel-edit"),
|
||||
},
|
||||
{
|
||||
path: "add-products",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/sales-channels/sales-channel-add-products"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...settingsExtensions,
|
||||
],
|
||||
},
|
||||
...routeExtensions,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Navigate, RouteObject, useLocation } from "react-router-dom"
|
||||
import { SalesChannelDTO, UserDTO } from "@medusajs/types"
|
||||
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
|
||||
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
import { AdminCollectionsRes } from "@medusajs/medusa"
|
||||
import { ErrorBoundary } from "../../components/error/error-boundary"
|
||||
import { MainLayout } from "../../components/layout-v2/main-layout"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { SearchProvider } from "../search-provider"
|
||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||
import { useMe } from "../../hooks/api/users"
|
||||
import { ApiKeyRes } from "../../types/api-responses"
|
||||
import { SearchProvider } from "../search-provider"
|
||||
import { SidebarProvider } from "../sidebar-provider"
|
||||
import { ApiKeyDTO } from "@medusajs/types"
|
||||
import { Spinner } from "@medusajs/icons"
|
||||
import { useV2Session } from "../../lib/api-v2"
|
||||
|
||||
export const ProtectedRoute = () => {
|
||||
const { user, isLoading } = useV2Session()
|
||||
const { user, isLoading } = useMe()
|
||||
const location = useLocation()
|
||||
|
||||
if (isLoading) {
|
||||
@@ -363,7 +362,10 @@ export const v2Routes: RouteObject[] = [
|
||||
"../../v2-routes/api-key-management/api-key-management-detail"
|
||||
),
|
||||
handle: {
|
||||
crumb: (data: { api_key: ApiKeyDTO }) => data.api_key.title,
|
||||
crumb: (data: ApiKeyRes) => {
|
||||
console.log("data", data)
|
||||
return data.apiKey.title
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { LocationAddSalesChannels as Component } from "./location-add-sales-channels"
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useAdminAddLocationToSalesChannel } from "medusa-react"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
|
||||
export const LocationAddSalesChannels = () => {
|
||||
const { mutateAsync } = useAdminAddLocationToSalesChannel() // TODO: We need a batch mutation instead of this to avoid multiple requests
|
||||
|
||||
return <RouteFocusModal></RouteFocusModal>
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { LocationCreate as Component } from "./location-create"
|
||||
@@ -1,10 +0,0 @@
|
||||
import { CreateLocationForm } from "../../../modules/locations/location-create/components/create-location-form"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
|
||||
export const LocationCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateLocationForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { LocationDetail as Component } from "./location-detail"
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Outlet, json, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { LocationGeneralSection } from "../../../modules/locations/location-detail/components/location-general-section"
|
||||
import { LocationSalesChannelSection } from "../../../modules/locations/location-detail/components/location-sales-channel-section"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
|
||||
export const LocationDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { stock_locations, isLoading, isError, error } = useAdminStockLocations(
|
||||
{
|
||||
id,
|
||||
expand: "address,sales_channels",
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const stock_location = stock_locations?.[0]
|
||||
|
||||
if (!stock_location) {
|
||||
throw json({ message: "Not found" }, 404)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<LocationGeneralSection location={stock_location} />
|
||||
<LocationSalesChannelSection location={stock_location} />
|
||||
<JsonViewSection data={stock_location} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { LocationEdit as Component } from "./location-edit"
|
||||
@@ -1,36 +0,0 @@
|
||||
import { EditLocationForm } from "../../../modules/locations/location-edit/components/edit-location-form/edit-location-form"
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
export const LocationEdit = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const { stock_locations, isLoading, isError, error } = useAdminStockLocations(
|
||||
{
|
||||
id,
|
||||
expand: "address",
|
||||
}
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const stock_location = stock_locations?.[0]
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading className="capitalize">{t("locations.editLocation")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && stock_location && (
|
||||
<EditLocationForm location={stock_location} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { LocationList as Component } from "./location-list"
|
||||
@@ -1,11 +0,0 @@
|
||||
import { LocationsListTable } from "../../../modules/locations/location-list/components/locations-list-table"
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
export const LocationList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<LocationsListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./profile-general-section"
|
||||
@@ -1,71 +0,0 @@
|
||||
import { User } from "@medusajs/medusa"
|
||||
import { Button, Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { languages } from "../../../../../i18n/config"
|
||||
|
||||
type ProfileGeneralSectionProps = {
|
||||
user: Omit<User, "password_hash">
|
||||
}
|
||||
|
||||
export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading>{t("profile.domain")}</Heading>
|
||||
<Text className="text-ui-fg-subtle" size="small">
|
||||
{t("profile.manageYourProfileDetails")}
|
||||
</Text>
|
||||
</div>
|
||||
<Link to="/settings/profile/edit">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.edit")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.name")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.email")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.role")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{t(`users.roles.${user.role}`)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("profile.language")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{languages.find((lang) => lang.code === i18n.language)
|
||||
?.display_name || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("profile.usageInsights")}
|
||||
</Text>
|
||||
<StatusBadge color="red" className="w-fit">
|
||||
{t("general.disabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { ProfileDetail as Component } from "./profile-detail"
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useAdminGetSession } from "medusa-react"
|
||||
import { Outlet, json } from "react-router-dom"
|
||||
import { ProfileGeneralSection } from "./components/profile-general-section"
|
||||
|
||||
export const ProfileDetail = () => {
|
||||
const { user, isLoading, isError, error } = useAdminGetSession()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !user) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error has occured", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ProfileGeneralSection user={user} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { User } from "@medusajs/medusa"
|
||||
import { Button, Input, Select, Switch } from "@medusajs/ui"
|
||||
import { adminAuthKeys, useAdminUpdateUser } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { languages } from "../../../../../i18n/config"
|
||||
import { queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
type EditProfileProps = {
|
||||
user: Omit<User, "password_hash">
|
||||
usageInsights: boolean
|
||||
}
|
||||
|
||||
const EditProfileSchema = zod.object({
|
||||
first_name: zod.string().optional(),
|
||||
last_name: zod.string().optional(),
|
||||
language: zod.string(),
|
||||
usage_insights: zod.boolean(),
|
||||
})
|
||||
|
||||
export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditProfileSchema>>({
|
||||
defaultValues: {
|
||||
first_name: user.first_name ?? "",
|
||||
last_name: user.last_name ?? "",
|
||||
language: i18n.language,
|
||||
usage_insights: usageInsights,
|
||||
},
|
||||
resolver: zodResolver(EditProfileSchema),
|
||||
})
|
||||
|
||||
const changeLanguage = (code: string) => {
|
||||
i18n.changeLanguage(code)
|
||||
}
|
||||
|
||||
const sortedLanguages = languages.sort((a, b) =>
|
||||
a.display_name.localeCompare(b.display_name)
|
||||
)
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Invalidate the current user session.
|
||||
queryClient.invalidateQueries(adminAuthKeys.details())
|
||||
},
|
||||
onError: () => {
|
||||
return
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
changeLanguage(values.language)
|
||||
|
||||
handleSuccess()
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="first_name"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.firstName")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} size="small" />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="last_name"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.lastName")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} size="small" />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<Form.Item className="gap-y-4">
|
||||
<div>
|
||||
<Form.Label>{t("profile.language")}</Form.Label>
|
||||
<Form.Hint>{t("profile.languageHint")}</Form.Hint>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
size="small"
|
||||
>
|
||||
<Select.Trigger ref={ref} className="py-1 text-[13px]">
|
||||
<Select.Value placeholder="Choose language">
|
||||
{
|
||||
sortedLanguages.find(
|
||||
(language) => language.code === field.value
|
||||
)?.display_name
|
||||
}
|
||||
</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{languages.map((language) => (
|
||||
<Select.Item
|
||||
key={language.code}
|
||||
value={language.code}
|
||||
>
|
||||
{language.display_name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_insights"
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>{t("profile.usageInsights")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...rest}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey="profile.userInsightsHint"
|
||||
components={[
|
||||
<a
|
||||
key="hint-link"
|
||||
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline"
|
||||
href="https://docs.medusajs.com/usage#admin-analytics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { ProfileEdit as Component } from "./profile-edit"
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminGetSession } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
|
||||
|
||||
export const ProfileEdit = () => {
|
||||
const { user, isLoading, isError, error } = useAdminGetSession()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header className="capitalize">
|
||||
<Heading>{t("profile.editProfile")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && user && (
|
||||
<EditProfileForm user={user} usageInsights={false} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminAddProductsToSalesChannel,
|
||||
useAdminProducts,
|
||||
} from "medusa-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import {
|
||||
ProductAvailabilityCell,
|
||||
ProductCollectionCell,
|
||||
ProductStatusCell,
|
||||
ProductTitleCell,
|
||||
ProductVariantCell,
|
||||
} from "../../../../components/common/product-table-cells"
|
||||
import { OrderBy } from "../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../components/route-modal"
|
||||
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||
import { queryClient } from "../../../../lib/medusa"
|
||||
|
||||
type AddProductsToSalesChannelFormProps = {
|
||||
salesChannel: SalesChannel
|
||||
}
|
||||
|
||||
const AddProductsToSalesChannelSchema = zod.object({
|
||||
product_ids: zod.array(zod.string()).min(1),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const AddProductsToSalesChannelForm = ({
|
||||
salesChannel,
|
||||
}: AddProductsToSalesChannelFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
|
||||
defaultValues: {
|
||||
product_ids: [],
|
||||
},
|
||||
resolver: zodResolver(AddProductsToSalesChannelSchema),
|
||||
})
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const { mutateAsync, isLoading: isMutating } =
|
||||
useAdminAddProductsToSalesChannel(salesChannel.id)
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
"product_ids",
|
||||
Object.keys(rowSelection).filter((k) => rowSelection[k]),
|
||||
{
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
}
|
||||
)
|
||||
}, [rowSelection, setValue])
|
||||
|
||||
const params = useQueryParams(["q", "order"])
|
||||
|
||||
const { products, count } = useAdminProducts(
|
||||
{
|
||||
expand: "variants,sales_channels",
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: (products ?? []) as Product[],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
enableRowSelection(row) {
|
||||
return !row.original.sales_channels
|
||||
?.map((sc) => sc.id)
|
||||
.includes(salesChannel.id)
|
||||
},
|
||||
meta: {
|
||||
salesChannelId: salesChannel.id,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
product_ids: values.product_ids.map((p) => ({ id: p })),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
/**
|
||||
* Invalidate the products list query to refetch products and
|
||||
* determine if they are added to the sales channel or not.
|
||||
*/
|
||||
queryClient.invalidateQueries(adminProductKeys.lists())
|
||||
handleSuccess()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
{form.formState.errors.product_ids && (
|
||||
<Hint variant="error">
|
||||
{form.formState.errors.product_ids.message}
|
||||
</Hint>
|
||||
)}
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["title"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex-1 overflow-y-auto">
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
},
|
||||
{
|
||||
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||
row.original.sales_channels
|
||||
?.map((sc) => sc.id)
|
||||
.includes(salesChannel.id),
|
||||
}
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="w-full border-t">
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const { salesChannelId } = table.options.meta as {
|
||||
salesChannelId: string
|
||||
}
|
||||
|
||||
const isAdded = row.original.sales_channels
|
||||
?.map((sc) => sc.id)
|
||||
.includes(salesChannelId)
|
||||
|
||||
const isSelected = row.getIsSelected() || isAdded
|
||||
|
||||
const Component = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isAdded}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isAdded) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={t("salesChannels.productAlreadyAdded")}
|
||||
side="right"
|
||||
>
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ row }) => {
|
||||
const product = row.original
|
||||
|
||||
return <ProductTitleCell product={product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("collection", {
|
||||
header: t("fields.collection"),
|
||||
cell: ({ getValue }) => {
|
||||
const collection = getValue()
|
||||
|
||||
return <ProductCollectionCell collection={collection} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channels", {
|
||||
header: t("fields.availability"),
|
||||
cell: ({ getValue }) => {
|
||||
const salesChannels = getValue()
|
||||
|
||||
return <ProductAvailabilityCell salesChannels={salesChannels} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("variants", {
|
||||
header: t("fields.inventory"),
|
||||
cell: (cell) => {
|
||||
const variants = cell.getValue()
|
||||
|
||||
return <ProductVariantCell variants={variants} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
|
||||
return <ProductStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./add-products-to-sales-channel-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { SalesChannelAddProducts as Component } from "./sales-channel-add-products"
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useAdminSalesChannel } from "medusa-react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { AddProductsToSalesChannelForm } from "./components"
|
||||
|
||||
export const SalesChannelAddProducts = () => {
|
||||
const { id } = useParams()
|
||||
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{!isLoading && sales_channel && (
|
||||
<AddProductsToSalesChannelForm salesChannel={sales_channel} />
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { SalesChannelCreate as Component } from "./sales-channel-create"
|
||||
@@ -1,10 +0,0 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateSalesChannelForm } from "../../../modules/sales-channels/sales-channel-create/components/create-sales-channel-form"
|
||||
|
||||
export const SalesChannelCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateSalesChannelForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./sales-channel-product-section"
|
||||
@@ -1,349 +0,0 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminDeleteProductsFromSalesChannel,
|
||||
useAdminProducts,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import {
|
||||
ProductStatusCell,
|
||||
ProductTitleCell,
|
||||
ProductVariantCell,
|
||||
} from "../../../../../components/common/product-table-cells"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { FilterGroup } from "../../../../../components/filtering/filter-group"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
import { queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
type SalesChannelProductSection = {
|
||||
salesChannel: SalesChannel
|
||||
}
|
||||
|
||||
export const SalesChannelProductSection = ({
|
||||
salesChannel,
|
||||
}: SalesChannelProductSection) => {
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const params = useQueryParams(["q", "order"])
|
||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||
{
|
||||
sales_channel_id: [salesChannel.id],
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useListColumns(salesChannel.id)
|
||||
|
||||
const table = useReactTable({
|
||||
data: (products ?? []) as Product[],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
getRowId: (row) => row.id,
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminDeleteProductsFromSalesChannel(
|
||||
salesChannel.id
|
||||
)
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onRemove = async () => {
|
||||
const ids = Object.keys(rowSelection).map((k) => ({ id: k }))
|
||||
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("salesChannels.removeProductsWarning", {
|
||||
count: ids.length,
|
||||
sales_channel: salesChannel.name,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
product_ids: ids,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
queryClient.invalidateQueries(adminProductKeys.lists())
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("products.domain")}</Heading>
|
||||
<Link to={`/settings/sales-channels/${salesChannel.id}/add-products`}>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.add")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<FilterGroup
|
||||
filters={{
|
||||
collection: "Collection",
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["title", "status", "created_at", "updated_at"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>
|
||||
{t("general.countSelected", {
|
||||
count: Object.keys(rowSelection).length,
|
||||
})}
|
||||
</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
action={onRemove}
|
||||
shortcut="r"
|
||||
label={t("actions.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const listColumnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useListColumns = (id: string) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
listColumnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
listColumnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ row }) => {
|
||||
const product = row.original
|
||||
|
||||
return <ProductTitleCell product={product} />
|
||||
},
|
||||
}),
|
||||
listColumnHelper.accessor("variants", {
|
||||
header: t("fields.variants"),
|
||||
cell: (cell) => {
|
||||
const variants = cell.getValue()
|
||||
|
||||
return <ProductVariantCell variants={variants} />
|
||||
},
|
||||
}),
|
||||
listColumnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
|
||||
return <ProductStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
listColumnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<ProductListCellActions
|
||||
productId={row.original.id}
|
||||
salesChannelId={id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const ProductListCellActions = ({
|
||||
salesChannelId,
|
||||
productId,
|
||||
}: {
|
||||
productId: string
|
||||
salesChannelId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync } = useAdminDeleteProductsFromSalesChannel(salesChannelId)
|
||||
|
||||
const onRemove = async () => {
|
||||
await mutateAsync({
|
||||
product_ids: [{ id: productId }],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/products/${productId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { salesChannelLoader as loader } from "./loader"
|
||||
export { SalesChannelDetail as Component } from "./sales-channel-detail"
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AdminSalesChannelsRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminProductKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
const salesChannelDetailQuery = (id: string) => ({
|
||||
queryKey: adminProductKeys.detail(id),
|
||||
queryFn: async () => medusa.admin.salesChannels.retrieve(id),
|
||||
})
|
||||
|
||||
export const salesChannelLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = salesChannelDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminSalesChannelsRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useAdminSalesChannel } from "medusa-react"
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { SalesChannelGeneralSection } from "../../../modules/sales-channels/sales-channel-detail/components/sales-channel-general-section"
|
||||
import { SalesChannelProductSection } from "./components/sales-channel-product-section"
|
||||
import { salesChannelLoader } from "./loader"
|
||||
|
||||
export const SalesChannelDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof salesChannelLoader>
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { sales_channel, isLoading } = useAdminSalesChannel(id!, {
|
||||
initialData,
|
||||
})
|
||||
|
||||
if (isLoading || !sales_channel) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
console.log("SalesChannelDetail")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<SalesChannelGeneralSection salesChannel={sales_channel} />
|
||||
<SalesChannelProductSection salesChannel={sales_channel} />
|
||||
<JsonViewSection data={sales_channel} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { SalesChannelEdit as Component } from "./sales-channel-edit"
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminSalesChannel } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditSalesChannelForm } from "../../../modules/sales-channels/sales-channel-edit/components/edit-sales-channel-form"
|
||||
|
||||
export const SalesChannelEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { sales_channel, isLoading, isError, error } = useAdminSalesChannel(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading className="capitalize">
|
||||
{t("salesChannels.editSalesChannel")}
|
||||
</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && !!sales_channel && (
|
||||
<EditSalesChannelForm salesChannel={sales_channel} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { SalesChannelList as Component } from "./sales-channel-list";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { SalesChannelListTable } from "../../../modules/sales-channels/sales-channel-list/components"
|
||||
|
||||
export const SalesChannelList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<SalesChannelListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
import { Currency, type Store } from "@medusajs/medusa"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Hint,
|
||||
StatusBadge,
|
||||
Table,
|
||||
Tooltip,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminCurrencies, useAdminUpdateStore } from "medusa-react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
type AddCurrenciesFormProps = {
|
||||
store: Store
|
||||
}
|
||||
|
||||
const AddCurrenciesSchema = zod.object({
|
||||
currencies: zod.array(zod.string()).min(1),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof AddCurrenciesSchema>>({
|
||||
defaultValues: {
|
||||
currencies: [],
|
||||
},
|
||||
resolver: zodResolver(AddCurrenciesSchema),
|
||||
})
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
useEffect(() => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
setValue("currencies", ids, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}, [rowSelection, setValue])
|
||||
|
||||
const params = useQueryParams(["order"])
|
||||
const { currencies, count, isError, error } = useAdminCurrencies({
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
})
|
||||
|
||||
const preSelectedRows = store.currencies.map((c) => c.code)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: currencies ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
getRowId: (row) => row.code,
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
enableRowSelection: (row) => !preSelectedRows.includes(row.original.code),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading: isMutating } = useAdminUpdateStore()
|
||||
|
||||
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const currencies = Array.from(
|
||||
new Set([...data.currencies, ...preSelectedRows])
|
||||
) as string[]
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
currencies,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{form.formState.errors.currencies && (
|
||||
<Hint variant="error">
|
||||
{form.formState.errors.currencies.message}
|
||||
</Hint>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<OrderBy keys={["code"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
ref={tableContainerRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<Table className="relative">
|
||||
<Table.Header
|
||||
className={clx(
|
||||
"bg-ui-bg-base transition-fg sticky inset-x-0 top-0 z-10 border-t-0",
|
||||
{
|
||||
"shadow-elevation-card-hover": isScrolled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg last-of-type:border-b-0",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
},
|
||||
{
|
||||
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||
!row.getCanSelect(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="w-full border-t">
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</RouteFocusModal.Body>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Currency>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isPreSelected = !row.getCanSelect()
|
||||
const isSelected = row.getIsSelected() || isPreSelected
|
||||
|
||||
const Component = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isPreSelected}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isPreSelected) {
|
||||
return (
|
||||
<Tooltip content={t("store.currencyAlreadyAdded")} side="right">
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("code", {
|
||||
header: t("fields.code"),
|
||||
cell: ({ getValue }) => (
|
||||
<Badge size="small">{getValue().toUpperCase()}</Badge>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("includes_tax", {
|
||||
header: t("fields.taxInclusivePricing"),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
|
||||
return (
|
||||
<StatusBadge color={value ? "green" : "red"}>
|
||||
{value ? t("general.enabled") : t("general.disabled")}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { StoreAddCurrencies as Component } from "./store-add-currencies"
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useAdminStore } from "medusa-react"
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
|
||||
|
||||
export const StoreAddCurrencies = () => {
|
||||
const { store, isLoading, isError, error } = useAdminStore()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{!isLoading && store && <AddCurrenciesForm store={store} />}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./store-currency-section"
|
||||
@@ -1,292 +0,0 @@
|
||||
import { Trash } from "@medusajs/icons"
|
||||
import { Currency, Store } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminUpdateStore } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
||||
import { LocalizedTablePagination } from "../../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
type StoreCurrencySectionProps = {
|
||||
store: Store
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: store.currencies,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getRowId: (row) => row.code,
|
||||
pageCount: Math.ceil(store.currencies.length / PAGE_SIZE),
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
meta: {
|
||||
currencyCodes: store.currencies.map((c) => c.code),
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useAdminUpdateStore()
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDeleteCurrencies = async () => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeCurrencyWarning", {
|
||||
count: ids.length,
|
||||
}),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
currencies: store.currencies
|
||||
.filter((c) => !ids.includes(c.code))
|
||||
.map((c) => c.code),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("store.currencies")}</Heading>
|
||||
<div>
|
||||
<Link to="/settings/store/add-currencies">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.add")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx("transition-fg", {
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
})}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={store.currencies.length}
|
||||
pageIndex={table.getState().pagination.pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>
|
||||
{t("general.countSelected", {
|
||||
count: Object.keys(rowSelection).length,
|
||||
})}
|
||||
</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
action={handleDeleteCurrencies}
|
||||
shortcut="r"
|
||||
label={t("actions.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CurrencyActions = ({
|
||||
currency,
|
||||
currencyCodes,
|
||||
}: {
|
||||
currency: Currency
|
||||
currencyCodes: string[]
|
||||
}) => {
|
||||
const { mutateAsync } = useAdminUpdateStore()
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleRemove = async () => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeCurrencyWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: currency.name,
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
currencies: currencyCodes.filter((c) => c !== currency.code),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.remove"),
|
||||
onClick: handleRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Currency>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("code", {
|
||||
header: t("fields.code"),
|
||||
cell: ({ getValue }) => getValue().toUpperCase(),
|
||||
}),
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("includes_tax", {
|
||||
header: "Tax Inclusive Prices",
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue()
|
||||
const text = value ? t("general.enabled") : t("general.disabled")
|
||||
|
||||
return (
|
||||
<StatusBadge color={value ? "green" : "red"}>{text}</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { currencyCodes } = table.options.meta as {
|
||||
currencyCodes: string[]
|
||||
}
|
||||
|
||||
return (
|
||||
<CurrencyActions
|
||||
currency={row.original}
|
||||
currencyCodes={currencyCodes}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./store-general-section"
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Store } from "@medusajs/medusa"
|
||||
import { Badge, Button, Container, Copy, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
type StoreGeneralSectionProps = {
|
||||
store: Store
|
||||
}
|
||||
|
||||
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading>{t("store.domain")}</Heading>
|
||||
<Text className="text-ui-fg-subtle" size="small">
|
||||
{t("store.manageYourStoresDetails")}
|
||||
</Text>
|
||||
</div>
|
||||
<Link to={"/settings/store/edit"}>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.edit")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.name")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{store.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.defaultCurrency")}
|
||||
</Text>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge size="2xsmall">
|
||||
{store.default_currency_code.toUpperCase()}
|
||||
</Badge>
|
||||
<Text size="small" leading="compact">
|
||||
{store.default_currency.name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.swapLinkTemplate")}
|
||||
</Text>
|
||||
{store.swap_link_template ? (
|
||||
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{store.swap_link_template}
|
||||
</Text>
|
||||
<Copy
|
||||
content={store.swap_link_template}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.paymentLinkTemplate")}
|
||||
</Text>
|
||||
{store.payment_link_template ? (
|
||||
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{store.payment_link_template}
|
||||
</Text>
|
||||
<Copy
|
||||
content={store.payment_link_template}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("store.inviteLinkTemplate")}
|
||||
</Text>
|
||||
{store.invite_link_template ? (
|
||||
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{store.invite_link_template}
|
||||
</Text>
|
||||
<Copy
|
||||
content={store.invite_link_template}
|
||||
variant="mini"
|
||||
className="text-ui-fg-subtle"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { storeLoader as loader } from "./loader"
|
||||
export { StoreDetail as Component } from "./store-detail"
|
||||
@@ -1,37 +0,0 @@
|
||||
import { AdminExtendedStoresRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminStoreKeys } from "medusa-react"
|
||||
import { redirect } from "react-router-dom"
|
||||
|
||||
import { FetchQueryOptions } from "@tanstack/react-query"
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
const storeDetailQuery = () => ({
|
||||
queryKey: adminStoreKeys.details(),
|
||||
queryFn: async () => medusa.admin.store.retrieve(),
|
||||
})
|
||||
|
||||
const fetchQuery = async (
|
||||
query: FetchQueryOptions<Response<AdminExtendedStoresRes>>
|
||||
) => {
|
||||
try {
|
||||
const res = await queryClient.fetchQuery(query)
|
||||
return res
|
||||
} catch (error) {
|
||||
const err = error ? JSON.parse(JSON.stringify(error)) : null
|
||||
|
||||
if ((err as Error & { status: number })?.status === 401) {
|
||||
redirect("/login", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const storeLoader = async () => {
|
||||
const query = storeDetailQuery()
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminExtendedStoresRes>>(
|
||||
query.queryKey
|
||||
) ?? (await fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useAdminStore } from "medusa-react"
|
||||
import { Outlet, useLoaderData } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section/json-view-section.tsx"
|
||||
import { StoreCurrencySection } from "./components/store-currency-section/store-currencies-section.tsx/index.ts"
|
||||
import { StoreGeneralSection } from "./components/store-general-section/index.ts"
|
||||
import { storeLoader } from "./loader.ts"
|
||||
|
||||
export const StoreDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
|
||||
|
||||
const { store, isLoading, isError, error } = useAdminStore({
|
||||
initialData: initialData,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !store) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return <div>{JSON.stringify(error, null, 2)}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<StoreGeneralSection store={store} />
|
||||
<StoreCurrencySection store={store} />
|
||||
<JsonViewSection data={store} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import type { Store } from "@medusajs/medusa"
|
||||
import { Button, Input } from "@medusajs/ui"
|
||||
import { useAdminUpdateStore } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
|
||||
type EditStoreFormProps = {
|
||||
store: Store
|
||||
}
|
||||
|
||||
const EditStoreSchema = zod.object({
|
||||
name: zod.string().optional(),
|
||||
swap_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
|
||||
payment_link_template: zod.union([
|
||||
zod.literal(""),
|
||||
zod.string().trim().url(),
|
||||
]),
|
||||
invite_link_template: zod.union([zod.literal(""), zod.string().trim().url()]),
|
||||
})
|
||||
|
||||
export const EditStoreForm = ({ store }: EditStoreFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditStoreSchema>>({
|
||||
defaultValues: {
|
||||
name: store.name,
|
||||
swap_link_template: store.swap_link_template ?? "",
|
||||
payment_link_template: store.payment_link_template ?? "",
|
||||
invite_link_template: store.invite_link_template ?? "",
|
||||
},
|
||||
resolver: zodResolver(EditStoreSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateStore()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(
|
||||
{
|
||||
name: values.name,
|
||||
invite_link_template: values.invite_link_template || undefined,
|
||||
swap_link_template: values.swap_link_template || undefined,
|
||||
payment_link_template: values.payment_link_template || undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.name")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input size="small" {...field} placeholder="ACME" />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="swap_link_template"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("store.swapLinkTemplate")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
size="small"
|
||||
{...field}
|
||||
placeholder="https://www.store.com/swap={id}"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="payment_link_template"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("store.paymentLinkTemplate")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
size="small"
|
||||
{...field}
|
||||
placeholder="https://www.store.com/payment={id}"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="invite_link_template"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("store.inviteLinkTemplate")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
size="small"
|
||||
{...field}
|
||||
placeholder="https://www.admin.com/invite?token={invite_token}"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" isLoading={isLoading} type="submit">
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { StoreEdit as Component } from "./store-edit"
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useAdminStore } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { json } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { EditStoreForm } from "./components/edit-store-form/edit-store-form"
|
||||
|
||||
export const StoreEdit = () => {
|
||||
const { t } = useTranslation()
|
||||
const { store, isLoading, isError, error } = useAdminStore()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!store && !isLoading) {
|
||||
throw json("An unknown error has occured", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading className="capitalize">{t("store.editStore")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{store && <EditStoreForm store={store} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./user-general-section"
|
||||
@@ -1,96 +0,0 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { User } from "@medusajs/medusa"
|
||||
import { Container, Heading, Text, clx, usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteUser } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type UserGeneralSectionProps = {
|
||||
user: Omit<User, "password_hash">
|
||||
}
|
||||
|
||||
export const UserGeneralSection = ({ user }: UserGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteUser(user.id)
|
||||
|
||||
const name = [user.first_name, user.last_name].filter(Boolean).join(" ")
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("users.deleteUserWarning", {
|
||||
name: name ?? user.email,
|
||||
}),
|
||||
verificationText: name ?? user.email,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("..")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{user.email}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "edit",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDeleteUser,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.name")}
|
||||
</Text>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className={clx({
|
||||
"text-ui-fg-subtle": !name,
|
||||
})}
|
||||
>
|
||||
{name ?? "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.role")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{t(`users.roles.${user.role}`)}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { userLoader as loader } from "./loader"
|
||||
export { UserDetail as Component } from "./user-detail"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user