feat(dashboard, js-sdk): shipping option type mngmt dashboard (#13208)

* chore(types, api): support shipping option type api endpoints

* core flows

* api

* typos

* compiler errors

* integration tests

* remove metadata

* changeset

* modify test

* upsert

* change remote query

* minor to patch

* description optional

* chore(dashboard, js-sdk): shipping option type management on admin dashboard

* description optional

* woops my bad

* my bad again

* create and edit

* prettier

* build code from label

* remove metadata route

* remove some translation text that is not used

* remove unsued files

* changeset

* adapt test

* fix test

* suggestion

---------

Co-authored-by: william bouchard <williambouchard@williams-MacBook-Pro.local>
This commit is contained in:
William Bouchard
2025-08-14 15:21:33 -04:00
committed by GitHub
parent 257e71f988
commit 4b3c43fe92
36 changed files with 1344 additions and 5 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/fulfillment": patch
"@medusajs/admin-shared": patch
"@medusajs/dashboard": patch
"@medusajs/js-sdk": patch
---
feat(dashboard, js-sdk): shipping option type mngmt dashboard

View File

@@ -66,6 +66,23 @@ medusaIntegrationTestRunner({
})
it("returns a list of shipping option types matching free text search param", async () => {
const res = await api.get("/admin/shipping-option-types?q=st1", adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.shipping_option_types).toEqual([
{
id: expect.stringMatching(/sotype_.{24}/),
label: "Test1",
code: "test1",
description: "Test1 description",
created_at: expect.any(String),
updated_at: expect.any(String),
},
])
})
it("returns a list of shipping option types matching code search param", async () => {
const res = await api.get("/admin/shipping-option-types?code=test1", adminHeaders)
expect(res.status).toEqual(200)

View File

@@ -55,6 +55,13 @@ const PRODUCT_CATEGORY_INJECTION_ZONES = [
"product_category.list.after",
] as const
const SHIPPING_OPTION_TYPE_INJECTION_ZONES = [
"shipping_option_type.details.before",
"shipping_option_type.details.after",
"shipping_option_type.list.before",
"shipping_option_type.list.after",
] as const
const PRODUCT_TYPE_INJECTION_ZONES = [
"product_type.details.before",
"product_type.details.after",
@@ -204,6 +211,7 @@ export const INJECTION_ZONES = [
...PRODUCT_COLLECTION_INJECTION_ZONES,
...PRODUCT_CATEGORY_INJECTION_ZONES,
...PRODUCT_TYPE_INJECTION_ZONES,
...SHIPPING_OPTION_TYPE_INJECTION_ZONES,
...PRODUCT_TAG_INJECTION_ZONES,
...PRICE_LIST_INJECTION_ZONES,
...PROMOTION_INJECTION_ZONES,

View File

@@ -47,6 +47,10 @@ const useSettingRoutes = (): INavItem[] => {
label: t("salesChannels.domain"),
to: "/settings/sales-channels",
},
{
label: t("shippingOptionTypes.domain"),
to: "/settings/shipping-option-types",
},
{
label: t("productTypes.domain"),
to: "/settings/product-types",

View File

@@ -1369,6 +1369,59 @@ export function getRouteMap({
},
],
},
{
path: "shipping-option-types",
errorElement: <ErrorBoundary />,
element: <Outlet />,
handle: {
breadcrumb: () => t("shippingOptionTypes.domain"),
},
children: [
{
path: "",
lazy: () =>
import(
"../../routes/shipping-option-types/shipping-option-type-list"
),
children: [
{
path: "create",
lazy: () =>
import(
"../../routes/shipping-option-types/shipping-option-type-create"
),
},
],
},
{
path: ":id",
lazy: async () => {
const { Component, Breadcrumb, loader } = await import(
"../../routes/shipping-option-types/shipping-option-type-detail"
)
return {
Component,
loader,
handle: {
breadcrumb: (
match: UIMatch<HttpTypes.AdminShippingOptionTypeResponse>
) => <Breadcrumb {...match} />,
},
}
},
children: [
{
path: "edit",
lazy: () =>
import(
"../../routes/shipping-option-types/shipping-option-type-edit"
),
},
],
},
],
},
{
path: "product-types",
errorElement: <ErrorBoundary />,

View File

@@ -26,6 +26,7 @@ export * from "./regions"
export * from "./reservations"
export * from "./sales-channels"
export * from "./shipping-options"
export * from "./shipping-option-types"
export * from "./shipping-profiles"
export * from "./stock-locations"
export * from "./store"

View File

@@ -0,0 +1,128 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
const SHIPPING_OPTION_TYPES_QUERY_KEY = "shipping_option_types" as const
export const shippingOptionTypesQueryKeys = queryKeysFactory(
SHIPPING_OPTION_TYPES_QUERY_KEY
)
export const useShippingOptionType = (
id: string,
query?: HttpTypes.SelectParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminShippingOptionTypeResponse,
FetchError,
HttpTypes.AdminShippingOptionTypeResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.shippingOptionType.retrieve(id, query),
queryKey: shippingOptionTypesQueryKeys.detail(id),
...options,
})
return { ...data, ...rest }
}
export const useShippingOptionTypes = (
query?: HttpTypes.AdminShippingOptionTypeListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminShippingOptionTypeListResponse,
FetchError,
HttpTypes.AdminShippingOptionTypeListResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.shippingOptionType.list(query),
queryKey: shippingOptionTypesQueryKeys.list(query),
...options,
})
return { ...data, ...rest }
}
export const useCreateShippingOptionType = (
options?: UseMutationOptions<
HttpTypes.AdminShippingOptionTypeResponse,
FetchError,
HttpTypes.AdminCreateShippingOptionType
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.shippingOptionType.create(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: shippingOptionTypesQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateShippingOptionType = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminShippingOptionTypeResponse,
FetchError,
HttpTypes.AdminUpdateShippingOptionType
>
) => {
return useMutation({
mutationFn: (payload) => sdk.admin.shippingOptionType.update(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: shippingOptionTypesQueryKeys.detail(id),
})
queryClient.invalidateQueries({
queryKey: shippingOptionTypesQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeleteShippingOptionType = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminShippingOptionTypeDeleteResponse,
FetchError,
void
>
) => {
return useMutation({
mutationFn: () => sdk.admin.shippingOptionType.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: shippingOptionTypesQueryKeys.detail(id),
})
queryClient.invalidateQueries({
queryKey: shippingOptionTypesQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -0,0 +1,44 @@
import { HttpTypes } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { DateCell } from "../../../components/table/table-cells/common/date-cell"
import { TextCell } from "../../../components/table/table-cells/common/text-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminShippingOptionType>()
export const useShippingOptionTypeTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("label", {
header: () => t("fields.label"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("code", {
header: () => t("fields.code"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("description", {
header: () => t("fields.description"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("created_at", {
header: () => t("fields.createdAt"),
cell: ({ getValue }) => {
return <DateCell date={getValue()} />
},
}),
columnHelper.accessor("updated_at", {
header: () => t("fields.updatedAt"),
cell: ({ getValue }) => {
return <DateCell date={getValue()} />
},
}),
],
[t]
)
}

View File

@@ -0,0 +1,5 @@
import { useDateTableFilters } from "./use-date-table-filters"
export const useShippingOptionTypeTableFilters = () => {
return useDateTableFilters()
}

View File

@@ -0,0 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseShippingOptionTypeTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useShippingOptionTypeTableQuery = ({
prefix,
pageSize = 20,
}: UseShippingOptionTypeTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams: HttpTypes.AdminShippingOptionTypeListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}

View File

@@ -10411,6 +10411,77 @@
],
"additionalProperties": false
},
"shippingOptionTypes": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"create": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"hint": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["header", "hint", "successToast"],
"additionalProperties": false
},
"edit": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["header", "successToast"],
"additionalProperties": false
},
"delete": {
"type": "object",
"properties": {
"confirmation": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["confirmation", "successToast"],
"additionalProperties": false
},
"fields": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"code": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["label", "code", "description"],
"additionalProperties": false
}
},
"required": ["domain", "subtitle", "create", "edit", "delete", "fields"],
"additionalProperties": false
},
"productTypes": {
"type": "object",
"properties": {

View File

@@ -2799,6 +2799,28 @@
}
}
},
"shippingOptionTypes": {
"domain": "Shipping Option Types",
"subtitle": "Organize your shipping options into types.",
"create": {
"header": "Create Shipping Option Type",
"hint": "Create a new shipping option type to categorize your shipping options.",
"successToast": "Shipping option type {{label}} was successfully created."
},
"edit": {
"header": "Edit Shipping Option Type",
"successToast": "Shipping option type {{label}} was successfully updated."
},
"delete": {
"confirmation": "You are about to delete the shipping option type \"{{label}}\". This action cannot be undone.",
"successToast": "Shipping option type \"{{label}}\" was successfully deleted."
},
"fields": {
"label": "Label",
"code": "Code",
"description": "Description"
}
},
"productTypes": {
"domain": "Product Types",
"subtitle": "Organize your products into types.",
@@ -3051,4 +3073,4 @@
"seconds_one": "Second",
"seconds_other": "Seconds"
}
}
}

View File

@@ -0,0 +1,41 @@
import { useNavigate } from "react-router-dom"
import { toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useDeleteShippingOptionType } from "../../../../hooks/api/shipping-option-types"
export const useDeleteShippingOptionTypeAction = (
id: string,
label: string
) => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const { mutateAsync } = useDeleteShippingOptionType(id)
const handleDelete = async () => {
const result = await prompt({
title: t("general.areYouSure"),
description: t("shippingOptionTypes.delete.confirmation", { label }),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!result) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
navigate("/settings/shipping-option-types", { replace: true })
toast.success(t("shippingOptionTypes.delete.successToast", { label }))
},
onError: (e) => {
toast.error(e.message)
},
})
}
return handleDelete
}

View File

@@ -0,0 +1,168 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteFocusModal, useRouteModal, } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCreateShippingOptionType } from "../../../../../hooks/api"
const CreateShippingOptionTypeSchema = z.object({
label: z.string().min(1),
code: z.string().min(1),
description: z.string().optional(),
})
export const CreateShippingOptionTypeForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof CreateShippingOptionTypeSchema>>({
defaultValues: {
label: "",
code: "",
description: undefined,
},
resolver: zodResolver(CreateShippingOptionTypeSchema),
})
const generateCodeFromLabel = (label: string) => {
return label
.toLowerCase()
.replace(/[^a-z0-9]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "")
}
const { mutateAsync, isPending } = useCreateShippingOptionType()
const handleSubmit = form.handleSubmit(
async (values: z.infer<typeof CreateShippingOptionTypeSchema>) => {
await mutateAsync(values, {
onSuccess: ({ shipping_option_type }) => {
toast.success(
t("shippingOptionTypes.create.successToast", {
label: shipping_option_type.label.trim(),
})
)
handleSuccess(
`/settings/shipping-option-types/${shipping_option_type.id}`
)
},
onError: (e) => {
toast.error(e.message)
},
})
}
)
return (
<RouteFocusModal.Form form={form}>
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteFocusModal.Body className="flex flex-col items-center overflow-auto p-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("shippingOptionTypes.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("shippingOptionTypes.create.hint")}
</Text>
</div>
<div className="grid grid-cols-1 gap-4">
<Form.Field
control={form.control}
name="label"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.label")}
</Form.Label>
<Form.Control>
<Input
{...field}
onChange={(e) => {
if (
!form.getFieldState("code").isTouched ||
!form.getValues("code")
) {
form.setValue(
"code",
generateCodeFromLabel(e.target.value)
)
}
field.onChange(e)
}}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.code")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="description"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.description")}
<Text
size="small"
leading="compact"
className="text-ui-fg-muted ml-1 inline"
>
({t("fields.optional")})
</Text>
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
<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"
variant="primary"
type="submit"
isLoading={isPending}
>
{t("actions.create")}
</Button>
</div>
</RouteFocusModal.Footer>
</KeyboundForm>
</RouteFocusModal.Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./create-shipping-option-type-form"

