feat(admin-ui,medusa): Reservations management (#4081)

* add location filtering to list-location levels

* cleanup

* add location filtering to list-location levels

* cleanup

* Initial work on route,table,new reservation form

* generated types

* add block

* udpate clients

* initial create reservation

* update actionables for reservation table

* update edit-allocation modal

* misc naming updates

* update reservations table

* add expand capabilities for list-reservations

* expand fields and show columns

* update oas

* make remove item work in focus modal

* add yarn lock

* add integration test

* Fix display when label doesn't match search term

* remove unused file

* Update packages/admin-ui/ui/src/components/templates/reservations-table/components/reservation-form/index.tsx

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>

* Update packages/admin-ui/ui/src/domain/orders/details/allocations/edit-allocation-modal.tsx

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>

* Update packages/admin-ui/ui/src/components/templates/reservations-table/new/index.tsx

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>

* initial changes

* add changeset

* update font size

* cleanup reservations table + select

* add decorated inventory item type

* use type

* feedback changes

* Update packages/admin-ui/ui/src/components/molecules/item-search/index.tsx

Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>

* decorate response for list inventory item to include total quantities

* update decorated properties

* decorate type

* adrien feedback

* Update packages/generated/client-types/src/lib/models/DecoratedInventoryItemDTO.ts

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>

* update join-utils

* fix caching

---------

Co-authored-by: Rares Capilnar <rares.capilnar@gmail.com>
Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
Philip Korsholm
2023-05-23 05:24:28 +02:00
committed by GitHub
parent 87444488b5
commit 4f3c8f5d70
35 changed files with 1717 additions and 135 deletions

View File

@@ -0,0 +1,110 @@
import {
AdminGetInventoryItemsParams,
DecoratedInventoryItemDTO,
} from "@medusajs/medusa"
import { ControlProps, OptionProps, SingleValue } from "react-select"
import Control from "../select/next-select/components/control"
import { NextSelect } from "../select/next-select"
import SearchIcon from "../../fundamentals/icons/search-icon"
import { useAdminInventoryItems } from "medusa-react"
import { useDebounce } from "../../../hooks/use-debounce"
import { useState } from "react"
type Props = {
onItemSelect: (item: itemType) => void
clearOnSelect?: boolean
filters?: AdminGetInventoryItemsParams
}
type ItemOption = {
label: string | undefined
value: string | undefined
inventoryItem: DecoratedInventoryItemDTO
}
const ItemSearch = ({ onItemSelect, clearOnSelect, filters = {} }: Props) => {
const [itemSearchTerm, setItemSearchTerm] = useState<string | undefined>()
const debouncedItemSearchTerm = useDebounce(itemSearchTerm, 500)
const queryEnabled = !!debouncedItemSearchTerm?.length
const { isLoading, inventory_items } = useAdminInventoryItems(
{
q: debouncedItemSearchTerm,
...filters,
},
{ enabled: queryEnabled }
)
const onChange = (item: SingleValue<ItemOption>) => {
if (item) {
onItemSelect(item.inventoryItem)
}
}
const options = inventory_items?.map(
(inventoryItem: DecoratedInventoryItemDTO) => ({
label:
inventoryItem.title ||
inventoryItem.variants?.[0]?.product?.title ||
inventoryItem.sku,
value: inventoryItem.id,
inventoryItem,
})
) as ItemOption[]
const filterOptions = () => true
return (
<div>
<NextSelect
isMulti={false}
components={{ Option: ProductOption, Control: SearchControl }}
onInputChange={setItemSearchTerm}
options={options}
placeholder="Choose an item"
isSearchable={true}
noOptionsMessage={() => "No items found"}
openMenuOnClick={!!inventory_items?.length}
onChange={onChange}
value={null}
isLoading={queryEnabled && isLoading}
filterOption={filterOptions} // TODO: Remove this when we can q for inventory item titles
/>
</div>
)
}
const ProductOption = ({ innerProps, data }: OptionProps<ItemOption>) => {
return (
<div
{...innerProps}
className="text-small grid w-full cursor-pointer grid-cols-2 place-content-between px-4 py-2 transition-all hover:bg-gray-50"
>
<div>
<p>{data.label}</p>
<p className="text-grey-50">{data.inventoryItem.sku}</p>
</div>
<div className="text-right">
<p className="text-grey-50">{`${data.inventoryItem.stocked_quantity} stock`}</p>
<p className="text-grey-50">{`${
data.inventoryItem.stocked_quantity -
data.inventoryItem.reserved_quantity
} available`}</p>
</div>
</div>
)
}
const SearchControl = ({ children, ...props }: ControlProps<ItemOption>) => (
<Control {...props}>
<span className="mr-4">
<SearchIcon size={16} className="text-grey-50" />
</span>
{children}
</Control>
)
export default ItemSearch

View File

@@ -0,0 +1,45 @@
import { useEffect, useMemo } from "react"
import { useAdminStockLocations } from "medusa-react"
import { NextSelect } from "../select/next-select"
const LocationDropdown = ({
selectedLocation,
onChange,
}: {
selectedLocation?: string
onChange: (id: string) => void
}) => {
const { stock_locations: locations, isLoading } = useAdminStockLocations()
useEffect(() => {
if (!selectedLocation && !isLoading && locations?.length) {
onChange(locations[0].id)
}
}, [isLoading, locations, onChange, selectedLocation])
const selectedLocObj = useMemo(() => {
if (!isLoading && locations) {
return locations.find((l) => l.id === selectedLocation) ?? locations[0]
}
}, [selectedLocation, locations, isLoading])
if (isLoading || !locations || !selectedLocObj) {
return null
}
return (
<NextSelect
isMulti={false}
onChange={(loc) => {
onChange(loc!.value)
}}
options={locations.map((l) => ({
label: l.name,
value: l.id,
}))}
value={{ value: selectedLocObj.id, label: selectedLocObj.name }}
/>
)
}
export default LocationDropdown

View File

@@ -1,5 +1,6 @@
import { PropsWithChildren } from "react"
import { AnimatePresence, motion } from "framer-motion"
import * as Portal from "@radix-ui/react-portal"
const MODAL_WIDTH = 560
@@ -14,46 +15,48 @@ type SideModalProps = PropsWithChildren<{
function SideModal(props: SideModalProps) {
const { isVisible, children, close } = props
return (
<AnimatePresence>
{isVisible && (
<>
<motion.div
onClick={close}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeInOut" }}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 99,
background: "rgba(0,0,0,.3)",
}}
></motion.div>
<motion.div
transition={{ ease: "easeInOut" }}
initial={{ right: -MODAL_WIDTH }}
style={{
position: "fixed",
height: "100%",
width: MODAL_WIDTH,
background: "white",
right: 0,
top: 0,
zIndex: 200,
}}
className="overflow-hidden rounded border"
animate={{ right: 0 }}
exit={{ right: -MODAL_WIDTH }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
<Portal.Root>
<AnimatePresence>
{isVisible && (
<>
<motion.div
onClick={close}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ ease: "easeInOut" }}
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 99,
background: "rgba(0,0,0,.3)",
}}
></motion.div>
<motion.div
transition={{ ease: "easeInOut" }}
initial={{ right: -MODAL_WIDTH }}
style={{
position: "fixed",
height: "100%",
width: MODAL_WIDTH,
background: "white",
right: 0,
top: 0,
zIndex: 200,
}}
className="overflow-hidden rounded border"
animate={{ right: 0 }}
exit={{ right: -MODAL_WIDTH }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
</Portal.Root>
)
}

View File

@@ -1,15 +1,16 @@
import type { GroupBase, Props, SelectInstance } from "react-select"
import {
forwardRef,
MutableRefObject,
ReactElement,
RefAttributes,
forwardRef,
useContext,
useRef,
} from "react"
import type { GroupBase, Props, SelectInstance } from "react-select"
import ReactSelect from "react-select"
import { ModalContext } from "../../../modal"
import { AdjacentContainer } from "../components"
import { ModalContext } from "../../../modal"
import ReactSelect from "react-select"
import { useSelectProps } from "../use-select-props"
export type SelectComponent = <

View File

@@ -1,7 +1,7 @@
import isEqual from "lodash/isEqual"
import { useEffect, useState } from "react"
import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"
import Components from "./components"
import BaseComponents from "./components"
import { formatOptionLabel, hasLabel } from "./utils"
export const useSelectProps = <
@@ -64,7 +64,7 @@ export const useSelectProps = <
return {
label,
components: Components,
components: { ...BaseComponents, ...components },
styles: {
menuPortal: (base) => ({ ...base, zIndex: 60 }),
...styles,