feat(dashboard): Admin UI regions v2 (#6943)
This commit is contained in:
6
.changeset/hip-files-film.md
Normal file
6
.changeset/hip-files-film.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(medusa, types): list payment providers endpoint
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
24
packages/admin-next/dashboard/src/hooks/api/payments.ts
Normal file
24
packages/admin-next/dashboard/src/hooks/api/payments.ts
Normal 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 }
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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()} />
|
||||
),
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./auth"
|
||||
export * from "./store"
|
||||
export * from "./campaign"
|
||||
export * from "./currencies"
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
packages/admin-next/dashboard/src/lib/client/payments.ts
Normal file
13
packages/admin-next/dashboard/src/lib/client/payments.ts
Normal 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,
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})` : "")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./create-shipping-option-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { RegionCreateShippingOption as Component } from "./region-create-shipping-option"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./region-shipping-option-section"
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./edit-shipping-option-form"
|
||||
@@ -1 +0,0 @@
|
||||
export { RegionEditShippingOption as Component } from "./region-edit-shipping-option"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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>[]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>[]
|
||||
}
|
||||
@@ -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}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -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) => {
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user