View File

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

View File

@@ -0,0 +1,10 @@
import { RouteFocusModal } from "../../../components/modals"
import { CreateShippingOptionTypeForm } from "./components/create-shipping-option-type-form"
export const ShippingOptionTypeCreate = () => {
return (
<RouteFocusModal>
<CreateShippingOptionTypeForm />
</RouteFocusModal>
)
}

View File

@@ -0,0 +1,24 @@
import { HttpTypes } from "@medusajs/types"
import { UIMatch } from "react-router-dom"
import { useShippingOptionType } from "../../../hooks/api"
type ShippingOptionTypeDetailBreadcrumbProps =
UIMatch<HttpTypes.AdminShippingOptionTypeResponse>
export const ShippingOptionTypeDetailBreadcrumb = (
props: ShippingOptionTypeDetailBreadcrumbProps
) => {
const { id } = props.params || {}
const { shipping_option_type } = useShippingOptionType(id!, undefined, {
initialData: props.data,
enabled: Boolean(id),
})
if (!shipping_option_type) {
return null
}
return <span>{shipping_option_type.label}</span>
}

View File

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

View File

@@ -0,0 +1,66 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Container, Heading, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteShippingOptionTypeAction } from "../../../common/hooks/use-delete-shipping-option-type-action"
type ShippingOptionTypeGeneralSectionProps = {
shippingOptionType: HttpTypes.AdminShippingOptionType
}
export const ShippingOptionTypeGeneralSection = ({
shippingOptionType,
}: ShippingOptionTypeGeneralSectionProps) => {
const { t } = useTranslation()
const handleDelete = useDeleteShippingOptionTypeAction(
shippingOptionType.id,
shippingOptionType.label
)
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{shippingOptionType.label}</Heading>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: "edit",
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.code")}
</Text>
<Text size="small" leading="compact">
{shippingOptionType.code}
</Text>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("fields.description")}
</Text>
<Text size="small" leading="compact">
{shippingOptionType.description || "-"}
</Text>
</div>
</Container>
)
}

