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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = <
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user