feat(admin-ui): move inventory item fields into manage inventory modal (#3591)

This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-03-29 18:12:38 +02:00
1 parent 7428ffa300
commit a7e3f2d343
5 files changed
+166 -55

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": minor
---
feat(admin-ui): move customs and shipping into manage inventory modal
@@ -2,7 +2,7 @@ import { useFieldArray, UseFormReturn } from "react-hook-form"
import CustomsForm, { CustomsFormType } from "../../customs-form" import CustomsForm, { CustomsFormType } from "../../customs-form"
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form" import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
import VariantGeneralForm, { import VariantGeneralForm, {
VariantGeneralFormType VariantGeneralFormType,
} from "../variant-general-form" } from "../variant-general-form"
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form" import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
@@ -115,6 +115,7 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
</p> </p>
<DimensionsForm form={nestedForm(form, "dimensions")} /> <DimensionsForm form={nestedForm(form, "dimensions")} />
</div> </div>
{showStockAndInventory && (
<div className="mt-xlarge"> <div className="mt-xlarge">
<h3 className="inter-base-semibold mb-2xsmall">Customs</h3> <h3 className="inter-base-semibold mb-2xsmall">Customs</h3>
<p className="inter-base-regular text-grey-50 mb-large"> <p className="inter-base-regular text-grey-50 mb-large">
@@ -122,6 +123,7 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
</p> </p>
<CustomsForm form={nestedForm(form, "customs")} /> <CustomsForm form={nestedForm(form, "customs")} />
</div> </div>
)}
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>
) )
@@ -1,11 +1,8 @@
import { InventoryLevelDTO } from "@medusajs/medusa" import { InventoryLevelDTO } from "@medusajs/types"
import { UseFormReturn } from "react-hook-form" import { UseFormReturn } from "react-hook-form"
import { nestedForm } from "../../../../../utils/nested-form"
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form" import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
export type EditFlowVariantFormType = { export type EditFlowVariantFormType = VariantStockFormType
stock: VariantStockFormType
}
type Props = { type Props = {
form: UseFormReturn<EditFlowVariantFormType, any> form: UseFormReturn<EditFlowVariantFormType, any>
@@ -39,10 +36,7 @@ const EditFlowVariantForm = ({ form, isLoading, locationLevels }: Props) => {
return ( return (
<> <>
<VariantStockForm <VariantStockForm locationLevels={locationLevels} form={form} />
locationLevels={locationLevels}
form={nestedForm(form, "stock")}
/>
</> </>
) )
} }
@@ -1,15 +1,19 @@
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa" import { Controller, useFieldArray, UseFormReturn } from "react-hook-form"
import { useAdminStockLocations } from "medusa-react" import { nestedForm } from "../../../../../utils/nested-form"
import React, { useMemo, useState } from "react" import React, { useMemo, useState } from "react"
import { Controller, useFieldArray } from "react-hook-form"
import { NestedForm } from "../../../../../utils/nested-form" import Accordion from "../../../../organisms/accordion"
import Switch from "../../../../atoms/switch"
import Button from "../../../../fundamentals/button"
import IconBadge from "../../../../fundamentals/icon-badge"
import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon" import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon"
import Button from "../../../../fundamentals/button"
import CustomsForm, { CustomsFormType } from "../../customs-form"
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
import IconBadge from "../../../../fundamentals/icon-badge"
import InputField from "../../../../molecules/input" import InputField from "../../../../molecules/input"
import Modal from "../../../../molecules/modal" import Modal from "../../../../molecules/modal"
import Switch from "../../../../atoms/switch"
import { useAdminStockLocations } from "medusa-react"
import { useLayeredModal } from "../../../../molecules/modal/layered-modal" import { useLayeredModal } from "../../../../molecules/modal/layered-modal"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/types"
export type VariantStockFormType = { export type VariantStockFormType = {
manage_inventory?: boolean manage_inventory?: boolean
@@ -20,11 +24,13 @@ export type VariantStockFormType = {
upc: string | null upc: string | null
barcode: string | null barcode: string | null
location_levels?: Partial<InventoryLevelDTO>[] | null location_levels?: Partial<InventoryLevelDTO>[] | null
dimensions: DimensionsFormType
customs: CustomsFormType
} }
type Props = { type Props = {
locationLevels: InventoryLevelDTO[] locationLevels: InventoryLevelDTO[]
form: NestedForm<VariantStockFormType> form: UseFormReturn<VariantStockFormType>
} }
const VariantStockForm = ({ form, locationLevels }: Props) => { const VariantStockForm = ({ form, locationLevels }: Props) => {
@@ -44,9 +50,9 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
return new Map(locations.map((l) => [l.id, l])) return new Map(locations.map((l) => [l.id, l]))
}, [locations, isLoading]) }, [locations, isLoading])
const { path, control, register, watch } = form const { control, register, watch } = form
const manageInventory = watch(path("manage_inventory")) const manageInventory = watch("manage_inventory")
const { const {
fields: selectedLocations, fields: selectedLocations,
@@ -54,10 +60,10 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
remove, remove,
} = useFieldArray({ } = useFieldArray({
control, control,
name: path("location_levels"), name: "location_levels",
}) })
const selectedLocationLevels = watch(path("location_levels")) const selectedLocationLevels = watch("location_levels")
const levelMap = new Map( const levelMap = new Map(
selectedLocationLevels?.map((l) => [l.location_id, l]) selectedLocationLevels?.map((l) => [l.location_id, l])
@@ -83,39 +89,45 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
} }
return ( return (
<div> <Accordion type="multiple" defaultValue={["general"]}>
<div className="gap-y-xlarge flex flex-col"> <Accordion.Item title="General" value="general">
<div className="gap-y-xlarge mt-large flex flex-col">
<div className="flex flex-col gap-y-4"> <div className="flex flex-col gap-y-4">
<h3 className="inter-base-semibold">General</h3>
<div className="gap-large grid grid-cols-2"> <div className="gap-large grid grid-cols-2">
<InputField <InputField
label="Stock keeping unit (SKU)" label="Stock keeping unit (SKU)"
placeholder="SUN-G, JK1234..." placeholder="SUN-G, JK1234..."
{...register(path("sku"))} {...register("sku")}
/> />
<InputField <InputField
label="EAN (Barcode)" label="EAN (Barcode)"
placeholder="123456789102..." placeholder="123456789102..."
{...register(path("ean"))} {...register("ean")}
/> />
<InputField <InputField
label="UPC (Barcode)" label="UPC (Barcode)"
placeholder="023456789104..." placeholder="023456789104..."
{...register(path("upc"))} {...register("upc")}
/> />
<InputField <InputField
label="Barcode" label="Barcode"
placeholder="123456789104..." placeholder="123456789104..."
{...register(path("barcode"))} {...register("barcode")}
/> />
</div> </div>
</div> </div>
</div>
</Accordion.Item>
<Accordion.Item title="Inventory" value="inventory">
<div className="gap-y-small mt-large flex flex-col">
<div className="gap-y-2xsmall flex flex-col"> <div className="gap-y-2xsmall flex flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="inter-base-semibold mb-2xsmall">Manage inventory</h3> <h3 className="inter-base-semibold mb-2xsmall">
Manage inventory
</h3>
<Controller <Controller
control={control} control={control}
name={path("manage_inventory")} name={"manage_inventory"}
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => {
return <Switch checked={value} onCheckedChange={onChange} /> return <Switch checked={value} onCheckedChange={onChange} />
}} }}
@@ -135,15 +147,17 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
</h3> </h3>
<Controller <Controller
control={control} control={control}
name={path("allow_backorder")} name={"allow_backorder"}
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => {
return <Switch checked={value} onCheckedChange={onChange} /> return (
<Switch checked={value} onCheckedChange={onChange} />
)
}} }}
/> />
</div> </div>
<p className="inter-base-regular text-grey-50"> <p className="inter-base-regular text-grey-50">
When checked the product will be available for purchase despite When checked the product will be available for purchase
the product being sold out despite the product being sold out
</p> </p>
</div> </div>
<div className="flex w-full flex-col text-base"> <div className="flex w-full flex-col text-base">
@@ -155,7 +169,9 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
<div className="">In Stock</div> <div className="">In Stock</div>
</div> </div>
{selectedLocations.map((level, i) => { {selectedLocations.map((level, i) => {
const locationDetails = locationsMap.get(level.location_id) const locationDetails = locationsMap.get(
level.location_id
)
const locationLevel = levelMap.get(level.location_id) const locationLevel = levelMap.get(level.location_id)
return ( return (
@@ -181,7 +197,7 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
type="number" type="number"
min={0} min={0}
{...register( {...register(
path(`location_levels.${i}.stocked_quantity`), `location_levels.${i}.stocked_quantity`,
{ valueAsNumber: true } { valueAsNumber: true }
)} )}
/> />
@@ -216,7 +232,28 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
</> </>
)} )}
</div> </div>
</Accordion.Item>
<Accordion.Item title="Shipping" value="shipping">
<p className="inter-base-regular text-grey-50">
Shipping information can be required depending on your shipping
provider, and whether or not you are shipping internationally.
</p>
<div className="mt-large">
<h3 className="inter-base-semibold mb-2xsmall">Dimensions</h3>
<p className="inter-base-regular text-grey-50 mb-large">
Configure to calculate the most accurate shipping rates.
</p>
<DimensionsForm form={nestedForm(form, "dimensions")} />
</div> </div>
<div className="mt-xlarge">
<h3 className="inter-base-semibold mb-2xsmall">Customs</h3>
<p className="inter-base-regular text-grey-50 mb-large">
Configure if you are shipping internationally.
</p>
<CustomsForm form={nestedForm(form, "customs")} />
</div>
</Accordion.Item>
</Accordion>
) )
} }
@@ -14,12 +14,13 @@ import {
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import { InventoryLevelDTO } from "@medusajs/types" import { InventoryLevelDTO } from "@medusajs/types"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
import { queryClient } from "../../../constants/query-client" import { queryClient } from "../../../constants/query-client"
import { removeNullish } from "../../../utils/remove-nullish" import { removeNullish } from "../../../utils/remove-nullish"
import { useContext } from "react" import { useContext } from "react"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { countries } from "../../../utils/countries"
import { Option } from "../../../types/shared"
type Props = { type Props = {
onClose: () => void onClose: () => void
@@ -44,19 +45,41 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id) const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id)
const createUpdateInventoryItemPayload = (
data: Partial<EditFlowVariantFormType>
) => {
const updateDimensions = data.dimensions || {}
const updateCustoms = data.customs || {}
const originCountry = data.customs?.origin_country?.value
delete data.dimensions
delete data.customs
delete data.ean
delete data.barcode
delete data.upc
return removeNullish({
...updateDimensions,
...updateCustoms,
...data,
...(originCountry && { origin_country: originCountry }),
})
}
const onSubmit = async (data: EditFlowVariantFormType) => { const onSubmit = async (data: EditFlowVariantFormType) => {
const locationLevels = data.stock.location_levels || [] const locationLevels = data.location_levels || []
const manageInventory = data.stock.manage_inventory const manageInventory = data.manage_inventory
const variantInventoryItem = variantInventory?.inventory?.[0] const variantInventoryItem = variantInventory?.inventory?.[0]
const itemId = variantInventoryItem?.id const itemId = variantInventoryItem?.id
delete data.stock.manage_inventory delete data.manage_inventory
delete data.stock.location_levels delete data.location_levels
let inventoryItemId: string | undefined = itemId let inventoryItemId: string | undefined = itemId
const upsertPayload = removeNullish(data.stock) const { ean, barcode, upc } = data
const upsertPayload = createUpdateInventoryItemPayload(data)
let shouldInvalidateCache = false let shouldInvalidateCache = false
if (variantInventoryItem) { if (variantInventoryItem) {
@@ -135,14 +158,27 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
} }
} }
const { dimensions, customs, ...stock } = data
// @ts-ignore // @ts-ignore
onUpdateVariant(variant.id, createUpdatePayload(data), () => { onUpdateVariant(
variant.id,
removeNullish({
...dimensions,
...customs,
...stock,
ean,
barcode,
upc,
}),
() => {
refetch() refetch()
if (shouldInvalidateCache) { if (shouldInvalidateCache) {
queryClient.invalidateQueries(adminInventoryItemsKeys.lists()) queryClient.invalidateQueries(adminInventoryItemsKeys.lists())
} }
handleClose() handleClose()
}) }
)
} }
return ( return (
@@ -154,6 +190,7 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
<StockForm <StockForm
variantInventory={variantInventory!} variantInventory={variantInventory!}
onSubmit={onSubmit} onSubmit={onSubmit}
variant={variant}
isLoadingInventory={isLoadingInventory} isLoadingInventory={isLoadingInventory}
handleClose={handleClose} handleClose={handleClose}
updatingVariant={updatingVariant} updatingVariant={updatingVariant}
@@ -169,8 +206,10 @@ const StockForm = ({
isLoadingInventory, isLoadingInventory,
handleClose, handleClose,
updatingVariant, updatingVariant,
variant,
}: { }: {
variantInventory: VariantInventory variantInventory: VariantInventory
variant: ProductVariant
onSubmit: (data: EditFlowVariantFormType) => void onSubmit: (data: EditFlowVariantFormType) => void
isLoadingInventory: boolean isLoadingInventory: boolean
handleClose: () => void handleClose: () => void
@@ -178,7 +217,7 @@ const StockForm = ({
}) => { }) => {
const form = useForm<EditFlowVariantFormType>({ const form = useForm<EditFlowVariantFormType>({
// @ts-ignore // @ts-ignore
defaultValues: getEditVariantDefaultValues(variantInventory), defaultValues: getEditVariantDefaultValues(variantInventory, variant),
}) })
const { const {
@@ -231,34 +270,68 @@ const StockForm = ({
} }
export const getEditVariantDefaultValues = ( export const getEditVariantDefaultValues = (
variantInventory?: any variantInventory?: any,
variant?: ProductVariant
): EditFlowVariantFormType => { ): EditFlowVariantFormType => {
const inventoryItem = variantInventory?.inventory[0] const inventoryItem = variantInventory?.inventory[0]
if (!inventoryItem) { if (!inventoryItem) {
return { return {
stock: {
sku: null, sku: null,
ean: null, ean: variant?.ean || null,
barcode: variant?.barcode || null,
upc: variant?.upc || null,
inventory_quantity: null, inventory_quantity: null,
manage_inventory: false, manage_inventory: false,
allow_backorder: false, allow_backorder: false,
barcode: null,
upc: null,
location_levels: null, location_levels: null,
dimensions: {
height: null,
length: null,
width: null,
weight: null,
},
customs: {
origin_country: null,
mid_code: null,
hs_code: null,
}, },
} }
} }
let originCountry: Option | null = null
if (inventoryItem.origin_country) {
const country = countries.find(
(c) =>
c.alpha2 === inventoryItem.origin_country ||
c.alpha3 === inventoryItem.origin_country
)
if (country) {
originCountry = {
label: country?.name,
value: country?.alpha2,
}
}
}
return { return {
stock: {
sku: inventoryItem.sku, sku: inventoryItem.sku,
ean: inventoryItem.ean, ean: variant?.ean || null,
barcode: variant?.barcode || null,
upc: variant?.upc || null,
inventory_quantity: inventoryItem.inventory_quantity, inventory_quantity: inventoryItem.inventory_quantity,
manage_inventory: !!inventoryItem, manage_inventory: !!inventoryItem,
allow_backorder: inventoryItem.allow_backorder, allow_backorder: inventoryItem.allow_backorder,
barcode: inventoryItem.barcode,
upc: inventoryItem.upc,
location_levels: inventoryItem.location_levels, location_levels: inventoryItem.location_levels,
dimensions: {
height: inventoryItem.height,
length: inventoryItem.length,
width: inventoryItem.width,
weight: inventoryItem.weight,
},
customs: {
origin_country: originCountry,
mid_code: inventoryItem.mid_code,
hs_code: inventoryItem.hs_code,
}, },
} }
} }