View File

@@ -0,0 +1,3 @@
export { ShippingOptionTypeDetailBreadcrumb as Breadcrumb } from "./breadcrumb"
export { shippingOptionTypeLoader as loader } from "./loader"
export { ShippingOptionTypeDetail as Component } from "./shipping-option-type-detail"

View File

@@ -0,0 +1,19 @@
import { LoaderFunctionArgs } from "react-router-dom"
import { shippingOptionTypesQueryKeys } from "../../../hooks/api/shipping-option-types"
import { sdk } from "../../../lib/client"
import { queryClient } from "../../../lib/query-client"
const shippingOptionTypeDetailQuery = (id: string) => ({
queryKey: shippingOptionTypesQueryKeys.detail(id),
queryFn: async () => sdk.admin.shippingOptionType.retrieve(id),
})
export const shippingOptionTypeLoader = async ({
params,
}: LoaderFunctionArgs) => {
const id = params.id
const query = shippingOptionTypeDetailQuery(id!)
return queryClient.ensureQueryData(query)
}

View File

@@ -0,0 +1,46 @@
import { useLoaderData, useParams } from "react-router-dom"
import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
import { SingleColumnPage } from "../../../components/layout/pages"
import { useShippingOptionType } from "../../../hooks/api"
import { useExtension } from "../../../providers/extension-provider"
import { ShippingOptionTypeGeneralSection } from "./components/shipping-option-type-general-section"
import { shippingOptionTypeLoader } from "./loader"
export const ShippingOptionTypeDetail = () => {
const { id } = useParams()
const initialData = useLoaderData() as Awaited<
ReturnType<typeof shippingOptionTypeLoader>
>
const { shipping_option_type, isPending, isError, error } =
useShippingOptionType(id!, undefined, {
initialData,
})
const { getWidgets } = useExtension()
if (isPending || !shipping_option_type) {
return <SingleColumnPageSkeleton sections={2} showJSON showMetadata />
}
if (isError) {
throw error
}
return (
<SingleColumnPage
widgets={{
after: getWidgets("shipping_option_type.details.after"),
before: getWidgets("shipping_option_type.details.before"),
}}
showJSON
showMetadata
data={shipping_option_type}
>
<ShippingOptionTypeGeneralSection
shippingOptionType={shipping_option_type}
/>
</SingleColumnPage>
)
}

