feat(dashboard): Draft orders create (#6680)
**What** - Adds Create draft order form - Updates draft order details page to also display "custom" items. **Note** - Currently, the form is missing a way to input a discount code. Need to rethink this a bit, as the we can't implement the design in Figma. - The current design is missing a way to select from a customers existing shipping addresses, we should add that to keep the features we have today. - This PR uses `useInfiniteQuery` which does not work on our staging (due to duplicate dependencies as a result of building straight from the monorepo), so you will need to test locally.
This commit is contained in:
@@ -180,7 +180,7 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
const results = isSearchControlled ? options : matches
|
||||
|
||||
return (
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Root modal open={open} onOpenChange={setOpen}>
|
||||
<PrimitiveComboboxProvider
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
@@ -199,7 +199,7 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||
"has-[:invalid]:shadow-borders-error",
|
||||
"has-[:invalid]:shadow-borders-error has-[[aria-invalid=true]]:shadow-borders-error",
|
||||
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||
{
|
||||
"pl-0.5": hasValue && isArrayValue,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Tooltip } from "@medusajs/ui"
|
||||
import { PropsWithChildren, ReactNode } from "react"
|
||||
|
||||
type ConditionalTooltipProps = PropsWithChildren<{
|
||||
content: ReactNode
|
||||
showTooltip?: boolean
|
||||
}>
|
||||
|
||||
export const ConditionalTooltip = ({
|
||||
children,
|
||||
content,
|
||||
showTooltip = false,
|
||||
}: ConditionalTooltipProps) => {
|
||||
if (showTooltip) {
|
||||
return <Tooltip content={content}>{children}</Tooltip>
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./conditional-tooltip"
|
||||
+1
-68
@@ -2,6 +2,7 @@ import { Address, Cart, Order } from "@medusajs/medusa"
|
||||
import { Avatar, Copy, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { getFormattedAddress, isSameAddress } from "../../../lib/addresses"
|
||||
|
||||
const ID = ({ data }: { data: Cart | Order }) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -182,74 +183,6 @@ export const CustomerInfo = Object.assign(
|
||||
}
|
||||
)
|
||||
|
||||
const isSameAddress = (a: Address | null, b: Address | null) => {
|
||||
if (!a || !b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
a.first_name === b.first_name &&
|
||||
a.last_name === b.last_name &&
|
||||
a.address_1 === b.address_1 &&
|
||||
a.address_2 === b.address_2 &&
|
||||
a.city === b.city &&
|
||||
a.postal_code === b.postal_code &&
|
||||
a.province === b.province &&
|
||||
a.country_code === b.country_code
|
||||
)
|
||||
}
|
||||
|
||||
const getFormattedAddress = ({ address }: { address: Address }) => {
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
company,
|
||||
address_1,
|
||||
address_2,
|
||||
city,
|
||||
postal_code,
|
||||
province,
|
||||
country,
|
||||
country_code,
|
||||
} = address
|
||||
|
||||
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||
|
||||
const formattedAddress = []
|
||||
|
||||
if (name) {
|
||||
formattedAddress.push(name)
|
||||
}
|
||||
|
||||
if (company) {
|
||||
formattedAddress.push(company)
|
||||
}
|
||||
|
||||
if (address_1) {
|
||||
formattedAddress.push(address_1)
|
||||
}
|
||||
|
||||
if (address_2) {
|
||||
formattedAddress.push(address_2)
|
||||
}
|
||||
|
||||
const cityProvincePostal = [city, province, postal_code]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
if (cityProvincePostal) {
|
||||
formattedAddress.push(cityProvincePostal)
|
||||
}
|
||||
|
||||
if (country) {
|
||||
formattedAddress.push(country.display_name)
|
||||
} else if (country_code) {
|
||||
formattedAddress.push(country_code.toUpperCase())
|
||||
}
|
||||
|
||||
return formattedAddress
|
||||
}
|
||||
|
||||
const getCartOrOrderCustomer = (obj: Cart | Order) => {
|
||||
const { first_name: sFirstName, last_name: sLastName } =
|
||||
obj.shipping_address || {}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { ComponentPropsWithoutRef } from "react"
|
||||
|
||||
interface DividerProps
|
||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||
orientation?: "horizontal" | "vertical"
|
||||
variant?: "dashed" | "solid"
|
||||
}
|
||||
|
||||
export const Divider = ({
|
||||
orientation = "horizontal",
|
||||
variant = "solid",
|
||||
className,
|
||||
...props
|
||||
}: DividerProps) => {
|
||||
return (
|
||||
<div
|
||||
aria-orientation={orientation}
|
||||
role="separator"
|
||||
className={clx(
|
||||
{
|
||||
"w-full border-t": orientation === "horizontal",
|
||||
"h-full border-l": orientation === "vertical",
|
||||
"border-dashed": variant === "dashed",
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./divider"
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Photo } from "@medusajs/icons";
|
||||
import { Photo } from "@medusajs/icons"
|
||||
|
||||
type ThumbnailProps = {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
};
|
||||
src?: string | null
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export const Thumbnail = ({ src, alt }: ThumbnailProps) => {
|
||||
return (
|
||||
@@ -15,8 +15,8 @@ export const Thumbnail = ({ src, alt }: ThumbnailProps) => {
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
) : (
|
||||
<Photo />
|
||||
<Photo className="text-ui-fg-subtle" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { Button, clx } from "@medusajs/ui"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import * as Dialog from "@radix-ui/react-dialog"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import FocusLock from "react-focus-lock"
|
||||
|
||||
import { useMediaQuery } from "../../../hooks/use-media-query"
|
||||
|
||||
type SplitViewContextValue = {
|
||||
open: boolean
|
||||
@@ -34,136 +30,66 @@ type SplitViewProps = PropsWithChildren<{
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}>
|
||||
|
||||
const Root = ({
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: SplitViewProps) => {
|
||||
const Root = ({ open, onOpenChange, children }: SplitViewProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false)
|
||||
|
||||
const open = isControlled ? controlledOpen : uncontrolledOpen
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (!isControlled) {
|
||||
setUncontrolledOpen(newOpen)
|
||||
}
|
||||
|
||||
if (onOpenChange) {
|
||||
onOpenChange(newOpen)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SplitViewContext.Provider value={{ open, onOpenChange: handleOpenChange }}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex size-full overflow-hidden"
|
||||
>
|
||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
<div ref={containerRef} className="relative size-full overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</SplitViewContext.Provider>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const Content = ({ children }: PropsWithChildren) => {
|
||||
const { open, onOpenChange } = useSplitViewContext()
|
||||
const isLargeScreenSize = useMediaQuery("(min-width: 1024px)")
|
||||
|
||||
const contentWidth = !isLargeScreenSize ? "100%" : open ? "50%" : "100%"
|
||||
|
||||
const Content = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<"div">) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ width: "100%" }}
|
||||
animate={{ width: contentWidth }}
|
||||
transition={isLargeScreenSize ? { duration: 0.3 } : undefined}
|
||||
className="relative h-full overflow-y-auto"
|
||||
<div
|
||||
className={clx("relative h-full overflow-y-auto", className)}
|
||||
{...props}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
key="overlay"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.6 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="bg-ui-bg-base absolute inset-0 z-[1000] h-full cursor-pointer"
|
||||
tabIndex={-1}
|
||||
onClick={() => onOpenChange(false)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MotionFocusLock = motion(FocusLock)
|
||||
|
||||
const Drawer = ({ children }: PropsWithChildren) => {
|
||||
const { open } = useSplitViewContext()
|
||||
const isLargeScreenSize = useMediaQuery("(min-width: 1024px)")
|
||||
|
||||
return (
|
||||
<AnimatePresence mode={isLargeScreenSize ? "popLayout" : undefined}>
|
||||
{open && (
|
||||
<MotionFocusLock
|
||||
key="drawer"
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={clx(
|
||||
"bg-ui-bg-base absolute right-0 top-0 z-[9999] flex h-full w-4/5 overflow-hidden border-l lg:static lg:z-auto lg:w-1/2"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</MotionFocusLock>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div>
|
||||
<Dialog.Overlay
|
||||
className={clx(
|
||||
"bg-ui-bg-base absolute inset-0 opacity-40",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
)}
|
||||
/>
|
||||
<Dialog.Content
|
||||
className={clx(
|
||||
"bg-ui-bg-base border-ui-border-base absolute inset-y-0 right-0 flex w-full max-w-[calc(100%-128px)] flex-1 flex-col border-l focus:outline-none md:max-w-[80%] lg:max-w-[50%]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-1/2 data-[state=open]:slide-in-from-right-1/2 duration-200"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Close = ({
|
||||
variant = "secondary",
|
||||
size = "small",
|
||||
onClick,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof Button>) => {
|
||||
const { onOpenChange } = useSplitViewContext()
|
||||
const handleClick = onClick ?? (() => onOpenChange(false))
|
||||
|
||||
return (
|
||||
<Button size={size} variant={variant} onClick={handleClick} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const Open = ({
|
||||
variant = "secondary",
|
||||
size = "small",
|
||||
onClick,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof Button>) => {
|
||||
const { onOpenChange } = useSplitViewContext()
|
||||
const handleClick = onClick ?? (() => onOpenChange(true))
|
||||
|
||||
return (
|
||||
<Button
|
||||
id="split-view-open"
|
||||
size={size}
|
||||
variant={variant}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
<Dialog.Close asChild>
|
||||
<Button size={size} variant={variant} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,5 +100,4 @@ export const SplitView = Object.assign(Root, {
|
||||
Content,
|
||||
Drawer,
|
||||
Close,
|
||||
Open,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user