feat(dashboard): Migrate to new hooks and API client (#6963)

This commit is contained in:
Kasper Fabricius Kristensen
2024-04-05 18:27:08 +02:00
committed by GitHub
parent 5ba74ec5fc
commit 8a5c6928f7
195 changed files with 3919 additions and 6028 deletions

View File

@@ -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",

View File

@@ -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}}",

View File

@@ -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 (

View File

@@ -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()

View File

@@ -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 (

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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 = ({

View File

@@ -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) => {

View 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)
},
})
}

View 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,
})
}

View 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,
})
}

View 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 }
}

View 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,
})
}

View 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,
})
}

View 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 { 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 }
}

View 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 }
}

View 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 }
}

View 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,
})
}

View 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,
})
}

View 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,
})
}

View 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,
})
}

View 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,
})
}

View File

@@ -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 }
}

View File

@@ -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>[]
)
}

View File

@@ -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(

View File

@@ -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",

View File

@@ -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 {

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View 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",
})
}

View 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,
}

View 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,
}

View File

@@ -0,0 +1 @@
export * from "./client"

View 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,
}

View File

@@ -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,
}

View 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,
}

View 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,
}

View 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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View 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,
}

View 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,
}

View File

@@ -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,
}

View 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
}

View File

@@ -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]
)
}

View File

@@ -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")} />
}

View File

@@ -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]
)
}

View File

@@ -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,
],
},

View File

@@ -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: [
{

View File

@@ -1 +0,0 @@
export { LocationAddSalesChannels as Component } from "./location-add-sales-channels"

View File

@@ -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>
}

View File

@@ -1 +0,0 @@
export { LocationCreate as Component } from "./location-create"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { LocationDetail as Component } from "./location-detail"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { LocationEdit as Component } from "./location-edit"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { LocationList as Component } from "./location-list"

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { ProfileDetail as Component } from "./profile-detail"

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { ProfileEdit as Component } from "./profile-edit"

View File

@@ -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>
)
}

View File

@@ -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]
)
}

View File

@@ -1 +0,0 @@
export * from "./add-products-to-sales-channel-form"

View File

@@ -1 +0,0 @@
export { SalesChannelAddProducts as Component } from "./sales-channel-add-products"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { SalesChannelCreate as Component } from "./sales-channel-create"

View File

@@ -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>
)
}

View File

@@ -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,
},
],
},
]}
/>
)
}

View File

@@ -1,2 +0,0 @@
export { salesChannelLoader as loader } from "./loader"
export { SalesChannelDetail as Component } from "./sales-channel-detail"

View File

@@ -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))
)
}

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { SalesChannelEdit as Component } from "./sales-channel-edit"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { SalesChannelList as Component } from "./sales-channel-list";

View File

@@ -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>
)
}

View File

@@ -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]
)
}

View File

@@ -1 +0,0 @@
export { StoreAddCurrencies as Component } from "./store-add-currencies"

View File

@@ -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>
)
}

View File

@@ -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]
)
}

View File

@@ -1 +0,0 @@
export * from "./store-general-section"

View File

@@ -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>
)
}

View File

@@ -1,2 +0,0 @@
export { storeLoader as loader } from "./loader"
export { StoreDetail as Component } from "./store-detail"

View File

@@ -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))
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { StoreEdit as Component } from "./store-edit"

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export * from "./user-general-section"

View File

@@ -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>
)
}

View File

@@ -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