View File

@@ -0,0 +1,145 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types"
import { Button, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useUpdateShippingOptionType } from "../../../../../hooks/api/shipping-option-types"
const EditShippingOptionTypeSchema = z.object({
label: z.string().min(1),
code: z.string().min(1),
description: z.string().optional(),
})
type EditShippingOptionTypeFormProps = {
shippingOptionType: HttpTypes.AdminShippingOptionType
}
export const EditShippingOptionTypeForm = ({
shippingOptionType,
}: EditShippingOptionTypeFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof EditShippingOptionTypeSchema>>({
defaultValues: {
label: shippingOptionType.label,
code: shippingOptionType.code,
description: shippingOptionType.description,
},
resolver: zodResolver(EditShippingOptionTypeSchema),
})
const { mutateAsync, isPending } = useUpdateShippingOptionType(
shippingOptionType.id
)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
label: data.label,
code: data.code,
description: data.description,
},
{
onSuccess: ({ shipping_option_type }) => {
toast.success(
t("shippingOptionTypes.edit.successToast", {
value: shipping_option_type.label,
})
)
handleSuccess()
},
onError: (error) => {
toast.error(error.message)
},
}
)
})
return (
<RouteDrawer.Form form={form}>
<KeyboundForm
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="label"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.label")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="code"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.code")}
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="description"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("shippingOptionTypes.fields.description")}
<Text
size="small"
leading="compact"
className="text-ui-fg-muted ml-1 inline"
>
({t("fields.optional")})
</Text>
</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</KeyboundForm>
</RouteDrawer.Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./edit-shipping-option-type-form"

