fix(dashboard): Fix minor issues with Reservations and Inventroy Create forms (#8657)
This commit is contained in:
committed by
GitHub
parent
45f9961d19
commit
cfdd056d70
@@ -1,6 +1,6 @@
|
||||
import { castNumber } from "./cast-number"
|
||||
|
||||
export function parseOptionalFormValue<T>(
|
||||
export function transformNullableFormValue<T>(
|
||||
value: T,
|
||||
nullify = true
|
||||
): T | undefined | null {
|
||||
@@ -16,20 +16,21 @@ export function parseOptionalFormValue<T>(
|
||||
}
|
||||
|
||||
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>>(
|
||||
data: T,
|
||||
nullify = true
|
||||
): Nullable<T> {
|
||||
export function transformNullableFormData<
|
||||
T extends Record<string, unknown>,
|
||||
K extends boolean = true
|
||||
>(data: T, nullify: K = true as K): K extends true ? Nullable<T> : Optional<T> {
|
||||
return Object.entries(data).reduce((acc, [key, value]) => {
|
||||
return {
|
||||
...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,
|
||||
nullify = true
|
||||
) {
|
||||
@@ -46,3 +47,21 @@ export function parseOptionalFormNumber(
|
||||
|
||||
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",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/reservations/reservation-list/create-reservation"
|
||||
),
|
||||
import("../../routes/reservations/reservation-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./create-inventory-item-form.tsx"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./inventory-create-form.tsx"
|
||||
@@ -1,18 +1,23 @@
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useMemo } from "react"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
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 { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { CreateInventoryItemSchema } from "./schema"
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<{}>
|
||||
type InventoryAvailabilityFormProps = {
|
||||
form: UseFormReturn<CreateInventoryItemSchema>
|
||||
}
|
||||
|
||||
export const CreateInventoryAvailabilityForm = ({ form }: Props) => {
|
||||
export const InventoryAvailabilityForm = ({
|
||||
form,
|
||||
}: InventoryAvailabilityFormProps) => {
|
||||
const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 })
|
||||
const { setCloseOnEscape } = useRouteModal()
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import React, { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as zod from "zod"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
Input,
|
||||
ProgressStatus,
|
||||
ProgressTabs,
|
||||
Switch,
|
||||
Textarea,
|
||||
clx,
|
||||
toast,
|
||||
} from "@medusajs/ui"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { SwitchBox } from "../../../../../components/common/switch-box"
|
||||
import { CountrySelect } from "../../../../../components/inputs/country-select"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
@@ -27,9 +26,13 @@ import {
|
||||
useCreateInventoryItem,
|
||||
} from "../../../../../hooks/api/inventory"
|
||||
import { sdk } from "../../../../../lib/client"
|
||||
import {
|
||||
transformNullableFormData,
|
||||
transformNullableFormNumbers,
|
||||
} from "../../../../../lib/form-helpers"
|
||||
import { queryClient } from "../../../../../lib/query-client"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
import { CreateInventoryAvailabilityForm } from "./create-inventory-availability-form"
|
||||
import { InventoryAvailabilityForm } from "./inventory-availability-form"
|
||||
import { CreateInventoryItemSchema } from "./schema"
|
||||
|
||||
enum Tab {
|
||||
DETAILS = "details",
|
||||
@@ -40,31 +43,12 @@ type StepStatus = {
|
||||
[key in Tab]: ProgressStatus
|
||||
}
|
||||
|
||||
const CreateInventoryItemSchema = zod.object({
|
||||
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() {
|
||||
export function InventoryCreateForm() {
|
||||
const { t } = useTranslation()
|
||||
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: {
|
||||
title: "",
|
||||
sku: "",
|
||||
@@ -83,66 +67,86 @@ export function CreateInventoryItemForm() {
|
||||
resolver: zodResolver(CreateInventoryItemSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
trigger,
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const { mutateAsync: createInventoryItem, isPending: isLoading } =
|
||||
useCreateInventoryItem()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const { locations, ...payload } = data
|
||||
const { locations, weight, length, height, width, ...payload } = data
|
||||
|
||||
for (const k in payload) {
|
||||
if (payload[k] === "") {
|
||||
delete payload[k]
|
||||
continue
|
||||
const cleanData = transformNullableFormData(payload, false)
|
||||
const cleanNumbers = transformNullableFormNumbers(
|
||||
{
|
||||
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)) {
|
||||
payload[k] = parseInt(payload[k])
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { inventory_item } = await createInventoryItem(payload)
|
||||
|
||||
try {
|
||||
await sdk.admin.inventoryItem.batchUpdateLevels(inventory_item.id, {
|
||||
create: Object.entries(locations)
|
||||
.filter(([_, quantiy]) => !!quantiy)
|
||||
.map(([location_id, stocked_quantity]) => ({
|
||||
location_id,
|
||||
stocked_quantity,
|
||||
})),
|
||||
})
|
||||
|
||||
await sdk.admin.inventoryItem
|
||||
.batchUpdateLevels(inventory_item.id, {
|
||||
create: Object.entries(locations ?? {})
|
||||
.filter(([_, quantiy]) => !!quantiy)
|
||||
.map(([location_id, stocked_quantity]) => ({
|
||||
location_id,
|
||||
stocked_quantity,
|
||||
})),
|
||||
})
|
||||
.then(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
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)
|
||||
}
|
||||
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
handleSuccess()
|
||||
})
|
||||
})
|
||||
|
||||
const [status, setStatus] = React.useState<StepStatus>({
|
||||
const [status, setStatus] = useState<StepStatus>({
|
||||
[Tab.AVAILABILITY]: "not-started",
|
||||
[Tab.DETAILS]: "not-started",
|
||||
})
|
||||
|
||||
const onTabChange = React.useCallback(async (value: Tab) => {
|
||||
const result = await form.trigger()
|
||||
const onTabChange = useCallback(
|
||||
async (value: Tab) => {
|
||||
const result = await trigger()
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
setTab(value)
|
||||
}, [])
|
||||
setTab(value)
|
||||
},
|
||||
[trigger]
|
||||
)
|
||||
|
||||
const onNext = React.useCallback(async () => {
|
||||
const result = await form.trigger()
|
||||
const onNext = useCallback(async () => {
|
||||
const result = await trigger()
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
@@ -156,18 +160,18 @@ export function CreateInventoryItemForm() {
|
||||
case Tab.AVAILABILITY:
|
||||
break
|
||||
}
|
||||
}, [tab])
|
||||
}, [tab, trigger])
|
||||
|
||||
useEffect(() => {
|
||||
if (form.formState.isDirty) {
|
||||
if (isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
} else {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "not-started" }))
|
||||
}
|
||||
}, [form.formState.isDirty])
|
||||
}, [isDirty])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === Tab.DETAILS && form.formState.isDirty) {
|
||||
if (tab === Tab.DETAILS && isDirty) {
|
||||
setStatus((prev) => ({ ...prev, [Tab.DETAILS]: "in-progress" }))
|
||||
}
|
||||
|
||||
@@ -178,18 +182,18 @@ export function CreateInventoryItemForm() {
|
||||
[Tab.AVAILABILITY]: "in-progress",
|
||||
}))
|
||||
}
|
||||
}, [tab])
|
||||
}, [tab, isDirty])
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
className="h-full"
|
||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||
>
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
className="h-full"
|
||||
onValueChange={(tab) => onTabChange(tab as Tab)}
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<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>
|
||||
</ProgressTabs.Trigger>
|
||||
</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.Body
|
||||
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 }
|
||||
)}
|
||||
>
|
||||
<ProgressTabs.Content value={Tab.DETAILS} className="h-full w-full">
|
||||
<div className="mx-auto w-[720px] px-1 py-8">
|
||||
<Heading level="h2" className="mb-12 mt-8 text-2xl">
|
||||
{t("inventory.create.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-6 lg:grid-cols-2">
|
||||
<Form.Field
|
||||
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">
|
||||
<ProgressTabs.Content
|
||||
value={Tab.DETAILS}
|
||||
className="h-full w-full overflow-auto"
|
||||
>
|
||||
<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">
|
||||
<Heading>{t("inventory.create.title")}</Heading>
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.description.label")}
|
||||
</Form.Label>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="The item description"
|
||||
placeholder={t("fields.title")}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="requires_shipping"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
name="sku"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t("inventory.create.requiresShipping")}
|
||||
</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.Label>{t("fields.sku")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} placeholder="sku-123" />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -336,40 +269,43 @@ export function CreateInventoryItemForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <Form.Field*/}
|
||||
{/* className="col-span-1"*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="location_ids"*/}
|
||||
{/* render={({ field }) => {*/}
|
||||
{/* return (*/}
|
||||
{/* <Form.Item>*/}
|
||||
{/* <Form.Label optional>*/}
|
||||
{/* {t("inventory.create.locations")}*/}
|
||||
{/* </Form.Label>*/}
|
||||
{/* <Form.Control>*/}
|
||||
{/* <Combobox*/}
|
||||
{/* {...field}*/}
|
||||
{/* multiple*/}
|
||||
{/* options={locations.options}*/}
|
||||
{/* searchValue={locations.searchValue}*/}
|
||||
{/* onSearchValueChange={*/}
|
||||
{/* locations.onSearchValueChange*/}
|
||||
{/* }*/}
|
||||
{/* fetchNextPage={locations.fetchNextPage}*/}
|
||||
{/* />*/}
|
||||
{/* </Form.Control>*/}
|
||||
{/* <Form.ErrorMessage />*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/* )*/}
|
||||
{/* }}*/}
|
||||
{/* />*/}
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.description.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="The item description"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</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")}
|
||||
</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
|
||||
control={form.control}
|
||||
name="width"
|
||||
@@ -534,11 +470,32 @@ export function CreateInventoryItemForm() {
|
||||
value={Tab.AVAILABILITY}
|
||||
className="size-full"
|
||||
>
|
||||
<CreateInventoryAvailabilityForm form={form} />
|
||||
<InventoryAvailabilityForm form={form} />
|
||||
</ProgressTabs.Content>
|
||||
</RouteFocusModal.Body>
|
||||
</ProgressTabs>
|
||||
</form>
|
||||
<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
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
>
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { CreateInventoryItemForm } from "./components/create-inventory-item-form"
|
||||
import { InventoryCreateForm } from "./components/inventory-create-form"
|
||||
|
||||
export function InventoryCreate() {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateInventoryItemForm />
|
||||
<InventoryCreateForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from "../../../../../components/modals"
|
||||
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
||||
import {
|
||||
parseOptionalFormData,
|
||||
parseOptionalFormNumber,
|
||||
transformNullableFormData,
|
||||
transformNullableFormNumber,
|
||||
} from "../../../../../lib/form-helpers"
|
||||
import { optionalInt } from "../../../../../lib/validation"
|
||||
|
||||
@@ -98,15 +98,15 @@ export const ProductEditVariantForm = ({
|
||||
...optional
|
||||
} = data
|
||||
|
||||
const nullableData = parseOptionalFormData(optional)
|
||||
const nullableData = transformNullableFormData(optional)
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
id: variant.id,
|
||||
weight: parseOptionalFormNumber(weight),
|
||||
height: parseOptionalFormNumber(height),
|
||||
width: parseOptionalFormNumber(width),
|
||||
length: parseOptionalFormNumber(length),
|
||||
weight: transformNullableFormNumber(weight),
|
||||
height: transformNullableFormNumber(height),
|
||||
width: transformNullableFormNumber(width),
|
||||
length: transformNullableFormNumber(length),
|
||||
title,
|
||||
allow_backorder,
|
||||
manage_inventory,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||
import { parseOptionalFormData } from "../../../../../lib/form-helpers"
|
||||
import { transformNullableFormData } from "../../../../../lib/form-helpers"
|
||||
|
||||
type EditProductFormProps = {
|
||||
product: HttpTypes.AdminProduct
|
||||
@@ -50,7 +50,7 @@ export const EditProductForm = ({ product }: EditProductFormProps) => {
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const { title, discountable, handle, status, ...optional } = data
|
||||
|
||||
const nullableData = parseOptionalFormData(optional)
|
||||
const nullableData = transformNullableFormData(optional)
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./reservation-create-from"
|
||||
@@ -4,19 +4,19 @@ import { Button, Heading, Input, Text, Textarea, toast } from "@medusajs/ui"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../../components/modals"
|
||||
} from "../../../../../components/modals"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { InventoryTypes } from "@medusajs/types"
|
||||
import React from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../../components/inputs/combobox"
|
||||
import { useInventoryItems } from "../../../../../../hooks/api/inventory"
|
||||
import { useCreateReservationItem } from "../../../../../../hooks/api/reservations"
|
||||
import { useStockLocations } from "../../../../../../hooks/api/stock-locations"
|
||||
import { InventoryItemRes } from "../../../../../../types/api-responses"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { useInventoryItems } from "../../../../../hooks/api/inventory"
|
||||
import { useCreateReservationItem } from "../../../../../hooks/api/reservations"
|
||||
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
|
||||
import { InventoryItemRes } from "../../../../../types/api-responses"
|
||||
|
||||
export const CreateReservationSchema = zod.object({
|
||||
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 { handleSuccess } = useRouteModal()
|
||||
const [inventorySearch, setInventorySearch] = React.useState<string | null>(
|
||||
@@ -109,25 +109,12 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<RouteFocusModal.Header>
|
||||
<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.Header>
|
||||
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex size-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header />
|
||||
<RouteFocusModal.Body className="flex flex-1 flex-col items-center overflow-auto py-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<Heading>{t("inventory.reservation.create")}</Heading>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -193,7 +180,7 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
title={t("fields.title")}
|
||||
value={
|
||||
@@ -282,6 +269,23 @@ export const CreateReservationForm = (props: { inventoryItemId?: string }) => {
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ReservationCreate as Component } from "./reservation-create";
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export * from "./create-reservation-form"
|
||||
@@ -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 +0,0 @@
|
||||
export { CreateReservationModal as Component } from "./create-reservation-modal"
|
||||
Reference in New Issue
Block a user