From cfdd056d706ddd7c111c950d3f5e4e97966f682d Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:31:31 +0200 Subject: [PATCH] fix(dashboard): Fix minor issues with Reservations and Inventroy Create forms (#8657) --- .../dashboard/src/lib/form-helpers.ts | 35 +- .../providers/router-provider/route-map.tsx | 4 +- .../create-inventory-item-form/index.ts | 1 - .../components/inventory-create-form/index.ts | 1 + .../inventory-availability-form.tsx} | 19 +- .../inventory-create-form.tsx} | 369 ++++++++---------- .../inventory-create-form/schema.ts | 23 ++ .../inventory-create/inventory-create.tsx | 4 +- .../product-edit-variant-form.tsx | 14 +- .../edit-product-form/edit-product-form.tsx | 4 +- .../reservation-create-from/index.ts | 1 + .../reservation-create-from.tsx} | 60 +-- .../reservations/reservation-create/index.ts | 2 + .../reservation-create/reservation-create.tsx | 16 + .../create-reservation-form/index.ts | 1 - .../create-reservation-modal.tsx | 15 - .../create-reservation/index.ts | 1 - 17 files changed, 289 insertions(+), 281 deletions(-) delete mode 100644 packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/index.ts rename packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/{create-inventory-item-form/create-inventory-availability-form.tsx => inventory-create-form/inventory-availability-form.tsx} (84%) rename packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/{create-inventory-item-form/create-inventory-item-form.tsx => inventory-create-form/inventory-create-form.tsx} (64%) create mode 100644 packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/schema.ts create mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-create/components/reservation-create-from/index.ts rename packages/admin-next/dashboard/src/routes/reservations/{reservation-list/create-reservation/components/create-reservation-form/create-reservation-form.tsx => reservation-create/components/reservation-create-from/reservation-create-from.tsx} (91%) create mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-create/reservation-create.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-list/create-reservation/components/create-reservation-form/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-list/create-reservation/create-reservation-modal.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/reservations/reservation-list/create-reservation/index.ts diff --git a/packages/admin-next/dashboard/src/lib/form-helpers.ts b/packages/admin-next/dashboard/src/lib/form-helpers.ts index 969673df06..9f3657c47b 100644 --- a/packages/admin-next/dashboard/src/lib/form-helpers.ts +++ b/packages/admin-next/dashboard/src/lib/form-helpers.ts @@ -1,6 +1,6 @@ import { castNumber } from "./cast-number" -export function parseOptionalFormValue( +export function transformNullableFormValue( value: T, nullify = true ): T | undefined | null { @@ -16,20 +16,21 @@ export function parseOptionalFormValue( } type Nullable = { [K in keyof T]: T[K] | null } +type Optional = { [K in keyof T]: T[K] | undefined } -export function parseOptionalFormData>( - data: T, - nullify = true -): Nullable { +export function transformNullableFormData< + T extends Record, + K extends boolean = true +>(data: T, nullify: K = true as K): K extends true ? Nullable : Optional { return Object.entries(data).reduce((acc, [key, value]) => { return { ...acc, - [key]: parseOptionalFormValue(value, nullify), + [key]: transformNullableFormValue(value, nullify), } - }, {} as Nullable) + }, {} as K extends true ? Nullable : Optional) } -export function parseOptionalFormNumber( +export function transformNullableFormNumber( value?: string | number, nullify = true ) { @@ -46,3 +47,21 @@ export function parseOptionalFormNumber( return value } + +type NullableNumbers = Record +type OptionalNumbers = Record + +export function transformNullableFormNumbers< + T extends Record, + 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) +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index c2c8a35b17..2c2c4853bc 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -546,9 +546,7 @@ export const RouteMap: RouteObject[] = [ { path: "create", lazy: () => - import( - "../../routes/reservations/reservation-list/create-reservation" - ), + import("../../routes/reservations/reservation-create"), }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/index.ts b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/index.ts deleted file mode 100644 index d3ec61a3ca..0000000000 --- a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-inventory-item-form.tsx" diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/index.ts b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/index.ts new file mode 100644 index 0000000000..2322ccaff7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/index.ts @@ -0,0 +1 @@ +export * from "./inventory-create-form.tsx" diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-availability-form.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-availability-form.tsx similarity index 84% rename from packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-availability-form.tsx rename to packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-availability-form.tsx index af06445ea2..1df1d30dda 100644 --- a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-availability-form.tsx +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-availability-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 } -export const CreateInventoryAvailabilityForm = ({ form }: Props) => { +export const InventoryAvailabilityForm = ({ + form, +}: InventoryAvailabilityFormProps) => { const { isPending, stock_locations = [] } = useStockLocations({ limit: 999 }) const { setCloseOnEscape } = useRouteModal() diff --git a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-item-form.tsx b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-create-form.tsx similarity index 64% rename from packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-item-form.tsx rename to packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-create-form.tsx index d75ae098a4..411099c606 100644 --- a/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/create-inventory-item-form/create-inventory-item-form.tsx +++ b/packages/admin-next/dashboard/src/routes/inventory/inventory-create/components/inventory-create-form/inventory-create-form.tsx @@ -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.DETAILS) + const [tab, setTab] = useState(Tab.DETAILS) - const form = useForm>({ + const form = useForm({ 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({ + const [status, setStatus] = useState({ [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 ( -
onTabChange(tab as Tab)} > - onTabChange(tab as Tab)} + @@ -212,123 +216,52 @@ export function CreateInventoryItemForm() { -
- - - - -
- -
- - {t("inventory.create.title")} - - -
-
- { - return ( - - {t("fields.title")} - - - - - - ) - }} - /> - - { - return ( - - {t("fields.sku")} - - - - - - ) - }} - /> - -
+ +
+
+ {t("inventory.create.title")} +
+
{ return ( - - {t("products.fields.description.label")} - + {t("fields.title")} -