feat(dashboard): Admin UI regions v2 (#6943)

This commit is contained in:
Frane Polić
2024-04-06 17:41:54 +02:00
committed by GitHub
parent df0751f122
commit 58c68f6715
72 changed files with 475 additions and 1687 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(medusa, types): list payment providers endpoint

View File

@@ -0,0 +1,33 @@
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
jest.setTimeout(50000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Payment Providers", () => {
let appContainer
beforeAll(async () => {
appContainer = getContainer()
})
it("should list payment providers", async () => {
let response = await api.get(`/admin/payments/payment-providers`)
expect(response.status).toEqual(200)
expect(response.data.payment_providers).toEqual([
expect.objectContaining({
id: "pp_system_default_2",
}),
expect.objectContaining({
id: "pp_system_default",
}),
])
expect(response.data.count).toEqual(2)
})
})
},
})

View File

@@ -817,7 +817,7 @@
"removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.",
"removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.",
"taxInclusiveHint": "When enabled prices in the region will be tax inclusive.",
"providersHint": " Add which fulfillment and payment providers should be available in this region.",
"providersHint": " Add which payment providers should be available in this region.",
"shippingOptions": "Shipping Options",
"deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.",
"return": "Return",

View File

@@ -24,7 +24,7 @@ async function generateCountries() {
const dest = path.join(__dirname, "../src/lib/countries.ts")
const destDir = path.dirname(dest)
const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { Country } from "@medusajs/medusa"\n\nexport const countries: Omit<Country, "region" | "region_id" | "id">[] = ${json}`
const fileContent = `/** This file is auto-generated. Do not modify it manually. */\nimport type { RegionCountryDTO } from "@medusajs/types"\n\nexport const countries: Omit<RegionCountryDTO, "id">[] = ${json}`
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })

View File

@@ -1,9 +1,12 @@
import { Country } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { RegionCountryDTO } from "@medusajs/types"
import { PlaceholderCell } from "../../common/placeholder-cell"
import { ListSummary } from "../../../../common/list-summary"
import { countries as COUNTRIES } from "../../../../../lib/countries"
type CountriesCellProps = {
countries?: Country[] | null
countries?: RegionCountryDTO[] | null
}
export const CountriesCell = ({ countries }: CountriesCellProps) => {
@@ -13,26 +16,14 @@ export const CountriesCell = ({ countries }: CountriesCellProps) => {
return <PlaceholderCell />
}
const displayValue = countries
.slice(0, 2)
.map((c) => c.display_name)
.join(", ")
const additionalCountries = countries
.slice(2)
.map((c) => c.display_name).length
const text = `${displayValue}${
additionalCountries > 0
? ` ${t("general.plusCountMore", {
count: additionalCountries,
})}`
: ""
}`
return (
<div className="flex size-full items-center overflow-hidden">
<span className="truncate">{text}</span>
<ListSummary
list={countries.map(
(country) =>
COUNTRIES.find((c) => c.iso_2 === country.iso_2)!.display_name
)}
/>
</div>
)
}

View File

@@ -1,39 +1,26 @@
import { PaymentProvider } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { PaymentProviderDTO } from "@medusajs/types"
import { formatProvider } from "../../../../../lib/format-provider"
import { PlaceholderCell } from "../../common/placeholder-cell"
import { ListSummary } from "../../../../common/list-summary"
type PaymentProvidersCellProps = {
paymentProviders?: PaymentProvider[] | null
paymentProviders?: PaymentProviderDTO[] | null
}
export const PaymentProvidersCell = ({
paymentProviders,
}: PaymentProvidersCellProps) => {
const { t } = useTranslation()
if (!paymentProviders || paymentProviders.length === 0) {
return <PlaceholderCell />
}
const displayValue = paymentProviders
.slice(0, 2)
.map((p) => formatProvider(p.id))
.join(", ")
const additionalProviders = paymentProviders.slice(2).length
const text = `${displayValue}${
additionalProviders > 0
? ` ${t("general.plusCountMore", {
count: additionalProviders,
})}`
: ""
}`
const displayValues = paymentProviders.map((p) => formatProvider(p.id))
return (
<div className="flex size-full items-center overflow-hidden">
<span className="truncate">{text}</span>
<ListSummary list={displayValues} />
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
import { PaymentProvidersListRes } from "../../types/api-responses"
import { client } from "../../lib/client"
export const usePaymentProviders = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<
PaymentProvidersListRes,
Error,
PaymentProvidersListRes,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: async () => client.payments.listPaymentProviders(query),
queryKey: [],
...options,
})
return { ...data, ...rest }
}

View File

@@ -20,6 +20,7 @@ const regionsQueryKeys = queryKeysFactory(REGIONS_QUERY_KEY)
export const useRegion = (
id: string,
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<RegionRes, Error, RegionRes, QueryKey>,
"queryFn" | "queryKey"
@@ -27,7 +28,7 @@ export const useRegion = (
) => {
const { data, ...rest } = useQuery({
queryKey: regionsQueryKeys.detail(id),
queryFn: async () => client.regions.retrieve(id),
queryFn: async () => client.regions.retrieve(id, query),
...options,
})

View File

@@ -1,15 +1,11 @@
import { Region } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { RegionDTO } from "@medusajs/types"
import {
CountriesCell,
CountriesHeader,
} from "../../../components/table/table-cells/region/countries-cell"
import {
FulfillmentProvidersCell,
FulfillmentProvidersHeader,
} from "../../../components/table/table-cells/region/fulfillment-providers-cell"
import {
PaymentProvidersCell,
PaymentProvidersHeader,
@@ -19,7 +15,7 @@ import {
RegionHeader,
} from "../../../components/table/table-cells/region/region-cell"
const columnHelper = createColumnHelper<Region>()
const columnHelper = createColumnHelper<RegionDTO>()
export const useRegionTableColumns = () => {
return useMemo(
@@ -38,12 +34,6 @@ export const useRegionTableColumns = () => {
<PaymentProvidersCell paymentProviders={getValue()} />
),
}),
columnHelper.accessor("fulfillment_providers", {
header: () => <FulfillmentProvidersHeader />,
cell: ({ getValue }) => (
<FulfillmentProvidersCell fulfillmentProviders={getValue()} />
),
}),
],
[]
)

View File

@@ -1,2 +1,3 @@
export * from "./auth"
export * from "./store"
export * from "./campaign"
export * from "./currencies"

View File

@@ -1,23 +0,0 @@
import { RegionDTO } from "@medusajs/types"
import { adminRegionKeys, useAdminCustomQuery } from "medusa-react"
import { V2ListRes } from "./types/common"
export const useV2Regions = (query?: any, options?: any) => {
const { data, ...rest } = useAdminCustomQuery(
"/regions",
adminRegionKeys.list(query),
query,
options
)
const typedData: {
regions: RegionDTO[] | undefined
} & V2ListRes = {
regions: data?.regions,
count: data?.count,
offset: data?.offset,
limit: data?.limit,
}
return { ...typedData, ...rest }
}

View File

@@ -1,32 +0,0 @@
import {
adminStoreKeys,
useAdminCustomPost,
useAdminCustomQuery,
} from "medusa-react"
import { Store } from "./types/store"
// TODO: Add types once we export V2 API types
export const useV2Store = (options?: any) => {
const { data, isLoading, isError, error, ...rest } = useAdminCustomQuery(
"/admin/stores",
adminStoreKeys.details(),
undefined,
options
)
const store = data?.stores[0] as Store | undefined
let hasError = isError
let err: Error | null = error
if (!isLoading && !isError && typeof store === "undefined") {
hasError = true
err = new Error("Store not found")
}
return { store, isLoading, isError: hasError, error: err, ...rest }
}
export const useV2UpdateStore = (id: string) => {
return useAdminCustomPost(`/admin/stores/${id}`, adminStoreKeys.detail(id))
}

View File

@@ -1,5 +1,7 @@
import { CurrencyDTO, StoreDTO } from "@medusajs/types"
import { CurrencyDTO, PaymentProviderDTO, StoreDTO } from "@medusajs/types"
export type Store = StoreDTO & {
default_currency: CurrencyDTO | null
currencies?: CurrencyDTO[]
payment_providers?: PaymentProviderDTO[]
}

View File

@@ -8,6 +8,7 @@ import { customers } from "./customers"
import { invites } from "./invites"
import { productTypes } from "./product-types"
import { products } from "./products"
import { payments } from "./payments"
import { promotions } from "./promotions"
import { regions } from "./regions"
import { salesChannels } from "./sales-channels"
@@ -26,6 +27,7 @@ export const client = {
currencies: currencies,
collections: collections,
promotions: promotions,
payments: payments,
stores: stores,
salesChannels: salesChannels,
tags: tags,

View File

@@ -0,0 +1,13 @@
import { getRequest } from "./common"
import { PaymentProvidersListRes } from "../../types/api-responses"
async function listPaymentProviders(query?: Record<string, any>) {
return getRequest<PaymentProvidersListRes, Record<string, any>>(
`/admin/payments/payment-providers`,
query
)
}
export const payments = {
listPaymentProviders,
}

View File

@@ -1,7 +1,7 @@
/** This file is auto-generated. Do not modify it manually. */
import type { Country } from "@medusajs/medusa"
import type { RegionCountryDTO } from "@medusajs/types"
export const countries: Omit<Country, "region" | "region_id" | "id">[] = [
export const countries: Omit<RegionCountryDTO, "id">[] = [
{
iso_2: "af",
iso_3: "afg",
@@ -888,8 +888,8 @@ export const countries: Omit<Country, "region" | "region_id" | "id">[] = [
iso_2: "ly",
iso_3: "lby",
num_code: 434,
name: "LIBYAN ARAB JAMAHIRIYA",
display_name: "Libyan Arab Jamahiriya",
name: "LIBYA",
display_name: "Libya",
},
{
iso_2: "li",

View File

@@ -1,5 +1,5 @@
/** This file is auto-generated. Do not modify it manually. */
type CurrencyInfo = {
export type CurrencyInfo = {
code: string
name: string
symbol_native: string

View File

@@ -1,12 +1,18 @@
/**
* Providers only have an ID to identify them. This function formats the ID
* into a human-readable string.
*
* Format example: pp_stripe-blik_dkk
*
* @param id - The ID of the provider
* @returns A formatted string
*/
export const formatProvider = (id: string) => {
return id
.split("-")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(" ")
const [_, name, type] = id.split("_")
return (
name
.split("-")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(" ") + (type ? ` (${type})` : "")
)
}

View File

@@ -254,6 +254,43 @@ export const v2Routes: RouteObject[] = [
},
],
},
{
path: "regions",
element: <Outlet />,
handle: {
crumb: () => "Regions",
},
children: [
{
path: "",
lazy: () => import("../../v2-routes/regions/region-list"),
children: [
{
path: "create",
lazy: () => import("../../v2-routes/regions/region-create"),
},
],
},
{
path: ":id",
lazy: () => import("../../v2-routes/regions/region-detail"),
handle: {
crumb: (data: AdminRegionsRes) => data.region.name,
},
children: [
{
path: "edit",
lazy: () => import("../../v2-routes/regions/region-edit"),
},
{
path: "countries/add",
lazy: () =>
import("../../v2-routes/regions/region-add-countries"),
},
],
},
],
},
{
path: "store",
lazy: () => import("../../v2-routes/store/store-detail"),

View File

@@ -1,563 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
FulfillmentOption,
Region,
ShippingOptionRequirement,
ShippingProfile,
} from "@medusajs/medusa"
import {
Button,
CurrencyInput,
Heading,
Input,
RadioGroup,
Select,
Switch,
Text,
clx,
} from "@medusajs/ui"
import { useAdminCreateShippingOption } from "medusa-react"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { IncludesTaxTooltip } from "../../../../../components/common/tax-badge/tax-badge"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { formatProvider } from "../../../../../lib/format-provider"
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
import { ShippingOptionPriceType } from "../../../shared/constants"
type CreateShippingOptionProps = {
region: Region
shippingProfiles: ShippingProfile[]
fulfillmentOptions: FulfillmentOption[]
}
enum ShippingOptionType {
OUTBOUND = "outbound",
RETURN = "return",
}
const CreateShippingOptionSchema = zod
.object({
name: zod.string().min(1),
type: zod.nativeEnum(ShippingOptionType),
admin_only: zod.boolean(),
provider_id: zod.string().min(1),
profile_id: zod.string().min(1),
includes_tax: zod.boolean(),
price_type: zod.nativeEnum(ShippingOptionPriceType),
amount: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return false
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Amount must be a positive number")
.optional(),
min_subtotal: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return true
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Min. subtotal must be a positive number")
.optional(),
max_subtotal: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return true
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Max. subtotal must be a positive number")
.optional(),
})
.superRefine((data, ctx) => {
if (data.price_type === ShippingOptionPriceType.FLAT_RATE) {
if (typeof data.amount === "string" && data.amount === "") {
return ctx.addIssue({
code: "custom",
message: "Amount is required",
path: ["amount"],
})
}
}
})
export const CreateShippingOptionForm = ({
region,
fulfillmentOptions,
shippingProfiles,
}: CreateShippingOptionProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<zod.infer<typeof CreateShippingOptionSchema>>({
defaultValues: {
name: "",
type: ShippingOptionType.OUTBOUND,
admin_only: false,
provider_id: "",
profile_id: "",
price_type: ShippingOptionPriceType.FLAT_RATE,
includes_tax: false,
amount: "",
min_subtotal: "",
max_subtotal: "",
},
resolver: zodResolver(CreateShippingOptionSchema),
})
const watchedPriceType = useWatch({
control: form.control,
name: "price_type",
defaultValue: ShippingOptionPriceType.FLAT_RATE,
})
const isFlatRate = watchedPriceType === ShippingOptionPriceType.FLAT_RATE
const includesTax = useWatch({
control: form.control,
name: "includes_tax",
defaultValue: false,
})
const { mutateAsync, isLoading } = useAdminCreateShippingOption()
const getPricePayload = (amount?: string | number) => {
if (!amount) {
return undefined
}
const amountValue = typeof amount === "string" ? Number(amount) : amount
return getDbAmount(amountValue, region.currency_code)
}
const handleSubmit = form.handleSubmit(async (values) => {
const { type, amount, min_subtotal, max_subtotal, ...rest } = values
const amountPayload = getPricePayload(amount)
const minSubtotalPayload = getPricePayload(min_subtotal)
const maxSubtotalPayload = getPricePayload(max_subtotal)
const minSubtotalRequirement = minSubtotalPayload
? {
amount: minSubtotalPayload,
type: "min_subtotal",
}
: undefined
const maxSubtotalRequirement = maxSubtotalPayload
? {
amount: maxSubtotalPayload,
type: "max_subtotal",
}
: undefined
const requirements = [
minSubtotalRequirement,
maxSubtotalRequirement,
].filter(Boolean) as ShippingOptionRequirement[]
await mutateAsync(
{
region_id: region.id,
data: {},
is_return: type === ShippingOptionType.RETURN,
amount: isFlatRate ? amountPayload : undefined,
requirements: requirements.length ? requirements : undefined,
...rest,
},
{
onSuccess: () => {
handleSuccess()
},
onError: (error) => {
console.error(error)
},
}
)
})
return (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<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={isLoading}>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="overflow-hidden">
<div
className={clx(
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
)}
id="form-section"
>
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>
{t("regions.shippingOption.createShippingOption")}
</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("regions.shippingOption.createShippingOptionHint")}
</Text>
</div>
<Form.Field
control={form.control}
name="type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.type")}</Form.Label>
<Form.Control>
<RadioGroup
{...field}
onValueChange={field.onChange}
className="grid grid-cols-1 gap-4 md:grid-cols-2"
>
<RadioGroup.ChoiceBox
value={ShippingOptionType.OUTBOUND}
label={t("regions.shippingOption.type.outbound")}
description={t(
"regions.shippingOption.type.outboundHint"
)}
/>
<RadioGroup.ChoiceBox
value={ShippingOptionType.RETURN}
label={t("regions.shippingOption.type.return")}
description={t(
"regions.shippingOption.type.returnHint"
)}
/>
</RadioGroup>
</Form.Control>
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<Form.Field
control={form.control}
name="admin_only"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-center justify-between">
<Form.Label>
{t("regions.shippingOption.availability.adminOnly")}
</Form.Label>
<Form.Control>
<Switch
checked={value}
onCheckedChange={onChange}
{...field}
/>
</Form.Control>
</div>
<Form.Hint>
{t(
"regions.shippingOption.availability.adminOnlyHint"
)}
</Form.Hint>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<Form.Field
control={form.control}
name="includes_tax"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-start justify-between">
<Form.Label>
{t("fields.taxInclusivePricing")}
</Form.Label>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.Hint>
{t("regions.shippingOption.taxInclusiveHint")}
</Form.Hint>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="price_type"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>
{t("regions.shippingOption.priceType.label")}
</Form.Label>
<Form.Control>
<Select onValueChange={onChange} {...field}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item
value={ShippingOptionPriceType.FLAT_RATE}
>
{t(
"regions.shippingOption.priceType.flatRate"
)}
</Select.Item>
<Select.Item
value={ShippingOptionPriceType.CALCULATED}
>
{t(
"regions.shippingOption.priceType.calculated"
)}
</Select.Item>
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{isFlatRate && (
<Form.Field
control={form.control}
name="amount"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={
<IncludesTaxTooltip includesTax={includesTax} />
}
>
{t("fields.price")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
</div>
</div>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="profile_id"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("fields.shippingProfile")}</Form.Label>
<Form.Control>
<Select onValueChange={onChange} {...field}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{shippingProfiles.map((profile) => (
<Select.Item
key={profile.id}
value={profile.id}
>
{profile.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="provider_id"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>
{t("fields.fulfillmentProvider")}
</Form.Label>
<Form.Control>
<Select onValueChange={onChange} {...field}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{fulfillmentOptions.map((option) => (
<Select.Item
key={option.provider_id}
value={option.provider_id}
>
{formatProvider(option.provider_id)}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<div>
<Text size="small" leading="compact" weight="plus">
{t("regions.shippingOption.requirements.label")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("regions.shippingOption.requirements.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="min_subtotal"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={
<IncludesTaxTooltip includesTax={includesTax} />
}
optional
>
{t("fields.minSubtotal")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="max_subtotal"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={
<IncludesTaxTooltip includesTax={includesTax} />
}
optional
>
{t("fields.maxSubtotal")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</div>
</div>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}

View File

@@ -1 +0,0 @@
export { RegionCreateShippingOption as Component } from "./region-create-shipping-option"

View File

@@ -1,59 +0,0 @@
import {
useAdminRegion,
useAdminRegionFulfillmentOptions,
useAdminShippingProfiles,
} from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { CreateShippingOptionForm } from "./components/create-shipping-option-form"
export const RegionCreateShippingOption = () => {
const { id } = useParams()
const {
region,
isLoading: isLoadingRegion,
isError: isRegionError,
error: regionError,
} = useAdminRegion(id!)
const {
shipping_profiles,
isLoading: isLoadingProfiles,
isError: isProfilesError,
error: profileError,
} = useAdminShippingProfiles()
const {
fulfillment_options,
isLoading: isLoadingOptions,
isError: isOptionsError,
error: optionsError,
} = useAdminRegionFulfillmentOptions(id!)
const isLoading = isLoadingProfiles || isLoadingOptions || isLoadingRegion
if (isRegionError) {
throw regionError
}
if (isProfilesError) {
throw profileError
}
if (isOptionsError) {
throw optionsError
}
return (
<RouteFocusModal>
{!isLoading && region && shipping_profiles && fulfillment_options && (
<CreateShippingOptionForm
region={region}
fulfillmentOptions={fulfillment_options}
shippingProfiles={shipping_profiles}
/>
)}
</RouteFocusModal>
)
}

View File

@@ -1,27 +0,0 @@
import { useAdminStore } from "medusa-react"
import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
import { CreateRegionForm } from "./components/create-region-form"
export const RegionCreate = () => {
const { store, isLoading, isError, error } = useAdminStore()
const currencies = store?.currencies ?? []
const paymentProviders = store?.payment_providers ?? []
const fulfillmentProviders = store?.fulfillment_providers ?? []
if (isError) {
throw error
}
return (
<RouteFocusModal>
{!isLoading && store && (
<CreateRegionForm
currencies={currencies}
fulfillmentProviders={fulfillmentProviders}
paymentProviders={paymentProviders}
/>
)}
</RouteFocusModal>
)
}

View File

@@ -1 +0,0 @@
export * from "./region-shipping-option-section"

View File

@@ -1,173 +0,0 @@
import { Region } from "@medusajs/medusa"
import { Container, Heading, usePrompt } from "@medusajs/ui"
import {
useAdminDeleteShippingOption,
useAdminShippingOptions,
} from "medusa-react"
import { useTranslation } from "react-i18next"
import { PencilSquare, PlusMini, Trash } from "@medusajs/icons"
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useShippingOptionTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-table-columns"
import { useShippingOptionTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-table-filters"
import { useShippingOptionTableQuery } from "../../../../../hooks/table/query/use-shipping-option-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type RegionShippingOptionSectionProps = {
region: Region
}
const PAGE_SIZE = 10
export const RegionShippingOptionSection = ({
region,
}: RegionShippingOptionSectionProps) => {
const { searchParams, raw } = useShippingOptionTableQuery({
pageSize: PAGE_SIZE,
regionId: region.id,
})
const { shipping_options, count, isError, error, isLoading } =
useAdminShippingOptions(
{
...searchParams,
},
{
keepPreviousData: true,
}
)
const filters = useShippingOptionTableFilters()
const columns = useColumns()
const { table } = useDataTable({
data: (shipping_options ?? []) as unknown as PricedShippingOption[],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id!,
pageSize: PAGE_SIZE,
})
const { t } = useTranslation()
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("regions.shippingOptions")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.create"),
icon: <PlusMini />,
to: "shipping-options/create",
},
],
},
]}
/>
</div>
<DataTable
table={table}
columns={columns}
count={count}
filters={filters}
orderBy={[
"name",
"price_type",
"price_incl_tax",
"is_return",
"admin_only",
"created_at",
"updated_at",
]}
isLoading={isLoading}
pageSize={PAGE_SIZE}
pagination
search
queryObject={raw}
/>
</Container>
)
}
const ShippingOptionActions = ({
shippingOption,
}: {
shippingOption: PricedShippingOption
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!)
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("regions.deleteShippingOptionWarning", {
name: shippingOption.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync()
}
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: `shipping-options/${shippingOption.id}/edit`,
icon: <PencilSquare />,
},
],
},
{
actions: [
{
label: t("actions.delete"),
onClick: handleDelete,
icon: <Trash />,
},
],
},
]}
/>
)
}
const columnHelper = createColumnHelper<PricedShippingOption>()
const useColumns = () => {
const base = useShippingOptionTableColumns()
return useMemo(
() => [
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <ShippingOptionActions shippingOption={row.original} />
},
}),
],
[base]
)
}

View File

@@ -1,428 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod"
import {
Region,
ShippingOption,
ShippingOptionRequirement,
} from "@medusajs/medusa"
import {
Button,
CurrencyInput,
Input,
Select,
Switch,
Text,
} from "@medusajs/ui"
import { useAdminUpdateShippingOption } from "medusa-react"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { IncludesTaxTooltip } from "../../../../../components/common/tax-badge/tax-badge"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import {
getDbAmount,
getPresentationalAmount,
} from "../../../../../lib/money-amount-helpers"
import { ShippingOptionPriceType } from "../../../shared/constants"
type EditShippingOptionFormProps = {
region: Region
shippingOption: ShippingOption
}
const EditShippingOptionSchema = zod
.object({
name: zod.string().min(1),
admin_only: zod.boolean(),
price_type: zod.nativeEnum(ShippingOptionPriceType),
includes_tax: zod.boolean(),
amount: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return false
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Amount must be a positive number")
.optional(),
min_subtotal: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return true
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Min. subtotal must be a positive number")
.optional(),
max_subtotal: zod
.union([zod.string(), zod.number()])
.refine((value) => {
if (value === "") {
return true
}
const num = Number(value)
if (isNaN(num)) {
return false
}
return num >= 0
}, "Max. subtotal must be a positive number")
.optional(),
})
.superRefine((data, ctx) => {
if (data.price_type === ShippingOptionPriceType.FLAT_RATE) {
if (typeof data.amount === "string" && data.amount === "") {
return ctx.addIssue({
code: "custom",
message: "Amount is required",
path: ["amount"],
})
}
}
})
export const EditShippingOptionForm = ({
region,
shippingOption,
}: EditShippingOptionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const defaultAmount = shippingOption.amount
? getPresentationalAmount(shippingOption.amount, region.currency_code)
: ""
const defaultMinSubtotal = shippingOption.requirements.find(
(r) => r.type === "min_subtotal"
)
const defaultMinSubtotalAmount = defaultMinSubtotal
? getPresentationalAmount(defaultMinSubtotal.amount, region.currency_code)
: ""
const defaultMaxSubtotal = shippingOption.requirements.find(
(r) => r.type === "max_subtotal"
)
const defaultMaxSubtotalAmount = defaultMaxSubtotal
? getPresentationalAmount(defaultMaxSubtotal.amount, region.currency_code)
: ""
const form = useForm<zod.infer<typeof EditShippingOptionSchema>>({
defaultValues: {
admin_only: shippingOption.admin_only,
name: shippingOption.name,
amount: defaultAmount,
max_subtotal: defaultMaxSubtotalAmount,
min_subtotal: defaultMinSubtotalAmount,
price_type: shippingOption.price_type,
includes_tax: shippingOption.includes_tax,
},
resolver: zodResolver(EditShippingOptionSchema),
})
const watchedPriceType = useWatch({
control: form.control,
name: "price_type",
defaultValue: ShippingOptionPriceType.FLAT_RATE,
})
const isFlatRate = watchedPriceType === ShippingOptionPriceType.FLAT_RATE
const includesTax = useWatch({
control: form.control,
name: "includes_tax",
})
const { mutateAsync, isLoading } = useAdminUpdateShippingOption(
shippingOption.id
)
const getPricePayload = (amount?: string | number) => {
if (!amount) {
return undefined
}
const amountValue = typeof amount === "string" ? Number(amount) : amount
return getDbAmount(amountValue, region.currency_code)
}
const handleSubmit = form.handleSubmit(async (values) => {
const { amount, min_subtotal, max_subtotal, ...rest } = values
const amountPayload = getPricePayload(amount)
const minSubtotalPayload = getPricePayload(min_subtotal)
const maxSubtotalPayload = getPricePayload(max_subtotal)
const minSubtotalRequirement = minSubtotalPayload
? {
amount: minSubtotalPayload,
type: "min_subtotal",
}
: undefined
const maxSubtotalRequirement = maxSubtotalPayload
? {
amount: maxSubtotalPayload,
type: "max_subtotal",
}
: undefined
const requirements = [
minSubtotalRequirement,
maxSubtotalRequirement,
].filter(Boolean) as ShippingOptionRequirement[]
await mutateAsync(
{
amount: amountPayload,
requirements,
...rest,
},
{
onSuccess: () => {
handleSuccess()
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<form
onSubmit={handleSubmit}
className="flex flex-1 flex-col overflow-hidden"
>
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
<Form.Field
control={form.control}
name="admin_only"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-center justify-between">
<Form.Label>
{t("regions.shippingOption.availability.adminOnly")}
</Form.Label>
<Form.Control>
<Switch
checked={value}
onCheckedChange={onChange}
{...field}
/>
</Form.Control>
</div>
<Form.Hint>
{t("regions.shippingOption.availability.adminOnlyHint")}
</Form.Hint>
</div>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<Form.Field
control={form.control}
name="includes_tax"
render={({ field: { value, onChange, ...field } }) => {
return (
<Form.Item>
<div>
<div className="flex items-start justify-between">
<Form.Label>{t("fields.taxInclusivePricing")}</Form.Label>
<Form.Control>
<Switch
{...field}
checked={value}
onCheckedChange={onChange}
/>
</Form.Control>
</div>
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
<Form.ErrorMessage />
</div>
</Form.Item>
)
}}
/>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="price_type"
render={({ field: { onChange, ref, ...field } }) => {
return (
<Form.Item>
<Form.Label>
{t("regions.shippingOption.priceType.label")}
</Form.Label>
<Form.Control>
<Select onValueChange={onChange} {...field}>
<Select.Trigger ref={ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item
value={ShippingOptionPriceType.FLAT_RATE}
>
{t("regions.shippingOption.priceType.flatRate")}
</Select.Item>
<Select.Item
value={ShippingOptionPriceType.CALCULATED}
>
{t("regions.shippingOption.priceType.calculated")}
</Select.Item>
</Select.Content>
</Select>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{isFlatRate && (
<Form.Field
control={form.control}
name="amount"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={<IncludesTaxTooltip includesTax={includesTax} />}
>
{t("fields.price")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
</div>
<div className="bg-ui-border-base h-px w-full" />
<div className="flex flex-col gap-y-4">
<div>
<Text size="small" leading="compact" weight="plus">
{t("regions.shippingOption.requirements.label")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("regions.shippingOption.requirements.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4">
<Form.Field
control={form.control}
name="min_subtotal"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={<IncludesTaxTooltip includesTax={includesTax} />}
optional
>
{t("fields.minSubtotal")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="max_subtotal"
shouldUnregister
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label
icon={<IncludesTaxTooltip includesTax={includesTax} />}
optional
>
{t("fields.maxSubtotal")}
</Form.Label>
<Form.Control>
<CurrencyInput
code={region.currency_code}
symbol={region.currency.symbol_native}
onValueChange={onChange}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small">
{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 { RegionEditShippingOption as Component } from "./region-edit-shipping-option"

View File

@@ -1,49 +0,0 @@
import { Heading } from "@medusajs/ui"
import { useAdminRegion, useAdminShippingOption } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditShippingOptionForm } from "./components/edit-shipping-option-form"
export const RegionEditShippingOption = () => {
const { id, so_id } = useParams()
const { t } = useTranslation()
const {
region,
isLoading: isLoadingRegion,
isError: isRegionError,
error: regionError,
} = useAdminRegion(id!)
const {
shipping_option,
isLoading: isLoadingOption,
isError: isOptionError,
error: optionError,
} = useAdminShippingOption(so_id!)
const isLoading = isLoadingRegion || isLoadingOption
if (isRegionError) {
throw regionError
}
if (isOptionError) {
throw optionError
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("regions.shippingOption.editShippingOption")}</Heading>
</RouteDrawer.Header>
{!isLoading && region && shipping_option && (
<EditShippingOptionForm
region={region}
shippingOption={shipping_option}
/>
)}
</RouteDrawer>
)
}

View File

@@ -8,6 +8,7 @@ import {
CurrencyDTO,
CustomerDTO,
InviteDTO,
PaymentProviderDTO,
ProductCategoryDTO,
ProductCollectionDTO,
ProductDTO,
@@ -122,6 +123,12 @@ export type TagsListRes = { tags: ProductTagDTO[] } & ListRes
export type ProductTypeRes = { product_type: ProductTypeDTO }
export type ProductTypeListRes = { product_types: ProductTypeDTO[] } & ListRes
// Payments
export type PaymentProvidersListRes = {
payment_providers: PaymentProviderDTO[]
}
// Stock Locations
export type ExtendedStockLocationDTO = StockLocationDTO & {
address: StockLocationAddressDTO | null

View File

@@ -1,5 +1,5 @@
import { Country } from "@medusajs/medusa"
import { json } from "react-router-dom"
import { RegionCountryDTO } from "@medusajs/types"
const acceptedOrderKeys = ["name", "code"]
@@ -14,7 +14,7 @@ export const useCountries = ({
limit,
offset = 0,
}: {
countries: Country[]
countries: RegionCountryDTO[]
limit: number
offset?: number
order?: string

View File

@@ -1,9 +1,10 @@
import { Country } from "@medusajs/medusa"
import { RegionCountryDTO } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
const columnHelper = createColumnHelper<Country>()
const columnHelper = createColumnHelper<RegionCountryDTO>()
export const useCountryTableColumns = () => {
const { t } = useTranslation()

View File

@@ -1,5 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Country, Region } from "@medusajs/medusa"
import {
ColumnDef,
RowSelectionState,
@@ -11,7 +10,7 @@ import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Button, Checkbox } from "@medusajs/ui"
import { useAdminUpdateRegion } from "medusa-react"
import { RegionCountryDTO, RegionDTO } from "@medusajs/types"
import {
RouteFocusModal,
useRouteModal,
@@ -19,12 +18,13 @@ import {
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { countries as staticCountries } from "../../../../../lib/countries"
import { useCountries } from "../../../shared/hooks/use-countries"
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
import { useCountries } from "../../../common/hooks/use-countries"
import { useCountryTableColumns } from "../../../common/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../common/hooks/use-country-table-query"
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
type AddCountriesFormProps = {
region: Region
region: RegionDTO
}
const AddCountriesSchema = zod.object({
@@ -66,12 +66,12 @@ export const AddCountriesForm = ({ region }: AddCountriesFormProps) => {
countries: staticCountries.map((c, i) => ({
display_name: c.display_name,
name: c.name,
id: i,
id: i as any,
iso_2: c.iso_2,
iso_3: c.iso_3,
num_code: c.num_code,
region_id: null,
region: {} as Region,
region: {} as RegionDTO,
})),
...searchParams,
})
@@ -98,7 +98,7 @@ export const AddCountriesForm = ({ region }: AddCountriesFormProps) => {
prefix: PREFIX,
})
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
const { mutateAsync, isLoading } = useUpdateRegion(region.id)
const handleSubmit = form.handleSubmit(async (values) => {
const payload = [
@@ -155,7 +155,7 @@ export const AddCountriesForm = ({ region }: AddCountriesFormProps) => {
)
}
const columnHelper = createColumnHelper<Country>()
const columnHelper = createColumnHelper<RegionCountryDTO>()
const useColumns = () => {
const base = useCountryTableColumns()
@@ -196,5 +196,5 @@ const useColumns = () => {
...base,
],
[base]
) as ColumnDef<Country>[]
) as ColumnDef<RegionCountryDTO>[]
}

View File

@@ -1,12 +1,14 @@
import { useAdminRegion } from "medusa-react"
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCountriesForm } from "./components/add-countries-form"
import { useRegion } from "../../../hooks/api/regions"
export const RegionAddCountries = () => {
const { id } = useParams()
const { region, isLoading, isError, error } = useAdminRegion(id!)
const { region, isLoading, isError, error } = useRegion(id!, {
fields: "*payment_providers",
})
if (isError) {
throw error

View File

@@ -1,11 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { XMarkMini } from "@medusajs/icons"
import {
Country,
Currency,
FulfillmentProvider,
PaymentProvider,
} from "@medusajs/medusa"
import {
Button,
Checkbox,
@@ -17,29 +11,33 @@ import {
clx,
} from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useAdminCreateRegion } from "medusa-react"
import { useMemo, useState } from "react"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { RegionCountryDTO, PaymentProviderDTO } from "@medusajs/types"
import { Combobox } from "../../../../../components/common/combobox"
import { Form } from "../../../../../components/common/form"
import { SplitView } from "../../../../../components/layout/split-view"
import { useRouteModal } from "../../../../../components/route-modal"
import { RouteFocusModal } from "../../../../../components/route-modal/route-focus-modal"
import {
useRouteModal,
RouteFocusModal,
} from "../../../../../components/route-modal"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { countries as staticCountries } from "../../../../../lib/countries"
import { formatProvider } from "../../../../../lib/format-provider"
import { useCountries } from "../../../shared/hooks/use-countries"
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
import { useCountries } from "../../../common/hooks/use-countries"
import { useCountryTableColumns } from "../../../common/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../common/hooks/use-country-table-query"
import { CurrencyInfo } from "../../../../../lib/currencies"
import { useCreateRegion } from "../../../../../hooks/api/regions"
type CreateRegionFormProps = {
currencies: Currency[]
paymentProviders: PaymentProvider[]
fulfillmentProviders: FulfillmentProvider[]
currencies: CurrencyInfo[]
paymentProviders: PaymentProviderDTO[]
}
const CreateRegionSchema = zod.object({
@@ -47,22 +45,7 @@ const CreateRegionSchema = zod.object({
currency_code: zod.string().min(2, "Select a currency"),
includes_tax: zod.boolean(),
countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })),
fulfillment_providers: zod.array(zod.string()).min(1),
payment_providers: zod.array(zod.string()).min(1),
tax_rate: zod.union([zod.string(), zod.number()]).refine((value) => {
if (value === "") {
return false
}
const num = Number(value)
if (num >= 0 && num <= 100) {
return true
}
return false
}, "Tax rate must be a number between 0 and 100"),
tax_code: zod.string().optional(),
})
const PREFIX = "cr"
@@ -71,7 +54,6 @@ const PAGE_SIZE = 50
export const CreateRegionForm = ({
currencies,
paymentProviders,
fulfillmentProviders,
}: CreateRegionFormProps) => {
const [open, setOpen] = useState(false)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
@@ -83,10 +65,7 @@ export const CreateRegionForm = ({
currency_code: "",
includes_tax: false,
countries: [],
fulfillment_providers: [],
payment_providers: [],
tax_code: "",
tax_rate: "",
},
resolver: zodResolver(CreateRegionSchema),
})
@@ -99,7 +78,7 @@ export const CreateRegionForm = ({
const { t } = useTranslation()
const { mutateAsync, isLoading } = useAdminCreateRegion()
const { mutateAsync, isLoading } = useCreateRegion()
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
@@ -107,14 +86,8 @@ export const CreateRegionForm = ({
name: values.name,
countries: values.countries.map((c) => c.code),
currency_code: values.currency_code,
fulfillment_providers: values.fulfillment_providers,
payment_providers: values.payment_providers,
tax_rate:
typeof values.tax_rate === "string"
? Number(values.tax_rate)
: values.tax_rate,
tax_code: values.tax_code,
includes_tax: values.includes_tax,
automatic_taxes: values.includes_tax,
},
{
onSuccess: ({ region }) => {
@@ -290,40 +263,6 @@ export const CreateRegionForm = ({
}}
/>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="tax_rate"
render={({ field: { value, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("fields.taxRate")}</Form.Label>
<Form.Control>
<Input type="number" value={value} {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="tax_code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("fields.taxCode")}
</Form.Label>
<Form.Control>
<Input type="number" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
<Form.Field
control={form.control}
@@ -383,9 +322,9 @@ export const CreateRegionForm = ({
</div>
)}
<div className="flex items-center justify-end">
<SplitView.Open type="button">
<Button onClick={() => setOpen(true)} type="button">
{t("regions.addCountries")}
</SplitView.Open>
</Button>
</div>
</div>
<div className="bg-ui-border-base h-px w-full" />
@@ -422,29 +361,6 @@ export const CreateRegionForm = ({
)
}}
/>
<Form.Field
control={form.control}
name="fulfillment_providers"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("fields.fulfillmentProviders")}
</Form.Label>
<Form.Control>
<Combobox
options={fulfillmentProviders.map((fp) => ({
label: formatProvider(fp.id),
value: fp.id,
}))}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</div>
@@ -481,7 +397,7 @@ export const CreateRegionForm = ({
)
}
const columnHelper = createColumnHelper<Country>()
const columnHelper = createColumnHelper<RegionCountryDTO>()
const useColumns = () => {
const base = useCountryTableColumns()

View File

@@ -0,0 +1,29 @@
import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
import { CreateRegionForm } from "./components/create-region-form"
import { currencies } from "../../../lib/currencies"
import { usePaymentProviders } from "../../../hooks/api/payments"
import { useStore } from "../../../hooks/api/store"
export const RegionCreate = () => {
const { store, isLoading, isError, error } = useStore()
const storeCurrencies = (store?.supported_currency_codes ?? []).map(
(code) => currencies[code.toUpperCase()]
)
const { payment_providers: paymentProviders = [] } = usePaymentProviders()
if (isError) {
throw error
}
return (
<RouteFocusModal>
{!isLoading && store && (
<CreateRegionForm
currencies={storeCurrencies}
paymentProviders={paymentProviders}
/>
)}
</RouteFocusModal>
)
}

View File

@@ -1,32 +1,23 @@
import { PlusMini, Trash } from "@medusajs/icons"
import { Region } from "@medusajs/medusa"
import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
import { RegionCountryDTO, RegionDTO } from "@medusajs/types"
import {
ColumnDef,
RowSelectionState,
createColumnHelper,
} from "@tanstack/react-table"
import { useAdminUpdateRegion } from "medusa-react"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCountries } from "../../../shared/hooks/use-countries"
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
import { useCountries } from "../../../common/hooks/use-countries"
import { useCountryTableColumns } from "../../../common/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../common/hooks/use-country-table-query"
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
type RegionCountrySectionProps = {
region: Region
}
type Country = {
id: number
iso_2: string
iso_3: string
num_code: number
name: string
display_name: string
region: RegionDTO
}
const PREFIX = "c"
@@ -67,7 +58,7 @@ export const RegionCountrySection = ({ region }: RegionCountrySectionProps) => {
},
})
const { mutateAsync } = useAdminUpdateRegion(region.id)
const { mutateAsync } = useUpdateRegion(region.id)
const handleRemoveCountries = async () => {
const ids = Object.keys(rowSelection).filter((k) => rowSelection[k])
@@ -140,12 +131,12 @@ const CountryActions = ({
country,
region,
}: {
country: Country
region: Region
country: RegionCountryDTO
region: RegionDTO
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useAdminUpdateRegion(region.id)
const { mutateAsync } = useUpdateRegion(region.id)
const payload = region.countries
?.filter((c) => c.iso_2 !== country.iso_2)
@@ -189,7 +180,7 @@ const CountryActions = ({
)
}
const columnHelper = createColumnHelper<Country>()
const columnHelper = createColumnHelper<RegionCountryDTO>()
const useColumns = () => {
const base = useCountryTableColumns()
@@ -228,12 +219,12 @@ const useColumns = () => {
columnHelper.display({
id: "actions",
cell: ({ row, table }) => {
const { region } = table.options.meta as { region: Region }
const { region } = table.options.meta as { region: RegionDTO }
return <CountryActions country={row.original} region={region} />
},
}),
],
[base]
) as ColumnDef<Country>[]
) as ColumnDef<RegionCountryDTO>[]
}

View File

@@ -1,13 +1,17 @@
import { BuildingTax, PencilSquare, Trash } from "@medusajs/icons"
import { Region } from "@medusajs/medusa"
import { PencilSquare, Trash } from "@medusajs/icons"
import { RegionDTO } from "@medusajs/types"
import { Badge, Container, Heading, Text, usePrompt } from "@medusajs/ui"
import { useAdminDeleteRegion } from "medusa-react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { formatProvider } from "../../../../../lib/format-provider"
import { currencies } from "../../../../../lib/currencies"
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { ListSummary } from "../../../../../components/common/list-summary"
import { useNavigate } from "react-router-dom"
type RegionGeneralSectionProps = {
region: Region
region: RegionDTO
}
export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
@@ -28,7 +32,7 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
{region.currency_code}
</Badge>
<Text size="small" leading="compact">
{region.currency?.name}
{currencies[region.currency_code.toUpperCase()].name}
</Text>
</div>
</div>
@@ -36,33 +40,24 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
<Text size="small" leading="compact" weight="plus">
{t("fields.paymentProviders")}
</Text>
<Text size="small" leading="compact">
{region.payment_providers.length > 0
? region.payment_providers
.map((p) => formatProvider(p.id))
.join(", ")
: "-"}
</Text>
</div>
<div className="grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.fulfillmentProviders")}
</Text>
<Text size="small" leading="compact">
{region.fulfillment_providers.length > 0
? region.fulfillment_providers
.map((p) => formatProvider(p.id))
.join(", ")
: "-"}
</Text>
<div className="inline-flex">
{region.payment_providers?.length > 0 ? (
<ListSummary
list={region.payment_providers.map((p) => formatProvider(p.id))}
/>
) : (
"-"
)}
</div>
</div>
</Container>
)
}
const RegionActions = ({ region }: { region: Region }) => {
const RegionActions = ({ region }: { region: RegionDTO }) => {
const navigate = useNavigate()
const { t } = useTranslation()
const { mutateAsync } = useAdminDeleteRegion(region.id)
const { mutateAsync } = useDeleteRegion(region.id)
const prompt = usePrompt()
const handleDelete = async () => {
@@ -82,6 +77,7 @@ const RegionActions = ({ region }: { region: Region }) => {
}
await mutateAsync(undefined)
navigate("/settings/regions", { replace: true })
}
return (
@@ -94,11 +90,6 @@ const RegionActions = ({ region }: { region: Region }) => {
label: t("actions.edit"),
to: `/settings/regions/${region.id}/edit`,
},
{
icon: <BuildingTax />,
label: "Tax settings",
to: `/settings/taxes/${region.id}`,
},
],
},
{

View File

@@ -7,7 +7,8 @@ import { medusa, queryClient } from "../../../lib/medusa"
const regionQuery = (id: string) => ({
queryKey: adminRegionKeys.detail(id),
queryFn: async () => medusa.admin.regions.retrieve(id),
queryFn: async () =>
medusa.admin.regions.retrieve(id, { fields: "*payment_providers" }),
})
export const regionLoader = async ({ params }: LoaderFunctionArgs) => {

View File

@@ -1,11 +1,9 @@
import { useAdminRegion } from "medusa-react"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { RegionCountrySection } from "./components/region-country-section"
import { RegionGeneralSection } from "./components/region-general-section"
import { RegionShippingOptionSection } from "./components/region-shipping-option-section"
import { regionLoader } from "./loader"
import { useRegion } from "../../../hooks/api/regions"
export const RegionDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -13,9 +11,13 @@ export const RegionDetail = () => {
>
const { id } = useParams()
const { region, isLoading, isError, error } = useAdminRegion(id!, {
initialData,
})
const { region, isLoading, isError, error } = useRegion(
id!,
{ fields: "*payment_providers" },
{
initialData,
}
)
// TODO: Move to loading.tsx and set as Suspense fallback for the route
if (isLoading || !region) {
@@ -30,8 +32,6 @@ export const RegionDetail = () => {
<div className="flex flex-col gap-y-2">
<RegionGeneralSection region={region} />
<RegionCountrySection region={region} />
<RegionShippingOptionSection region={region} />
<JsonViewSection data={region} />
<Outlet />
</div>
)

View File

@@ -1,14 +1,8 @@
import {
Currency,
FulfillmentProvider,
PaymentProvider,
Region,
} from "@medusajs/medusa"
import { Button, Input, Select, Text } from "@medusajs/ui"
import { useAdminUpdateRegion } from "medusa-react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { PaymentProviderDTO, RegionDTO } from "@medusajs/types"
import { Combobox } from "../../../../../components/common/combobox"
import { Form } from "../../../../../components/common/form"
@@ -17,26 +11,25 @@ import {
useRouteModal,
} from "../../../../../components/route-modal"
import { formatProvider } from "../../../../../lib/format-provider"
import { CurrencyInfo } from "../../../../../lib/currencies"
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
type EditRegionFormProps = {
region: Region
currencies: Currency[]
paymentProviders: PaymentProvider[]
fulfillmentProviders: FulfillmentProvider[]
region: RegionDTO
currencies: CurrencyInfo[]
paymentProviders: PaymentProviderDTO[]
}
const EditRegionSchema = zod.object({
name: zod.string().min(1),
currency_code: zod.string(),
payment_providers: zod.array(zod.string()),
fulfillment_providers: zod.array(zod.string()),
})
export const EditRegionForm = ({
region,
currencies,
paymentProviders,
fulfillmentProviders,
}: EditRegionFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
@@ -44,20 +37,18 @@ export const EditRegionForm = ({
const form = useForm<zod.infer<typeof EditRegionSchema>>({
defaultValues: {
name: region.name,
currency_code: region.currency_code,
fulfillment_providers: region.fulfillment_providers.map((fp) => fp.id),
payment_providers: region.payment_providers.map((pp) => pp.id),
currency_code: region.currency_code.toUpperCase(),
payment_providers: region.payment_providers?.map((pp) => pp.id) || [],
},
})
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
const { mutateAsync, isLoading } = useUpdateRegion(region.id)
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(
{
name: values.name,
currency_code: values.currency_code,
fulfillment_providers: values.fulfillment_providers,
currency_code: values.currency_code.toLowerCase(),
payment_providers: values.payment_providers,
},
{
@@ -146,29 +137,6 @@ export const EditRegionForm = ({
)
}}
/>
<Form.Field
control={form.control}
name="fulfillment_providers"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("fields.fulfillmentProviders")}
</Form.Label>
<Form.Control>
<Combobox
options={fulfillmentProviders.map((fp) => ({
label: formatProvider(fp.id),
value: fp.id,
}))}
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</RouteDrawer.Body>

View File

@@ -1,10 +1,13 @@
import { Heading } from "@medusajs/ui"
import { useAdminRegion, useAdminStore } from "medusa-react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditRegionForm } from "./components/edit-region-form"
import { currencies } from "../../../lib/currencies"
import { useRegion } from "../../../hooks/api/regions"
import { usePaymentProviders } from "../../../hooks/api/payments"
import { useStore } from "../../../hooks/api/store"
export const RegionEdit = () => {
const { t } = useTranslation()
@@ -15,20 +18,23 @@ export const RegionEdit = () => {
isLoading: isRegionLoading,
isError: isRegionError,
error: regionError,
} = useAdminRegion(id!)
} = useRegion(id!, { fields: "*payment_providers" })
const {
store,
isLoading: isStoreLoading,
isError: isStoreError,
error: storeError,
} = useAdminStore()
} = useStore()
const isLoading = isRegionLoading || isStoreLoading
const currencies = store?.currencies || []
const paymentProviders = store?.payment_providers || []
const fulfillmentProviders = store?.fulfillment_providers || []
const storeCurrencies = (store?.supported_currency_codes ?? []).map(
(code) => currencies[code.toUpperCase()]
)
const { payment_providers: paymentProviders = [] } = usePaymentProviders({
limit: 999,
})
if (isRegionError) {
throw regionError
@@ -46,9 +52,8 @@ export const RegionEdit = () => {
{!isLoading && region && (
<EditRegionForm
region={region}
currencies={currencies}
currencies={storeCurrencies}
paymentProviders={paymentProviders}
fulfillmentProviders={fulfillmentProviders}
/>
)}
</RouteDrawer>

View File

@@ -1,11 +1,10 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Region } from "@medusajs/medusa"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteRegion, useAdminRegions } from "medusa-react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { RegionDTO } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
@@ -13,6 +12,7 @@ import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-re
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
import { useRegionTableQuery } from "../../../../../hooks/table/query/use-region-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
const PAGE_SIZE = 20
@@ -20,9 +20,10 @@ export const RegionListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useRegionTableQuery({ pageSize: PAGE_SIZE })
const { regions, count, isLoading, isError, error } = useAdminRegions(
const { regions, count, isLoading, isError, error } = useRegions(
{
...searchParams,
fields: "*payment_providers",
},
{
keepPreviousData: true,
@@ -33,7 +34,7 @@ export const RegionListTable = () => {
const columns = useColumns()
const { table } = useDataTable({
data: (regions ?? []) as Region[],
data: (regions ?? []) as RegionDTO[],
columns,
count,
enablePagination: true,
@@ -72,11 +73,11 @@ export const RegionListTable = () => {
)
}
const RegionActions = ({ region }: { region: Region }) => {
const RegionActions = ({ region }: { region: RegionDTO }) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useAdminDeleteRegion(region.id)
const { mutateAsync } = useDeleteRegion(region.id)
const handleDelete = async () => {
const res = await prompt({
@@ -123,7 +124,7 @@ const RegionActions = ({ region }: { region: Region }) => {
)
}
const columnHelper = createColumnHelper<Region>()
const columnHelper = createColumnHelper<RegionDTO>()
const useColumns = () => {
const base = useRegionTableColumns()

View File

@@ -19,9 +19,9 @@ import {
} from "../../../../../components/route-modal"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useV2UpdateStore } from "../../../../../lib/api-v2"
import { useCurrenciesTableColumns } from "../../../common/hooks/use-currencies-table-columns"
import { useCurrenciesTableQuery } from "../../../common/hooks/use-currencies-table-query"
import { useUpdateStore } from "../../../../../hooks/api/store"
type AddCurrenciesFormProps = {
store: StoreDTO
@@ -95,7 +95,7 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
},
})
const { mutateAsync, isLoading: isMutating } = useV2UpdateStore(store.id)
const { mutateAsync, isLoading: isMutating } = useUpdateStore(store.id)
const handleSubmit = form.handleSubmit(async (data) => {
const currencies = Array.from(

View File

@@ -1,9 +1,9 @@
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
import { useV2Store } from "../../../lib/api-v2"
import { useStore } from "../../../hooks/api/store"
export const StoreAddCurrencies = () => {
const { store, isLoading, isError, error } = useV2Store({})
const { store, isLoading, isError, error } = useStore({})
if (isError) {
throw error

View File

@@ -9,6 +9,7 @@ import {
AdminPostRegionsRegionFulfillmentProvidersReq,
AdminPostRegionsRegionPaymentProvidersReq,
AdminGetRegionsRegionFulfillmentOptionsRes,
AdminGetRegionsRegionParams,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../../typings"
@@ -17,12 +18,12 @@ import BaseResource from "../base"
/**
* This class is used to send requests to [Admin Region API Routes](https://docs.medusajs.com/api/admin#regions). All its method
* are available in the JS Client under the `medusa.admin.regions` property.
*
*
* All methods in this class require {@link AdminAuthResource.createSession | user authentication}.
*
*
* Regions are different countries or geographical regions that the commerce store serves customers in.
* Admins can manage these regions, their providers, and more.
*
*
* Related Guide: [How to manage regions](https://docs.medusajs.com/modules/regions-and-currencies/admin/manage-regions).
*/
class AdminRegionsResource extends BaseResource {
@@ -31,7 +32,7 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminPostRegionsReq} payload - The region to create.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -68,7 +69,7 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminPostRegionsRegionReq} payload - The attributes to update in the region.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -94,7 +95,7 @@ class AdminRegionsResource extends BaseResource {
* @param {string} id - The region's ID.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsDeleteRes>} Resolves to the deletion operation's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -115,9 +116,10 @@ class AdminRegionsResource extends BaseResource {
/**
* Retrieve a region's details.
* @param {string} id - The region's ID.
* @param query - Query params
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -129,9 +131,16 @@ class AdminRegionsResource extends BaseResource {
*/
retrieve(
id: string,
query?: AdminGetRegionsRegionParams,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminRegionsRes> {
const path = `/admin/regions/${id}`
let path = `/admin/regions/${id}`
if (query) {
const queryString = qs.stringify(query)
path = `/admin/regions/${id}?${queryString}`
}
return this.client.request("GET", path, undefined, {}, customHeaders)
}
@@ -140,10 +149,10 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminGetRegionsParams} query - Filters and pagination configurations to apply on the retrieved regions.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsListRes>} Resolves to the list of regions with pagination fields.
*
*
* @example
* To list regions:
*
*
* ```ts
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -153,9 +162,9 @@ class AdminRegionsResource extends BaseResource {
* console.log(regions.length);
* })
* ```
*
*
* By default, only the first `50` records are retrieved. You can control pagination by specifying the `limit` and `offset` properties:
*
*
* ```ts
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -189,7 +198,7 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminPostRegionsRegionCountriesReq} payload - The country to add.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -216,7 +225,7 @@ class AdminRegionsResource extends BaseResource {
* @param {string} country_code - The code of the country to delete from the region.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -241,7 +250,7 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminPostRegionsRegionFulfillmentProvidersReq} payload - The fulfillment provider to add.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -268,7 +277,7 @@ class AdminRegionsResource extends BaseResource {
* @param {string} provider_id - The ID of the fulfillment provider to delete from the region.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -292,7 +301,7 @@ class AdminRegionsResource extends BaseResource {
* @param {string} id - The region's ID.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminGetRegionsRegionFulfillmentOptionsRes>} Resolves to the list of fulfillment options.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -316,7 +325,7 @@ class AdminRegionsResource extends BaseResource {
* @param {AdminPostRegionsRegionPaymentProvidersReq} payload - The payment provider to add.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -339,11 +348,11 @@ class AdminRegionsResource extends BaseResource {
/**
* Delete a payment provider from a region. The payment provider will still be available for usage in other regions.
* @param {string} id - The region's ID.
* @param {string} id - The region's ID.
* @param {string} provider_id - The ID of the payment provider to delete from the region.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminRegionsRes>} Resolves to the region's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })

View File

@@ -4,6 +4,7 @@ import { authenticate } from "../../../utils/authenticate-middleware"
import * as queryConfig from "./query-config"
import {
AdminGetPaymentsParams,
AdminGetPaymentsPaymentProvidersParams,
AdminPostPaymentsCapturesReq,
AdminPostPaymentsRefundsReq,
} from "./validators"
@@ -24,6 +25,16 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["GET"],
matcher: "/admin/payments/payment-providers",
middlewares: [
transformQuery(
AdminGetPaymentsPaymentProvidersParams,
queryConfig.listTransformPaymentProvidersQueryConfig
),
],
},
{
method: ["GET"],
matcher: "/admin/payments/:id",

View File

@@ -0,0 +1,30 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPaymentModuleService } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import { AdminGetPaymentsPaymentProvidersParams } from "../validators"
export const GET = async (
req: AuthenticatedMedusaRequest<AdminGetPaymentsPaymentProvidersParams>,
res: MedusaResponse
) => {
const paymentModule = req.scope.resolve<IPaymentModuleService>(
ModuleRegistrationName.PAYMENT
)
const [payment_providers, count] =
await paymentModule.listAndCountPaymentProviders(req.filterableFields, {
skip: req.listConfig.skip,
take: req.listConfig.take,
})
res.status(200).json({
count,
payment_providers,
offset: req.listConfig.skip,
limit: req.listConfig.take,
})
}

View File

@@ -28,3 +28,12 @@ export const retrieveTransformQueryConfig = {
allowedRelations: allowedAdminPaymentRelations,
isList: false,
}
export const defaultAdminPaymentPaymentProviderFields = ["id", "is_enabled"]
export const listTransformPaymentProvidersQueryConfig = {
defaultFields: defaultAdminPaymentPaymentProviderFields,
defaultRelations: [],
allowedRelations: [],
isList: true,
}

View File

@@ -1,5 +1,5 @@
import { Type } from "class-transformer"
import { IsInt, IsOptional, ValidateNested } from "class-validator"
import { IsBoolean, IsInt, IsOptional, ValidateNested } from "class-validator"
import {
DateComparisonOperator,
FindParams,
@@ -56,3 +56,24 @@ export class AdminPostPaymentsRefundsReq {
@IsOptional()
amount?: number
}
export class AdminGetPaymentsPaymentProvidersParams extends extendedFindParamsMixin(
{
limit: 20,
offset: 0,
}
) {
/**
* IDs to filter users by.
*/
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
/**
* Filter providers by `enabled` flag
*/
@IsBoolean()
@IsOptional()
is_enabled?: boolean
}

View File

@@ -2,6 +2,7 @@ import { OperatorMap } from "@medusajs/types"
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
IsObject,
IsOptional,
IsString,
@@ -87,6 +88,10 @@ export class AdminPostRegionsReq {
@IsOptional()
countries?: string[]
@IsBoolean()
@IsOptional()
automatic_taxes?: boolean
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
@@ -110,6 +115,10 @@ export class AdminPostRegionsRegionReq {
@IsOptional()
countries?: string[]
@IsBoolean()
@IsOptional()
automatic_taxes?: boolean
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>

View File

@@ -231,3 +231,4 @@ export * from "./add-payment-provider"
export * from "./create-region"
export * from "./list-regions"
export * from "./update-region"
export * from "./get-region"

View File

@@ -733,4 +733,24 @@ export default class PaymentModuleService<
}
)
}
@InjectManager("baseRepository_")
async listAndCountPaymentProviders(
filters: FilterablePaymentProviderProps = {},
config: FindConfig<PaymentProviderDTO> = {},
@MedusaContext() sharedContext?: Context
): Promise<[PaymentProviderDTO[], number]> {
const [providers, count] = await this.paymentProviderService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<PaymentProviderDTO[]>(providers, {
populate: true,
}),
count,
]
}
}

