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

This commit is contained in:
Philip Korsholm
2023-03-29 18:12:38 +02:00
committed by GitHub
parent 7428ffa300
commit a7e3f2d343
5 changed files with 302 additions and 191 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": minor
---
feat(admin-ui): move customs and shipping into manage inventory modal

View File

@@ -2,7 +2,7 @@ import { useFieldArray, UseFormReturn } from "react-hook-form"
import CustomsForm, { CustomsFormType } from "../../customs-form"
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
import VariantGeneralForm, {
VariantGeneralFormType
VariantGeneralFormType,
} from "../variant-general-form"
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
@@ -115,13 +115,15 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
</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>
{showStockAndInventory && (
<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>
)

View File

@@ -1,11 +1,8 @@
import { InventoryLevelDTO } from "@medusajs/medusa"
import { InventoryLevelDTO } from "@medusajs/types"
import { UseFormReturn } from "react-hook-form"
import { nestedForm } from "../../../../../utils/nested-form"
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
export type EditFlowVariantFormType = {
stock: VariantStockFormType
}
export type EditFlowVariantFormType = VariantStockFormType
type Props = {
form: UseFormReturn<EditFlowVariantFormType, any>
@@ -39,10 +36,7 @@ const EditFlowVariantForm = ({ form, isLoading, locationLevels }: Props) => {
return (
<>
<VariantStockForm
locationLevels={locationLevels}
form={nestedForm(form, "stock")}
/>
<VariantStockForm locationLevels={locationLevels} form={form} />
</>
)
}

View File

@@ -1,15 +1,19 @@
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
import { useAdminStockLocations } from "medusa-react"
import { Controller, useFieldArray, UseFormReturn } from "react-hook-form"
import { nestedForm } from "../../../../../utils/nested-form"
import React, { useMemo, useState } from "react"
import { Controller, useFieldArray } from "react-hook-form"
import { NestedForm } from "../../../../../utils/nested-form"
import Switch from "../../../../atoms/switch"
import Button from "../../../../fundamentals/button"
import IconBadge from "../../../../fundamentals/icon-badge"
import Accordion from "../../../../organisms/accordion"
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 Modal from "../../../../molecules/modal"
import Switch from "../../../../atoms/switch"
import { useAdminStockLocations } from "medusa-react"
import { useLayeredModal } from "../../../../molecules/modal/layered-modal"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/types"
export type VariantStockFormType = {
manage_inventory?: boolean
@@ -20,11 +24,13 @@ export type VariantStockFormType = {
upc: string | null
barcode: string | null
location_levels?: Partial<InventoryLevelDTO>[] | null
dimensions: DimensionsFormType
customs: CustomsFormType
}
type Props = {
locationLevels: InventoryLevelDTO[]
form: NestedForm<VariantStockFormType>
form: UseFormReturn<VariantStockFormType>
}
const VariantStockForm = ({ form, locationLevels }: Props) => {
@@ -44,9 +50,9 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
return new Map(locations.map((l) => [l.id, l]))
}, [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 {
fields: selectedLocations,
@@ -54,10 +60,10 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
remove,
} = useFieldArray({
control,
name: path("location_levels"),
name: "location_levels",
})
const selectedLocationLevels = watch(path("location_levels"))
const selectedLocationLevels = watch("location_levels")
const levelMap = new Map(
selectedLocationLevels?.map((l) => [l.location_id, l])
@@ -83,140 +89,171 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
}
return (
<div>
<div className="gap-y-xlarge flex flex-col">
<div className="flex flex-col gap-y-4">
<h3 className="inter-base-semibold">General</h3>
<div className="gap-large grid grid-cols-2">
<InputField
label="Stock keeping unit (SKU)"
placeholder="SUN-G, JK1234..."
{...register(path("sku"))}
/>
<InputField
label="EAN (Barcode)"
placeholder="123456789102..."
{...register(path("ean"))}
/>
<InputField
label="UPC (Barcode)"
placeholder="023456789104..."
{...register(path("upc"))}
/>
<InputField
label="Barcode"
placeholder="123456789104..."
{...register(path("barcode"))}
/>
<Accordion type="multiple" defaultValue={["general"]}>
<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="gap-large grid grid-cols-2">
<InputField
label="Stock keeping unit (SKU)"
placeholder="SUN-G, JK1234..."
{...register("sku")}
/>
<InputField
label="EAN (Barcode)"
placeholder="123456789102..."
{...register("ean")}
/>
<InputField
label="UPC (Barcode)"
placeholder="023456789104..."
{...register("upc")}
/>
<InputField
label="Barcode"
placeholder="123456789104..."
{...register("barcode")}
/>
</div>
</div>
</div>
<div className="gap-y-2xsmall flex flex-col">
<div className="flex items-center justify-between">
<h3 className="inter-base-semibold mb-2xsmall">Manage inventory</h3>
<Controller
control={control}
name={path("manage_inventory")}
render={({ field: { value, onChange } }) => {
return <Switch checked={value} onCheckedChange={onChange} />
}}
/>
</div>
<p className="inter-base-regular text-grey-50">
When checked Medusa will regulate the inventory when orders and
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
)
)
</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="flex items-center justify-between">
<h3 className="inter-base-semibold mb-2xsmall">
Manage inventory
</h3>
<Controller
control={control}
name={"manage_inventory"}
render={({ field: { value, onChange } }) => {
return <Switch checked={value} onCheckedChange={onChange} />
}}
>
Manage locations
</Button>
/>
</div>
</>
)}
</div>
</div>
<p className="inter-base-regular text-grey-50">
When checked Medusa will regulate the inventory when orders and
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={"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>
)
}

