fix(dashboard): Fix minor issues with Reservations and Inventroy Create forms (#8657)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { castNumber } from "./cast-number"
|
import { castNumber } from "./cast-number"
|
||||||
|
|
||||||
export function parseOptionalFormValue<T>(
|
export function transformNullableFormValue<T>(
|
||||||
value: T,
|
value: T,
|
||||||
nullify = true
|
nullify = true
|
||||||
): T | undefined | null {
|
): T | undefined | null {
|
||||||
@@ -16,20 +16,21 @@ export function parseOptionalFormValue<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Nullable<T> = { [K in keyof T]: T[K] | null }
|
type Nullable<T> = { [K in keyof T]: T[K] | null }
|
||||||
|
type Optional<T> = { [K in keyof T]: T[K] | undefined }
|
||||||
|
|
||||||
export function parseOptionalFormData<T extends Record<string, unknown>>(
|
export function transformNullableFormData<
|
||||||
data: T,
|
T extends Record<string, unknown>,
|
||||||
nullify = true
|
K extends boolean = true
|
||||||
): Nullable<T> {
|
>(data: T, nullify: K = true as K): K extends true ? Nullable<T> : Optional<T> {
|
||||||
return Object.entries(data).reduce((acc, [key, value]) => {
|
return Object.entries(data).reduce((acc, [key, value]) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[key]: parseOptionalFormValue(value, nullify),
|
[key]: transformNullableFormValue(value, nullify),
|
||||||
}
|
}
|
||||||
}, {} as Nullable<T>)
|
}, {} as K extends true ? Nullable<T> : Optional<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseOptionalFormNumber(
|
export function transformNullableFormNumber(
|
||||||
value?: string | number,
|
value?: string | number,
|
||||||
nullify = true
|
nullify = true
|
||||||
) {
|
) {
|
||||||
@@ -46,3 +47,21 @@ export function parseOptionalFormNumber(
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NullableNumbers = Record<string, number | null>
|
||||||
|
type OptionalNumbers = Record<string, number | undefined>
|
||||||
|
|
||||||
|
export function transformNullableFormNumbers<
|
||||||
|
T extends Record<string, string | number | undefined>,
|
||||||
|
K extends boolean = true
|
||||||
|
>(
|
||||||
|
data: T,
|
||||||
|
nullify: K = true as K
|
||||||
|
): K extends true ? NullableNumbers : OptionalNumbers {
|
||||||
|
return Object.entries(data).reduce((acc, [key, value]) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: transformNullableFormNumber(value, nullify),
|
||||||
|
}
|
||||||
|
}, {} as K extends true ? NullableNumbers : OptionalNumbers)
|
||||||
|
}
|
||||||
|
|||||||
@@ -546,9 +546,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
{
|
{
|
||||||
path: "create",
|
path: "create",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import(
|
import("../../routes/reservations/reservation-create"),
|
||||||
"../../routes/reservations/reservation-list/create-reservation"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./create-inventory-item-form.tsx"
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./inventory-create-form.tsx"
|
||||||
+12
-7
@@ -1,18 +1,23 @@
|
|||||||
import { useMemo } from "react"
|
|
||||||
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { useMemo } from "react"
|
||||||
import { UseFormReturn } from "react-hook-form"
|
import { UseFormReturn } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { DataGrid } from "../../../../../components/data-grid"
|
|
||||||
import { createDataGridHelper } from "../../../../../components/data-grid/utils"
|
import {
|
||||||
|
DataGrid,
|
||||||
|
createDataGridHelper,
|
||||||
|
} from "../../../../../components/data-grid"
|
||||||
import { useRouteModal } from "../../../../../components/modals"
|
import { useRouteModal } from "../../../../../components/modals"
|
||||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||||
|
import { CreateInventoryItemSchema } from "./schema"
|
||||||
|
|
||||||
type Props = {
|
type InventoryAvailabilityFormProps = {
|
||||||
form: UseFormReturn<{}>
|
form: UseFormReturn<CreateInventoryItemSchema>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateInventoryAvailabilityForm = ({ form }: Props) => {
|
export const InventoryAvailabilityForm = ({
|
||||||
|
form,
|
||||||
|
}: InventoryAvailabilityFormProps) => {
|
||||||
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||||
const { setCloseOnEscape } = useRouteModal()
|
const { setCloseOnEscape } = useRouteModal()
|
||||||
|
|
||||||
+163
-206
@@ -1,22 +1,21 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import React, { useEffect } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import * as zod from "zod"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
ProgressStatus,
|
ProgressStatus,
|
||||||
ProgressTabs,
|
ProgressTabs,
|
||||||
Switch,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
clx,
|
clx,
|
||||||
toast,
|
toast,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Divider } from "../../../../../components/common/divider"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||||
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
@@ -27,9 +26,13 @@ import {
|
|||||||
useCreateInventoryItem,
|
useCreateInventoryItem,
|
||||||
} from "../../../../../hooks/api/inventory"
|
} from "../../../../../hooks/api/inventory"
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
|
import {
|
||||||
|
transformNullableFormData,
|
||||||
|
transformNullableFormNumbers,
|
||||||
|
} from "../../../../../lib/form-helpers"
|
||||||
import { queryClient } from "../../../../../lib/query-client"
|
import { queryClient } from "../../../../../lib/query-client"
|
||||||
import { optionalInt } from "../../../../../lib/validation"
|
import { InventoryAvailabilityForm } from "./inventory-availability-form"
|
||||||
import { CreateInventoryAvailabilityForm } from "./create-inventory-availability-form"
|
import { CreateInventoryItemSchema } from "./schema"
|
||||||
|
|
||||||
enum Tab {
|
enum Tab {
|
||||||
DETAILS = "details",
|
DETAILS = "details",
|
||||||
@@ -40,31 +43,12 @@ type StepStatus = {
|
|||||||
[key in Tab]: ProgressStatus
|
[key in Tab]: ProgressStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateInventoryItemSchema = zod.object({
|
export function InventoryCreateForm() {
|
||||||
title: zod.string().min(1),
|
|
||||||
|
|
||||||
sku: zod.string().optional(),
|
|
||||||
hs_code: zod.string().optional(),
|
|
||||||
weight: optionalInt,
|
|
||||||
length: optionalInt,
|
|
||||||
height: optionalInt,
|
|
||||||
width: optionalInt,
|
|
||||||
origin_country: zod.string().optional(),
|
|
||||||
mid_code: zod.string().optional(),
|
|
||||||
material: zod.string().optional(),
|
|
||||||
description: zod.string().optional(),
|
|
||||||
requires_shipping: zod.boolean().optional(),
|
|
||||||
thumbnail: zod.string().optional(),
|
|
||||||
locations: zod.record(zod.string(), zod.number().optional()).optional(),
|
|
||||||
// metadata: zod.record(zod.string(), zod.unknown()).optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function CreateInventoryItemForm() {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [tab, setTab] = React.useState<Tab>(Tab.DETAILS)
|
const [tab, setTab] = useState<Tab>(Tab.DETAILS)
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateInventoryItemSchema>>({
|
const form = useForm<CreateInventoryItemSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
sku: "",
|
sku: "",
|
||||||
@@ -83,66 +67,86 @@ export function CreateInventoryItemForm() {
|
|||||||
resolver: zodResolver(CreateInventoryItemSchema),
|
resolver: zodResolver(CreateInventoryItemSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
trigger,
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
const { mutateAsync: createInventoryItem, isPending: isLoading } =
|
const { mutateAsync: createInventoryItem, isPending: isLoading } =
|
||||||
useCreateInventoryItem()
|
useCreateInventoryItem()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const { locations, ...payload } = data
|
const { locations, weight, length, height, width, ...payload } = data
|
||||||
|
|
||||||
for (const k in payload) {
|
const cleanData = transformNullableFormData(payload, false)
|
||||||
if (payload[k] === "") {
|
const cleanNumbers = transformNullableFormNumbers(
|
||||||
delete payload[k]
|
{
|
||||||
continue
|
weight,
|
||||||
|
length,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
const { inventory_item } = await createInventoryItem(
|
||||||
|
{
|
||||||
|
...cleanData,
|
||||||
|
...cleanNumbers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (e) => {
|
||||||
|
toast.error(e.message)
|
||||||
|
return
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (["weight", "length", "height", "width"].includes(k)) {
|
await sdk.admin.inventoryItem
|
||||||
payload[k] = parseInt(payload[k])
|
.batchUpdateLevels(inventory_item.id, {
|
||||||
}
|
create: Object.entries(locations ?? {})
|
||||||
}
|
.filter(([_, quantiy]) => !!quantiy)
|
||||||
|
.map(([location_id, stocked_quantity]) => ({
|
||||||
try {
|
location_id,
|
||||||
const { inventory_item } = await createInventoryItem(payload)
|
stocked_quantity,
|
||||||
|
})),
|
||||||
try {
|
})
|
||||||
await sdk.admin.inventoryItem.batchUpdateLevels(inventory_item.id, {
|
.then(async () => {
|
||||||
create: Object.entries(locations)
|
|
||||||
.filter(([_, quantiy]) => !!quantiy)
|
|
||||||
.map(([location_id, stocked_quantity]) => ({
|
|
||||||
location_id,
|
|
||||||
stocked_quantity,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: inventoryItemsQueryKeys.lists(),
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// Since the inventory item is created, we only log the error,
|
||||||
|
// but still close the modal to prevent the user from trying to
|
||||||
|
// create the same item again.
|
||||||
toast.error(e.message)
|
toast.error(e.message)
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
} catch (e) {
|
})
|
||||||
toast.error(e.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const [status, setStatus] = React.useState<StepStatus>({
|
const [status, setStatus] = useState<StepStatus>({
|
||||||
[Tab.AVAILABILITY]: "not-started",
|
[Tab.AVAILABILITY]: "not-started",
|
||||||
[Tab.DETAILS]: "not-started",
|
[Tab.DETAILS]: "not-started",
|
||||||
})
|
})
|
||||||
|
|
||||||
const onTabChange = React.useCallback(async (value: Tab) => {
|
const onTabChange = useCallback(
|
||||||
const result = await form.trigger()
|
async (value: Tab) => {
|
||||||
|
const result = await trigger()
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTab(value)
|
setTab(value)
|
||||||
}, [])
|
},
|
||||||
|
[trigger]
|
||||||
|
)
|
||||||
|
|
||||||
const onNext = React.useCallback(async () => {
|
const onNext = useCallback(async () => {
|
||||||
const result = await form.trigger()
|
const result = await trigger()
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return
|
return
|
||||||
@@ -156,18 +160,18 @@ export function CreateInventoryItemForm() {
|
|||||||
case Tab.AVAILABILITY:
|
case Tab.AVAILABILITY:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [tab])
|
}, [tab, trigger])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.formState.isDirty) {
|
if (isDirty) {
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||||
} else {
|
} else {
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
||||||
}
|
}
|
||||||
}, [form.formState.isDirty])
|
}, [isDirty])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab === Tab.DETAILS && form.formState.isDirty) {
|
if (tab === Tab.DETAILS && isDirty) {
|
||||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,18 +182,18 @@ export function CreateInventoryItemForm() {
|
|||||||
[Tab.AVAILABILITY]: "in-progress",
|
[Tab.AVAILABILITY]: "in-progress",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}, [tab])
|
}, [tab, isDirty])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form
|
<ProgressTabs
|
||||||
className="flex h-full flex-col overflow-hidden"
|
value={tab}
|
||||||
onSubmit={handleSubmit}
|
className="h-full"
|
||||||
|
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||||
>
|
>
|
||||||
<ProgressTabs
|
<form
|
||||||
value={tab}
|
className="flex h-full flex-col overflow-hidden"
|
||||||
className="h-full"
|
onSubmit={handleSubmit}
|
||||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
|
||||||
>
|
>
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header>
|
||||||
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
<ProgressTabs.List className="border-ui-border-base -my-2 ml-2 min-w-0 flex-1 border-l">
|
||||||
@@ -212,123 +216,52 @@ export function CreateInventoryItemForm() {
|
|||||||
</span>
|
</span>
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
</ProgressTabs.List>
|
</ProgressTabs.List>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
|
||||||
<RouteFocusModal.Close asChild>
|
|
||||||
<Button variant="secondary" size="small">
|
|
||||||
{t("actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</RouteFocusModal.Close>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={tab !== Tab.AVAILABILITY ? onNext : undefined}
|
|
||||||
key={tab === Tab.AVAILABILITY ? "details" : "pricing"}
|
|
||||||
type={tab === Tab.AVAILABILITY ? "submit" : "button"}
|
|
||||||
>
|
|
||||||
{tab === Tab.AVAILABILITY
|
|
||||||
? t("actions.save")
|
|
||||||
: t("general.next")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
|
|
||||||
<RouteFocusModal.Body
|
<RouteFocusModal.Body
|
||||||
className={clx(
|
className={clx(
|
||||||
"flex h-full w-full flex-col items-center divide-y overflow-hidden",
|
"flex h-full w-full flex-col items-center divide-y overflow-hidden px-3",
|
||||||
{ "mx-auto": tab === Tab.DETAILS }
|
{ "mx-auto": tab === Tab.DETAILS }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
<ProgressTabs.Content
|
||||||
<div className="mx-auto w-[720px] px-1 py-8">
|
value={Tab.DETAILS}
|
||||||
<Heading level="h2" className="mb-12 mt-8 text-2xl">
|
className="h-full w-full overflow-auto"
|
||||||
{t("inventory.create.title")}
|
>
|
||||||
</Heading>
|
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-px py-16">
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
<div className="flex flex-col gap-y-6">
|
<Heading>{t("inventory.create.title")}</Heading>
|
||||||
<div className="grid grid-cols-1 gap-x-3 gap-y-6 lg:grid-cols-2">
|
<div className="flex flex-col gap-y-6">
|
||||||
<Form.Field
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
control={form.control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.title")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder={t("fields.title")}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Field
|
|
||||||
control={form.control}
|
|
||||||
name="sku"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item>
|
|
||||||
<Form.Label>{t("fields.sku")}</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Input {...field} placeholder="sku-123" />
|
|
||||||
</Form.Control>
|
|
||||||
<Form.ErrorMessage />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="title"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
{t("products.fields.description.label")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Textarea
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
placeholder="The item description"
|
placeholder={t("fields.title")}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="requires_shipping"
|
name="sku"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex flex-col gap-y-1">
|
<Form.Label>{t("fields.sku")}</Form.Label>
|
||||||
<div className="flex items-center justify-between">
|
<Form.Control>
|
||||||
<Form.Label>
|
<Input {...field} placeholder="sku-123" />
|
||||||
{t("inventory.create.requiresShipping")}
|
</Form.Control>
|
||||||
</Form.Label>
|
|
||||||
<Form.Control>
|
|
||||||
<Switch
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onChange(!!checked)
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
<Form.Hint>
|
|
||||||
{t("inventory.create.requiresShippingHint")}
|
|
||||||
</Form.Hint>
|
|
||||||
</div>
|
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
@@ -336,40 +269,43 @@ export function CreateInventoryItemForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <Form.Field*/}
|
<Form.Field
|
||||||
{/* className="col-span-1"*/}
|
control={form.control}
|
||||||
{/* control={form.control}*/}
|
name="description"
|
||||||
{/* name="location_ids"*/}
|
render={({ field }) => {
|
||||||
{/* render={({ field }) => {*/}
|
return (
|
||||||
{/* return (*/}
|
<Form.Item>
|
||||||
{/* <Form.Item>*/}
|
<Form.Label optional>
|
||||||
{/* <Form.Label optional>*/}
|
{t("products.fields.description.label")}
|
||||||
{/* {t("inventory.create.locations")}*/}
|
</Form.Label>
|
||||||
{/* </Form.Label>*/}
|
<Form.Control>
|
||||||
{/* <Form.Control>*/}
|
<Textarea
|
||||||
{/* <Combobox*/}
|
{...field}
|
||||||
{/* {...field}*/}
|
placeholder="The item description"
|
||||||
{/* multiple*/}
|
/>
|
||||||
{/* options={locations.options}*/}
|
</Form.Control>
|
||||||
{/* searchValue={locations.searchValue}*/}
|
</Form.Item>
|
||||||
{/* onSearchValueChange={*/}
|
)
|
||||||
{/* locations.onSearchValueChange*/}
|
}}
|
||||||
{/* }*/}
|
/>
|
||||||
{/* fetchNextPage={locations.fetchNextPage}*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* </Form.Control>*/}
|
|
||||||
{/* <Form.ErrorMessage />*/}
|
|
||||||
{/* </Form.Item>*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* />*/}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Heading level="h3" className="my-6">
|
<SwitchBox
|
||||||
|
control={form.control}
|
||||||
|
name="requires_shipping"
|
||||||
|
label={t("inventory.create.requiresShipping")}
|
||||||
|
description={t("inventory.create.requiresShippingHint")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<Heading level="h2">
|
||||||
{t("inventory.create.attributes")}
|
{t("inventory.create.attributes")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-x-4 gap-y-4 lg:grid-cols-2 lg:gap-y-8">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="width"
|
name="width"
|
||||||
@@ -534,11 +470,32 @@ export function CreateInventoryItemForm() {
|
|||||||
value={Tab.AVAILABILITY}
|
value={Tab.AVAILABILITY}
|
||||||
className="size-full"
|
className="size-full"
|
||||||
>
|
>
|
||||||
<CreateInventoryAvailabilityForm form={form} />
|
<InventoryAvailabilityForm form={form} />
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</ProgressTabs>
|
<RouteFocusModal.Footer>
|
||||||
</form>
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={tab !== Tab.AVAILABILITY ? onNext : undefined}
|
||||||
|
key={tab === Tab.AVAILABILITY ? "details" : "pricing"}
|
||||||
|
type={tab === Tab.AVAILABILITY ? "submit" : "button"}
|
||||||
|
>
|
||||||
|
{tab === Tab.AVAILABILITY
|
||||||
|
? t("actions.save")
|
||||||
|
: t("general.next")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
|
</form>
|
||||||
|
</ProgressTabs>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
|
|
||||||
|
export const CreateInventoryItemSchema = z.object({
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
sku: z.string().optional(),
|
||||||
|
hs_code: z.string().optional(),
|
||||||
|
weight: optionalInt,
|
||||||
|
length: optionalInt,
|
||||||
|
height: optionalInt,
|
||||||
|
width: optionalInt,
|
||||||
|
origin_country: z.string().optional(),
|
||||||
|
mid_code: z.string().optional(),
|
||||||
|
material: z.string().optional(),
|
||||||
|
requires_shipping: z.boolean().optional(),
|
||||||
|
thumbnail: z.string().optional(),
|
||||||
|
locations: z.record(z.string(), z.number().optional()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CreateInventoryItemSchema = z.infer<
|
||||||
|
typeof CreateInventoryItemSchema
|
||||||
|
>
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { RouteFocusModal } from "../../../components/modals"
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
import { CreateInventoryItemForm } from "./components/create-inventory-item-form"
|
import { InventoryCreateForm } from "./components/inventory-create-form"
|
||||||
|
|
||||||
export function InventoryCreate() {
|
export function InventoryCreate() {
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<CreateInventoryItemForm />
|
<InventoryCreateForm />
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -15,8 +15,8 @@ import {
|
|||||||
} from "../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
||||||
import {
|
import {
|
||||||
parseOptionalFormData,
|
transformNullableFormData,
|
||||||
parseOptionalFormNumber,
|
transformNullableFormNumber,
|
||||||
} from "../../../../../lib/form-helpers"
|
} from "../../../../../lib/form-helpers"
|
||||||
import { optionalInt } from "../../../../../lib/validation"
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
|
|
||||||
@@ -98,15 +98,15 @@ export const ProductEditVariantForm = ({
|
|||||||
...optional
|
...optional
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const nullableData = parseOptionalFormData(optional)
|
const nullableData = transformNullableFormData(optional)
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
weight: parseOptionalFormNumber(weight),
|
weight: transformNullableFormNumber(weight),
|
||||||
height: parseOptionalFormNumber(height),
|
height: transformNullableFormNumber(height),
|
||||||
width: parseOptionalFormNumber(width),
|
width: transformNullableFormNumber(width),
|
||||||
length: parseOptionalFormNumber(length),
|
length: transformNullableFormNumber(length),
|
||||||
title,
|
title,
|
||||||
allow_backorder,
|
allow_backorder,
|
||||||
manage_inventory,
|
manage_inventory,
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import {
|
|||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
import { parseOptionalFormData } from "../../../../../lib/form-helpers"
|
import { transformNullableFormData } from "../../../../../lib/form-helpers"
|
||||||
|
|
||||||
type EditProductFormProps = {
|
type EditProductFormProps = {
|
||||||
product: HttpTypes.AdminProduct
|
product: HttpTypes.AdminProduct
|
||||||
@@ -50,7 +50,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
|||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const { title, discountable, handle, status, ...optional } = data
|
const { title, discountable, handle, status, ...optional } = data
|
||||||
|
|
||||||
const nullableData = parseOptionalFormData(optional)
|
const nullableData = transformNullableFormData(optional)
|
||||||
|
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./reservation-create-from"
|
||||||
+32
-28
@@ -4,19 +4,19 @@ import { Button, Heading, Input, Text, Textarea, toast } from "@medusajs/ui"
|
|||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { InventoryTypes } from "@medusajs/types"
|
import { InventoryTypes } from "@medusajs/types"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Form } from "../../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { Combobox } from "../../../../../../components/inputs/combobox"
|
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||||
import { useInventoryItems } from "../../../../../../hooks/api/inventory"
|
import { useInventoryItems } from "../../../../../hooks/api/inventory"
|
||||||
import { useCreateReservationItem } from "../../../../../../hooks/api/reservations"
|
import { useCreateReservationItem } from "../../../../../hooks/api/reservations"
|
||||||
import { useStockLocations } from "../../../../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
import { InventoryItemRes } from "../../../../../types/api-responses"
|
||||||
|
|
||||||
export const CreateReservationSchema = zod.object({
|
export const CreateReservationSchema = zod.object({
|
||||||
inventory_item_id: zod.string().min(1),
|
inventory_item_id: zod.string().min(1),
|
||||||
@@ -44,7 +44,7 @@ const AttributeGridRow = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
export const ReservationCreateForm = (props: { inventoryItemId?: string }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const [inventorySearch, setInventorySearch] = React.useState<string | null>(
|
const [inventorySearch, setInventorySearch] = React.useState<string | null>(
|
||||||
@@ -109,25 +109,12 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form onSubmit={handleSubmit}>
|
<form
|
||||||
<RouteFocusModal.Header>
|
onSubmit={handleSubmit}
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
className="flex size-full flex-col overflow-hidden"
|
||||||
<RouteFocusModal.Close asChild>
|
>
|
||||||
<Button variant="secondary" size="small">
|
<RouteFocusModal.Header />
|
||||||
{t("actions.cancel")}
|
<RouteFocusModal.Body className="flex flex-1 flex-col items-center overflow-auto py-16">
|
||||||
</Button>
|
|
||||||
</RouteFocusModal.Close>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
isLoading={isPending}
|
|
||||||
>
|
|
||||||
{t("actions.create")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</RouteFocusModal.Header>
|
|
||||||
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
|
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<Heading>{t("inventory.reservation.create")}</Heading>
|
<Heading>{t("inventory.reservation.create")}</Heading>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@@ -193,7 +180,7 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle shadow-elevation-card-rest grid grid-rows-4 divide-y rounded-lg border">
|
<div className="text-ui-fg-subtle shadow-elevation-card-rest grid grid-rows-4 divide-y rounded-lg">
|
||||||
<AttributeGridRow
|
<AttributeGridRow
|
||||||
title={t("fields.title")}
|
title={t("fields.title")}
|
||||||
value={
|
value={
|
||||||
@@ -282,6 +269,23 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
isLoading={isPending}
|
||||||
|
>
|
||||||
|
{t("actions.create")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { ReservationCreate as Component } from "./reservation-create";
|
||||||
|
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
|
import { ReservationCreateForm } from "./components/reservation-create-from"
|
||||||
|
|
||||||
|
export const ReservationCreate = () => {
|
||||||
|
const [params] = useSearchParams()
|
||||||
|
|
||||||
|
const inventoryItemId = params.get("item_id")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<ReservationCreateForm inventoryItemId={inventoryItemId} />
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./create-reservation-form"
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import { useSearchParams } from "react-router-dom"
|
|
||||||
import { RouteFocusModal } from "../../../../components/modals"
|
|
||||||
import { CreateReservationForm } from "./components/create-reservation-form"
|
|
||||||
|
|
||||||
export const CreateReservationModal = () => {
|
|
||||||
const [params] = useSearchParams()
|
|
||||||
|
|
||||||
const inventoryItemId = params.get("item_id")
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouteFocusModal>
|
|
||||||
<CreateReservationForm inventoryItemId={inventoryItemId} />
|
|
||||||
</RouteFocusModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { CreateReservationModal as Component } from "./create-reservation-modal"
|
|
||||||
Reference in New Issue
Block a user