View File

@@ -71,6 +71,23 @@ export default class PaymentProviderService {
)
}
@InjectManager("paymentProviderRepository_")
async listAndCount(
filters: FilterablePaymentProviderProps,
config: FindConfig<PaymentProviderDTO>,
@MedusaContext() sharedContext?: Context
): Promise<[PaymentProvider[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<PaymentProvider>(
filters,
config
)
return await this.paymentProviderRepository_.findAndCount(
queryOptions,
sharedContext
)
}
retrieveProvider(providerId: string): IPaymentProvider {
try {
return this.container_[providerId] as IPaymentProvider

View File

@@ -701,6 +701,12 @@ export interface IPaymentModuleService extends IModuleService {
sharedContext?: Context
): Promise<PaymentProviderDTO[]>
listAndCountPaymentProviders(
filters?: FilterablePaymentProviderProps,
config?: FindConfig<PaymentProviderDTO>,
sharedContext?: Context
): Promise<[PaymentProviderDTO[], number]>
/**
* This method retrieves a paginated list of captures based on optional filters and configuration.
*

View File

@@ -1,4 +1,5 @@
import { BaseFilterable, OperatorMap } from "../dal"
import { PaymentProviderDTO } from "../payment"
/**
* The region details.
@@ -28,6 +29,10 @@ export interface RegionDTO {
* The countries of the region.
*/
countries: RegionCountryDTO[]
/**
* Payment providers available in the region
*/
payment_providers: PaymentProviderDTO[]
/**
* Holds custom data in key-value pairs.

View File

@@ -18,6 +18,10 @@ export interface CreateRegionDTO {
* The region's countries.
*/
countries?: string[]
/**
* The region's payment providers.
*/
payment_providers?: string[]
/**
* Holds custom data in key-value pairs.
*/