Feat(medusa, admin-ui): Update edit allocation modal (#4071)

* update edit-allocation modal

* add changeset

* update edit-allocation modal

* update allocation modal
This commit is contained in:
Philip Korsholm
2023-05-22 15:58:51 +02:00
committed by GitHub
parent c0e527d6e0
commit 0476f52519
6 changed files with 173 additions and 101 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feaet(medusa): add description to reservation default fields
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
feat(admin-ui): update edit allocation modal design
@@ -1,10 +1,10 @@
import clsx from "clsx"
import ImagePlaceholderIcon from "../../fundamentals/icons/image-placeholder-icon" import ImagePlaceholderIcon from "../../fundamentals/icons/image-placeholder-icon"
import clsx from "clsx"
type Props = { type Props = {
src?: string | null src?: string | null
className?: string className?: string
size?: "small" | "medium" | "large" size?: "xsmall" | "small" | "medium" | "large"
} }
export const Thumbnail = ({ src, className, size = "small" }: Props) => { export const Thumbnail = ({ src, className, size = "small" }: Props) => {
@@ -13,6 +13,7 @@ export const Thumbnail = ({ src, className, size = "small" }: Props) => {
className={clsx( className={clsx(
"bg-grey-5 rounded-rounded flex items-center justify-center overflow-hidden", "bg-grey-5 rounded-rounded flex items-center justify-center overflow-hidden",
{ {
"h-6 w-[18px]": size === "xsmall",
"h-10 w-[30px]": size === "small", "h-10 w-[30px]": size === "small",
"h-12 w-9": size === "medium", "h-12 w-9": size === "medium",
"h-[226px] w-[170px]": size === "large", "h-[226px] w-[170px]": size === "large",
@@ -1,5 +1,4 @@
import { Controller, useForm, useWatch } from "react-hook-form" import { Controller, useForm, useWatch } from "react-hook-form"
import { LineItem } from "@medusajs/medusa"
import { NestedForm, nestedForm } from "../../../../utils/nested-form" import { NestedForm, nestedForm } from "../../../../utils/nested-form"
import React, { useEffect, useMemo } from "react" import React, { useEffect, useMemo } from "react"
import { import {
@@ -13,6 +12,8 @@ import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon" import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import FocusModal from "../../../../components/molecules/modal/focus-modal" import FocusModal from "../../../../components/molecules/modal/focus-modal"
import InputField from "../../../../components/molecules/input" import InputField from "../../../../components/molecules/input"
import { LineItem } from "@medusajs/medusa"
import { ReservationItemDTO } from "@medusajs/types"
import Select from "../../../../components/molecules/select/next-select/select" import Select from "../../../../components/molecules/select/next-select/select"
import Thumbnail from "../../../../components/atoms/thumbnail" import Thumbnail from "../../../../components/atoms/thumbnail"
import clsx from "clsx" import clsx from "clsx"
@@ -20,7 +21,6 @@ import { getErrorMessage } from "../../../../utils/error-messages"
import { getFulfillableQuantity } from "../create-fulfillment/item-table" import { getFulfillableQuantity } from "../create-fulfillment/item-table"
import { sum } from "lodash" import { sum } from "lodash"
import useNotification from "../../../../hooks/use-notification" import useNotification from "../../../../hooks/use-notification"
import { ReservationItemDTO } from "@medusajs/types"
type AllocationModalFormData = { type AllocationModalFormData = {
location?: { label: string; value: string } location?: { label: string; value: string }
@@ -205,6 +205,7 @@ export type AllocationLineItemForm = {
inventory_item_id: string inventory_item_id: string
line_item_id: string line_item_id: string
quantity: number quantity: number
description?: string | null
} }
export const AllocationLineItem: React.FC<{ export const AllocationLineItem: React.FC<{
@@ -1,47 +1,72 @@
import { AllocationLineItemForm } 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 {
InventoryLevelDTO,
ReservationItemDTO,
StockLocationDTO,
} from "@medusajs/types"
import MetadataForm, {
MetadataFormType,
getMetadataFormValues,
getSubmittableMetadata,
} from "../../../../components/forms/general/metadata-form"
import { import {
useAdminDeleteReservation, useAdminDeleteReservation,
useAdminInventoryItem,
useAdminStockLocations, useAdminStockLocations,
useAdminUpdateReservation, useAdminUpdateReservation,
useAdminVariantsInventory,
} from "medusa-react" } from "medusa-react"
import { useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import { AllocationLineItemForm } from "./allocate-items-modal"
import Button from "../../../../components/fundamentals/button" import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon" import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import InputField from "../../../../components/molecules/input"
import { LineItem } from "@medusajs/medusa"
import Select from "../../../../components/molecules/select/next-select/select" import Select from "../../../../components/molecules/select/next-select/select"
import SideModal from "../../../../components/molecules/modal/side-modal" import SideModal from "../../../../components/molecules/modal/side-modal"
import useNotification from "../../../../hooks/use-notification"
import Thumbnail from "../../../../components/atoms/thumbnail" import Thumbnail from "../../../../components/atoms/thumbnail"
import { getFulfillableQuantity } from "../create-fulfillment/item-table" import { getFulfillableQuantity } from "../create-fulfillment/item-table"
import { nestedForm } from "../../../../utils/nested-form"
import useNotification from "../../../../hooks/use-notification"
import useToggleState from "../../../../hooks/use-toggle-state"
type EditAllocationLineItemForm = { type EditAllocationLineItemForm = {
location: { label: string; value: string } location: { label: string; value: string }
item: AllocationLineItemForm item: AllocationLineItemForm
metadata: MetadataFormType
} }
const EditAllocationDrawer = ({ const EditAllocationDrawer = ({
close, close,
reservation, reservation,
item, item,
totalReservedQuantity, totalReservedQuantity = 0,
}: { }: {
close: () => void close: () => void
reservation?: ReservationItemDTO reservation: ReservationItemDTO
item: LineItem item?: LineItem
totalReservedQuantity: number totalReservedQuantity?: number
}) => { }) => {
const form = useForm<EditAllocationLineItemForm>() const form = useForm<EditAllocationLineItemForm>({
defaultValues: {
item: {
description: reservation.description,
},
metadata: getMetadataFormValues(reservation?.metadata),
},
})
const { control, setValue, handleSubmit } = form const { state: hasMetadata, toggle: toggleHasMetadata } = useToggleState(
!!reservation.metadata
)
const { control, setValue, handleSubmit, register } = form
const { stock_locations, isLoading: isLoadingStockLocations } = const { stock_locations, isLoading: isLoadingStockLocations } =
useAdminStockLocations() useAdminStockLocations()
const { variant, isLoading } = useAdminVariantsInventory( const { inventory_item, isLoading } = useAdminInventoryItem(
item.variant_id as string reservation.inventory_item_id
) )
const { mutate: updateReservation } = useAdminUpdateReservation( const { mutate: updateReservation } = useAdminUpdateReservation(
@@ -55,7 +80,8 @@ const EditAllocationDrawer = ({
if (!stock_locations || isLoadingStockLocations) { if (!stock_locations || isLoadingStockLocations) {
return [] return []
} }
return stock_locations.map((sl) => ({
return stock_locations.map((sl: StockLocationDTO) => ({
value: sl.id, value: sl.id,
label: sl.name, label: sl.name,
})) }))
@@ -86,7 +112,7 @@ const EditAllocationDrawer = ({
useEffect(() => { useEffect(() => {
if (stock_locations?.length && reservation) { if (stock_locations?.length && reservation) {
const defaultLocation = stock_locations.find( const defaultLocation = stock_locations.find(
(sl) => sl.id === reservation.location_id (sl: StockLocationDTO) => sl.id === reservation.location_id
) )
if (defaultLocation) { if (defaultLocation) {
@@ -113,6 +139,8 @@ const EditAllocationDrawer = ({
{ {
quantity: data.item.quantity, quantity: data.item.quantity,
location_id: data.location.value, location_id: data.location.value,
description: data.item.description,
metadata: hasMetadata ? getSubmittableMetadata(data.metadata) : null,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -131,12 +159,12 @@ const EditAllocationDrawer = ({
} }
const { availableQuantity, inStockQuantity } = useMemo(() => { const { availableQuantity, inStockQuantity } = useMemo(() => {
if (isLoading || !selectedLocation?.value || !variant) { if (isLoading || !selectedLocation?.value || !inventory_item) {
return {} return {}
} }
const { inventory } = variant
const locationInventory = inventory[0].location_levels?.find( const locationInventory = inventory_item.location_levels?.find(
(inv) => inv.location_id === selectedLocation?.value (inv: InventoryLevelDTO) => inv.location_id === selectedLocation?.value
) )
if (!locationInventory) { if (!locationInventory) {
return {} return {}
@@ -145,9 +173,13 @@ const EditAllocationDrawer = ({
availableQuantity: locationInventory.available_quantity, availableQuantity: locationInventory.available_quantity,
inStockQuantity: locationInventory.stocked_quantity, inStockQuantity: locationInventory.stocked_quantity,
} }
}, [variant, selectedLocation, isLoading]) }, [isLoading, selectedLocation?.value, inventory_item])
const maxReservation = useMemo(() => {
if (!item) {
return typeof availableQuantity === "number" ? availableQuantity : 0
}
// we can adjust up to fulfillable quantity - the quantity reserved in other reservations
const lineItemReservationCapacity = const lineItemReservationCapacity =
getFulfillableQuantity(item) - getFulfillableQuantity(item) -
(totalReservedQuantity - (reservation?.quantity || 0)) (totalReservedQuantity - (reservation?.quantity || 0))
@@ -155,10 +187,16 @@ const EditAllocationDrawer = ({
const inventoryItemReservationCapacity = const inventoryItemReservationCapacity =
typeof availableQuantity === "number" ? availableQuantity : 0 typeof availableQuantity === "number" ? availableQuantity : 0
const maxReservation = Math.min( return Math.min(
lineItemReservationCapacity, lineItemReservationCapacity,
inventoryItemReservationCapacity inventoryItemReservationCapacity
) )
}, [availableQuantity, item, reservation?.quantity, totalReservedQuantity])
const closeModal = (e) => {
e.preventDefault()
close()
}
return ( return (
<SideModal isVisible close={close}> <SideModal isVisible close={close}>
@@ -167,14 +205,20 @@ const EditAllocationDrawer = ({
onSubmit={handleSubmit(submit)} onSubmit={handleSubmit(submit)}
> >
<div className="flex h-full flex-col justify-between"> <div className="flex h-full flex-col justify-between">
<div className="flex grow flex-col">
<div className="border-grey-20 flex items-center justify-between border-b px-8 py-6"> <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"
type="button"
onClick={closeModal}
>
<CrossIcon /> <CrossIcon />
</Button> </Button>
</div> </div>
<div className="flex grow flex-col overflow-y-auto">
<div className="flex h-full flex-col justify-between gap-y-8 px-8 pb-8 pt-6"> <div className="flex h-full flex-col justify-between gap-y-8 px-8 pb-8 pt-6">
<div className="flex flex-col gap-y-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">
@@ -193,72 +237,86 @@ const EditAllocationDrawer = ({
/> />
)} )}
/> />
</div>
<div> <div>
<h2 className="inter-base-semibold mt-8"> <h2 className="inter-base-semibold">Items to Allocate</h2>
Items to Allocate
</h2>
<span className="inter-base-regular text-grey-50"> <span className="inter-base-regular text-grey-50">
Select the number of items that you wish to allocate. Select the number of items that you wish to allocate.
</span> </span>
<div className="gap-x-base mt-6 flex w-full">
<div className="min-w-9">
<Thumbnail size="medium" src={item.thumbnail} />
</div>
<div className="text-grey-50 truncate">
<p className="inter-base-semibold text-grey-90 truncate">
{item.title}
</p>
<p className="inter-base-semibold gap-x-2xsmall flex">
<p>{`(${item.variant.sku})`}</p>
<span>&#183;</span>
<span className="inter-base-regular gap-x-2xsmall flex">
{item.variant.options
?.map((option, i) => [
<span key={`${option.id}-${i}`}>
{option.value}
</span>,
<span key={`${option.id}-${i}.dot`}>&#183;</span>,
])
.flat()
.slice(0, -1) ||
item.variant.title ||
"-"}
</span>
</p>
</div>
</div>
<div <div
className={` className={`
bg-grey-5 text-grey-50 border-grey-20 bg-grey-5 text-grey-50 border-grey-20
mt-8 inter-base-regular
mt-6
grid border-collapse grid-cols-2 grid-rows-3 grid border-collapse grid-cols-2 grid-rows-3
[&>*]:border-r [&>*]:border-b [&>*]:py-2 [&>*]:border-r [&>*]:border-b [&>*]:py-2
[&>*:nth-child(odd)]:border-l [&>*:nth-child(odd)]:pl-4 [&>*:nth-child(odd)]:border-l [&>*:nth-child(odd)]:pl-4
[&>*:nth-child(even)]:pr-4 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:pr-4 [&>*:nth-child(even)]:text-right
[&>*:nth-child(-n+2)]:border-t`} [&>*:nth-child(-n+2)]:border-t`}
> >
<div className="rounded-tl-rounded">In stock</div> <div className="rounded-tl-rounded">Item</div>
<div className="rounded-tr-rounded"> <div className="rounded-tr-rounded flex justify-end space-x-3">
{inStockQuantity ?? "N/A"} <p className="inter-base-regular text-grey-50 truncate ">
{inventory_item?.title ?? item?.title ?? "-"}
</p>
<Thumbnail
size="xsmall"
src={inventory_item?.thumbnail ?? item?.thumbnail}
/>
</div> </div>
<div className="">Available</div> <div>SKU</div>
<div className="">{availableQuantity ?? "N/A"}</div> <div>{inventory_item?.sku ?? "N/A"}</div>
<div>In stock</div>
<div>{inStockQuantity ?? "N/A"}</div>
<div>Available</div>
<div>{availableQuantity ?? "N/A"}</div>
<div className="rounded-bl-rounded">Allocate</div> <div className="rounded-bl-rounded">Allocate</div>
<div className="bg-grey-0 rounded-br-rounded text-grey-80 flex items-center"> <div className="bg-grey-0 rounded-br-rounded text-grey-80 flex items-center">
<input <input
className="remove-number-spinner inter-base-regular w-full shrink border-none bg-transparent text-right font-normal outline-none outline-0" className="remove-number-spinner inter-base-regular w-full shrink border-none bg-transparent text-right font-normal outline-none outline-0"
{...form.register("item.quantity", { {...register("item.quantity", {
valueAsNumber: true, valueAsNumber: true,
})} })}
type="number" type="number"
min={0} min={0}
max={maxReservation} max={maxReservation}
/> />
<span className="text-grey-50 nowrap whitespace-nowrap pl-2">{` / ${maxReservation} requested`}</span> <span className="text-grey-50 nowrap whitespace-nowrap pl-2">
{maxReservation
? ` / ${maxReservation} requested`
: " reserved"}
</span>
</div> </div>
</div> </div>
</div> </div>
<div className="border-grey-20 inter-base-regular border-t pt-6">
<p className="inter-base-semibold">Description</p>
<p className="text-grey-50 mb-6">
What type of reservation is this?
</p>
<InputField
{...register("item.description")}
placeholder="Description"
/>
</div>
<div className="border-grey border-grey-20 w-full items-center border-t pt-6">
<div className="mb-2 flex justify-between">
<p className="inter-base-semibold ">Metadata</p>
<Button
size="small"
variant="ghost"
type="button"
className="border"
onClick={toggleHasMetadata}
>
{hasMetadata ? "Remove metadata" : "Add metadata"}
</Button>
</div>
{hasMetadata && (
<MetadataForm form={nestedForm(form, "metadata")} />
)}
</div>
</div> </div>
<Button <Button
@@ -1,14 +1,15 @@
import { ReservationItemDTO } from "@medusajs/types"
import { Router } from "express"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common" import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import middlewares, { import middlewares, {
transformBody, transformBody,
transformQuery, transformQuery,
} from "../../../middlewares" } from "../../../middlewares"
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
import { AdminPostReservationsReq } from "./create-reservation"
import { AdminGetReservationsParams } from "./list-reservations" import { AdminGetReservationsParams } from "./list-reservations"
import { AdminPostReservationsReq } from "./create-reservation"
import { AdminPostReservationsReservationReq } from "./update-reservation" import { AdminPostReservationsReservationReq } from "./update-reservation"
import { ReservationItemDTO } from "@medusajs/types"
import { Router } from "express"
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
const route = Router() const route = Router()
@@ -102,6 +103,7 @@ export const defaultReservationFields = [
"inventory_item_id", "inventory_item_id",
"quantity", "quantity",
"line_item_id", "line_item_id",
"description",
"metadata", "metadata",
"created_at", "created_at",
"updated_at", "updated_at",