View File

@@ -14,12 +14,13 @@ import {
import Button from "../../fundamentals/button"
import { InventoryLevelDTO } from "@medusajs/types"
import Modal from "../../molecules/modal"
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
import { queryClient } from "../../../constants/query-client"
import { removeNullish } from "../../../utils/remove-nullish"
import { useContext } from "react"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { useForm } from "react-hook-form"
import { countries } from "../../../utils/countries"
import { Option } from "../../../types/shared"
type Props = {
onClose: () => void
@@ -44,19 +45,41 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
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 locationLevels = data.stock.location_levels || []
const manageInventory = data.stock.manage_inventory
const locationLevels = data.location_levels || []
const manageInventory = data.manage_inventory
const variantInventoryItem = variantInventory?.inventory?.[0]
const itemId = variantInventoryItem?.id
delete data.stock.manage_inventory
delete data.stock.location_levels
delete data.manage_inventory
delete data.location_levels
let inventoryItemId: string | undefined = itemId
const upsertPayload = removeNullish(data.stock)
const { ean, barcode, upc } = data
const upsertPayload = createUpdateInventoryItemPayload(data)
let shouldInvalidateCache = false
if (variantInventoryItem) {
@@ -135,14 +158,27 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
}
}
const { dimensions, customs, ...stock } = data
// @ts-ignore
onUpdateVariant(variant.id, createUpdatePayload(data), () => {
refetch()
if (shouldInvalidateCache) {
queryClient.invalidateQueries(adminInventoryItemsKeys.lists())
onUpdateVariant(
variant.id,
removeNullish({
...dimensions,
...customs,
...stock,
ean,
barcode,
upc,
}),
() => {
refetch()
if (shouldInvalidateCache) {
queryClient.invalidateQueries(adminInventoryItemsKeys.lists())
}
handleClose()
}
handleClose()
})
)
}
return (
@@ -154,6 +190,7 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
<StockForm
variantInventory={variantInventory!}
onSubmit={onSubmit}
variant={variant}
isLoadingInventory={isLoadingInventory}
handleClose={handleClose}
updatingVariant={updatingVariant}
@@ -169,8 +206,10 @@ const StockForm = ({
isLoadingInventory,
handleClose,
updatingVariant,
variant,
}: {
variantInventory: VariantInventory
variant: ProductVariant
onSubmit: (data: EditFlowVariantFormType) => void
isLoadingInventory: boolean
handleClose: () => void
@@ -178,7 +217,7 @@ const StockForm = ({
}) => {
const form = useForm<EditFlowVariantFormType>({
// @ts-ignore
defaultValues: getEditVariantDefaultValues(variantInventory),
defaultValues: getEditVariantDefaultValues(variantInventory, variant),
})
const {
@@ -231,34 +270,68 @@ const StockForm = ({
}
export const getEditVariantDefaultValues = (
variantInventory?: any
variantInventory?: any,
variant?: ProductVariant
): EditFlowVariantFormType => {
const inventoryItem = variantInventory?.inventory[0]
if (!inventoryItem) {
return {
stock: {
sku: null,
ean: null,
inventory_quantity: null,
manage_inventory: false,
allow_backorder: false,
barcode: null,
upc: null,
location_levels: null,
sku: null,
ean: variant?.ean || null,
barcode: variant?.barcode || null,
upc: variant?.upc || null,
inventory_quantity: null,
manage_inventory: false,
allow_backorder: false,
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 {
stock: {
sku: inventoryItem.sku,
ean: inventoryItem.ean,
inventory_quantity: inventoryItem.inventory_quantity,
manage_inventory: !!inventoryItem,
allow_backorder: inventoryItem.allow_backorder,
barcode: inventoryItem.barcode,
upc: inventoryItem.upc,
location_levels: inventoryItem.location_levels,
sku: inventoryItem.sku,
ean: variant?.ean || null,
barcode: variant?.barcode || null,
upc: variant?.upc || null,
inventory_quantity: inventoryItem.inventory_quantity,
manage_inventory: !!inventoryItem,
allow_backorder: inventoryItem.allow_backorder,
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,
},
}
}