Fix(medusa, admin-ui): Order allocations (#3419)

**What**
- Fix `delete-reservation` endpoint to allow deletions of reservations 
- remove `inventory_item_id` from reservation update call to properly update reservation
- invalidate all list caches to properly update order overview after an allocation has been created
- Fix overlap for long product titles in edit-allocation-modal

Fixes CORE-1214
This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-03-09 17:07:49 +00:00
1 parent cdbc5ff3d8
commit 80b95a2300
7 files changed
+107 -69

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): Resolve on scope instead of req
+5
View File
@@ -0,0 +1,5 @@
---
"medusa-react": patch
---
fix(medusa-react): invalidate all reservations list queries
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
Fix(admin-ui): Fix minor bugs related to the edit-allocation modal
@@ -1,24 +1,25 @@
import React, { useEffect, useMemo } from "react" import { Controller, useForm, useWatch } from "react-hook-form"
import { LineItem, Order, ReservationItemDTO } from "@medusajs/medusa" import { LineItem, Order, ReservationItemDTO } from "@medusajs/medusa"
import FocusModal from "../../../../components/molecules/modal/focus-modal" import { NestedForm, nestedForm } from "../../../../utils/nested-form"
import Button from "../../../../components/fundamentals/button" import React, { useEffect, useMemo } from "react"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import Select from "../../../../components/molecules/select/next-select/select"
import { import {
useAdminCreateReservation, useAdminCreateReservation,
useAdminStockLocations, useAdminStockLocations,
useAdminVariantsInventory, useAdminVariantsInventory,
useMedusa, useMedusa,
} from "medusa-react" } from "medusa-react"
import { Controller, useForm, useWatch } from "react-hook-form"
import Thumbnail from "../../../../components/atoms/thumbnail" import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import FocusModal from "../../../../components/molecules/modal/focus-modal"
import InputField from "../../../../components/molecules/input" import InputField from "../../../../components/molecules/input"
import { NestedForm, nestedForm } from "../../../../utils/nested-form" import Select from "../../../../components/molecules/select/next-select/select"
import { sum } from "lodash" import Thumbnail from "../../../../components/atoms/thumbnail"
import clsx from "clsx" import clsx from "clsx"
import { getFulfillableQuantity } from "../create-fulfillment/item-table"
import useNotification from "../../../../hooks/use-notification"
import { getErrorMessage } from "../../../../utils/error-messages" import { getErrorMessage } from "../../../../utils/error-messages"
import { getFulfillableQuantity } from "../create-fulfillment/item-table"
import { sum } from "lodash"
import useNotification from "../../../../hooks/use-notification"
type AllocationModalFormData = { type AllocationModalFormData = {
location?: { label: string; value: string } location?: { label: string; value: string }
@@ -121,11 +122,11 @@ const AllocateItemsModal: React.FC<AllocateItemsModalProps> = ({
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<FocusModal> <FocusModal>
<FocusModal.Header> <FocusModal.Header>
<div className="flex w-full justify-between px-8 medium:w-8/12"> <div className="medium:w-8/12 flex w-full justify-between px-8">
<Button size="small" variant="ghost" type="button" onClick={close}> <Button size="small" variant="ghost" type="button" onClick={close}>
<CrossIcon size={20} /> <CrossIcon size={20} />
</Button> </Button>
<div className="flex gap-x-small"> <div className="gap-x-small flex">
<Button <Button
size="small" size="small"
variant="secondary" variant="secondary"
@@ -170,7 +171,7 @@ const AllocateItemsModal: React.FC<AllocateItemsModalProps> = ({
</div> </div>
<div <div
className={clsx( className={clsx(
"mt-8 flex w-full flex-col border-t border-grey-20", "border-grey-20 mt-8 flex w-full flex-col border-t",
{ {
"pointer-events-none opacity-50": !selectedLocation?.value, "pointer-events-none opacity-50": !selectedLocation?.value,
} }
@@ -217,7 +218,8 @@ export const AllocationLineItem: React.FC<{
item: LineItem item: LineItem
locationId?: string locationId?: string
reservedQuantity?: number reservedQuantity?: number
}> = ({ form, item, locationId, reservedQuantity }) => { compact?: boolean
}> = ({ form, item, locationId, reservedQuantity, compact }) => {
const { variant, isLoading } = useAdminVariantsInventory( const { variant, isLoading } = useAdminVariantsInventory(
item.variant_id as string item.variant_id as string
) )
@@ -260,44 +262,58 @@ export const AllocationLineItem: React.FC<{
inventoryItemReservationCapacity inventoryItemReservationCapacity
) )
return ( return (
<div> <div className="mt-8 flex w-full items-start justify-between">
<div className="mt-8 flex w-full items-center justify-between"> <div className="gap-x-base flex w-7/12">
<div className="flex gap-x-base"> <div className="min-w-9">
<Thumbnail size="medium" src={item.thumbnail} /> <Thumbnail size="medium" src={item.thumbnail} />
<div className="text-grey-50">
<p className="flex gap-x-2xsmall">
<p className="inter-base-semibold text-grey-90">{item.title}</p>
{`(${item.variant.sku})`}
</p>
<p className="inter-base-regular ">
{item.variant.options?.map((option) => option.value) ||
item.variant.title ||
"-"}
</p>
</div>
</div> </div>
<div className="flex items-center gap-x-large"> <div className="text-grey-50 truncate">
<div className="inter-base-regular flex flex-col items-end whitespace-nowrap text-grey-50"> <p className="gap-x-2xsmall nowrap flex grow ">
<p>{availableQuantity || "N/A"} available</p> <p className="inter-base-semibold text-grey-90 truncate">
<p>({inStockQuantity || "N/A"} in stock)</p> {item.title}
</div> </p>
<InputField {`(${item.variant.sku})`}
{...register(path(`quantity`), { valueAsNumber: true })} </p>
type="number" <p className="inter-base-regular ">
defaultValue={0} {item.variant.options?.map((option) => option.value) ||
disabled={lineItemReservationCapacity < 0} item.variant.title ||
min={0} "-"}
max={maxReservation > 0 ? maxReservation : 0} </p>
suffix={ </div>
<span className="flex"> </div>
{"/"}{" "} <div
<span className="ml-1"> className={clsx("gap-x-large flex items-center", {
{maxReservation > 0 ? maxReservation : 0} "gap-y-xsmall flex-col-reverse": compact,
</span> })}
</span> >
<div
className={clsx(
"inter-base-regular text-grey-50 gap-x-xsmall flex items-end whitespace-nowrap",
{
"flex-col": !compact,
} }
/> )}
>
<p>{availableQuantity || "N/A"} available</p>
<p>({inStockQuantity || "N/A"} in stock)</p>
</div> </div>
<InputField
{...register(path(`quantity`), { valueAsNumber: true })}
type="number"
className="min-w-[120px]"
defaultValue={0}
disabled={lineItemReservationCapacity < 0}
min={0}
max={maxReservation > 0 ? maxReservation : 0}
suffix={
<span className="flex">
{"/"}{" "}
<span className="ml-1">
{maxReservation > 0 ? maxReservation : 0}
</span>
</span>
}
/>
</div> </div>
</div> </div>
) )
@@ -1,21 +1,22 @@
import { useEffect, useMemo } from "react"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import Button from "../../../../components/fundamentals/button"
import { import {
AllocationLineItem, AllocationLineItem,
AllocationLineItemForm, AllocationLineItemForm,
} from "./allocate-items-modal" } from "./allocate-items-modal"
import { Controller, useForm, useWatch } from "react-hook-form" import { Controller, useForm, useWatch } from "react-hook-form"
import { LineItem, ReservationItemDTO } from "@medusajs/medusa"
import { import {
useAdminDeleteReservation, useAdminDeleteReservation,
useAdminStockLocations, useAdminStockLocations,
useAdminUpdateReservation, useAdminUpdateReservation,
} from "medusa-react" } from "medusa-react"
import { useEffect, useMemo } from "react"
import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import Select from "../../../../components/molecules/select/next-select/select" import Select from "../../../../components/molecules/select/next-select/select"
import { LineItem, ReservationItemDTO } from "@medusajs/medusa"
import useNotification from "../../../../hooks/use-notification"
import { nestedForm } from "../../../../utils/nested-form"
import SideModal from "../../../../components/molecules/modal/side-modal" import SideModal from "../../../../components/molecules/modal/side-modal"
import { nestedForm } from "../../../../utils/nested-form"
import useNotification from "../../../../hooks/use-notification"
type EditAllocationLineItemForm = { type EditAllocationLineItemForm = {
location: { label: string; value: string } location: { label: string; value: string }
@@ -104,11 +105,14 @@ const EditAllocationDrawer = ({
}, [reservation, setValue]) }, [reservation, setValue])
const submit = (data: EditAllocationLineItemForm) => { const submit = (data: EditAllocationLineItemForm) => {
if (!data.item.quantity) {
return handleDelete()
}
updateReservation( updateReservation(
{ {
quantity: data.item.quantity, quantity: data.item.quantity,
location_id: data.location.value, location_id: data.location.value,
inventory_item_id: data.item.inventory_item_id,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -125,18 +129,18 @@ const EditAllocationDrawer = ({
return ( return (
<SideModal isVisible close={close}> <SideModal isVisible close={close}>
<form <form
className="w-full h-full text-grey-90" className="text-grey-90 h-full w-full"
onSubmit={handleSubmit(submit)} onSubmit={handleSubmit(submit)}
> >
<div className="flex flex-col justify-between h-full "> <div className="flex h-full flex-col justify-between ">
<div> <div>
<div className="flex items-center justify-between px-8 py-6 border-b border-grey-20"> <div className="border-grey-20 flex items-center justify-between border-b px-8 py-6">
<h1 className="inter-large-semibold ">Edit allocation</h1> <h1 className="inter-large-semibold ">Edit allocation</h1>
<Button variant="ghost" className="p-1.5" onClick={close}> <Button variant="ghost" className="p-1.5" onClick={close}>
<CrossIcon /> <CrossIcon />
</Button> </Button>
</div> </div>
<div className="flex flex-col px-8 pt-6 gap-y-8"> <div className="flex flex-col gap-y-8 px-8 pt-6">
<div> <div>
<h2 className="inter-base-semibold">Location</h2> <h2 className="inter-base-semibold">Location</h2>
<span className="inter-base-regular text-grey-50"> <span className="inter-base-regular text-grey-50">
@@ -163,6 +167,7 @@ const EditAllocationDrawer = ({
<AllocationLineItem <AllocationLineItem
form={nestedForm(form, `item` as "item")} form={nestedForm(form, `item` as "item")}
item={item} item={item}
compact
locationId={selectedLocation?.value} locationId={selectedLocation?.value}
reservedQuantity={ reservedQuantity={
totalReservedQuantity - (reservation?.quantity || 0) totalReservedQuantity - (reservation?.quantity || 0)
@@ -171,7 +176,7 @@ const EditAllocationDrawer = ({
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
className="w-full my-1 border text-rose-50" className="my-1 w-full border text-rose-50"
size="small" size="small"
onClick={handleDelete} onClick={handleDelete}
> >
@@ -179,7 +184,7 @@ const EditAllocationDrawer = ({
</Button> </Button>
</div> </div>
</div> </div>
<div className="flex justify-end w-full px-8 pt-4 pb-6 border-t gap-x-xsmall"> <div className="gap-x-xsmall flex w-full justify-end border-t px-8 pt-4 pb-6">
<Button <Button
variant="ghost" variant="ghost"
size="small" size="small"
@@ -4,15 +4,16 @@ import {
AdminReservationsDeleteRes, AdminReservationsDeleteRes,
AdminReservationsRes, AdminReservationsRes,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js/src"
import { import {
useMutation,
UseMutationOptions, UseMutationOptions,
useMutation,
useQueryClient, useQueryClient,
} from "@tanstack/react-query" } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions" import { Response } from "@medusajs/medusa-js/src"
import { adminReservationsKeys } from "./queries" import { adminReservationsKeys } from "./queries"
import { buildOptions } from "../../utils/buildOptions"
import { useMedusa } from "../../../contexts"
export const useAdminCreateReservation = ( export const useAdminCreateReservation = (
options?: UseMutationOptions< options?: UseMutationOptions<
@@ -27,7 +28,7 @@ export const useAdminCreateReservation = (
return useMutation( return useMutation(
(payload: AdminPostReservationsReq) => (payload: AdminPostReservationsReq) =>
client.admin.reservations.create(payload), client.admin.reservations.create(payload),
buildOptions(queryClient, [adminReservationsKeys.list()], options) buildOptions(queryClient, [adminReservationsKeys.lists()], options)
) )
} }
@@ -54,8 +54,9 @@ import { IInventoryService } from "../../../../interfaces"
*/ */
export default async (req, res) => { export default async (req, res) => {
const { id } = req.params const { id } = req.params
const inventoryService: IInventoryService = req.resolve("inventoryService") const inventoryService: IInventoryService =
const manager: EntityManager = req.resolve("manager") req.scope.resolve("inventoryService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (manager) => { await manager.transaction(async (manager) => {
await inventoryService.withTransaction(manager).deleteReservationItem(id) await inventoryService.withTransaction(manager).deleteReservationItem(id)