diff --git a/.changeset/fast-buckets-marry.md b/.changeset/fast-buckets-marry.md
new file mode 100644
index 0000000000..8fc6f6ac70
--- /dev/null
+++ b/.changeset/fast-buckets-marry.md
@@ -0,0 +1,7 @@
+---
+"medusa-react": patch
+"@medusajs/medusa-js": patch
+"@medusajs/medusa": patch
+---
+
+Add create-inventory-item endpoint
diff --git a/.changeset/slow-beers-train.md b/.changeset/slow-beers-train.md
new file mode 100644
index 0000000000..c8248d3516
--- /dev/null
+++ b/.changeset/slow-beers-train.md
@@ -0,0 +1,5 @@
+---
+"@medusajs/admin-ui": patch
+---
+
+fix(admin-ui): create/update/delete inventory items according to inventory items on the variant
diff --git a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js
index 002b5696a1..1eae8976b0 100644
--- a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js
+++ b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js
@@ -278,6 +278,62 @@ describe("Inventory Items endpoints", () => {
})
})
+ it.only("Creates an inventory item using the api", async () => {
+ const product = await simpleProductFactory(dbConnection, {})
+
+ const api = useApi()
+
+ const productRes = await api.get(
+ `/admin/products/${product.id}`,
+ adminHeaders
+ )
+
+ const variantId = productRes.data.product.variants[0].id
+
+ let variantInventoryRes = await api.get(
+ `/admin/variants/${variantId}/inventory`,
+ adminHeaders
+ )
+
+ expect(variantInventoryRes.data).toEqual({
+ variant: {
+ id: variantId,
+ inventory: [],
+ sales_channel_availability: [],
+ },
+ })
+ expect(variantInventoryRes.status).toEqual(200)
+
+ const inventoryItemCreateRes = await api.post(
+ `/admin/inventory-items`,
+ { variant_id: variantId },
+ adminHeaders
+ )
+
+ variantInventoryRes = await api.get(
+ `/admin/variants/${variantId}/inventory`,
+ adminHeaders
+ )
+
+ expect(variantInventoryRes.data).toEqual({
+ variant: {
+ id: variantId,
+ inventory: [
+ expect.objectContaining({
+ ...inventoryItemCreateRes.data.inventory_item,
+ }),
+ ],
+ sales_channel_availability: [
+ expect.objectContaining({
+ available_quantity: 0,
+ channel_name: "Default Sales Channel",
+ }),
+ ],
+ },
+ })
+ expect(variantInventoryRes.status).toEqual(200)
+ })
+
describe("List inventory items", () => {
it("Lists inventory items with location", async () => {
const api = useApi()
diff --git a/packages/admin-ui/ui/src/domain/discounts/details/configurations/use-discount-configurations.tsx b/packages/admin-ui/ui/src/domain/discounts/details/configurations/use-discount-configurations.tsx
index e505df70c3..b2a8515b05 100644
--- a/packages/admin-ui/ui/src/domain/discounts/details/configurations/use-discount-configurations.tsx
+++ b/packages/admin-ui/ui/src/domain/discounts/details/configurations/use-discount-configurations.tsx
@@ -1,14 +1,15 @@
-import { Discount } from "@medusajs/medusa"
-import { parse } from "iso8601-duration"
-import { useAdminUpdateDiscount } from "medusa-react"
-import moment from "moment"
import { ReactNode } from "react"
-import ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
-import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
+
import { ActionType } from "../../../../components/molecules/actionables"
-import useNotification from "../../../../hooks/use-notification"
+import ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
+import { Discount } from "@medusajs/medusa"
+import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import { getErrorMessage } from "../../../../utils/error-messages"
+import moment from "moment"
+import { parse } from "iso8601-duration"
import { removeNullish } from "../../../../utils/remove-nullish"
+import { useAdminUpdateDiscount } from "medusa-react"
+import useNotification from "../../../../hooks/use-notification"
type displaySetting = {
title: string
diff --git a/packages/admin-ui/ui/src/domain/orders/details/returns/index.tsx b/packages/admin-ui/ui/src/domain/orders/details/returns/index.tsx
index cd7a0b9bde..c2d57f5b54 100644
--- a/packages/admin-ui/ui/src/domain/orders/details/returns/index.tsx
+++ b/packages/admin-ui/ui/src/domain/orders/details/returns/index.tsx
@@ -4,21 +4,24 @@ import {
Order,
StockLocationDTO,
} from "@medusajs/medusa"
-import { useAdminStockLocations } from "medusa-react"
-import { useAdminRequestReturn, useAdminShippingOptions } from "medusa-react"
+import {
+ useAdminRequestReturn,
+ useAdminShippingOptions,
+ useAdminStockLocations,
+} from "medusa-react"
import React, { useContext, useEffect, useState } from "react"
import Spinner from "../../../../components/atoms/spinner"
+import LayeredModal, {
+ LayeredModalContext,
+} from "../../../../components/molecules/modal/layered-modal"
+
import Button from "../../../../components/fundamentals/button"
import CheckIcon from "../../../../components/fundamentals/icons/check-icon"
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
import IconTooltip from "../../../../components/molecules/icon-tooltip"
import Modal from "../../../../components/molecules/modal"
-import LayeredModal, {
- LayeredModalContext,
-} from "../../../../components/molecules/modal/layered-modal"
import RMAShippingPrice from "../../../../components/molecules/rma-select-shipping"
import Select from "../../../../components/molecules/select/next-select/select"
-// import Select from "../../../../components/molecules/select"
import CurrencyInput from "../../../../components/organisms/currency-input"
import RMASelectProductTable from "../../../../components/organisms/rma-select-product-table"
import useNotification from "../../../../hooks/use-notification"
diff --git a/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/edit-flow-variant-form/index.tsx b/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/edit-flow-variant-form/index.tsx
index a7e4fcdfbd..2d7b4fe185 100644
--- a/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/edit-flow-variant-form/index.tsx
+++ b/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/edit-flow-variant-form/index.tsx
@@ -10,9 +10,7 @@ export type EditFlowVariantFormType = {
type Props = {
form: UseFormReturn
locationLevels: InventoryLevelDTO[]
- refetchInventory: () => void
isLoading: boolean
- itemId: string
}
/**
@@ -34,13 +32,7 @@ type Props = {
* )
* }
*/
-const EditFlowVariantForm = ({
- form,
- isLoading,
- locationLevels,
- refetchInventory,
- itemId,
-}: Props) => {
+const EditFlowVariantForm = ({ form, isLoading, locationLevels }: Props) => {
if (isLoading) {
return null
}
@@ -49,8 +41,6 @@ const EditFlowVariantForm = ({
<>
>
diff --git a/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/variant-stock-form/index.tsx b/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/variant-stock-form/index.tsx
index f712598c83..42c4680928 100644
--- a/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/variant-stock-form/index.tsx
+++ b/packages/admin-ui/ui/src/domain/products/components/variant-inventory-form/variant-stock-form/index.tsx
@@ -1,13 +1,9 @@
import React, { useMemo, useState, useContext } from "react"
import Modal from "../../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
-import {
- useAdminCreateLocationLevel,
- useAdminDeleteLocationLevel,
- useAdminStockLocations,
-} from "medusa-react"
+import { useAdminStockLocations } from "medusa-react"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
-import { Controller } from "react-hook-form"
+import { Controller, useFieldArray } from "react-hook-form"
import Button from "../../../../../components/fundamentals/button"
import Switch from "../../../../../components/atoms/switch"
import InputField from "../../../../../components/molecules/input"
@@ -16,55 +12,60 @@ import IconBadge from "../../../../../components/fundamentals/icon-badge"
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
export type VariantStockFormType = {
- manage_inventory: boolean
+ manage_inventory?: boolean
allow_backorder: boolean
inventory_quantity: number | null
sku: string | null
ean: string | null
upc: string | null
barcode: string | null
- location_levels: InventoryLevelDTO[] | null
+ location_levels?: Partial[] | null
}
type Props = {
- itemId: string
locationLevels: InventoryLevelDTO[]
- refetchInventory: () => void
form: NestedForm
}
-const VariantStockForm = ({
- form,
- locationLevels,
- refetchInventory,
- itemId,
-}: Props) => {
+const VariantStockForm = ({ form, locationLevels }: Props) => {
+ const locationLevelMap = useMemo(
+ () => new Map(locationLevels.map((l) => [l.location_id, l])),
+ [locationLevels]
+ )
+
const layeredModalContext = useContext(LayeredModalContext)
const { stock_locations: locations, isLoading } = useAdminStockLocations()
- const deleteLevel = useAdminDeleteLocationLevel(itemId)
- const createLevel = useAdminCreateLocationLevel(itemId)
+ const { path, control, register, watch } = form
- const { path, control, register } = form
+ const manageInventory = watch(path("manage_inventory"))
- const handleUpdateLocations = async (value) => {
- await Promise.all(
- value.removed.map(async (id) => {
- await deleteLevel.mutateAsync(id)
- })
+ const {
+ fields: selectedLocations,
+ append,
+ remove,
+ } = useFieldArray({
+ control,
+ name: path("location_levels"),
+ })
+
+ const handleUpdateLocations = async (data: {
+ added: string[]
+ removed: string[]
+ }) => {
+ const removed = data.removed.map((r) =>
+ selectedLocations.findIndex((sl) => sl.location_id === r)
)
- await Promise.all(
- value.added.map(async (id) => {
- await createLevel.mutateAsync({
- stocked_quantity: 0,
- location_id: id,
- })
- })
- )
+ removed.forEach((r) => remove(r))
- refetchInventory()
+ data.added.forEach((added) => {
+ append({
+ location_id: added,
+ stocked_quantity: locationLevelMap.get(added)?.stocked_quantity ?? 0,
+ })
+ })
}
return (
@@ -111,88 +112,95 @@ const VariantStockForm = ({
returns are made.
-
-
-
Allow backorders
- {
- return
- }}
- />
-
-
- When checked the product will be available for purchase despite the
- product being sold out
-
-
-
-
Quantity
- {!isLoading && locations && (
-
-
-
Location
-
In Stock
+ {manageInventory && (
+ <>
+
+
+
+ Allow backorders
+
+ {
+ return
+ }}
+ />
- {locationLevels.map((level, i) => {
- const locationDetails = locations.find(
- (l) => l.id === level.location_id
- )
-
- return (
-
-
-
-
-
- {locationDetails?.name}
-
-
-
-
- {`${level.reserved_quantity} reserved`}
-
- {`${
- level.stocked_quantity - level.reserved_quantity
- } available`}
-
-
-
-
- )
- })}
+
+ When checked the product will be available for purchase despite
+ the product being sold out
+
- )}
-
-
-
-
+
+
Quantity
+ {!isLoading && locations && (
+
+
+ {selectedLocations.map((level, i) => {
+ console.log(level)
+ const locationDetails = locations.find(
+ (l: StockLocationDTO) => l.id === level.location_id
+ )
+
+ return (
+
+
+
+
+
+ {locationDetails?.name}
+
+
+
+
+ {`${level.reserved_quantity} reserved`}
+
+ {`${
+ level.stocked_quantity - level.reserved_quantity
+ } available`}
+
+
+
+
+ )
+ })}
+
+ )}
+
+
+
+
+ >
+ )}
)
@@ -200,7 +208,7 @@ const VariantStockForm = ({
export const ManageLocationsScreen = (
pop: () => void,
- levels: InventoryLevelDTO[],
+ levels: Partial[],
locations: StockLocationDTO[],
onSubmit: (value: any) => Promise
) => {
@@ -218,7 +226,7 @@ export const ManageLocationsScreen = (
}
type ManageLocationFormProps = {
- existingLevels: InventoryLevelDTO[]
+ existingLevels: Partial[]
locationOptions: StockLocationDTO[]
onSubmit: (value: any) => Promise
}
@@ -232,7 +240,7 @@ const ManageLocationsForm = ({
const { pop } = layeredModalContext
const existingLocations = useMemo(() => {
- return existingLevels.map((level) => level.location_id)
+ return existingLevels.map((level) => level.location_id!)
}, [existingLevels])
const [selectedLocations, setSelectedLocations] =
@@ -266,7 +274,7 @@ const ManageLocationsForm = ({
(locationId: string) => !existingLocations.includes(locationId)
)
const removedLevels = existingLocations.filter(
- (locationId) => !selectedLocations.includes(locationId)
+ (locationId) => !!locationId && !selectedLocations.includes(locationId)
)
await onSubmit({
diff --git a/packages/admin-ui/ui/src/domain/products/edit/hooks/use-edit-product-actions.tsx b/packages/admin-ui/ui/src/domain/products/edit/hooks/use-edit-product-actions.tsx
index 44c88e2fbf..d3fdc45495 100644
--- a/packages/admin-ui/ui/src/domain/products/edit/hooks/use-edit-product-actions.tsx
+++ b/packages/admin-ui/ui/src/domain/products/edit/hooks/use-edit-product-actions.tsx
@@ -11,11 +11,12 @@ import {
useAdminUpdateProduct,
useAdminUpdateVariant,
} from "medusa-react"
-import { useNavigate } from "react-router-dom"
-import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
-import useNotification from "../../../../hooks/use-notification"
+
import { getErrorMessage } from "../../../../utils/error-messages"
import { removeNullish } from "../../../../utils/remove-nullish"
+import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
+import { useNavigate } from "react-router-dom"
+import useNotification from "../../../../hooks/use-notification"
const useEditProductActions = (productId: string) => {
const dialog = useImperativeDialog()
@@ -70,8 +71,11 @@ const useEditProductActions = (productId: string) => {
successMessage = "Variant was updated successfully"
) => {
updateVariant.mutate(
- // @ts-ignore - TODO fix type on request
- { variant_id: id, ...removeNullish(payload) },
+ {
+ variant_id: id,
+ ...removeNullish(payload),
+ manage_inventory: payload.manage_inventory,
+ },
{
onSuccess: () => {
notification("Success", successMessage, "success")
diff --git a/packages/admin-ui/ui/src/domain/products/edit/sections/variants/edit-variant-inventory-modal.tsx b/packages/admin-ui/ui/src/domain/products/edit/sections/variants/edit-variant-inventory-modal.tsx
index 881bbd8a3e..3211c53e41 100644
--- a/packages/admin-ui/ui/src/domain/products/edit/sections/variants/edit-variant-inventory-modal.tsx
+++ b/packages/admin-ui/ui/src/domain/products/edit/sections/variants/edit-variant-inventory-modal.tsx
@@ -1,20 +1,24 @@
-import { Product, ProductVariant } from "@medusajs/medusa"
import {
- useAdminUpdateLocationLevel,
- useAdminVariantsInventory,
-} from "medusa-react"
-import React, { useContext } from "react"
+ InventoryLevelDTO,
+ Product,
+ ProductVariant,
+ VariantInventory,
+} from "@medusajs/medusa"
+import { useMedusa } from "medusa-react"
+import { useAdminVariantsInventory } from "medusa-react"
+import { useContext } from "react"
import { useForm } from "react-hook-form"
-import Button from "../../../../../components/fundamentals/button"
-import Modal from "../../../../../components/molecules/modal"
-import LayeredModal, {
- LayeredModalContext,
-} from "../../../../../components/molecules/modal/layered-modal"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../components/variant-inventory-form/edit-flow-variant-form"
-import useEditProductActions from "../../hooks/use-edit-product-actions"
+import LayeredModal, {
+ LayeredModalContext,
+} from "../../../../../components/molecules/modal/layered-modal"
+import Button from "../../../../../components/fundamentals/button"
+import Modal from "../../../../../components/molecules/modal"
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
+import useEditProductActions from "../../hooks/use-edit-product-actions"
+import { removeNullish } from "../../../../../utils/remove-nullish"
type Props = {
onClose: () => void
@@ -24,6 +28,7 @@ type Props = {
}
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
+ const { client } = useMedusa()
const layeredModalContext = useContext(LayeredModalContext)
const {
// @ts-ignore
@@ -32,31 +37,94 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
refetch,
} = useAdminVariantsInventory(variant.id)
- const itemId = variantInventory?.inventory[0]?.id
+ const variantInventoryItem = variantInventory?.inventory[0]
+ const itemId = variantInventoryItem?.id
- const { mutate: updateLocationLevel } = useAdminUpdateLocationLevel(
- itemId || ""
- )
const handleClose = () => {
onClose()
}
const { onUpdateVariant, updatingVariant } = useEditProductActions(product.id)
- const onSubmit = async (data) => {
- const { location_levels } = data.stock
-
- await Promise.all(
- location_levels.map(async (level) => {
- await updateLocationLevel({
- stockLocationId: level.location_id,
- stocked_quantity: level.stocked_quantity,
- })
- })
- )
- // / TODO: Call update location level with new values
+ const onSubmit = async (data: EditFlowVariantFormType) => {
+ const locationLevels = data.stock.location_levels || []
+ const manageInventory = data.stock.manage_inventory
+ delete data.stock.manage_inventory
delete data.stock.location_levels
+ let inventoryItemId: string | undefined = itemId
+
+ const upsertPayload = removeNullish(data.stock)
+
+ if (variantInventoryItem) {
+ // variant inventory exists and we can remove location levels
+ // (it's important to do this before potentially deleting the inventory item)
+ const deleteLocations = manageInventory
+ ? variantInventoryItem?.location_levels?.filter(
+ (itemLevel: InventoryLevelDTO) => {
+ return !locationLevels.find(
+ (level) => level.location_id === itemLevel.location_id
+ )
+ }
+ ) ?? []
+ : []
+
+ if (inventoryItemId) {
+ await Promise.all(
+ deleteLocations.map(async (location: InventoryLevelDTO) => {
+ await client.admin.inventoryItems.deleteLocationLevel(
+ inventoryItemId!,
+ location.id
+ )
+ })
+ )
+ }
+
+ if (!manageInventory) {
+ // has an inventory item but no longer wants to manage inventory
+ await client.admin.inventoryItems.delete(itemId!)
+ inventoryItemId = undefined
+ } else {
+ // has an inventory item and wants to update inventory
+ await client.admin.inventoryItems.update(itemId!, upsertPayload)
+ }
+ } else if (manageInventory) {
+ // does not have an inventory item but wants to manage inventory
+ const { inventory_item } = await client.admin.inventoryItems.create({
+ variant_id: variant.id,
+ ...upsertPayload,
+ })
+ inventoryItemId = inventory_item.id
+ }
+
+ // If some inventory Item exists update location levels
+ if (inventoryItemId) {
+ await Promise.all(
+ locationLevels.map(async (level) => {
+ if (!level.location_id) {
+ return
+ }
+ if (level.id) {
+ await client.admin.inventoryItems.updateLocationLevel(
+ inventoryItemId!,
+ level.location_id,
+ {
+ stocked_quantity: level.stocked_quantity,
+ }
+ )
+ } else {
+ await client.admin.inventoryItems.createLocationLevel(
+ inventoryItemId!,
+ {
+ location_id: level.location_id,
+ stocked_quantity: level.stocked_quantity!,
+ }
+ )
+ }
+ })
+ )
+ }
+
// @ts-ignore
onUpdateVariant(variant.id, createUpdatePayload(data), () => {
refetch()
@@ -71,8 +139,7 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
{!isLoadingInventory && (
{
const StockForm = ({
variantInventory,
onSubmit,
- refetchInventory,
isLoadingInventory,
handleClose,
updatingVariant,
+}: {
+ variantInventory: VariantInventory
+ onSubmit: (data: EditFlowVariantFormType) => void
+ isLoadingInventory: boolean
+ handleClose: () => void
+ updatingVariant: boolean
}) => {
const form = useForm({
defaultValues: getEditVariantDefaultValues(variantInventory),
@@ -99,32 +171,20 @@ const StockForm = ({
formState: { isDirty },
handleSubmit,
reset,
- watch,
} = form
- const locationLevels = watch("stock.location_levels")
-
- const { location_levels } = variantInventory.inventory[0]
-
- React.useEffect(() => {
- form.setValue("stock.location_levels", location_levels)
- }, [form, location_levels])
+ const locationLevels = variantInventory.inventory[0]?.location_levels || []
const handleOnSubmit = handleSubmit((data) => {
- // @ts-ignore
onSubmit(data)
})
- const itemId = variantInventory.inventory[0].id
-
return (