View File

@@ -0,0 +1 @@
export { ShippingOptionTypeEdit as Component } from "./shipping-option-type-edit"

View File

@@ -0,0 +1,31 @@
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/modals"
import { useShippingOptionType } from "../../../hooks/api"
import { EditShippingOptionTypeForm } from "./components/edit-shipping-option-type-form"
export const ShippingOptionTypeEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const { shipping_option_type, isPending, isError, error } =
useShippingOptionType(id!)
const ready = !isPending && !!shipping_option_type
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("shippingOptionTypes.edit.header")}</Heading>
</RouteDrawer.Header>
{ready && (
<EditShippingOptionTypeForm shippingOptionType={shipping_option_type} />
)}
</RouteDrawer>
)
}

View File

@@ -0,0 +1 @@
export * from "./shipping-option-type-list-table"

View File

@@ -0,0 +1,99 @@
import { HttpTypes } from "@medusajs/types"
import { Button, Container, Heading, Text } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { _DataTable } from "../../../../../components/table/data-table"
import { useShippingOptionTypes } from "../../../../../hooks/api"
import { useShippingOptionTypeTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-type-table-columns"
import { useShippingOptionTypeTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-type-table-filters"
import { useShippingOptionTypeTableQuery } from "../../../../../hooks/table/query/use-shipping-option-type-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { ShippingOptionTypeRowActions } from "./shipping-option-type-table-row-actions"
const PAGE_SIZE = 20
export const ShippingOptionTypeListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useShippingOptionTypeTableQuery({
pageSize: PAGE_SIZE,
})
const { shipping_option_types, count, isLoading, isError, error } =
useShippingOptionTypes(searchParams, {
placeholderData: keepPreviousData,
})
const filters = useShippingOptionTypeTableFilters()
const columns = useColumns()
const { table } = useDataTable({
columns,
data: shipping_option_types,
count,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<div>
<Heading>{t("shippingOptionTypes.domain")}</Heading>
<Text className="text-ui-fg-subtle" size="small">
{t("shippingOptionTypes.subtitle")}
</Text>
</div>
<Button size="small" variant="secondary" asChild>
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<_DataTable
table={table}
filters={filters}
isLoading={isLoading}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
orderBy={[
{ key: "label", label: t("fields.label") },
{ key: "code", label: t("fields.code") },
{ key: "description", label: t("fields.description") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
navigateTo={({ original }) => original.id}
queryObject={raw}
pagination
search
/>
</Container>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminShippingOptionType>()
const useColumns = () => {
const base = useShippingOptionTypeTableColumns()
return useMemo(
() => [
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return (
<ShippingOptionTypeRowActions shippingOptionType={row.original} />
)
},
}),
],
[base]
)
}

View File

@@ -0,0 +1,44 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteShippingOptionTypeAction } from "../../../common/hooks/use-delete-shipping-option-type-action"
type ShippingOptionTypeRowActionsProps = {
shippingOptionType: HttpTypes.AdminShippingOptionType
}
export const ShippingOptionTypeRowActions = ({
shippingOptionType,
}: ShippingOptionTypeRowActionsProps) => {
const { t } = useTranslation()
const handleDelete = useDeleteShippingOptionTypeAction(
shippingOptionType.id,
shippingOptionType.label
)
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `/settings/shipping-option-types/${shippingOptionType.id}/edit`,
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
)
}

View File

@@ -0,0 +1 @@
export { ShippingOptionTypeList as Component } from "./shipping-option-type-list"

View File

@@ -0,0 +1,18 @@
import { SingleColumnPage } from "../../../components/layout/pages"
import { useExtension } from "../../../providers/extension-provider"
import { ShippingOptionTypeListTable } from "./components/shipping-option-type-list-table"
export const ShippingOptionTypeList = () => {
const { getWidgets } = useExtension()
return (
<SingleColumnPage
widgets={{
after: getWidgets("shipping_option_type.list.after"),
before: getWidgets("shipping_option_type.list.before"),
}}
>
<ShippingOptionTypeListTable />
</SingleColumnPage>
)
}

View File

@@ -43,6 +43,7 @@ import { TaxRegion } from "./tax-region"
import { Upload } from "./upload"
import { User } from "./user"
import { WorkflowExecution } from "./workflow-execution"
import { ShippingOptionType } from "./shipping-option-type"
export class Admin {
/**
@@ -113,6 +114,10 @@ export class Admin {
* @tags fulfillment
*/
public shippingOption: ShippingOption
/**
* @tags fulfillment
*/
public shippingOptionType: ShippingOptionType
/**
* @tags fulfillment
*/
@@ -240,6 +245,7 @@ export class Admin {
this.fulfillment = new Fulfillment(client)
this.fulfillmentProvider = new FulfillmentProvider(client)
this.shippingOption = new ShippingOption(client)
this.shippingOptionType = new ShippingOptionType(client)
this.shippingProfile = new ShippingProfile(client)
this.inventoryItem = new InventoryItem(client)
this.notification = new Notification(client)

View File

@@ -0,0 +1,219 @@
import { HttpTypes } from "@medusajs/types"
import { Client } from "../client"
import { ClientHeaders } from "../types"
export class ShippingOptionType {
/**
* @ignore
*/
private client: Client
/**
* @ignore
*/
constructor(client: Client) {
this.client = client
}
/**
* This method creates a shipping option type. It sends a request to the
* [Create Shipping Option Type](TODO HERE)
* API route.
*
* @param body - The shipping option type's details.
* @param query - Configure the fields to retrieve in the shipping option type.
* @param headers - Headers to pass in the request
* @returns The shipping option type's details.
*
* @example
* sdk.admin.shippingOptionType.create({
* label: "Standard",
* code: "standard",
* description: "Ship in 2-3 days."
* })
* .then(({ shipping_option_type }) => {
* console.log(shipping_option_type)
* })
*/
async create(
body: HttpTypes.AdminCreateShippingOptionType,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminShippingOptionTypeResponse>(
`/admin/shipping-option-types`,
{
method: "POST",
headers,
body,
query,
}
)
}
/**
* This method updates a shipping option type. It sends a request to the
* [Update Shipping Option Type](TODO HERE)
* API route.
*
* @param id - The shipping option type's ID.
* @param body - The data to update in the shipping option type.
* @param query - Configure the fields to retrieve in the shipping option type.
* @param headers - Headers to pass in the request
* @returns The shipping option type's details.
*
* @example
* sdk.admin.shippingOptionType.update("sotype_123", {
* code: "express"
* })
* .then(({ shipping_option_type }) => {
* console.log(shipping_option_type)
* })
*/
async update(
id: string,
body: HttpTypes.AdminUpdateShippingOptionType,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminShippingOptionTypeResponse>(
`/admin/shipping-option-types/${id}`,
{
method: "POST",
headers,
body,
query,
}
)
}
/**
* This method retrieves a paginated list of shipping option types. It sends a request to the
* [List Shipping Option Types](TODO HERE) API route.
*
* @param query - Filters and pagination configurations.
* @param headers - Headers to pass in the request.
* @returns The paginated list of shipping option types.
*
* @example
* To retrieve the list of shipping option types:
*
* ```ts
* sdk.admin.shippingOptionType.list()
* .then(({ shipping_option_types, count, limit, offset }) => {
* console.log(shipping_option_types)
* })
* ```
*
* To configure the pagination, pass the `limit` and `offset` query parameters.
*
* For example, to retrieve only 10 items and skip 10 items:
*
* ```ts
* sdk.admin.shippingOptionType.list({
* limit: 10,
* offset: 10
* })
* .then(({ shipping_option_types, count, limit, offset }) => {
* console.log(shipping_option_types)
* })
* ```
*
* Using the `fields` query parameter, you can specify the fields and relations to retrieve
* in each shipping option type:
*
* ```ts
* sdk.admin.shippingOptionType.list({
* fields: "id,*shippingOptions"
* })
* .then(({ shipping_option_types, count, limit, offset }) => {
* console.log(shipping_option_types)
* })
* ```
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
*/
async list(
query?: HttpTypes.AdminShippingOptionTypeListParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminShippingOptionTypeListResponse>(
`/admin/shipping-option-types`,
{
headers,
query: query,
}
)
}
/**
* This method retrieves a shipping option type by its ID. It sends a request to the
* [Get Shipping Option Type](TODO HERE)
* API route.
*
* @param id - The shipping option type's ID.
* @param query - Configure the fields to retrieve in the shipping option type.
* @param headers - Headers to pass in the request
* @returns The shipping option type's details.
*
* @example
* To retrieve a shipping option type by its ID:
*
* ```ts
* sdk.admin.shippingOptionType.retrieve("sotype_123")
* .then(({ shipping_option_type }) => {
* console.log(shipping_option_type)
* })
* ```
*
* To specify the fields and relations to retrieve:
*
* ```ts
* sdk.admin.shippingOptionType.retrieve("sotype_123", {
* fields: "id,*shippingOptions"
* })
* .then(({ shipping_option_type }) => {
* console.log(shipping_option_type)
* })
* ```
*
* Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations).
*/
async retrieve(
id: string,
query?: HttpTypes.SelectParams,
headers?: ClientHeaders
) {
return this.client.fetch<HttpTypes.AdminShippingOptionTypeResponse>(
`/admin/shipping-option-types/${id}`,
{
query,
headers,
}
)
}
/**
* This method deletes a shipping option type. It sends a request to the
* [Delete Shipping Option Type](TODO HERE)
* API route.
*
* @param id - The shipping option type's ID.
* @param headers - Headers to pass in the request
* @returns The shipping option type's details.
*
* @example
* sdk.admin.shippingOptionType.delete("sotype_123")
* .then(({ deleted }) => {
* console.log(deleted)
* })
*/
async delete(id: string, headers?: ClientHeaders) {
return this.client.fetch<HttpTypes.AdminShippingOptionTypeDeleteResponse>(
`/admin/shipping-option-types/${id}`,
{
method: "DELETE",
headers,
}
)
}
}

View File

@@ -4,9 +4,9 @@ import { ShippingOption } from "./shipping-option"
export const ShippingOptionType = model.define("shipping_option_type", {
id: model.id({ prefix: "sotype" }).primaryKey(),
label: model.text(),
description: model.text().nullable(),
code: model.text(),
label: model.text().searchable(),
description: model.text().searchable().nullable(),
code: model.text().searchable(),
shipping_option: model.hasOne(() => ShippingOption, {
mappedBy: "type",
}),

View File

@@ -10,7 +10,7 @@ import { ShippingProfile } from "./shipping-profile"
export const ShippingOption = model
.define("shipping_option", {
id: model.id({ prefix: "so" }).primaryKey(),
name: model.text(),
name: model.text().searchable(),
price_type: model
.enum(ShippingOptionPriceType)
.default(ShippingOptionPriceType.FLAT),