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
+302 -191

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,13 +115,15 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
</p> </p>
<DimensionsForm form={nestedForm(form, "dimensions")} /> <DimensionsForm form={nestedForm(form, "dimensions")} />
</div> </div>
<div className="mt-xlarge"> {showStockAndInventory && (
<h3 className="inter-base-semibold mb-2xsmall">Customs</h3> <div className="mt-xlarge">
<p className="inter-base-regular text-grey-50 mb-large"> <h3 className="inter-base-semibold mb-2xsmall">Customs</h3>
Configure if you are shipping internationally. <p className="inter-base-regular text-grey-50 mb-large">
</p> Configure if you are shipping internationally.
<CustomsForm form={nestedForm(form, "customs")} /> </p>
</div> <CustomsForm form={nestedForm(form, "customs")} />
</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,140 +89,171 @@ 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="flex flex-col gap-y-4"> <div className="gap-y-xlarge mt-large flex flex-col">
<h3 className="inter-base-semibold">General</h3> <div className="flex flex-col gap-y-4">
<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>
<div className="gap-y-2xsmall flex flex-col"> </Accordion.Item>
<div className="flex items-center justify-between"> <Accordion.Item title="Inventory" value="inventory">
<h3 className="inter-base-semibold mb-2xsmall">Manage inventory</h3> <div className="gap-y-small mt-large flex flex-col">
<Controller <div className="gap-y-2xsmall flex flex-col">
control={control} <div className="flex items-center justify-between">
name={path("manage_inventory")} <h3 className="inter-base-semibold mb-2xsmall">
render={({ field: { value, onChange } }) => { Manage inventory
return <Switch checked={value} onCheckedChange={onChange} /> </h3>
}} <Controller
/> control={control}
</div> name={"manage_inventory"}
<p className="inter-base-regular text-grey-50"> render={({ field: { value, onChange } }) => {
When checked Medusa will regulate the inventory when orders and return <Switch checked={value} onCheckedChange={onChange} />
returns are made.
</p>
</div>
{manageInventory && (
<>
<div className="gap-y-2xsmall flex flex-col">
<div className="flex items-center justify-between">
<h3 className="inter-base-semibold mb-2xsmall">
Allow backorders
</h3>
<Controller
control={control}
name={path("allow_backorder")}
render={({ field: { value, onChange } }) => {
return <Switch checked={value} onCheckedChange={onChange} />
}}
/>
</div>
<p className="inter-base-regular text-grey-50">
When checked the product will be available for purchase despite
the product being sold out
</p>
</div>
<div className="flex w-full flex-col text-base">
<h3 className="inter-base-semibold mb-2xsmall">Quantity</h3>
{!isLoading && locations && (
<div className="flex w-full flex-col">
<div className="inter-base-regular text-grey-50 flex justify-between py-3">
<div className="">Location</div>
<div className="">In Stock</div>
</div>
{selectedLocations.map((level, i) => {
const locationDetails = locationsMap.get(level.location_id)
const locationLevel = levelMap.get(level.location_id)
return (
<div key={level.id} className="flex items-center py-3">
<div className="inter-base-regular flex items-center">
<IconBadge className="mr-base">
<BuildingsIcon />
</IconBadge>
{locationDetails?.name}
</div>
<div className="ml-auto flex">
<div className="mr-base text-small text-grey-50 flex flex-col">
<span className="whitespace-nowrap text-right">
{`${locationLevel!.reserved_quantity} reserved`}
</span>
<span className="whitespace-nowrap text-right">{`${
locationLevel!.stocked_quantity! -
locationLevel!.reserved_quantity!
} available`}</span>
</div>
<InputField
placeholder={"0"}
type="number"
min={0}
{...register(
path(`location_levels.${i}.stocked_quantity`),
{ valueAsNumber: true }
)}
/>
</div>
</div>
)
})}
</div>
)}
</div>
<div className="flex">
<Button
variant="secondary"
size="small"
type="button"
className="w-full"
onClick={() => {
layeredModalContext.push(
// @ts-ignore
ManageLocationsScreen(
layeredModalContext.pop,
selectedLocations,
locations || [],
handleUpdateLocations
)
)
}} }}
> />
Manage locations
</Button>
</div> </div>
</> <p className="inter-base-regular text-grey-50">
)} When checked Medusa will regulate the inventory when orders and
</div> returns are made.
</div> </p>
</div>
{manageInventory && (
<>
<div className="gap-y-2xsmall flex flex-col">
<div className="flex items-center justify-between">
<h3 className="inter-base-semibold mb-2xsmall">
Allow backorders
</h3>
<Controller
control={control}
name={"allow_backorder"}
render={({ field: { value, onChange } }) => {
return (
<Switch checked={value} onCheckedChange={onChange} />
)
}}
/>
</div>
<p className="inter-base-regular text-grey-50">
When checked the product will be available for purchase
despite the product being sold out
</p>
</div>
<div className="flex w-full flex-col text-base">
<h3 className="inter-base-semibold mb-2xsmall">Quantity</h3>
{!isLoading && locations && (
<div className="flex w-full flex-col">
<div className="inter-base-regular text-grey-50 flex justify-between py-3">
<div className="">Location</div>
<div className="">In Stock</div>
</div>
{selectedLocations.map((level, i) => {
const locationDetails = locationsMap.get(
level.location_id
)
const locationLevel = levelMap.get(level.location_id)
return (
<div key={level.id} className="flex items-center py-3">
<div className="inter-base-regular flex items-center">
<IconBadge className="mr-base">
<BuildingsIcon />
</IconBadge>
{locationDetails?.name}
</div>
<div className="ml-auto flex">
<div className="mr-base text-small text-grey-50 flex flex-col">
<span className="whitespace-nowrap text-right">
{`${locationLevel!.reserved_quantity} reserved`}
</span>
<span className="whitespace-nowrap text-right">{`${
locationLevel!.stocked_quantity! -
locationLevel!.reserved_quantity!
} available`}</span>
</div>
<InputField
placeholder={"0"}
type="number"
min={0}
{...register(
`location_levels.${i}.stocked_quantity`,
{ valueAsNumber: true }
)}
/>
</div>
</div>
)
})}
</div>
)}
</div>
<div className="flex">
<Button
variant="secondary"
size="small"
type="button"
className="w-full"
onClick={() => {
layeredModalContext.push(
// @ts-ignore
ManageLocationsScreen(
layeredModalContext.pop,
selectedLocations,
locations || [],
handleUpdateLocations
)
)
}}
>
Manage locations
</Button>
</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 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(
refetch() variant.id,
if (shouldInvalidateCache) { removeNullish({
queryClient.invalidateQueries(adminInventoryItemsKeys.lists()) ...dimensions,
...customs,
...stock,
ean,
barcode,
upc,
}),
() => {
refetch()
if (shouldInvalidateCache) {
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: variant?.ean || null,
ean: null, barcode: variant?.barcode || null,
inventory_quantity: null, upc: variant?.upc || null,
manage_inventory: false, inventory_quantity: null,
allow_backorder: false, manage_inventory: false,
barcode: null, allow_backorder: false,
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: variant?.ean || null,
ean: inventoryItem.ean, barcode: variant?.barcode || null,
inventory_quantity: inventoryItem.inventory_quantity, upc: variant?.upc || null,
manage_inventory: !!inventoryItem, inventory_quantity: inventoryItem.inventory_quantity,
allow_backorder: inventoryItem.allow_backorder, manage_inventory: !!inventoryItem,
barcode: inventoryItem.barcode, allow_backorder: inventoryItem.allow_backorder,
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,
}, },
} }
} }