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:
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@medusajs/ui-preset": patch
|
||||
"@medusajs/ui": patch
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/client-types": patch
|
||||
---
|
||||
|
||||
feat(ui-preset): Pull latest styles from Figma.
|
||||
fix(ui): Fix invalid state styling of Select, so it correctly shows when aria-invalid is true.
|
||||
fix(medusa): Align query params between `/admin/products/:id/variants` and `/admin/variants`.
|
||||
chore(client-types): Update `medusa` client types to reflect changes to the API.
|
||||
@@ -62,7 +62,8 @@
|
||||
"edit": "Edit",
|
||||
"download": "Download",
|
||||
"clearAll": "Clear all",
|
||||
"apply": "Apply"
|
||||
"apply": "Apply",
|
||||
"add": "Add"
|
||||
},
|
||||
"filters": {
|
||||
"date": {
|
||||
@@ -391,6 +392,34 @@
|
||||
"status": {
|
||||
"open": "Open",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"create": {
|
||||
"createDraftOrder": "Create Draft Order",
|
||||
"createDraftOrderHint": "Create a new draft order to manage the details of an order before it is placed.",
|
||||
"chooseRegionHint": "Choose region",
|
||||
"existingItemsLabel": "Existing items",
|
||||
"existingItemsHint": "Add existing products to the draft order.",
|
||||
"customItemsLabel": "Custom items",
|
||||
"customItemsHint": "Add custom items to the draft order.",
|
||||
"addExistingItemsAction": "Add existing items",
|
||||
"addCustomItemAction": "Add custom item",
|
||||
"noCustomItemsAddedLabel": "No custom items added yet",
|
||||
"noExistingItemsAddedLabel": "No existing items added yet",
|
||||
"chooseRegionTooltip": "Choose a region first",
|
||||
"useExistingCustomerLabel": "Use existing customer",
|
||||
"addShippingMethodsAction": "Add shipping methods",
|
||||
"unitPriceOverrideLabel": "Unit price override",
|
||||
"shippingOptionLabel": "Shipping option",
|
||||
"shippingOptionHint": "Choose the shipping option for the draft order.",
|
||||
"shippingPriceOverrideLabel": "Shipping price override",
|
||||
"shippingPriceOverrideHint": "Override the shipping price for the draft order.",
|
||||
"sendNotificationLabel": "Send notification",
|
||||
"sendNotificationHint": "Send a notification to the customer when the draft order is created."
|
||||
},
|
||||
"validation": {
|
||||
"requiredEmailOrCustomer": "Email or customer is required.",
|
||||
"requiredItems": "At least one item is required.",
|
||||
"invalidEmail": "Email must be a valid email address."
|
||||
}
|
||||
},
|
||||
"discounts": {
|
||||
@@ -846,6 +875,7 @@
|
||||
"orders": "Orders",
|
||||
"account": "Account",
|
||||
"total": "Total",
|
||||
"totalExclTax": "Total excl. tax",
|
||||
"subtotal": "Subtotal",
|
||||
"shipping": "Shipping",
|
||||
"tax": "Tax",
|
||||
@@ -887,6 +917,7 @@
|
||||
"thumbnail": "Thumbnail",
|
||||
"sku": "SKU",
|
||||
"managedInventory": "Managed inventory",
|
||||
"allowBackorder": "Allow backorder",
|
||||
"inStock": "In stock",
|
||||
"location": "Location",
|
||||
"quantity": "Quantity",
|
||||
@@ -896,9 +927,11 @@
|
||||
"maxSubtotal": "Max. Subtotal",
|
||||
"shippingProfile": "Shipping Profile",
|
||||
"summary": "Summary",
|
||||
"details": "Details",
|
||||
"label": "Label",
|
||||
"rate": "Rate",
|
||||
"requiresShipping": "Requires shipping",
|
||||
"unitPrice": "Unit price",
|
||||
"startDate": "Start date",
|
||||
"endDate": "End date",
|
||||
"draft": "Draft"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Address } from "@medusajs/medusa"
|
||||
|
||||
export 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
|
||||
)
|
||||
}
|
||||
|
||||
export const getFormattedAddress = ({
|
||||
address,
|
||||
}: {
|
||||
address?: Partial<Address> | null
|
||||
}) => {
|
||||
if (!address) {
|
||||
return []
|
||||
}
|
||||
|
||||
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: string[] = []
|
||||
|
||||
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
|
||||
}
|
||||
@@ -143,6 +143,13 @@ export const v1Routes: RouteObject[] = [
|
||||
path: "",
|
||||
lazy: () =>
|
||||
import("../../routes/draft-orders/draft-order-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../routes/draft-orders/draft-order-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import i18n from "i18next"
|
||||
import { z } from "zod"
|
||||
import { castNumber } from "../../../../../lib/cast-number"
|
||||
|
||||
export const AddressPayload = z.object({
|
||||
first_name: z.string().min(1),
|
||||
last_name: z.string().min(1),
|
||||
address_1: z.string().min(1),
|
||||
address_2: z.string().optional(),
|
||||
city: z.string().min(1),
|
||||
province: z.string().optional(),
|
||||
postal_code: z.string().min(1),
|
||||
country_code: z.string().min(1),
|
||||
phone: z.string().optional(),
|
||||
company: z.string().optional(),
|
||||
})
|
||||
|
||||
export const ExistingItemSchema = z
|
||||
.object({
|
||||
product_title: z.string().optional(),
|
||||
thumbnail: z.string().optional(),
|
||||
variant_title: z.string().optional(),
|
||||
variant_id: z.string().min(1),
|
||||
sku: z.string().optional(),
|
||||
quantity: z.number().min(1),
|
||||
unit_price: z.number().min(0),
|
||||
custom_unit_price: z.union([z.number(), z.string()]).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.custom_unit_price && isNaN(castNumber(data.custom_unit_price))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid custom unit price",
|
||||
path: ["custom_unit_price"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const CustomItemSchema = z
|
||||
.object({
|
||||
title: z.string().min(1),
|
||||
quantity: z.number().min(1),
|
||||
unit_price: z.union([z.number(), z.string()]),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
typeof data.unit_price === "string" &&
|
||||
isNaN(castNumber(data.unit_price))
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid unit price",
|
||||
path: ["unit_price"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const ShippingMethodSchema = z
|
||||
.object({
|
||||
option_id: z.string().min(1),
|
||||
option_title: z.string(),
|
||||
amount: z.union([z.number(), z.string()]).optional(),
|
||||
custom_amount: z.union([z.number(), z.string()]).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.custom_amount && isNaN(castNumber(data.custom_amount))) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Invalid custom amount",
|
||||
path: ["custom_amount"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const CreateDraftOrderSchema = z
|
||||
.object({
|
||||
email: z.string().optional(),
|
||||
region_id: z.string().min(1),
|
||||
customer_id: z.string().optional(),
|
||||
shipping_address: AddressPayload,
|
||||
billing_address: AddressPayload.nullable(),
|
||||
existing_items: z.array(ExistingItemSchema).optional(),
|
||||
custom_items: z.array(CustomItemSchema).optional(),
|
||||
shipping_method: ShippingMethodSchema,
|
||||
notification_order: z.boolean().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.existing_items?.length && !data.custom_items?.length) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t("draftOrders.validation.requiredItems"),
|
||||
path: ["custom_items"],
|
||||
})
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t("draftOrders.validation.requiredItems"),
|
||||
path: ["existing_items"],
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.email && !data.customer_id) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t("draftOrders.validation.requiredEmailOrCustomer"),
|
||||
path: ["customer_id"],
|
||||
})
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t("draftOrders.validation.requiredEmailOrCustomer"),
|
||||
path: ["email"],
|
||||
})
|
||||
} else if (!data.customer_id && data.email) {
|
||||
const parsedEmail = z.string().email().safeParse(data.email)
|
||||
|
||||
if (!parsedEmail.success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: i18n.t("draftOrders.validation.invalidEmail"),
|
||||
path: ["email"],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export enum View {
|
||||
EXISTING_ITEMS = "existing_items",
|
||||
CUSTOM_ITEMS = "custom_items",
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { createContext } from "react"
|
||||
import { CreateDraftOrderContextValue } from "./types"
|
||||
|
||||
export const CreateDraftOrderContext =
|
||||
createContext<CreateDraftOrderContextValue | null>(null)
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
import { Region } from "@medusajs/medusa"
|
||||
import { Checkbox, Heading, Input, Label, Select, Text } from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { Control } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
|
||||
import { ConditionalTooltip } from "../../../../../../components/common/conditional-tooltip"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { CreateDraftOrderSchema } from "../constants"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderAddressDetails = () => {
|
||||
const { t } = useTranslation()
|
||||
const { form, region, sameAsShipping, setSameAsShipping } =
|
||||
useCreateDraftOrder()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level="h2">{t("fields.address")}</Heading>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("addresses.shippingAddress.label")}
|
||||
</Text>
|
||||
<AddressFieldset
|
||||
field="shipping_address"
|
||||
region={region}
|
||||
control={form.control}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("addresses.billingAddress.label")}
|
||||
</Text>
|
||||
<Label className="flex cursor-pointer items-center gap-x-2">
|
||||
<Checkbox
|
||||
checked={sameAsShipping}
|
||||
onCheckedChange={(checked) => setSameAsShipping(checked === true)}
|
||||
/>
|
||||
{t("addresses.billingAddress.sameAsShipping")}
|
||||
</Label>
|
||||
</div>
|
||||
<Collapsible.Root open={!sameAsShipping}>
|
||||
<Collapsible.Content>
|
||||
<AddressFieldset
|
||||
field="billing_address"
|
||||
region={region}
|
||||
control={form.control}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AddressFieldset = ({
|
||||
field,
|
||||
control,
|
||||
region,
|
||||
}: {
|
||||
field: "shipping_address" | "billing_address"
|
||||
region: Region | null
|
||||
control: Control<z.infer<typeof CreateDraftOrderSchema>>
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<fieldset className="flex flex-col gap-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.first_name`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.firstName")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.last_name`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.lastName")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.company`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.company")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.phone`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.phone")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.address_1`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.address")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.address_2`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.address2")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.city`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.city")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.postal_code`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.postalCode")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.province`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.province")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={control}
|
||||
name={`${field}.country_code`}
|
||||
render={({
|
||||
field: { onChange, ref, disabled, ...field },
|
||||
fieldState: { error },
|
||||
}) => {
|
||||
return (
|
||||
<ConditionalTooltip
|
||||
showTooltip={!region}
|
||||
content={t("draftOrders.create.chooseRegionTooltip")}
|
||||
>
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle font-normal">
|
||||
{t("fields.country")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
disabled={!region || disabled}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
>
|
||||
<Select.Trigger aria-invalid={!!error} ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{region?.countries.map((c) => (
|
||||
<Select.Item key={c.iso_2} value={c.iso_2}>
|
||||
{c.display_name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
</ConditionalTooltip>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import { Checkbox, Heading, Input, Label } from "@medusajs/ui"
|
||||
import { useInfiniteQuery } from "@tanstack/react-query"
|
||||
import { debounce } from "lodash"
|
||||
import { useMedusa } from "medusa-react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { json } from "react-router-dom"
|
||||
|
||||
import { Combobox } from "../../../../../../components/common/combobox"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderCustomerDetails = () => {
|
||||
const [useExistingCustomer, setUseExistingCustomer] = useState(true)
|
||||
const [query, setQuery] = useState("")
|
||||
const [debouncedQuery, setDebouncedQuery] = useState("")
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { form, setCustomer } = useCreateDraftOrder()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedUpdate = useCallback(
|
||||
debounce((query) => setDebouncedQuery(query), 300),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
debouncedUpdate(query)
|
||||
|
||||
return () => debouncedUpdate.cancel()
|
||||
}, [query, debouncedUpdate])
|
||||
|
||||
const { client } = useMedusa()
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery(
|
||||
["customers", debouncedQuery],
|
||||
async ({ pageParam = 0 }) => {
|
||||
const res = await client.admin.customers.list({
|
||||
q: debouncedQuery,
|
||||
limit: 10,
|
||||
offset: pageParam,
|
||||
has_account: true, // Only show customers with confirmed accounts
|
||||
})
|
||||
return res
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
const moreCustomersExist =
|
||||
lastPage.count > lastPage.offset + lastPage.limit
|
||||
return moreCustomersExist ? lastPage.offset + lastPage.limit : undefined
|
||||
},
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const createLabel = (customer?: Customer) => {
|
||||
if (!customer) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const { first_name, last_name, email } = customer
|
||||
|
||||
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||
|
||||
if (name) {
|
||||
return `${name} (${email})`
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
const handleCustomerChange = (cusId: string | undefined) => {
|
||||
if (!cusId) {
|
||||
setCustomer(null)
|
||||
return
|
||||
}
|
||||
|
||||
const customer = data?.pages
|
||||
.flatMap((page) => page.customers)
|
||||
.find((c) => c.id === cusId)
|
||||
|
||||
if (!customer) {
|
||||
throw json({ message: "Customer not found" }, 400)
|
||||
}
|
||||
|
||||
form.setValue("email", customer.email, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
setCustomer(customer)
|
||||
}
|
||||
|
||||
const options =
|
||||
data?.pages.flatMap((page) =>
|
||||
page.customers.map((c) => ({ label: createLabel(c), value: c.id }))
|
||||
) ?? []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level="h2">{t("fields.customer")}</Heading>
|
||||
<fieldset className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{useExistingCustomer ? (
|
||||
<Form.Field
|
||||
key="customer-input"
|
||||
control={form.control}
|
||||
name="customer_id"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle !font-normal">
|
||||
{t("fields.customer")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
onChange={(val) => {
|
||||
onChange(val)
|
||||
handleCustomerChange(val)
|
||||
}}
|
||||
searchValue={query}
|
||||
onSearchValueChange={setQuery}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
options={options}
|
||||
autoComplete="false"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Form.Field
|
||||
key="email-input"
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="text-ui-fg-subtle !font-normal">
|
||||
{t("fields.email")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
<Label className="flex w-fit items-center gap-x-2">
|
||||
<Checkbox
|
||||
checked={useExistingCustomer}
|
||||
onCheckedChange={(val) => setUseExistingCustomer(!!val)}
|
||||
/>
|
||||
<span>{t("draftOrders.create.useExistingCustomerLabel")}</span>
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Divider } from "../../../../../../components/common/divider"
|
||||
import { CreateDraftOrderAddressDetails } from "./create-draft-order-address-details"
|
||||
import { CreateDraftOrderCustomerDetails } from "./create-draft-order-customer-details"
|
||||
import { CreateDraftOrderItemsDetails } from "./create-draft-order-items-details"
|
||||
import { CreateDraftOrderRegionDetails } from "./create-draft-order-region-details"
|
||||
import { CreateDraftOrderShippingMethodDetails } from "./create-draft-order-shipping-method-details"
|
||||
|
||||
export const CreateDraftOrderDetails = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("draftOrders.create.createDraftOrder")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("draftOrders.create.createDraftOrderHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<CreateDraftOrderRegionDetails />
|
||||
<Divider />
|
||||
<CreateDraftOrderCustomerDetails />
|
||||
<Divider />
|
||||
<CreateDraftOrderItemsDetails />
|
||||
<Divider />
|
||||
<CreateDraftOrderShippingMethodDetails />
|
||||
<Divider />
|
||||
<CreateDraftOrderAddressDetails />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
import { Trash } from "@medusajs/icons"
|
||||
import { Button, CurrencyInput, Input, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
||||
import { ConditionalTooltip } from "../../../../../../components/common/conditional-tooltip"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { Thumbnail } from "../../../../../../components/common/thumbnail"
|
||||
import { getStylizedAmount } from "../../../../../../lib/money-amount-helpers"
|
||||
import { View } from "../constants"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderItemsDetails = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { region, variants, custom, form, onOpenDrawer } = useCreateDraftOrder()
|
||||
const { currency_code, currency } = region || {}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<fieldset className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="existing_items"
|
||||
render={({ field: { name } }) => {
|
||||
return (
|
||||
<Form.Item className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Form.Label>
|
||||
{t("draftOrders.create.existingItemsLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("draftOrders.create.existingItemsHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
{variants.items.length > 0 ? (
|
||||
variants.items.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={item.ei_id}
|
||||
className="bg-ui-bg-component shadow-elevation-card-rest divide-y rounded-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="shadow-borders-base size-fit overflow-hidden rounded-[4px]">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
{item.product_title}
|
||||
</Text>
|
||||
{item.sku && (
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
({item.sku})
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
{item.variant_title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
{getStylizedAmount(
|
||||
item.unit_price,
|
||||
currency_code!
|
||||
)}
|
||||
</Text>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.remove"),
|
||||
onClick: () => variants.remove(index),
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="grid grid-cols-1 gap-3 p-3 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`${name}.${index}.quantity`}
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("fields.quantity")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
className="!bg-ui-bg-field-component hover:!bg-ui-bg-field-component-hover"
|
||||
type="number"
|
||||
onChange={(e) =>
|
||||
onChange(Number(e.target.value))
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`${name}.${index}.custom_unit_price`}
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t(
|
||||
"draftOrders.create.unitPriceOverrideLabel"
|
||||
)}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<CurrencyInput
|
||||
className="!bg-ui-bg-field-component hover:!bg-ui-bg-field-component-hover"
|
||||
code={currency_code!}
|
||||
symbol={currency?.symbol_native!}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex items-center justify-center px-2 py-3">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("draftOrders.create.noExistingItemsAddedLabel")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-end">
|
||||
<ConditionalTooltip
|
||||
content={t("draftOrders.create.chooseRegionTooltip")}
|
||||
showTooltip={!region}
|
||||
>
|
||||
<Button
|
||||
disabled={!region}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={() => onOpenDrawer(View.EXISTING_ITEMS)}
|
||||
>
|
||||
{t("draftOrders.create.addExistingItemsAction")}
|
||||
</Button>
|
||||
</ConditionalTooltip>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="md:grid-grid-cols-2 grid grid-cols-1 gap-3 p-3">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="custom_items"
|
||||
render={({ field: { name } }) => {
|
||||
return (
|
||||
<Form.Item className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Form.Label>
|
||||
{t("draftOrders.create.customItemsLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("draftOrders.create.customItemsHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
{custom.items.length > 0 ? (
|
||||
custom.items.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={item.ci_id}
|
||||
className="bg-ui-bg-component shadow-elevation-card-rest divide-y rounded-xl"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.remove"),
|
||||
onClick: () => custom.remove(index),
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="grid grid-cols-1 gap-3 p-3 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`${name}.${index}.quantity`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("fields.quantity")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
className="!bg-ui-bg-field-component hover:!bg-ui-bg-field-component-hover"
|
||||
type="number"
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`${name}.${index}.unit_price`}
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("fields.unitPrice")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<CurrencyInput
|
||||
className="!bg-ui-bg-field-component hover:!bg-ui-bg-field-component-hover"
|
||||
code={currency_code!}
|
||||
symbol={currency?.symbol_native!}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex items-center justify-center px-2 py-3">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("draftOrders.create.noCustomItemsAddedLabel")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-end">
|
||||
<ConditionalTooltip
|
||||
content={t("draftOrders.create.chooseRegionTooltip")}
|
||||
showTooltip={!region}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
disabled={!region}
|
||||
onClick={() => onOpenDrawer(View.CUSTOM_ITEMS)}
|
||||
>
|
||||
{t("draftOrders.create.addCustomItemAction")}
|
||||
</Button>
|
||||
</ConditionalTooltip>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { Select } from "@medusajs/ui"
|
||||
import { useAdminRegions, useMedusa } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { json } from "react-router-dom"
|
||||
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderRegionDetails = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
form,
|
||||
setRegion,
|
||||
variants: { rebase },
|
||||
} = useCreateDraftOrder()
|
||||
const { client } = useMedusa()
|
||||
|
||||
const existingItems = useWatch({
|
||||
control: form.control,
|
||||
name: "existing_items",
|
||||
})
|
||||
|
||||
const { regions, isLoading, isError, error } = useAdminRegions({
|
||||
limit: 1000,
|
||||
fields: "id,name,currency_code",
|
||||
})
|
||||
|
||||
const handleRebaseUnitPrices = async (regionId: string) => {
|
||||
if (!existingItems?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const { variants } = await client.admin.variants
|
||||
.list({
|
||||
region_id: regionId,
|
||||
id: existingItems.map((i) => i.variant_id),
|
||||
})
|
||||
.catch((_err) => {
|
||||
// Show toast with error message
|
||||
return { variants: [] }
|
||||
})
|
||||
|
||||
rebase(variants)
|
||||
}
|
||||
|
||||
const handleResetShippingDetails = () => {
|
||||
form.resetField("shipping_method")
|
||||
form.resetField("shipping_address")
|
||||
form.resetField("billing_address")
|
||||
}
|
||||
|
||||
const handleRegionChange = (regId: string) => {
|
||||
const region = regions?.find((r) => r.id === regId)
|
||||
|
||||
if (!region) {
|
||||
throw json({ message: "Region not found" }, 400)
|
||||
}
|
||||
|
||||
setRegion(region)
|
||||
}
|
||||
|
||||
const onValueChange = (fn: (...event: any[]) => void) => {
|
||||
return async (id: string) => {
|
||||
fn(id)
|
||||
await handleRebaseUnitPrices(id)
|
||||
handleResetShippingDetails()
|
||||
handleRegionChange(id)
|
||||
}
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="region_id"
|
||||
render={({
|
||||
field: { ref, onChange, disabled, ...field },
|
||||
fieldState: { error },
|
||||
}) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label className="!h2-core">{t("fields.region")}</Form.Label>
|
||||
<Form.Hint>{t("draftOrders.create.chooseRegionHint")}</Form.Hint>
|
||||
<Form.Control>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={onValueChange(onChange)}
|
||||
disabled={isLoading || disabled}
|
||||
>
|
||||
<Select.Trigger aria-invalid={!!error} ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{regions?.map((r) => (
|
||||
<Select.Item key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query"
|
||||
import debounce from "lodash/debounce"
|
||||
import { useMedusa } from "medusa-react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ShippingOption } from "@medusajs/medusa"
|
||||
import { CurrencyInput, Heading, Input } from "@medusajs/ui"
|
||||
import { json } from "react-router-dom"
|
||||
import { Combobox } from "../../../../../../components/common/combobox"
|
||||
import { ConditionalTooltip } from "../../../../../../components/common/conditional-tooltip"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { getLocaleAmount } from "../../../../../../lib/money-amount-helpers"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderShippingMethodDetails = () => {
|
||||
const [query, setQuery] = useState("")
|
||||
const [debouncedQuery, setDebouncedQuery] = useState("")
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { form, region } = useCreateDraftOrder()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedUpdate = useCallback(
|
||||
debounce((query) => setDebouncedQuery(query), 300),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
debouncedUpdate(query)
|
||||
|
||||
return () => debouncedUpdate.cancel()
|
||||
}, [query, debouncedUpdate])
|
||||
|
||||
const { client } = useMedusa()
|
||||
|
||||
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery(
|
||||
["shipping_options", region?.id, debouncedQuery],
|
||||
async ({ pageParam = 0 }) => {
|
||||
const res = await client.admin.shippingOptions.list({
|
||||
q: debouncedQuery || undefined,
|
||||
limit: 10,
|
||||
offset: pageParam,
|
||||
is_return: false,
|
||||
region_id: region?.id,
|
||||
})
|
||||
return res
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
const moreCustomersExist =
|
||||
lastPage.count > lastPage.offset + lastPage.limit
|
||||
return moreCustomersExist ? lastPage.offset + lastPage.limit : undefined
|
||||
},
|
||||
enabled: !!region?.id,
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const createLabel = (shippingOption?: ShippingOption) => {
|
||||
if (!shippingOption) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return `${shippingOption.name} - ${getLocaleAmount(
|
||||
shippingOption.amount || 0,
|
||||
region?.currency_code!
|
||||
)}`
|
||||
}
|
||||
|
||||
const options =
|
||||
data?.pages
|
||||
.flatMap((page) => page.shipping_options)
|
||||
.map((so) => ({
|
||||
label: createLabel(so),
|
||||
value: so.id,
|
||||
})) || []
|
||||
|
||||
const handleShippingMethodChange = (optionId: string | undefined) => {
|
||||
if (!optionId) {
|
||||
return
|
||||
}
|
||||
|
||||
const option = data?.pages
|
||||
.flatMap((page) => page.shipping_options)
|
||||
.find((so) => so.id === optionId)
|
||||
|
||||
if (!option) {
|
||||
throw json({ message: "Shipping option not found" }, 400)
|
||||
}
|
||||
|
||||
form.setValue("shipping_method.option_title", option.name, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
form.setValue("shipping_method.amount", option.amount || 0, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Heading level="h2">{t("fields.shipping")}</Heading>
|
||||
<fieldset className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="shipping_method.option_id"
|
||||
render={({ field: { onChange, disabled, ...field } }) => {
|
||||
return (
|
||||
<ConditionalTooltip
|
||||
showTooltip={!region}
|
||||
content={t("draftOrders.create.chooseRegionTooltip")}
|
||||
>
|
||||
<Form.Item>
|
||||
<div>
|
||||
<Form.Label>
|
||||
{t("draftOrders.create.shippingOptionLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("draftOrders.create.shippingOptionHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
onChange={(val) => {
|
||||
handleShippingMethodChange(val)
|
||||
onChange(val)
|
||||
}}
|
||||
disabled={!region || disabled}
|
||||
searchValue={query}
|
||||
onSearchValueChange={setQuery}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
options={options}
|
||||
autoComplete="false"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
</ConditionalTooltip>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="shipping_method.custom_amount"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<ConditionalTooltip
|
||||
showTooltip={!region}
|
||||
content={t("draftOrders.create.chooseRegionTooltip")}
|
||||
>
|
||||
<Form.Item>
|
||||
<div>
|
||||
<Form.Label optional>
|
||||
{t("draftOrders.create.shippingPriceOverrideLabel")}
|
||||
</Form.Label>
|
||||
<Form.Hint>
|
||||
{t("draftOrders.create.shippingPriceOverrideHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.Control>
|
||||
{region ? (
|
||||
<CurrencyInput
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
code={region.currency.code}
|
||||
symbol={region.currency.symbol_native}
|
||||
/>
|
||||
) : (
|
||||
<Input disabled />
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
</ConditionalTooltip>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-draft-order-details"
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { Button, CurrencyInput, Hint, Input, Label } from "@medusajs/ui"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
import { SplitView } from "../../../../../../components/layout/split-view"
|
||||
import { castNumber } from "../../../../../../lib/cast-number"
|
||||
import { CustomItemSchema } from "../constants"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
import { CustomItem } from "../types"
|
||||
|
||||
export const AddCustomItemDrawer = () => {
|
||||
const { region, custom } = useCreateDraftOrder()
|
||||
const { currency } = region || {}
|
||||
|
||||
const currencyCode = currency?.code || ""
|
||||
const nativeSymbol = currency?.symbol_native || ""
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [item, setItem] = useState<Partial<CustomItem>>({
|
||||
title: "",
|
||||
quantity: 1,
|
||||
unit_price: undefined,
|
||||
})
|
||||
const [errors, setErrors] = useState<z.ZodError<
|
||||
z.infer<typeof CustomItemSchema>
|
||||
> | null>(null)
|
||||
|
||||
const handleSave = () => {
|
||||
const parsed = CustomItemSchema.safeParse(item)
|
||||
|
||||
if (!parsed.success) {
|
||||
setErrors(parsed.error)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
custom.update(parsed.data)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<div className="size-full flex-1 overflow-auto px-6 py-4">
|
||||
<div className="flex flex-col gap-y-4 [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2">
|
||||
<div>
|
||||
<Label weight="plus" size="small">
|
||||
{t("fields.title")}
|
||||
</Label>
|
||||
<Input
|
||||
value={item.title}
|
||||
onChange={(e) =>
|
||||
setItem((prev) => ({ ...prev, title: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<ErrorMessage errors={errors} field="title" />
|
||||
</div>
|
||||
<div>
|
||||
<Label weight="plus" size="small">
|
||||
{t("fields.quantity")}
|
||||
</Label>
|
||||
<Input
|
||||
value={item.quantity}
|
||||
type="number"
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const val = castNumber(e.target.value)
|
||||
setItem((prev) => ({ ...prev, quantity: val }))
|
||||
}}
|
||||
/>
|
||||
<ErrorMessage errors={errors} field="quantity" />
|
||||
</div>
|
||||
<div>
|
||||
<Label weight="plus" size="small">
|
||||
{t("fields.unitPrice")}
|
||||
</Label>
|
||||
<CurrencyInput
|
||||
code={currencyCode}
|
||||
symbol={nativeSymbol}
|
||||
value={item.unit_price}
|
||||
onValueChange={(value) => {
|
||||
setItem((prev) => ({ ...prev, unit_price: value }))
|
||||
}}
|
||||
/>
|
||||
<ErrorMessage errors={errors} field="unit_price" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||
<SplitView.Close type="button" asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</SplitView.Close>
|
||||
<Button size="small" type="button" onClick={handleSave}>
|
||||
{t("actions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getFirstErrorMessage = (
|
||||
errors: z.ZodError<z.infer<typeof CustomItemSchema>> | null,
|
||||
field: string
|
||||
): string | null => {
|
||||
if (!errors) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fieldError = errors.errors.find((error) => error.path[0] === field)
|
||||
|
||||
return fieldError ? fieldError.message : null
|
||||
}
|
||||
|
||||
const ErrorMessage = ({
|
||||
field,
|
||||
errors,
|
||||
}: {
|
||||
errors: z.ZodError<z.infer<typeof CustomItemSchema>> | null
|
||||
field: string
|
||||
}) => {
|
||||
const message = getFirstErrorMessage(errors, field)
|
||||
|
||||
return message ? <Hint variant="error">{message}</Hint> : null
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { PricedVariant } from "@medusajs/medusa/dist/types/pricing"
|
||||
import { Button, Checkbox } from "@medusajs/ui"
|
||||
import {
|
||||
OnChangeFn,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminVariants } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { SplitView } from "../../../../../../components/layout/split-view"
|
||||
import { DataTable } from "../../../../../../components/table/data-table"
|
||||
import { MoneyAmountCell } from "../../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { PlaceholderCell } from "../../../../../../components/table/table-cells/common/placeholder-cell"
|
||||
import { ProductCell } from "../../../../../../components/table/table-cells/product/product-cell"
|
||||
import { useDataTable } from "../../../../../../hooks/use-data-table"
|
||||
import { useProductVariantTableFilters } from "../../../../../products/product-detail/components/product-variant-section/use-variant-table-filters"
|
||||
import { useProductVariantTableQuery } from "../../../../../products/product-detail/components/product-variant-section/use-variant-table-query"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
import { ExistingItem } from "../types"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PREFIX = "av"
|
||||
|
||||
const initRowState = (items: ExistingItem[]): RowSelectionState => {
|
||||
return items.reduce((acc, curr) => {
|
||||
acc[curr.variant_id] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState)
|
||||
}
|
||||
|
||||
export const AddVariantDrawer = () => {
|
||||
const { region, customer, variants: existing } = useCreateDraftOrder()
|
||||
const { currency_code } = region || {}
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>(
|
||||
initRowState(existing.items)
|
||||
)
|
||||
const [intermediate, setIntermediate] = useState<ExistingItem[]>(
|
||||
existing.items
|
||||
)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { searchParams, raw } = useProductVariantTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
const { variants, count, isLoading, isError, error } = useAdminVariants({
|
||||
region_id: region?.id,
|
||||
customer_id: customer?.id,
|
||||
...searchParams,
|
||||
})
|
||||
|
||||
const updater: OnChangeFn<RowSelectionState> = (fn) => {
|
||||
const newState: RowSelectionState =
|
||||
typeof fn === "function" ? fn(rowSelection) : fn
|
||||
|
||||
const diff = Object.keys(newState).filter(
|
||||
(k) => newState[k] !== rowSelection[k]
|
||||
)
|
||||
|
||||
const addedVariants = variants?.filter((p) => diff.includes(p.id!)) ?? []
|
||||
|
||||
const newVariants: ExistingItem[] = addedVariants.map((v) => ({
|
||||
variant_id: v.id!,
|
||||
variant_title: v.title!,
|
||||
unit_price: v.original_price!,
|
||||
sku: v.sku ?? undefined,
|
||||
product_title: v.product?.title,
|
||||
thumbnail: v.product?.thumbnail ?? undefined,
|
||||
quantity: 1,
|
||||
}))
|
||||
|
||||
setIntermediate((prev) => {
|
||||
const filteredPrev = prev.filter((p) =>
|
||||
Object.keys(newState).includes(p.variant_id)
|
||||
)
|
||||
|
||||
const update = Array.from(new Set([...filteredPrev, ...newVariants]))
|
||||
return update
|
||||
})
|
||||
|
||||
setRowSelection(newState)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
existing.update(intermediate)
|
||||
}
|
||||
|
||||
const columns = useVariantTableColumns()
|
||||
const filters = useProductVariantTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: variants || [],
|
||||
columns,
|
||||
count,
|
||||
pageSize: PAGE_SIZE,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
getRowId: (row) => row.id!,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater,
|
||||
},
|
||||
meta: {
|
||||
currencyCode: currency_code,
|
||||
},
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col overflow-hidden">
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
count={count}
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
pageSize={PAGE_SIZE}
|
||||
filters={filters}
|
||||
orderBy={["title", "created_at", "updated_at"]}
|
||||
pagination
|
||||
search
|
||||
layout="fill"
|
||||
prefix={PREFIX}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||
<SplitView.Close type="button" asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</SplitView.Close>
|
||||
<Button size="small" type="button" onClick={handleSave}>
|
||||
{t("actions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<PricedVariant>()
|
||||
|
||||
const useVariantTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("product", {
|
||||
header: t("fields.product"),
|
||||
cell: ({ getValue }) => {
|
||||
const product = getValue()
|
||||
|
||||
if (!product) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return <ProductCell product={product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("options", {
|
||||
header: t("fields.variant"),
|
||||
cell: ({ getValue }) => {
|
||||
const options = getValue()
|
||||
|
||||
const displayValue = options?.map((o) => o.value).join(" · ")
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">{displayValue}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sku", {
|
||||
header: t("fields.sku"),
|
||||
cell: ({ getValue }) => {
|
||||
const sku = getValue()
|
||||
|
||||
return (
|
||||
<div className="flex size-full items-center overflow-hidden">
|
||||
<span className="truncate">{sku ?? "-"}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("original_price", {
|
||||
header: () => (
|
||||
<div className="flex size-full items-center justify-end overflow-hidden text-right">
|
||||
<span className="truncate">{t("fields.unitPrice")}</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ getValue, table }) => {
|
||||
const price = getValue()
|
||||
const { currencyCode } = table.options.meta as {
|
||||
currencyCode?: string
|
||||
}
|
||||
|
||||
if (!price || !currencyCode) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<MoneyAmountCell
|
||||
align="right"
|
||||
amount={price}
|
||||
currencyCode={currencyCode}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { View } from "../constants"
|
||||
import { AddCustomItemDrawer } from "./add-custom-item-drawer"
|
||||
import { AddVariantDrawer } from "./add-variant-drawer"
|
||||
|
||||
export const CreateDraftOrderDrawer = ({ view }: { view: View | null }) => {
|
||||
switch (view) {
|
||||
case View.EXISTING_ITEMS:
|
||||
return <AddVariantDrawer />
|
||||
case View.CUSTOM_ITEMS:
|
||||
return <AddCustomItemDrawer />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-draft-order-drawer"
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Customer, Region } from "@medusajs/medusa"
|
||||
import { Button, ProgressTabs } from "@medusajs/ui"
|
||||
import { useAdminCreateDraftOrder } from "medusa-react"
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import { z } from "zod"
|
||||
|
||||
import { SplitView } from "../../../../../components/layout/split-view"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
import { PricedVariant } from "@medusajs/medusa/dist/types/pricing"
|
||||
import { castNumber } from "../../../../../lib/cast-number"
|
||||
import { CreateDraftOrderSchema, View } from "./constants"
|
||||
import { CreateDraftOrderContext } from "./context"
|
||||
import { CreateDraftOrderDetails } from "./create-draft-order-details"
|
||||
import { CreateDraftOrderDrawer } from "./create-draft-order-drawer"
|
||||
import { CreateDraftOrderSummary } from "./create-draft-order-summary"
|
||||
import { CustomItem, ExistingItem } from "./types"
|
||||
|
||||
enum Tab {
|
||||
DETAILS = "details",
|
||||
SUMMARY = "summary",
|
||||
}
|
||||
|
||||
export const CreateDraftOrderForm = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const [view, setView] = useState<View | null>(null)
|
||||
const [tab, setTab] = useState<Tab>(Tab.DETAILS)
|
||||
const [detailsValidated, setDetailsValidated] = useState(false)
|
||||
|
||||
const [region, setRegion] = useState<Region | null>(null)
|
||||
const [customer, setCustomer] = useState<Customer | null>(null)
|
||||
|
||||
const [sameAsShipping, setSameAsShipping] = useState(true)
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [, setSearchParams] = useSearchParams()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof CreateDraftOrderSchema>>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
region_id: "",
|
||||
shipping_method: {
|
||||
amount: "",
|
||||
custom_amount: "",
|
||||
option_id: "",
|
||||
option_title: "",
|
||||
},
|
||||
shipping_address: {
|
||||
address_1: "",
|
||||
address_2: "",
|
||||
city: "",
|
||||
company: "",
|
||||
country_code: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
phone: "",
|
||||
postal_code: "",
|
||||
province: "",
|
||||
},
|
||||
billing_address: null,
|
||||
existing_items: [],
|
||||
custom_items: [],
|
||||
customer_id: "",
|
||||
notification_order: true,
|
||||
},
|
||||
resolver: zodResolver(CreateDraftOrderSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
clearErrors,
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const {
|
||||
append: createCustomItem,
|
||||
remove: deleteCustomItem,
|
||||
fields: customItems,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: "custom_items",
|
||||
keyName: "ci_id",
|
||||
})
|
||||
|
||||
const {
|
||||
append: createExistingItem,
|
||||
remove: deleteExistingItem,
|
||||
update: updateExistingItem,
|
||||
fields: existingItems,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: "existing_items",
|
||||
keyName: "ei_id",
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateDraftOrder()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
let {
|
||||
shipping_address,
|
||||
billing_address,
|
||||
existing_items,
|
||||
custom_items,
|
||||
shipping_method,
|
||||
email,
|
||||
notification_order,
|
||||
...rest
|
||||
} = values
|
||||
|
||||
const emailValue = email || customer?.email
|
||||
|
||||
if (!emailValue) {
|
||||
form.setError("email", {
|
||||
type: "manual",
|
||||
message: "Email is required",
|
||||
})
|
||||
|
||||
form.setError("customer_id", {
|
||||
type: "manual",
|
||||
message: "Customer is required",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!billing_address) {
|
||||
billing_address = shipping_address
|
||||
}
|
||||
|
||||
const preparedExistingItems =
|
||||
existing_items?.map((item) => {
|
||||
const { custom_unit_price, variant_id, quantity } = item
|
||||
|
||||
const customUnitPriceCast = Number(custom_unit_price)
|
||||
const customUnitPriceValue = !isNaN(customUnitPriceCast)
|
||||
? getDbAmount(customUnitPriceCast, region?.currency_code!)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
variant_id,
|
||||
quantity,
|
||||
unit_price: customUnitPriceValue,
|
||||
}
|
||||
}) || []
|
||||
|
||||
const preparedCustomItems =
|
||||
custom_items?.map((item) => {
|
||||
const { unit_price, quantity, title } = item
|
||||
|
||||
const unitPriceCast = castNumber(unit_price)
|
||||
const unitPriceValue = !isNaN(unitPriceCast)
|
||||
? getDbAmount(unitPriceCast, region?.currency_code!)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
title,
|
||||
quantity,
|
||||
unit_price: unitPriceValue,
|
||||
}
|
||||
}) || []
|
||||
|
||||
const items = [...preparedExistingItems, ...preparedCustomItems]
|
||||
|
||||
const preparedShippingMethods = [
|
||||
{
|
||||
option_id: shipping_method.option_id,
|
||||
price: shipping_method.custom_amount
|
||||
? getDbAmount(
|
||||
castNumber(shipping_method.custom_amount),
|
||||
region?.currency_code!
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
...rest,
|
||||
email: emailValue,
|
||||
shipping_address,
|
||||
billing_address,
|
||||
items: items,
|
||||
shipping_methods: preparedShippingMethods,
|
||||
no_notification_order: !notification_order,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ draft_order }) => {
|
||||
handleSuccess(`../${draft_order.id}`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const clearItemErrors = useCallback(() => {
|
||||
clearErrors("custom_items")
|
||||
clearErrors("existing_items")
|
||||
}, [clearErrors])
|
||||
|
||||
const handleUpdateExistingItems = useCallback(
|
||||
(items: ExistingItem[]) => {
|
||||
handleUpdateEntities(
|
||||
items,
|
||||
existingItems,
|
||||
deleteExistingItem,
|
||||
createExistingItem,
|
||||
"variant_id"
|
||||
)
|
||||
|
||||
setView(null)
|
||||
setOpen(false)
|
||||
clearItemErrors()
|
||||
},
|
||||
[createExistingItem, deleteExistingItem, existingItems, clearItemErrors]
|
||||
)
|
||||
|
||||
const handleCreateCustomItem = useCallback(
|
||||
(item: CustomItem) => {
|
||||
createCustomItem(item)
|
||||
|
||||
setView(null)
|
||||
setOpen(false)
|
||||
clearItemErrors()
|
||||
},
|
||||
[createCustomItem, clearItemErrors]
|
||||
)
|
||||
|
||||
const handleOpenDrawer = (view: View) => {
|
||||
setView(view)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setView(null)
|
||||
setSearchParams(
|
||||
{},
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const handleContinue = form.handleSubmit(() => {
|
||||
setTab(Tab.SUMMARY)
|
||||
setDetailsValidated(true)
|
||||
})
|
||||
|
||||
const handleRebaseUnitPrices = useCallback(
|
||||
(variants: PricedVariant[]) => {
|
||||
if (!variants.length) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const variant of variants) {
|
||||
const index = existingItems.findIndex(
|
||||
(item) => item.variant_id === variant.id
|
||||
)
|
||||
|
||||
if (index === -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
updateExistingItem(index, {
|
||||
...existingItems[index],
|
||||
unit_price: variant.original_price!,
|
||||
})
|
||||
}
|
||||
},
|
||||
[updateExistingItem, existingItems]
|
||||
)
|
||||
|
||||
const handleTabChange = (tab: Tab) => {
|
||||
switch (tab) {
|
||||
case Tab.DETAILS:
|
||||
setDetailsValidated(false)
|
||||
setTab(tab)
|
||||
break
|
||||
case Tab.SUMMARY:
|
||||
handleContinue()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const detailsProgress = useMemo(() => {
|
||||
if (detailsValidated) {
|
||||
return "completed"
|
||||
}
|
||||
|
||||
if (isDirty) {
|
||||
return "in-progress"
|
||||
}
|
||||
|
||||
return "not-started"
|
||||
}, [detailsValidated, isDirty])
|
||||
|
||||
return (
|
||||
<CreateDraftOrderContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
custom: {
|
||||
items: customItems,
|
||||
remove: deleteCustomItem,
|
||||
update: handleCreateCustomItem,
|
||||
},
|
||||
variants: {
|
||||
items: existingItems,
|
||||
remove: deleteExistingItem,
|
||||
update: handleUpdateExistingItems,
|
||||
rebase: handleRebaseUnitPrices,
|
||||
},
|
||||
form,
|
||||
region,
|
||||
setRegion,
|
||||
customer,
|
||||
setCustomer,
|
||||
sameAsShipping,
|
||||
setSameAsShipping,
|
||||
onOpenDrawer: handleOpenDrawer,
|
||||
}),
|
||||
[
|
||||
customItems,
|
||||
deleteCustomItem,
|
||||
handleCreateCustomItem,
|
||||
existingItems,
|
||||
deleteExistingItem,
|
||||
handleUpdateExistingItems,
|
||||
handleRebaseUnitPrices,
|
||||
form,
|
||||
customer,
|
||||
region,
|
||||
sameAsShipping,
|
||||
]
|
||||
)}
|
||||
>
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<ProgressTabs
|
||||
value={tab}
|
||||
onValueChange={(tab) => handleTabChange(tab as Tab)}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<RouteFocusModal.Header>
|
||||
<div className="flex w-full items-center justify-between gap-x-4">
|
||||
<div className="-my-2 w-full max-w-[400px] border-l">
|
||||
<ProgressTabs.List className="grid w-full grid-cols-2">
|
||||
<ProgressTabs.Trigger
|
||||
className="w-full"
|
||||
value={Tab.DETAILS}
|
||||
status={detailsProgress}
|
||||
>
|
||||
{t("fields.details")}
|
||||
</ProgressTabs.Trigger>
|
||||
<ProgressTabs.Trigger
|
||||
className="w-full"
|
||||
value={Tab.SUMMARY}
|
||||
>
|
||||
{t("fields.summary")}
|
||||
</ProgressTabs.Trigger>
|
||||
</ProgressTabs.List>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
{tab === Tab.SUMMARY ? (
|
||||
<Button
|
||||
key="save-btn"
|
||||
type="submit"
|
||||
size="small"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key="continue-btn"
|
||||
type="button"
|
||||
onClick={handleContinue}
|
||||
size="small"
|
||||
>
|
||||
{t("actions.continue")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</RouteFocusModal.Header>
|
||||
<RouteFocusModal.Body className="size-full overflow-hidden">
|
||||
<ProgressTabs.Content
|
||||
value={Tab.DETAILS}
|
||||
className="size-full overflow-hidden"
|
||||
>
|
||||
<SplitView open={open} onOpenChange={handleOpenChange}>
|
||||
<SplitView.Content>
|
||||
<CreateDraftOrderDetails />
|
||||
</SplitView.Content>
|
||||
<SplitView.Drawer>
|
||||
<CreateDraftOrderDrawer view={view} />
|
||||
</SplitView.Drawer>
|
||||
</SplitView>
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content
|
||||
value={Tab.SUMMARY}
|
||||
className="flex h-full w-full flex-col items-center overflow-hidden"
|
||||
>
|
||||
<CreateDraftOrderSummary />
|
||||
</ProgressTabs.Content>
|
||||
</RouteFocusModal.Body>
|
||||
</ProgressTabs>
|
||||
</form>
|
||||
</RouteFocusModal.Form>
|
||||
</CreateDraftOrderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const handleUpdateEntities = <T,>(
|
||||
newEntities: T[],
|
||||
existingEntities: T[],
|
||||
deleteEntity: (indices: number[]) => void,
|
||||
createEntity: (entities: T[]) => void,
|
||||
entityIdKey: keyof T
|
||||
) => {
|
||||
const newEntitiesIdMap = newEntities.map((e) => e[entityIdKey])
|
||||
const existingEntitiesIdMap = existingEntities.map((e) => e[entityIdKey])
|
||||
|
||||
const indicesToDelete = existingEntities.reduce((acc, e, i) => {
|
||||
if (!newEntitiesIdMap.includes(e[entityIdKey])) {
|
||||
acc.push(i)
|
||||
}
|
||||
return acc
|
||||
}, [] as number[])
|
||||
|
||||
const entitiesToAdd = newEntities.filter(
|
||||
(e) => !existingEntitiesIdMap.includes(e[entityIdKey])
|
||||
)
|
||||
|
||||
deleteEntity(indicesToDelete)
|
||||
createEntity(entitiesToAdd)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Text } from "@medusajs/ui"
|
||||
|
||||
import { Divider } from "../../../../../../components/common/divider"
|
||||
import { castNumber } from "../../../../../../lib/cast-number"
|
||||
import {
|
||||
getDbAmount,
|
||||
getLocaleAmount,
|
||||
} from "../../../../../../lib/money-amount-helpers"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderCustomItemsSummary = () => {
|
||||
const { form, region } = useCreateDraftOrder()
|
||||
const { currency_code } = region || {}
|
||||
|
||||
const items = form.getValues("custom_items") || []
|
||||
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{items.map((item, index) => {
|
||||
const price = item.unit_price
|
||||
? getDbAmount(castNumber(item.unit_price), currency_code!)
|
||||
: item.unit_price
|
||||
const subtotal = price * item.quantity
|
||||
|
||||
return (
|
||||
<div key={index} className="grid grid-cols-2 items-start gap-4">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-base"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(price, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="min-w-[27px] text-right">
|
||||
<Text>
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(subtotal, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Divider variant="dashed" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { Avatar, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getFormattedAddress } from "../../../../../../lib/addresses"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderCustomerSummary = () => {
|
||||
const { form, customer, sameAsShipping, region } = useCreateDraftOrder()
|
||||
const { countries } = region || {}
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const shippingAddress = form.getValues("shipping_address")
|
||||
const shippingAddressCountry = countries?.find(
|
||||
(c) => c.iso_2 === shippingAddress?.country_code
|
||||
)
|
||||
|
||||
const billingAddress = form.getValues("billing_address")
|
||||
const billingAddressCountry = countries?.find(
|
||||
(c) => c.iso_2 === billingAddress?.country_code
|
||||
)
|
||||
|
||||
const email = form.getValues("email")
|
||||
const phone =
|
||||
shippingAddress?.phone || billingAddress?.phone || customer?.phone
|
||||
|
||||
const { first_name, last_name } = customer || shippingAddress || {}
|
||||
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||
const fallback = name
|
||||
? name[0].toUpperCase()
|
||||
: email?.[0].toUpperCase() || "?"
|
||||
|
||||
return (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-1 gap-2">
|
||||
{customer && (
|
||||
<div className="grid grid-cols-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.id")}
|
||||
</Text>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Avatar fallback={fallback} size="2xsmall" />
|
||||
<Text size="small" leading="compact">
|
||||
{name || email}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.email")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{email}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.phone")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{phone || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("addresses.shippingAddress.label")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{getFormattedAddress({
|
||||
address: { ...shippingAddress, country: shippingAddressCountry },
|
||||
}).map((line, i) => {
|
||||
return (
|
||||
<span key={i} className="break-words">
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<Text size="small" leading="compact">
|
||||
{t("addresses.billingAddress.label")}
|
||||
</Text>
|
||||
{sameAsShipping ? (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{t("addresses.billingAddress.sameAsShipping")}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
{getFormattedAddress({
|
||||
address: { ...billingAddress, country: billingAddressCountry },
|
||||
}).map((line, i) => {
|
||||
return (
|
||||
<span key={i} className="break-words">
|
||||
{line}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { Switch } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Form } from "../../../../../../components/common/form"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderFieldsSummary = () => {
|
||||
const { t } = useTranslation()
|
||||
const { form } = useCreateDraftOrder()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="notification_order"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t("draftOrders.create.sendNotificationLabel")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
onCheckedChange={onChange}
|
||||
checked={value}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
{t("draftOrders.create.sendNotificationHint")}
|
||||
</Form.Hint>
|
||||
</div>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Divider } from "../../../../../../components/common/divider"
|
||||
import { CreateDraftOrderCustomItemsSummary } from "./create-draft-order-custom-items-summary"
|
||||
import { CreateDraftOrderCustomerSummary } from "./create-draft-order-customer-summary"
|
||||
import { CreateDraftOrderFieldsSummary } from "./create-draft-order-fields-summary"
|
||||
import { CreateDraftOrderTotalSummary } from "./create-draft-order-total-summary"
|
||||
import { CreateDraftOrderVariantItemsSummary } from "./create-draft-order-variant-items-summary"
|
||||
|
||||
export const CreateDraftOrderSummary = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col items-center overflow-auto p-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<Heading>{t("fields.summary")}</Heading>
|
||||
<CreateDraftOrderCustomerSummary />
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Divider variant="dashed" />
|
||||
<CreateDraftOrderVariantItemsSummary />
|
||||
<CreateDraftOrderCustomItemsSummary />
|
||||
<CreateDraftOrderTotalSummary />
|
||||
<Divider variant="dashed" />
|
||||
</div>
|
||||
<CreateDraftOrderFieldsSummary />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Divider } from "../../../../../../components/common/divider"
|
||||
import { castNumber } from "../../../../../../lib/cast-number"
|
||||
import {
|
||||
getDbAmount,
|
||||
getLocaleAmount,
|
||||
getStylizedAmount,
|
||||
} from "../../../../../../lib/money-amount-helpers"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
import { CustomItem, ExistingItem, ShippingMethod } from "../types"
|
||||
|
||||
export const CreateDraftOrderTotalSummary = () => {
|
||||
const { t } = useTranslation()
|
||||
const { form, region } = useCreateDraftOrder()
|
||||
const { currency_code } = region || {}
|
||||
|
||||
const variantItems = form.getValues("existing_items") || []
|
||||
const variantItemsSubtotal = variantItems.reduce((acc, item) => {
|
||||
const amount = getExistingItemSubtotal(item, currency_code!)
|
||||
|
||||
return (acc += amount)
|
||||
}, 0)
|
||||
|
||||
const customItems = form.getValues("custom_items") || []
|
||||
const customItemsSubtotal = customItems.reduce((acc, item) => {
|
||||
const amount = getCustomItemSubtotal(item, currency_code!)
|
||||
|
||||
return (acc += amount)
|
||||
}, 0)
|
||||
|
||||
const count = variantItems.length + customItems.length
|
||||
const subtotal = variantItemsSubtotal + customItemsSubtotal
|
||||
|
||||
const shippingMethod = form.getValues("shipping_method")
|
||||
const shippingSubtotal = getShippingSubtotal(shippingMethod, currency_code!)
|
||||
|
||||
const total = subtotal + shippingSubtotal
|
||||
|
||||
return (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-4">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.subtotal")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-right">
|
||||
{t("general.items", { count })}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-right">
|
||||
{getLocaleAmount(subtotal, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.shipping")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-right">
|
||||
{shippingMethod.option_title}
|
||||
</Text>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
{shippingMethod.custom_amount && (
|
||||
<Text size="small" className="text-ui-fg-muted line-through">
|
||||
{getLocaleAmount(
|
||||
castNumber(shippingMethod.amount || 0),
|
||||
currency_code!
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="small" leading="compact" className="text-right">
|
||||
{getLocaleAmount(shippingSubtotal, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider variant="dashed" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Text size="small" leading="compact">
|
||||
{t("fields.totalExclTax")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact" className="text-right">
|
||||
{getStylizedAmount(total, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getExistingItemSubtotal = (item: ExistingItem, currency_code: string) => {
|
||||
if (item.custom_unit_price) {
|
||||
const customUnitPrice = castNumber(item.custom_unit_price)
|
||||
return getDbAmount(customUnitPrice, currency_code) * item.quantity
|
||||
}
|
||||
|
||||
return item.unit_price * item.quantity
|
||||
}
|
||||
|
||||
const getCustomItemSubtotal = (item: CustomItem, currency_code: string) => {
|
||||
return getDbAmount(castNumber(item.unit_price), currency_code) * item.quantity
|
||||
}
|
||||
|
||||
const getShippingSubtotal = (
|
||||
shippingMethod: ShippingMethod,
|
||||
currency_code: string
|
||||
) => {
|
||||
if (shippingMethod.custom_amount) {
|
||||
const customAmount = castNumber(shippingMethod.custom_amount)
|
||||
return getDbAmount(customAmount, currency_code)
|
||||
}
|
||||
|
||||
if (shippingMethod.amount) {
|
||||
const amount =
|
||||
typeof shippingMethod.amount === "string"
|
||||
? Number(shippingMethod.amount.replace(",", "."))
|
||||
: shippingMethod.amount
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Copy, Text } from "@medusajs/ui"
|
||||
|
||||
import { Divider } from "../../../../../../components/common/divider"
|
||||
import { Thumbnail } from "../../../../../../components/common/thumbnail"
|
||||
import { castNumber } from "../../../../../../lib/cast-number"
|
||||
import {
|
||||
getDbAmount,
|
||||
getLocaleAmount,
|
||||
} from "../../../../../../lib/money-amount-helpers"
|
||||
import { useCreateDraftOrder } from "../hooks"
|
||||
|
||||
export const CreateDraftOrderVariantItemsSummary = () => {
|
||||
const { form, region } = useCreateDraftOrder()
|
||||
const { currency_code } = region || {}
|
||||
|
||||
const items = form.getValues("existing_items") || []
|
||||
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{items.map((item) => {
|
||||
const price = item.custom_unit_price
|
||||
? getDbAmount(castNumber(item.custom_unit_price), currency_code!)
|
||||
: item.unit_price
|
||||
const subtotal = price * item.quantity
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.variant_id}
|
||||
className="grid grid-cols-2 items-start gap-4"
|
||||
>
|
||||
<div className="flex items-start gap-x-4">
|
||||
<Thumbnail src={item.thumbnail} />
|
||||
<div>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-base"
|
||||
>
|
||||
{item.product_title}
|
||||
</Text>
|
||||
{item.sku && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Text size="small">{item.sku}</Text>
|
||||
<Copy content={item.sku} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small">{item.variant_title}</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
{item.custom_unit_price && (
|
||||
<Text size="small" className="text-ui-fg-muted line-through">
|
||||
{getLocaleAmount(item.unit_price, currency_code!)}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="small">
|
||||
{getLocaleAmount(price, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="min-w-[27px] text-right">
|
||||
<Text>
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(subtotal, currency_code!)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Divider variant="dashed" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-draft-order-summary"
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useContext } from "react"
|
||||
import { CreateDraftOrderContext } from "./context"
|
||||
|
||||
export const useCreateDraftOrder = () => {
|
||||
const context = useContext(CreateDraftOrderContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useCreateDraftOrder must be used within a CreateDraftOrderProvider"
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./create-draft-order-form"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Customer, Region } from "@medusajs/medusa"
|
||||
import { FieldArrayWithId, UseFormReturn } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
|
||||
import { PricedVariant } from "@medusajs/medusa/dist/types/pricing"
|
||||
import {
|
||||
CreateDraftOrderSchema,
|
||||
CustomItemSchema,
|
||||
ExistingItemSchema,
|
||||
ShippingMethodSchema,
|
||||
View,
|
||||
} from "./constants"
|
||||
|
||||
export type ExistingItem = z.infer<typeof ExistingItemSchema>
|
||||
export type CustomItem = z.infer<typeof CustomItemSchema>
|
||||
export type ShippingMethod = z.infer<typeof ShippingMethodSchema>
|
||||
|
||||
export type CreateDraftOrderContextValue = {
|
||||
form: UseFormReturn<z.infer<typeof CreateDraftOrderSchema>>
|
||||
region: Region | null
|
||||
setRegion: (region: Region | null) => void
|
||||
customer: Customer | null
|
||||
setCustomer: (customer: Customer | null) => void
|
||||
sameAsShipping: boolean
|
||||
setSameAsShipping: (sameAsShipping: boolean) => void
|
||||
variants: {
|
||||
items: FieldArrayWithId<
|
||||
z.infer<typeof CreateDraftOrderSchema>,
|
||||
"existing_items",
|
||||
"ei_id"
|
||||
>[]
|
||||
remove: (index: number) => void
|
||||
update: (items: ExistingItem[]) => void
|
||||
rebase: (variants: PricedVariant[]) => void
|
||||
}
|
||||
custom: {
|
||||
items: FieldArrayWithId<
|
||||
z.infer<typeof CreateDraftOrderSchema>,
|
||||
"custom_items",
|
||||
"ci_id"
|
||||
>[]
|
||||
remove: (index: number) => void
|
||||
update: (items: CustomItem) => void
|
||||
}
|
||||
onOpenDrawer: (view: View) => void
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/route-modal"
|
||||
import { CreateDraftOrderForm } from "./components/create-draft-order-form/create-draft-order-form"
|
||||
|
||||
export const DraftOrderCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<CreateDraftOrderForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DraftOrderCreate as Component } from "./draft-order-create"
|
||||
+98
-3
@@ -5,6 +5,7 @@ import { Container, Copy, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useAdminReservations } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { Divider } from "../../../../../components/common/divider"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import {
|
||||
getLocaleAmount,
|
||||
@@ -51,6 +52,69 @@ const Header = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const CustomItem = ({
|
||||
item,
|
||||
currencyCode,
|
||||
reservation,
|
||||
}: {
|
||||
item: {
|
||||
id: string
|
||||
title: string
|
||||
unit_price: number
|
||||
subtotal: number
|
||||
quantity: number
|
||||
}
|
||||
currencyCode: string
|
||||
reservation?: ReservationItemDTO | null
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="text-ui-fg-subtle grid grid-cols-2 items-start gap-x-4 px-6 py-4"
|
||||
>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="text-ui-fg-base"
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<div className="grid grid-cols-3 items-center gap-x-4">
|
||||
<div className="flex items-center justify-end gap-x-4">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.unit_price, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="w-fit min-w-[27px]">
|
||||
<Text>
|
||||
<span className="tabular-nums">{item.quantity}</span>x
|
||||
</Text>
|
||||
</div>
|
||||
<div className="overflow-visible">
|
||||
<StatusBadge
|
||||
color={reservation ? "green" : "orange"}
|
||||
className="text-nowrap"
|
||||
>
|
||||
{reservation
|
||||
? t("orders.reservations.allocatedLabel")
|
||||
: t("orders.reservations.notAllocatedLabel")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small">
|
||||
{getLocaleAmount(item.subtotal || 0, currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
item,
|
||||
currencyCode,
|
||||
@@ -129,9 +193,19 @@ const ItemBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
|
||||
throw error
|
||||
}
|
||||
|
||||
const variantBasedItems = draftOrder.cart.items.filter(
|
||||
(i) => i.variant_id !== null
|
||||
)
|
||||
|
||||
const customItems = draftOrder.cart.items.filter((i) => i.variant_id === null)
|
||||
|
||||
const showDivider = variantBasedItems.length > 0 && customItems.length > 0
|
||||
|
||||
console.log("customItems", customItems)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{draftOrder.cart.items.map((item) => {
|
||||
{variantBasedItems.map((item) => {
|
||||
const reservation = reservations
|
||||
? reservations.find((r) => r.line_item_id === item.id)
|
||||
: null
|
||||
@@ -145,6 +219,27 @@ const ItemBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{showDivider && <Divider variant="dashed" />}
|
||||
{customItems.map((item) => {
|
||||
const reservation = reservations
|
||||
? reservations.find((r) => r.line_item_id === item.id)
|
||||
: null
|
||||
|
||||
return (
|
||||
<CustomItem
|
||||
key={item.id}
|
||||
item={{
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
unit_price: item.unit_price,
|
||||
subtotal: item.unit_price * item.quantity,
|
||||
quantity: item.quantity,
|
||||
}}
|
||||
currencyCode={draftOrder.cart.region.currency_code}
|
||||
reservation={reservation}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -194,7 +289,7 @@ const CostBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Calculate tax rate since it's not included in the cart
|
||||
const taxRate = calculateCartTaxRate(draftOrder.cart)
|
||||
const taxRate = calculateCartTaxRate(draftOrder.cart).toFixed(2)
|
||||
|
||||
return (
|
||||
<div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
|
||||
@@ -236,7 +331,7 @@ const CostBreakdown = ({ draftOrder }: { draftOrder: DraftOrder }) => {
|
||||
/>
|
||||
<Cost
|
||||
label={t("fields.tax")}
|
||||
secondaryValue={`${taxRate || 0}%`}
|
||||
secondaryValue={`${taxRate || 0} %`}
|
||||
value={
|
||||
draftOrder.cart.tax_total
|
||||
? getLocaleAmount(
|
||||
|
||||
+2
@@ -1,9 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { DraftOrderListTable } from "./components/draft-order-list-table"
|
||||
|
||||
export const DraftOrderList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<DraftOrderListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+22
-1
@@ -22,6 +22,22 @@ export const useProductVariantTableFilters = () => {
|
||||
],
|
||||
}
|
||||
|
||||
const allowBackorderFilter: Filter = {
|
||||
key: "allow_backorder",
|
||||
label: t("fields.allowBackorder"),
|
||||
type: "select",
|
||||
options: [
|
||||
{
|
||||
label: t("fields.true"),
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
label: t("fields.false"),
|
||||
value: "false",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
@@ -31,7 +47,12 @@ export const useProductVariantTableFilters = () => {
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, manageInventoryFilter, ...dateFilters]
|
||||
filters = [
|
||||
...filters,
|
||||
manageInventoryFilter,
|
||||
allowBackorderFilter,
|
||||
...dateFilters,
|
||||
]
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
+19
-3
@@ -9,12 +9,27 @@ export const useProductVariantTableQuery = ({
|
||||
prefix?: string
|
||||
}) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "manage_inventory", "order", "created_at", "updated_at"],
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"manage_inventory",
|
||||
"allow_backorder",
|
||||
"order",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, manage_inventory, created_at, updated_at, q, order } =
|
||||
queryObject
|
||||
const {
|
||||
offset,
|
||||
manage_inventory,
|
||||
allow_backorder,
|
||||
created_at,
|
||||
updated_at,
|
||||
q,
|
||||
order,
|
||||
} = queryObject
|
||||
|
||||
const searchParams: AdminGetProductsVariantsParams = {
|
||||
limit: pageSize,
|
||||
@@ -22,6 +37,7 @@ export const useProductVariantTableQuery = ({
|
||||
manage_inventory: manage_inventory
|
||||
? manage_inventory === "true"
|
||||
: undefined,
|
||||
allow_backorder: allow_backorder ? allow_backorder === "true" : undefined,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
|
||||
+6
-3
@@ -394,7 +394,7 @@ export const CreateShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="amount"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -408,6 +408,7 @@ export const CreateShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
@@ -499,7 +500,7 @@ export const CreateShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="min_subtotal"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -514,6 +515,7 @@ export const CreateShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
@@ -526,7 +528,7 @@ export const CreateShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="max_subtotal"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -541,6 +543,7 @@ export const CreateShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
+6
-3
@@ -321,7 +321,7 @@ export const EditShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="amount"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -333,6 +333,7 @@ export const EditShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
@@ -358,7 +359,7 @@ export const EditShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="min_subtotal"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -371,6 +372,7 @@ export const EditShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
@@ -383,7 +385,7 @@ export const EditShippingOptionForm = ({
|
||||
control={form.control}
|
||||
name="max_subtotal"
|
||||
shouldUnregister
|
||||
render={({ field }) => {
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
@@ -396,6 +398,7 @@ export const EditShippingOptionForm = ({
|
||||
<CurrencyInput
|
||||
code={region.currency_code}
|
||||
symbol={region.currency.symbol_native}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
@@ -2,68 +2,6 @@ export const theme = {
|
||||
"extend": {
|
||||
"colors": {
|
||||
"ui": {
|
||||
"button": {
|
||||
"inverted": {
|
||||
"DEFAULT": "var(--button-inverted)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-inverted-pressed)"
|
||||
},
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-inverted-hover)"
|
||||
}
|
||||
},
|
||||
"neutral": {
|
||||
"DEFAULT": "var(--button-neutral)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-neutral-pressed)"
|
||||
},
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-neutral-hover)"
|
||||
}
|
||||
},
|
||||
"danger": {
|
||||
"DEFAULT": "var(--button-danger)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-danger-pressed)"
|
||||
},
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-danger-hover)"
|
||||
}
|
||||
},
|
||||
"transparent": {
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-transparent-hover)"
|
||||
},
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-transparent-pressed)"
|
||||
},
|
||||
"DEFAULT": "var(--button-transparent)"
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"fg": {
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--code-fg-subtle)"
|
||||
},
|
||||
"muted": {
|
||||
"DEFAULT": "var(--code-fg-muted)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--code-fg-base)"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--code-bg-subtle)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--code-bg-base)"
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"DEFAULT": "var(--code-border)"
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"green": {
|
||||
"bg": {
|
||||
@@ -82,23 +20,6 @@ export const theme = {
|
||||
"DEFAULT": "var(--tag-green-text)"
|
||||
}
|
||||
},
|
||||
"neutral": {
|
||||
"bg": {
|
||||
"DEFAULT": "var(--tag-neutral-bg)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--tag-neutral-bg-hover)"
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"DEFAULT": "var(--tag-neutral-border)"
|
||||
},
|
||||
"text": {
|
||||
"DEFAULT": "var(--tag-neutral-text)"
|
||||
},
|
||||
"icon": {
|
||||
"DEFAULT": "var(--tag-neutral-icon)"
|
||||
}
|
||||
},
|
||||
"red": {
|
||||
"bg": {
|
||||
"DEFAULT": "var(--tag-red-bg)",
|
||||
@@ -166,26 +87,43 @@ export const theme = {
|
||||
"text": {
|
||||
"DEFAULT": "var(--tag-blue-text)"
|
||||
}
|
||||
},
|
||||
"neutral": {
|
||||
"border": {
|
||||
"DEFAULT": "var(--tag-neutral-border)"
|
||||
},
|
||||
"text": {
|
||||
"DEFAULT": "var(--tag-neutral-text)"
|
||||
},
|
||||
"bg": {
|
||||
"DEFAULT": "var(--tag-neutral-bg)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--tag-neutral-bg-hover)"
|
||||
}
|
||||
},
|
||||
"icon": {
|
||||
"DEFAULT": "var(--tag-neutral-icon)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"strong": {
|
||||
"DEFAULT": "var(--border-strong)"
|
||||
},
|
||||
"interactive": {
|
||||
"DEFAULT": "var(--border-interactive)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--border-base)"
|
||||
},
|
||||
"error": {
|
||||
"DEFAULT": "var(--border-error)"
|
||||
},
|
||||
"danger": {
|
||||
"DEFAULT": "var(--border-danger)"
|
||||
},
|
||||
"loud": {
|
||||
"DEFAULT": "var(--border-loud)"
|
||||
},
|
||||
"danger": {
|
||||
"DEFAULT": "var(--border-danger)"
|
||||
"strong": {
|
||||
"DEFAULT": "var(--border-strong)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--border-base)"
|
||||
},
|
||||
"transparent": {
|
||||
"DEFAULT": "var(--border-transparent)"
|
||||
@@ -208,16 +146,16 @@ export const theme = {
|
||||
}
|
||||
},
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--bg-subtle)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--bg-subtle-pressed)"
|
||||
},
|
||||
"DEFAULT": "var(--bg-subtle)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--bg-subtle-hover)"
|
||||
}
|
||||
},
|
||||
"component": {
|
||||
"DEFAULT": "var(--bg-component)"
|
||||
"interactive": {
|
||||
"DEFAULT": "var(--bg-interactive)"
|
||||
},
|
||||
"overlay": {
|
||||
"DEFAULT": "var(--bg-overlay)"
|
||||
@@ -231,16 +169,28 @@ export const theme = {
|
||||
}
|
||||
},
|
||||
"field": {
|
||||
"DEFAULT": "var(--bg-field)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--bg-field-hover)"
|
||||
},
|
||||
"DEFAULT": "var(--bg-field)"
|
||||
},
|
||||
"interactive": {
|
||||
"DEFAULT": "var(--bg-interactive)"
|
||||
"component": {
|
||||
"DEFAULT": "var(--bg-field-component)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--bg-field-component-hover)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
"DEFAULT": "var(--bg-disabled)"
|
||||
},
|
||||
"component": {
|
||||
"hover": {
|
||||
"DEFAULT": "var(--bg-component-hover)"
|
||||
},
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--bg-component-pressed)"
|
||||
},
|
||||
"DEFAULT": "var(--bg-component)"
|
||||
}
|
||||
},
|
||||
"fg": {
|
||||
@@ -261,33 +211,79 @@ export const theme = {
|
||||
"error": {
|
||||
"DEFAULT": "var(--fg-error)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--fg-base)"
|
||||
"muted": {
|
||||
"DEFAULT": "var(--fg-muted)"
|
||||
},
|
||||
"disabled": {
|
||||
"DEFAULT": "var(--fg-disabled)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--fg-base)"
|
||||
},
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--fg-subtle)"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"danger": {
|
||||
"DEFAULT": "var(--button-danger)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-danger-pressed)"
|
||||
},
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-danger-hover)"
|
||||
}
|
||||
},
|
||||
"muted": {
|
||||
"DEFAULT": "var(--fg-muted)"
|
||||
"transparent": {
|
||||
"DEFAULT": "var(--button-transparent)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-transparent-hover)"
|
||||
},
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-transparent-pressed)"
|
||||
}
|
||||
},
|
||||
"neutral": {
|
||||
"DEFAULT": "var(--button-neutral)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-neutral-hover)"
|
||||
},
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-neutral-pressed)"
|
||||
}
|
||||
},
|
||||
"inverted": {
|
||||
"DEFAULT": "var(--button-inverted)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--button-inverted-pressed)"
|
||||
},
|
||||
"hover": {
|
||||
"DEFAULT": "var(--button-inverted-hover)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contrast": {
|
||||
"bg": {
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--contrast-bg-subtle)"
|
||||
"fg": {
|
||||
"primary": {
|
||||
"DEFAULT": "var(--contrast-fg-primary)"
|
||||
},
|
||||
"secondary": {
|
||||
"DEFAULT": "var(--contrast-fg-secondary)"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"base": {
|
||||
"DEFAULT": "var(--contrast-bg-base)",
|
||||
"pressed": {
|
||||
"DEFAULT": "var(--contrast-bg-base-pressed)"
|
||||
},
|
||||
"DEFAULT": "var(--contrast-bg-base)",
|
||||
"hover": {
|
||||
"DEFAULT": "var(--contrast-bg-base-hover)"
|
||||
}
|
||||
},
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--contrast-bg-subtle)"
|
||||
},
|
||||
"highlight": {
|
||||
"DEFAULT": "var(--contrast-bg-highlight)"
|
||||
}
|
||||
@@ -296,41 +292,57 @@ export const theme = {
|
||||
"base": {
|
||||
"DEFAULT": "var(--contrast-border-base)"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"fg": {
|
||||
"primary": {
|
||||
"DEFAULT": "var(--contrast-fg-primary)"
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--code-fg-subtle)"
|
||||
},
|
||||
"secondary": {
|
||||
"DEFAULT": "var(--contrast-fg-secondary)"
|
||||
"muted": {
|
||||
"DEFAULT": "var(--code-fg-muted)"
|
||||
},
|
||||
"base": {
|
||||
"DEFAULT": "var(--code-fg-base)"
|
||||
}
|
||||
},
|
||||
"bg": {
|
||||
"base": {
|
||||
"DEFAULT": "var(--code-bg-base)"
|
||||
},
|
||||
"subtle": {
|
||||
"DEFAULT": "var(--code-bg-subtle)"
|
||||
}
|
||||
},
|
||||
"border": {
|
||||
"DEFAULT": "var(--code-border)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"boxShadow": {
|
||||
"borders-interactive-with-active": "var(--borders-interactive-with-active)",
|
||||
"buttons-danger-focus": "var(--buttons-danger-focus)",
|
||||
"buttons-danger": "var(--buttons-danger)",
|
||||
"details-contrast-on-bg-interactive": "var(--details-contrast-on-bg-interactive)",
|
||||
"borders-interactive-with-focus": "var(--borders-interactive-with-focus)",
|
||||
"borders-error": "var(--borders-error)",
|
||||
"borders-focus": "var(--borders-focus)",
|
||||
"borders-interactive-with-shadow": "var(--borders-interactive-with-shadow)",
|
||||
"details-switch-handle": "var(--details-switch-handle)",
|
||||
"buttons-neutral": "var(--buttons-neutral)",
|
||||
"details-commandbar": "var(--details-commandbar)",
|
||||
"details-switch-background-focus": "var(--details-switch-background-focus)",
|
||||
"buttons-inverted-focus": "var(--buttons-inverted-focus)",
|
||||
"details-switch-background": "var(--details-switch-background)",
|
||||
"borders-base": "var(--borders-base)",
|
||||
"elevation-tooltip": "var(--elevation-tooltip)",
|
||||
"elevation-card-hover": "var(--elevation-card-hover)",
|
||||
"elevation-modal": "var(--elevation-modal)",
|
||||
"buttons-inverted": "var(--buttons-inverted)",
|
||||
"elevation-card-rest": "var(--elevation-card-rest)",
|
||||
"buttons-neutral-focus": "var(--buttons-neutral-focus)",
|
||||
"borders-interactive-with-active": "var(--borders-interactive-with-active)",
|
||||
"buttons-danger-focus": "var(--buttons-danger-focus)",
|
||||
"borders-base": "var(--borders-base)",
|
||||
"buttons-danger": "var(--buttons-danger)",
|
||||
"details-contrast-on-bg-interactive": "var(--details-contrast-on-bg-interactive)",
|
||||
"details-switch-handle": "var(--details-switch-handle)",
|
||||
"buttons-neutral": "var(--buttons-neutral)",
|
||||
"borders-interactive-with-focus": "var(--borders-interactive-with-focus)",
|
||||
"details-switch-background-focus": "var(--details-switch-background-focus)",
|
||||
"borders-error": "var(--borders-error)",
|
||||
"buttons-inverted-focus": "var(--buttons-inverted-focus)",
|
||||
"borders-focus": "var(--borders-focus)",
|
||||
"details-switch-background": "var(--details-switch-background)",
|
||||
"elevation-tooltip": "var(--elevation-tooltip)",
|
||||
"borders-interactive-with-shadow": "var(--borders-interactive-with-shadow)",
|
||||
"elevation-flyout": "var(--elevation-flyout)",
|
||||
"details-commandbar": "var(--details-commandbar)",
|
||||
"elevation-modal": "var(--elevation-modal)"
|
||||
"elevation-flyout": "var(--elevation-flyout)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,8 @@
|
||||
export const colors = {
|
||||
"dark": {
|
||||
"--button-transparent-pressed": "rgba(46, 48, 53, 1)",
|
||||
"--border-base": "rgba(46, 48, 53, 1)",
|
||||
"--tag-neutral-icon": "rgba(125, 130, 138, 1)",
|
||||
"--bg-switch-off-hover": "rgba(70, 75, 80, 1)",
|
||||
"--fg-on-color": "rgba(255, 255, 255, 1)",
|
||||
"--bg-switch-off": "rgba(53, 55, 60, 1)",
|
||||
"--border-strong": "rgba(53, 55, 60, 1)",
|
||||
"--fg-subtle": "rgba(173, 177, 184, 1)",
|
||||
"--fg-base": "rgba(237, 238, 240, 1)",
|
||||
"--bg-base-hover": "rgba(39, 40, 45, 1)",
|
||||
"--bg-subtle-hover": "rgba(27, 27, 31, 1)",
|
||||
"--fg-disabled": "rgba(60, 63, 68, 1)",
|
||||
"--bg-subtle": "rgba(24, 24, 26, 1)",
|
||||
"--tag-neutral-border": "rgba(60, 63, 68, 1)",
|
||||
"--bg-subtle-pressed": "rgba(39, 40, 45, 1)",
|
||||
"--tag-neutral-text": "rgba(173, 177, 184, 1)",
|
||||
"--fg-muted": "rgba(105, 110, 119, 1)",
|
||||
"--border-loud": "rgba(237, 238, 240, 1)",
|
||||
"--bg-base-pressed": "rgba(46, 48, 53, 1)",
|
||||
"--bg-disabled": "rgba(39, 40, 45, 1)",
|
||||
"--code-fg-subtle": "rgba(105, 110, 119, 1)",
|
||||
"--code-fg-base": "rgba(237, 238, 240, 1)",
|
||||
"--code-bg-subtle": "rgba(24, 24, 26, 1)",
|
||||
"--code-fg-muted": "rgba(70, 75, 80, 1)",
|
||||
"--bg-highlight-hover": "rgba(30, 58, 138, 1)",
|
||||
"--border-danger": "rgba(190, 18, 60, 1)",
|
||||
"--border-interactive": "rgba(96, 165, 250, 1)",
|
||||
"--bg-highlight": "rgba(23, 37, 84, 1)",
|
||||
"--fg-interactive-hover": "rgba(59, 130, 246, 1)",
|
||||
"--fg-error": "rgba(251, 113, 133, 1)",
|
||||
"--bg-interactive": "rgba(96, 165, 250, 1)",
|
||||
@@ -36,24 +11,18 @@ export const colors = {
|
||||
"--button-danger-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--fg-interactive": "rgba(96, 165, 250, 1)",
|
||||
"--bg-overlay": "rgba(24, 24, 26, 0.7)",
|
||||
"--fg-on-inverted": "rgba(24, 24, 26, 1)",
|
||||
"--tag-red-border": "rgba(136, 19, 55, 1)",
|
||||
"--tag-red-bg": "rgba(76, 5, 25, 1)",
|
||||
"--button-transparent-hover": "rgba(39, 40, 45, 1)",
|
||||
"--tag-blue-text": "rgba(96, 165, 250, 1)",
|
||||
"--tag-orange-text": "rgba(251, 191, 36, 1)",
|
||||
"--tag-green-text": "rgba(52, 211, 153, 1)",
|
||||
"--tag-neutral-bg": "rgba(46, 48, 53, 1)",
|
||||
"--tag-orange-border": "rgba(120, 53, 15, 1)",
|
||||
"--tag-green-border": "rgba(6, 78, 59, 1)",
|
||||
"--bg-base": "rgba(27, 27, 31, 1)",
|
||||
"--tag-red-text": "rgba(251, 113, 133, 1)",
|
||||
"--tag-green-bg-hover": "rgba(6, 78, 59, 1)",
|
||||
"--tag-purple-bg-hover": "rgba(76, 29, 149, 1)",
|
||||
"--tag-red-bg-hover": "rgba(136, 19, 55, 1)",
|
||||
"--border-transparent": "rgba(255, 255, 255, 0)",
|
||||
"--code-border": "rgba(46, 48, 53, 1)",
|
||||
"--tag-orange-icon": "rgba(245, 158, 11, 1)",
|
||||
"--tag-purple-bg": "rgba(46, 16, 100, 1)",
|
||||
"--tag-blue-bg": "rgba(23, 37, 84, 1)",
|
||||
@@ -63,156 +32,195 @@ export const colors = {
|
||||
"--tag-blue-bg-hover": "rgba(30, 42, 138, 1)",
|
||||
"--tag-orange-bg": "rgba(69, 26, 3, 1)",
|
||||
"--tag-orange-bg-hover": "rgba(120, 53, 15, 1)",
|
||||
"--code-bg-base": "rgba(27, 27, 31, 1)",
|
||||
"--button-neutral": "rgba(39, 40, 45, 1)",
|
||||
"--button-neutral-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--tag-neutral-bg-hover": "rgba(53, 55, 60, 1)",
|
||||
"--tag-blue-icon": "rgba(59, 130, 246, 1)",
|
||||
"--tag-red-icon": "rgba(244, 63, 94, 1)",
|
||||
"--tag-purple-icon": "rgba(139, 92, 246, 1)",
|
||||
"--tag-purple-text": "rgba(167, 139, 250, 1)",
|
||||
"--tag-green-icon": "rgba(16, 185, 129, 1)",
|
||||
"--button-inverted": "rgba(237, 238, 240, 1)",
|
||||
"--button-inverted-gradient-from": "rgba(24, 24, 26, 0)",
|
||||
"--button-inverted-gradient-to": "rgba(24, 24, 26, 1)",
|
||||
"--bg-component": "rgba(39, 40, 45, 1)",
|
||||
"--bg-field": "rgba(39, 40, 45, 1)",
|
||||
"--bg-field-hover": "rgba(46, 48, 53, 1)",
|
||||
"--contrast-fg-primary": "rgba(28, 32, 36, 1)",
|
||||
"--contrast-bg-base": "rgba(228, 228, 233, 1)",
|
||||
"--contrast-fg-secondary": "rgba(96, 100, 108, 1)",
|
||||
"--contrast-border-base": "rgba(185, 187, 198, 1)",
|
||||
"--contrast-bg-base-pressed": "rgba(242, 242, 245, 1)",
|
||||
"--contrast-bg-subtle": "rgba(211, 212, 219, 1)",
|
||||
"--contrast-bg-base-hover": "rgba(235, 235, 239, 1)",
|
||||
"--contrast-bg-highlight": "rgba(242, 242, 245, 1)",
|
||||
"--button-neutral-pressed": "rgba(60, 63, 68, 1)",
|
||||
"--button-neutral-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-pressed": "rgba(237, 238, 240, 1)",
|
||||
"--button-inverted-pressed-gradient-from": "rgba(0, 0, 0, 0)",
|
||||
"--button-inverted-pressed-gradient-to": "rgba(24, 24, 26, 1)",
|
||||
"--button-neutral-hover": "rgba(53, 55, 60, 1)",
|
||||
"--button-neutral-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-hover": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-hover-gradient-from": "rgba(24, 24, 26, 0)",
|
||||
"--button-inverted-hover-gradient-to": "rgba(24, 24, 26, 1)",
|
||||
"--button-danger-pressed": "rgba(225, 29, 72, 1)",
|
||||
"--button-danger-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-danger-hover": "rgba(190, 18, 60, 1)",
|
||||
"--button-danger-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-transparent": "rgba(0, 0, 0, 0.01)"
|
||||
},
|
||||
"light": {
|
||||
"--button-inverted": "rgba(3, 7, 18, 1)",
|
||||
"--button-transparent": "rgba(0, 0, 0, 0.01)",
|
||||
"--code-bg-base": "rgba(9, 9, 11, 1)",
|
||||
"--button-neutral": "rgba(39, 39, 42, 1)",
|
||||
"--button-neutral-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-transparent-hover": "rgba(39, 39, 42, 1)",
|
||||
"--code-fg-subtle": "rgba(161, 161, 170, 1)",
|
||||
"--tag-neutral-bg-hover": "rgba(63, 63, 70, 1)",
|
||||
"--contrast-border-base": "rgba(82, 82, 91, 1)",
|
||||
"--button-neutral-pressed": "rgba(82, 82, 91, 1)",
|
||||
"--button-neutral-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--tag-neutral-bg": "rgba(39, 39, 42, 1)",
|
||||
"--bg-highlight": "rgba(23, 37, 84, 1)",
|
||||
"--border-base": "rgba(255, 255, 255, 0.1)",
|
||||
"--code-fg-base": "rgba(250, 250, 250, 1)",
|
||||
"--tag-neutral-icon": "rgba(113, 113, 122, 1)",
|
||||
"--bg-switch-off-hover": "rgba(82, 82, 91, 1)",
|
||||
"--bg-base": "rgba(24, 24, 27, 1)",
|
||||
"--button-inverted-pressed": "rgba(161, 161, 170, 1)",
|
||||
"--button-inverted-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-neutral-hover": "rgba(63, 63, 70, 1)",
|
||||
"--button-neutral-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--bg-switch-off": "rgba(63, 63, 70, 1)",
|
||||
"--border-strong": "rgba(255, 255, 255, 0.15)",
|
||||
"--fg-subtle": "rgba(161, 161, 170, 1)",
|
||||
"--bg-highlight-hover": "rgba(30, 58, 138, 1)",
|
||||
"--button-inverted": "rgba(82, 82, 91, 1)",
|
||||
"--button-inverted-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--code-fg-subtle": "rgba(156, 163, 175, 1)",
|
||||
"--code-fg-muted": "rgba(107, 114, 128, 1)",
|
||||
"--code-bg-subtle": "rgba(31, 41, 55, 1)",
|
||||
"--code-fg-base": "rgba(249, 250, 251, 1)",
|
||||
"--fg-base": "rgba(250, 250, 250, 1)",
|
||||
"--code-border": "rgba(39, 39, 42, 1)",
|
||||
"--contrast-bg-subtle": "rgba(39, 39, 42, 1)",
|
||||
"--contrast-bg-base-hover": "rgba(82, 82, 91, 1)",
|
||||
"--bg-base-hover": "rgba(39, 39, 42, 1)",
|
||||
"--bg-subtle-hover": "rgba(24, 24, 27, 1)",
|
||||
"--fg-disabled": "rgba(63, 63, 70, 1)",
|
||||
"--bg-subtle": "rgba(9, 9, 11, 1)",
|
||||
"--tag-neutral-border": "rgba(63, 63, 70, 1)",
|
||||
"--bg-subtle-pressed": "rgba(39, 39, 42, 1)",
|
||||
"--tag-neutral-text": "rgba(161, 161, 170, 1)",
|
||||
"--fg-muted": "rgba(82, 82, 91, 1)",
|
||||
"--bg-overlay": "rgba(9, 9, 11, 0.7)",
|
||||
"--button-inverted-hover": "rgba(113, 113, 122, 1)",
|
||||
"--button-inverted-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--fg-on-inverted": "rgba(9, 9, 11, 1)",
|
||||
"--code-bg-subtle": "rgba(24, 24, 27, 1)",
|
||||
"--bg-component": "rgba(39, 39, 42, 1)",
|
||||
"--border-loud": "rgba(250, 250, 250, 1)",
|
||||
"--bg-base-pressed": "rgba(63, 63, 70, 1)",
|
||||
"--bg-disabled": "rgba(39, 39, 42, 1)",
|
||||
"--contrast-bg-highlight": "rgba(82, 82, 91, 1)",
|
||||
"--button-transparent-pressed": "rgba(63, 63, 70, 1)",
|
||||
"--code-fg-muted": "rgba(82, 82, 91, 1)",
|
||||
"--contrast-fg-secondary": "rgba(161, 161, 170, 1)",
|
||||
"--contrast-bg-base-pressed": "rgba(113, 113, 122, 1)",
|
||||
"--contrast-fg-primary": "rgba(250, 250, 250, 1)",
|
||||
"--contrast-bg-base": "rgba(63, 63, 70, 1)",
|
||||
"--bg-component-pressed": "rgba(82, 82, 91, 1)",
|
||||
"--bg-component-hover": "rgba(63, 63, 70, 1)",
|
||||
"--bg-field": "rgba(39, 39, 42, 1)",
|
||||
"--bg-field-component": "rgba(24, 24, 27, 1)",
|
||||
"--bg-field-component-hover": "rgba(24, 24, 27, 1)",
|
||||
"--bg-field-hover": "rgba(39, 39, 42, 1)"
|
||||
},
|
||||
"light": {
|
||||
"--tag-green-bg": "rgba(209, 250, 229, 1)",
|
||||
"--border-strong": "rgba(209, 213, 219, 1)",
|
||||
"--border-interactive": "rgba(59, 130, 246, 1)",
|
||||
"--bg-highlight": "rgba(239, 246, 255, 1)",
|
||||
"--tag-neutral-bg": "rgba(243, 244, 246, 1)",
|
||||
"--tag-red-bg": "rgba(255, 228, 230, 1)",
|
||||
"--tag-orange-bg": "rgba(254, 244, 199, 1)",
|
||||
"--bg-base": "rgba(255, 255, 255, 1)",
|
||||
"--border-base": "rgba(229, 231, 235, 1)",
|
||||
"--tag-green-icon": "rgba(5, 150, 105, 1)",
|
||||
"--tag-purple-bg-hover": "rgba(221, 214, 254, 1)",
|
||||
"--tag-blue-border": "rgba(191, 219, 254, 1)",
|
||||
"--tag-orange-icon": "rgba(217, 119, 6, 1)",
|
||||
"--tag-purple-bg": "rgba(237, 233, 254, 1)",
|
||||
"--bg-subtle": "rgba(249, 250, 251, 1)",
|
||||
"--tag-purple-text": "rgba(109, 40, 217, 1)",
|
||||
"--tag-blue-bg": "rgba(219, 234, 254, 1)",
|
||||
"--tag-blue-icon": "rgba(37, 99, 235, 1)",
|
||||
"--border-error": "rgba(225, 29, 72, 1)",
|
||||
"--border-loud": "rgba(3, 7, 18, 1)",
|
||||
"--fg-on-inverted": "rgba(255, 255, 255, 1)",
|
||||
"--fg-on-color": "rgba(255, 255, 255, 1)",
|
||||
"--fg-interactive-hover": "rgba(37, 99, 235, 1)",
|
||||
"--fg-interactive": "rgba(59, 130, 246, 1)",
|
||||
"--fg-error": "rgba(225, 29, 72, 1)",
|
||||
"--border-danger": "rgba(190, 18, 60, 1)",
|
||||
"--border-transparent": "rgba(3, 7, 18, 0)",
|
||||
"--fg-base": "rgba(3, 7, 18, 1)",
|
||||
"--fg-disabled": "rgba(209, 213, 219, 1)",
|
||||
"--bg-subtle-pressed": "rgba(229, 231, 235, 1)",
|
||||
"--fg-subtle": "rgba(75, 85, 99, 1)",
|
||||
"--fg-muted": "rgba(156, 163, 175, 1)",
|
||||
"--bg-subtle-hover": "rgba(243, 244, 246, 1)",
|
||||
"--tag-neutral-border": "rgba(229, 231, 235, 1)",
|
||||
"--fg-muted": "rgba(161, 161, 170, 1)",
|
||||
"--bg-subtle-pressed": "rgba(228, 228, 231, 1)",
|
||||
"--tag-green-bg-hover": "rgba(167, 243, 208, 1)",
|
||||
"--tag-blue-bg-hover": "rgba(191, 219, 254, 1)",
|
||||
"--tag-red-icon": "rgba(225, 29, 72, 1)",
|
||||
"--tag-neutral-text": "rgba(75, 85, 99, 1)",
|
||||
"--tag-red-bg-hover": "rgba(254, 205, 211, 1)",
|
||||
"--tag-red-text": "rgba(190, 18, 60, 1)",
|
||||
"--tag-purple-icon": "rgba(124, 58, 237, 1)",
|
||||
"--tag-blue-text": "rgba(29, 78, 216, 1)",
|
||||
"--tag-orange-bg-hover": "rgba(253, 230, 138, 1)",
|
||||
"--tag-neutral-bg-hover": "rgba(229, 231, 235, 1)",
|
||||
"--tag-purple-border": "rgba(221, 214, 254, 1)",
|
||||
"--tag-orange-text": "rgba(180, 83, 9, 1)",
|
||||
"--tag-neutral-icon": "rgba(107, 114, 128, 1)",
|
||||
"--tag-orange-border": "rgba(253, 230, 138, 1)",
|
||||
"--tag-red-border": "rgba(254, 205, 211, 1)",
|
||||
"--tag-green-border": "rgba(167, 243, 208, 1)",
|
||||
"--tag-green-text": "rgba(4, 120, 87, 1)",
|
||||
"--code-bg-base": "rgba(17, 24, 39, 1)",
|
||||
"--code-border": "rgba(55, 65, 81, 1)",
|
||||
"--button-neutral": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-gradient-from": "rgba(3, 7, 18, 0)",
|
||||
"--button-neutral-gradient-to": "rgba(3, 7, 18, 1)",
|
||||
"--button-danger": "rgba(225, 29, 72, 1)",
|
||||
"--button-danger-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-pressed": "rgba(31, 41, 55, 1)",
|
||||
"--button-inverted-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-danger-pressed": "rgba(159, 18, 57, 1)",
|
||||
"--button-danger-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-hover": "rgba(17, 24, 39, 1)",
|
||||
"--button-inverted-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-danger-hover": "rgba(190, 18, 60, 1)",
|
||||
"--button-danger-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-danger-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--bg-component": "rgba(241, 243, 245, 1)",
|
||||
"--bg-overlay": "rgba(3, 7, 18, 0.4)",
|
||||
"--bg-switch-off": "rgba(229, 231, 235, 1)",
|
||||
"--bg-field-hover": "rgba(243, 244, 246, 1)",
|
||||
"--bg-field": "rgba(249, 250, 251, 1)",
|
||||
"--bg-interactive": "rgba(59, 130, 246, 1)",
|
||||
"--bg-highlight-hover": "rgba(219, 234, 254, 1)",
|
||||
"--bg-switch-off-hover": "rgba(209, 213, 219, 1)",
|
||||
"--bg-disabled": "rgba(243, 244, 246, 1)",
|
||||
"--contrast-bg-subtle": "rgba(46, 48, 53, 1)",
|
||||
"--contrast-bg-base": "rgba(27, 27, 31, 1)",
|
||||
"--contrast-border-base": "rgba(60, 63, 68, 1)",
|
||||
"--contrast-fg-primary": "rgba(237, 238, 240, 1)",
|
||||
"--contrast-bg-base-pressed": "rgba(53, 55, 60, 1)",
|
||||
"--contrast-fg-secondary": "rgba(173, 177, 184, 1)",
|
||||
"--contrast-bg-highlight": "rgba(53, 55, 60, 1)",
|
||||
"--contrast-bg-base-hover": "rgba(46, 48, 53, 1)",
|
||||
"--button-transparent-hover": "rgba(243, 244, 246, 1)",
|
||||
"--button-neutral-pressed": "rgba(229, 231, 235, 1)",
|
||||
"--button-neutral-pressed-gradient-from": "rgba(3, 7, 18, 0)",
|
||||
"--button-neutral-pressed-gradient-to": "rgba(3, 7, 18, 1)",
|
||||
"--button-transparent-pressed": "rgba(229, 231, 235, 1)",
|
||||
"--button-neutral-hover": "rgba(243, 244, 246, 1)",
|
||||
"--button-neutral-hover-gradient-from": "rgba(3, 7, 18, 0)",
|
||||
"--button-neutral-hover-gradient-to": "rgba(3, 7, 18, 1)",
|
||||
"--bg-base-pressed": "rgba(243, 244, 246, 1)",
|
||||
"--bg-base-hover": "rgba(249, 250, 251, 1)",
|
||||
"--button-transparent": "rgba(255, 255, 255, 0.01)"
|
||||
"--button-transparent": "rgba(255, 255, 255, 0.01)",
|
||||
"--bg-overlay": "rgba(9, 9, 11, 0.4)",
|
||||
"--tag-neutral-border": "rgba(228, 228, 231, 1)",
|
||||
"--border-loud": "rgba(9, 9, 11, 1)",
|
||||
"--contrast-fg-primary": "rgba(250, 250, 250, 1)",
|
||||
"--bg-switch-off": "rgba(228, 228, 231, 1)",
|
||||
"--contrast-bg-base-pressed": "rgba(63, 63, 70, 1)",
|
||||
"--bg-base-pressed": "rgba(228, 228, 231, 1)",
|
||||
"--tag-neutral-text": "rgba(82, 82, 91, 1)",
|
||||
"--button-transparent-hover": "rgba(244, 244, 245, 1)",
|
||||
"--contrast-bg-base": "rgba(24, 24, 27, 1)",
|
||||
"--fg-disabled": "rgba(212, 212, 216, 1)",
|
||||
"--bg-field": "rgba(250, 250, 250, 1)",
|
||||
"--border-strong": "rgba(212, 212, 216, 1)",
|
||||
"--bg-field-hover": "rgba(244, 244, 245, 1)",
|
||||
"--contrast-border-base": "rgba(82, 82, 91, 1)",
|
||||
"--fg-base": "rgba(9, 9, 11, 1)",
|
||||
"--contrast-bg-subtle": "rgba(39, 39, 42, 1)",
|
||||
"--contrast-fg-secondary": "rgba(161, 161, 170, 1)",
|
||||
"--code-fg-subtle": "rgba(161, 161, 170, 1)",
|
||||
"--tag-neutral-bg": "rgba(244, 244, 245, 1)",
|
||||
"--button-transparent-pressed": "rgba(228, 228, 231, 1)",
|
||||
"--tag-neutral-bg-hover": "rgba(228, 228, 231, 1)",
|
||||
"--code-fg-muted": "rgba(113, 113, 122, 1)",
|
||||
"--contrast-bg-highlight": "rgba(63, 63, 70, 1)",
|
||||
"--tag-neutral-icon": "rgba(113, 113, 122, 1)",
|
||||
"--border-base": "rgba(228, 228, 231, 1)",
|
||||
"--code-bg-base": "rgba(24, 24, 27, 1)",
|
||||
"--button-neutral": "rgba(255, 255, 255, 1)",
|
||||
"--button-neutral-gradient-from": "rgba(9, 9, 11, 0)",
|
||||
"--button-neutral-gradient-to": "rgba(9, 9, 11, 1)",
|
||||
"--code-bg-subtle": "rgba(39, 39, 42, 1)",
|
||||
"--button-neutral-hover": "rgba(244, 244, 245, 1)",
|
||||
"--button-neutral-hover-gradient-from": "rgba(9, 9, 11, 0)",
|
||||
"--button-neutral-hover-gradient-to": "rgba(9, 9, 11, 1)",
|
||||
"--contrast-bg-base-hover": "rgba(39, 39, 42, 1)",
|
||||
"--bg-subtle": "rgba(250, 250, 250, 1)",
|
||||
"--bg-switch-off-hover": "rgba(212, 212, 216, 1)",
|
||||
"--code-fg-base": "rgba(250, 250, 250, 1)",
|
||||
"--bg-disabled": "rgba(244, 244, 245, 1)",
|
||||
"--code-border": "rgba(63, 63, 70, 1)",
|
||||
"--fg-subtle": "rgba(82, 82, 91, 1)",
|
||||
"--bg-subtle-hover": "rgba(244, 244, 245, 1)",
|
||||
"--button-neutral-pressed": "rgba(228, 228, 231, 1)",
|
||||
"--button-neutral-pressed-gradient-from": "rgba(9, 9, 11, 0)",
|
||||
"--button-neutral-pressed-gradient-to": "rgba(9, 9, 11, 1)",
|
||||
"--border-transparent": "rgba(9, 9, 11, 0)",
|
||||
"--button-inverted": "rgba(9, 9, 11, 1)",
|
||||
"--button-inverted-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-pressed": "rgba(39, 39, 42, 1)",
|
||||
"--button-inverted-pressed-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-pressed-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--button-inverted-hover": "rgba(24, 24, 27, 1)",
|
||||
"--button-inverted-hover-gradient-from": "rgba(255, 255, 255, 1)",
|
||||
"--button-inverted-hover-gradient-to": "rgba(255, 255, 255, 0)",
|
||||
"--bg-component-hover": "rgba(244, 244, 245, 1)",
|
||||
"--bg-field-component": "rgba(255, 255, 255, 1)",
|
||||
"--bg-field-component-hover": "rgba(250, 250, 250, 1)",
|
||||
"--bg-component-pressed": "rgba(228, 228, 231, 1)",
|
||||
"--bg-component": "rgba(250, 250, 250, 1)",
|
||||
"--bg-base-hover": "rgba(244, 244, 245, 1)"
|
||||
}
|
||||
}
|
||||
@@ -4,30 +4,6 @@ export const components = {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-gradient-from), var(--button-danger-gradient-to))",
|
||||
"opacity": "10%"
|
||||
},
|
||||
".button-neutral-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-gradient-from), var(--button-neutral-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-gradient-from), var(--button-inverted-gradient-to))",
|
||||
"opacity": "12%"
|
||||
},
|
||||
".button-neutral-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-pressed-gradient-from), var(--button-neutral-pressed-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-pressed-gradient-from), var(--button-inverted-pressed-gradient-to))",
|
||||
"opacity": "12%"
|
||||
},
|
||||
".button-neutral-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-hover-gradient-from), var(--button-neutral-hover-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-hover-gradient-from), var(--button-inverted-hover-gradient-to))",
|
||||
"opacity": "12%"
|
||||
},
|
||||
".button-danger-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-pressed-gradient-from), var(--button-danger-pressed-gradient-to))",
|
||||
"opacity": "10%"
|
||||
@@ -35,44 +11,68 @@ export const components = {
|
||||
".button-danger-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-hover-gradient-from), var(--button-danger-hover-gradient-to))",
|
||||
"opacity": "10%"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
".button-inverted-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-gradient-from), var(--button-inverted-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-neutral-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-gradient-from), var(--button-neutral-gradient-to))",
|
||||
"opacity": "3%"
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-neutral-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-pressed-gradient-from), var(--button-neutral-pressed-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-pressed-gradient-from), var(--button-inverted-pressed-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-neutral-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-hover-gradient-from), var(--button-neutral-hover-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-gradient-from), var(--button-inverted-gradient-to))",
|
||||
"opacity": "6%"
|
||||
},
|
||||
".button-inverted-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-hover-gradient-from), var(--button-inverted-hover-gradient-to))",
|
||||
"opacity": "6%"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
".button-danger-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-gradient-from), var(--button-danger-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-inverted-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-pressed-gradient-from), var(--button-inverted-pressed-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-danger-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-pressed-gradient-from), var(--button-danger-pressed-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-inverted-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-hover-gradient-from), var(--button-inverted-hover-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-danger-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-danger-hover-gradient-from), var(--button-danger-hover-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-neutral-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-pressed-gradient-from), var(--button-neutral-pressed-gradient-to))",
|
||||
".button-neutral-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-gradient-from), var(--button-neutral-gradient-to))",
|
||||
"opacity": "3%"
|
||||
},
|
||||
".button-neutral-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-hover-gradient-from), var(--button-neutral-hover-gradient-to))",
|
||||
"opacity": "3%"
|
||||
},
|
||||
".button-neutral-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-neutral-pressed-gradient-from), var(--button-neutral-pressed-gradient-to))",
|
||||
"opacity": "3%"
|
||||
},
|
||||
".button-inverted-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-gradient-from), var(--button-inverted-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-inverted-pressed-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-pressed-gradient-from), var(--button-inverted-pressed-gradient-to))",
|
||||
"opacity": "16%"
|
||||
},
|
||||
".button-inverted-hover-gradient": {
|
||||
"backgroundImage": "linear-gradient(180deg, var(--button-inverted-hover-gradient-from), var(--button-inverted-hover-gradient-to))",
|
||||
"opacity": "16%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,50 @@
|
||||
export const effects = {
|
||||
"dark": {
|
||||
"--details-switch-background-focus": "0px 0px 0px 1px rgba(27, 27, 31, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8), 0px 1px 1px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.1) inset, 0px 0px 0px 0.5px rgba(255, 255, 255, 0.16) inset, 0px 0px 8px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.2)",
|
||||
"--buttons-neutral-focus": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--elevation-card-hover": "0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 2px 8px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--borders-focus": "0px 0px 0px 1px rgba(23, 23, 23, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8)",
|
||||
"--borders-interactive-with-focus": "0px 1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--borders-interactive-with-shadow": "0px 1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1)",
|
||||
"--details-contrast-on-bg-interactive": "0px 1px 2px 0px rgba(30, 58, 138, 0.6)",
|
||||
"--elevation-card-rest": "0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 2px 4px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--elevation-tooltip": "0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 4px 8px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--elevation-flyout": "0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 8px 16px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--details-switch-handle": "0px 0px 2px 1px rgba(255, 255, 255, 1) inset, 0px 1px 0px 0px rgba(255, 255, 255, 1) inset, 0px 0px 0px 0.5px rgba(0, 0, 0, 0.16), 0px 5px 4px 0px rgba(0, 0, 0, 0.1), 0px 3px 3px 0px rgba(0, 0, 0, 0.1), 0px 1px 2px 0px rgba(0, 0, 0, 0.1), 0px 0px 1px 0px rgba(0, 0, 0, 0.1)",
|
||||
"--details-switch-background": "0px 1px 1px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.1) inset, 0px 0px 0px 0.5px rgba(255, 255, 255, 0.16) inset, 0px 0px 8px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.2)",
|
||||
"--borders-interactive-with-active": "0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.25)",
|
||||
"--buttons-inverted": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.6), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--buttons-danger": "0px 1px 0px 0px rgba(190, 18, 60, 0.04), 0px -1px 0px 0px rgba(190, 18, 60, 0.04), 0px 0px 0px 1px rgba(190, 18, 60, 0.6), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 1px 1px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--buttons-neutral": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--buttons-danger-focus": "0px 1px 0px 0px rgba(190, 18, 60, 0.04), 0px -1px 0px 0px rgba(190, 18, 60, 0.04), 0px 0px 0px 1px rgba(190, 18, 60, 0.6), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--borders-base": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--borders-error": "0px 0px 0px 1px rgba(244, 63, 94, 1), 0px 0px 0px 4px rgba(225, 29, 72, 0.25)",
|
||||
"--buttons-inverted-focus": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -1px 0px 0px rgba(255, 255, 255, 0.04), 0px 0px 0px 1px rgba(255, 255, 255, 0.6), 0px 1px 1px 0px rgba(0, 0, 0, 0.4), 0px 2px 2px 0px rgba(0, 0, 0, 0.4), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--details-commandbar": "0px 0px 0px 1px rgba(228, 228, 233, 1) inset, 0px 0px 0px 1.5px rgba(46, 48, 53, 0.2) inset, 0px 16px 32px 0px rgba(0, 0, 0, 0.32), 0px 2px 24px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--elevation-modal": "0px 0px 0px 1px rgba(23, 23, 23, 1) inset, 0px 0px 0px 1.5px rgba(46, 48, 53, 0.6) inset, 0px 0px 0px 1px rgba(255, 255, 255, 0.1), 0px 16px 32px 0px rgba(0, 0, 0, 0.32), 0px 2px 24px 0px rgba(0, 0, 0, 0.32)"
|
||||
"--details-switch-background-focus": "0px 0px 0px 1px rgba(24, 24, 27, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8), 0px 1px 1px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.1) inset, 0px 0px 0px 0.5px rgba(255, 255, 255, 0.12) inset, 0px 0px 8px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.2)",
|
||||
"--elevation-card-rest": "0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 2px 4px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--borders-focus": "0px 0px 0px 1px rgba(24, 24, 27, 1), 0px 0px 0px 3px rgba(96, 165, 250, 0.8)",
|
||||
"--buttons-danger": "0px 1px 0px 0px rgba(255, 255, 255, 0.12), 0px -0.75px 0px 0px rgba(255, 255, 255, 0.24), 0px 0px 0px 1px rgba(159, 18, 57, 1), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 1px 1px 0px rgba(0, 0, 0, 0.3)",
|
||||
"--borders-interactive-with-focus": "0px 1px 2px 0px rgba(219, 234, 254, 0.5), 0px 0px 0px 1px rgba(96, 165, 250, 1), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--details-commandbar": "0px 0px 0px 1px rgba(63, 63, 70, 1) inset, 0px 0px 0px 1.5px rgba(113, 113, 122, 0.4) inset, 0px 16px 32px 0px rgba(0, 0, 0, 0.32), 0px 2px 24px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--elevation-tooltip": "0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 4px 8px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--elevation-flyout": "0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--details-switch-background": "0px 1px 1px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.1) inset, 0px 0px 0px 0.5px rgba(255, 255, 255, 0.12) inset, 0px 0px 8px 0px rgba(0, 0, 0, 0.1) inset, 0px 2px 4px 0px rgba(0, 0, 0, 0.2)",
|
||||
"--elevation-modal": "0px 0px 0px 1px rgba(24, 24, 27, 1) inset, 0px 0px 0px 1.5px rgba(63, 63, 70, 0.4) inset, 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.32), 0px 2px 24px 0px rgba(0, 0, 0, 0.32)",
|
||||
"--elevation-card-hover": "0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 2px -1px rgba(255, 255, 255, 0.16), 0px 2px 8px 0px rgba(0, 0, 0, 0.4)",
|
||||
"--buttons-inverted": "0px -0.75px 0px 0px rgba(255, 255, 255, 0.12), 0px 0px 0px 1px rgba(255, 255, 255, 0.36), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3)",
|
||||
"--buttons-neutral": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -0.75px 0px 0px rgba(255, 255, 255, 0.08), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3)",
|
||||
"--buttons-danger-focus": "0px 1px 0px 0px rgba(255, 255, 255, 0.12), 0px -0.75px 0px 0px rgba(255, 255, 255, 0.24), 0px 0px 0px 1px rgba(159, 18, 57, 1), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--borders-base": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -0.75px 0px 0px rgba(255, 255, 255, 0.06), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3)",
|
||||
"--buttons-neutral-focus": "0px 1px 0px 0px rgba(255, 255, 255, 0.04), 0px -0.75px 0px 0px rgba(255, 255, 255, 0.08), 0px 0px 0px 1px rgba(255, 255, 255, 0.12), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3), 0px 0px 0px 2px rgba(24, 24, 27, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)",
|
||||
"--buttons-inverted-focus": "0px -0.75px 0px 0px rgba(255, 255, 255, 0.12), 0px 0px 0px 1px rgba(255, 255, 255, 0.36), 0px 1px 1px 0px rgba(0, 0, 0, 0.3), 0px 2px 2px 0px rgba(0, 0, 0, 0.3), 0px 0px 0px 2px rgba(27, 27, 31, 1), 0px 0px 0px 4px rgba(96, 165, 250, 0.8)"
|
||||
},
|
||||
"light": {
|
||||
"--elevation-card-hover": "0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 1px 2px -1px rgba(3, 7, 18, 0.08), 0px 2px 8px 0px rgba(3, 7, 18, 0.1)",
|
||||
"--buttons-inverted": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(3, 7, 18, 0.4), 0px 0px 0px 1px rgba(3, 7, 18, 0.8)",
|
||||
"--elevation-card-rest": "0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 1px 2px -1px rgba(3, 7, 18, 0.08), 0px 2px 4px 0px rgba(3, 7, 18, 0.04)",
|
||||
"--buttons-neutral-focus": "0px 1px 2px 0px rgba(3, 7, 18, 0.12), 0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--borders-interactive-with-active": "0px 0px 0px 1px rgba(59, 130, 246, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.2)",
|
||||
"--buttons-danger-focus": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(190, 18, 60, 0.4), 0px 0px 0px 1px rgba(190, 18, 60, 0.8), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--borders-base": "0px 1px 2px 0px rgba(3, 7, 18, 0.12), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
|
||||
"--buttons-danger": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(190, 18, 60, 0.4), 0px 0px 0px 1px rgba(190, 18, 60, 0.8)",
|
||||
"--details-contrast-on-bg-interactive": "0px 1px 2px 0px rgba(30, 58, 138, 0.6)",
|
||||
"--details-switch-handle": "0px 0px 2px 1px rgba(255, 255, 255, 1) inset, 0px 1px 0px 0px rgba(255, 255, 255, 1) inset, 0px 0px 0px 0.5px rgba(3, 7, 18, 0.02), 0px 5px 4px 0px rgba(3, 7, 18, 0.02), 0px 3px 3px 0px rgba(3, 7, 18, 0.04), 0px 1px 2px 0px rgba(3, 7, 18, 0.12), 0px 0px 1px 0px rgba(3, 7, 18, 0.08)",
|
||||
"--buttons-neutral": "0px 1px 2px 0px rgba(3, 7, 18, 0.12), 0px 0px 0px 1px rgba(3, 7, 18, 0.08)",
|
||||
"--borders-interactive-with-focus": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(59, 130, 246, 1), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--details-switch-background-focus": "0px 0px 0px 1px rgba(255, 255, 255, 1), 0px 0px 0px 3px rgba(59, 130, 246, 0.6), 0px 1px 1px 0px rgba(3, 7, 18, 0.04) inset, 0px 2px 4px 0px rgba(3, 7, 18, 0.04) inset, 0px 0px 0px 0.5px rgba(3, 7, 18, 0.06) inset, 0px 0px 8px 0px rgba(3, 7, 18, 0.02) inset, 0px 2px 4px 0px rgba(3, 7, 18, 0.04)",
|
||||
"--borders-error": "0px 0px 0px 1px rgba(225, 29, 72, 1), 0px 0px 0px 3px rgba(225, 29, 72, 0.15)",
|
||||
"--buttons-inverted-focus": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(3, 7, 18, 0.4), 0px 0px 0px 1px rgba(3, 7, 18, 0.8), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--borders-focus": "0px 0px 0px 1px rgba(255, 255, 255, 1), 0px 0px 0px 3px rgba(59, 130, 246, 0.6)",
|
||||
"--details-switch-background": "0px 1px 1px 0px rgba(3, 7, 18, 0.04) inset, 0px 2px 4px 0px rgba(3, 7, 18, 0.04) inset, 0px 0px 0px 0.5px rgba(3, 7, 18, 0.06) inset, 0px 0px 8px 0px rgba(3, 7, 18, 0.02) inset, 0px 2px 4px 0px rgba(3, 7, 18, 0.04)",
|
||||
"--elevation-tooltip": "0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 4px 8px 0px rgba(3, 7, 18, 0.08)",
|
||||
"--borders-interactive-with-shadow": "0px 1px 2px 0px rgba(30, 58, 138, 0.5), 0px 0px 0px 1px rgba(59, 130, 246, 1)",
|
||||
"--elevation-flyout": "0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 8px 16px 0px rgba(3, 7, 18, 0.08)",
|
||||
"--details-commandbar": "0px 0px 0px 1px rgba(27, 27, 31, 1) inset, 0px 0px 0px 1.5px rgba(255, 255, 255, 0.2) inset, 0px 16px 32px 0px rgba(3, 7, 18, 0.16), 0px 2px 24px 0px rgba(3, 7, 18, 0.08)",
|
||||
"--elevation-modal": "0px 0px 0px 1px rgba(255, 255, 255, 1) inset, 0px 0px 0px 1.5px rgba(229, 231, 235, 0.6) inset, 0px 0px 0px 1px rgba(3, 7, 18, 0.08), 0px 16px 32px 0px rgba(3, 7, 18, 0.08), 0px 2px 24px 0px rgba(3, 7, 18, 0.08)"
|
||||
"--details-switch-handle": "0px 0px 2px 1px rgba(255, 255, 255, 1) inset, 0px 1px 0px 0px rgba(255, 255, 255, 1) inset, 0px 0px 0px 0.5px rgba(9, 9, 11, 0.02), 0px 5px 4px 0px rgba(9, 9, 11, 0.02), 0px 3px 3px 0px rgba(9, 9, 11, 0.04), 0px 1px 2px 0px rgba(9, 9, 11, 0.12), 0px 0px 1px 0px rgba(9, 9, 11, 0.08)",
|
||||
"--buttons-neutral": "0px 1px 2px 0px rgba(9, 9, 11, 0.12), 0px 0px 0px 1px rgba(9, 9, 11, 0.08)",
|
||||
"--details-commandbar": "0px 0px 0px 1px rgba(24, 24, 27, 1) inset, 0px 0px 0px 1.5px rgba(255, 255, 255, 0.2) inset, 0px 16px 32px 0px rgba(9, 9, 11, 0.16), 0px 2px 24px 0px rgba(9, 9, 11, 0.08)",
|
||||
"--details-switch-background-focus": "0px 0px 0px 1px rgba(255, 255, 255, 1), 0px 0px 0px 3px rgba(59, 130, 246, 0.6), 0px 1px 1px 0px rgba(9, 9, 11, 0.04) inset, 0px 2px 4px 0px rgba(9, 9, 11, 0.04) inset, 0px 0px 0px 0.5px rgba(9, 9, 11, 0.06) inset, 0px 0px 8px 0px rgba(9, 9, 11, 0.02) inset, 0px 2px 4px 0px rgba(9, 9, 11, 0.04)",
|
||||
"--buttons-inverted-focus": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(9, 9, 11, 0.4), 0px 0px 0px 1px rgba(9, 9, 11, 0.8), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--details-switch-background": "0px 1px 1px 0px rgba(9, 9, 11, 0.04) inset, 0px 2px 4px 0px rgba(9, 9, 11, 0.04) inset, 0px 0px 0px 0.5px rgba(9, 9, 11, 0.06) inset, 0px 0px 8px 0px rgba(9, 9, 11, 0.02) inset, 0px 2px 4px 0px rgba(9, 9, 11, 0.04)",
|
||||
"--borders-base": "0px 1px 2px 0px rgba(9, 9, 11, 0.12), 0px 0px 0px 1px rgba(9, 9, 11, 0.08)",
|
||||
"--elevation-tooltip": "0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 4px 8px 0px rgba(9, 9, 11, 0.08)",
|
||||
"--elevation-card-hover": "0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 1px 2px -1px rgba(9, 9, 11, 0.08), 0px 2px 8px 0px rgba(9, 9, 11, 0.1)",
|
||||
"--elevation-modal": "0px 0px 0px 1px rgba(255, 255, 255, 1) inset, 0px 0px 0px 1.5px rgba(228, 228, 231, 0.6) inset, 0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 16px 32px 0px rgba(9, 9, 11, 0.08), 0px 2px 24px 0px rgba(9, 9, 11, 0.08)",
|
||||
"--buttons-inverted": "0px 0.75px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 1px 2px 0px rgba(9, 9, 11, 0.4), 0px 0px 0px 1px rgba(9, 9, 11, 0.8)",
|
||||
"--elevation-card-rest": "0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 1px 2px -1px rgba(9, 9, 11, 0.08), 0px 2px 4px 0px rgba(9, 9, 11, 0.04)",
|
||||
"--buttons-neutral-focus": "0px 1px 2px 0px rgba(9, 9, 11, 0.12), 0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 0px 0px 2px rgba(255, 255, 255, 1), 0px 0px 0px 4px rgba(59, 130, 246, 0.6)",
|
||||
"--elevation-flyout": "0px 0px 0px 1px rgba(9, 9, 11, 0.08), 0px 8px 16px 0px rgba(9, 9, 11, 0.08)"
|
||||
}
|
||||
}
|
||||
@@ -179,16 +179,16 @@ export const typography = {
|
||||
"fontWeight": "500",
|
||||
"fontFamily": "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji"
|
||||
},
|
||||
".code-body": {
|
||||
"fontSize": "0.75rem",
|
||||
"lineHeight": "1.375rem",
|
||||
"fontWeight": "400",
|
||||
"fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace"
|
||||
},
|
||||
".code-label": {
|
||||
"fontSize": "0.75rem",
|
||||
"lineHeight": "1.25rem",
|
||||
"fontWeight": "400",
|
||||
"fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace"
|
||||
},
|
||||
".code-body": {
|
||||
"fontSize": "0.75rem",
|
||||
"lineHeight": "1.375rem",
|
||||
"fontWeight": "400",
|
||||
"fontFamily": "Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace"
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ const triggerVariants = cva({
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||
"invalid::border-ui-border-error invalid:shadow-borders-error",
|
||||
"invalid:border-ui-border-error invalid:shadow-borders-error",
|
||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
"group/trigger"
|
||||
),
|
||||
|
||||
@@ -40,6 +40,29 @@ export interface AdminGetProductsVariantsParams {
|
||||
* Filter product variants by whether they are allowed to be backordered or not.
|
||||
*/
|
||||
allow_backorder?: boolean
|
||||
/**
|
||||
* Filter by available inventory quantity
|
||||
*/
|
||||
inventory_quantity?:
|
||||
| number
|
||||
| {
|
||||
/**
|
||||
* filter by inventory quantity less than this number
|
||||
*/
|
||||
lt?: number
|
||||
/**
|
||||
* filter by inventory quantity greater than this number
|
||||
*/
|
||||
gt?: number
|
||||
/**
|
||||
* filter by inventory quantity less than or equal to this number
|
||||
*/
|
||||
lte?: number
|
||||
/**
|
||||
* filter by inventory quantity greater than or equal to this number
|
||||
*/
|
||||
gte?: number
|
||||
}
|
||||
/**
|
||||
* Filter by a creation date range.
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,18 @@ export interface AdminGetVariantsParams {
|
||||
* Limit the number of product variants returned.
|
||||
*/
|
||||
limit?: number
|
||||
/**
|
||||
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
|
||||
*/
|
||||
order?: string
|
||||
/**
|
||||
* Filter product variants by whether their inventory is managed or not.
|
||||
*/
|
||||
manage_inventory?: boolean
|
||||
/**
|
||||
* Filter product variants by whether they are allowed to be backordered or not.
|
||||
*/
|
||||
allow_backorder?: boolean
|
||||
/**
|
||||
* The ID of the cart to use for the price selection context.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Request, Response } from "express"
|
||||
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import { ProductVariantService } from "../../../../services"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
NumericalComparisonOperator,
|
||||
} from "../../../../types/common"
|
||||
import { IsType } from "../../../../utils"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
|
||||
@@ -32,6 +35,28 @@ import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
* - (query) manage_inventory {boolean} Filter product variants by whether their inventory is managed or not.
|
||||
* - (query) allow_backorder {boolean} Filter product variants by whether they are allowed to be backordered or not.
|
||||
* - in: query
|
||||
* name: inventory_quantity
|
||||
* description: Filter by available inventory quantity
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: number
|
||||
* description: a specific number to filter by.
|
||||
* - type: object
|
||||
* description: filter using less and greater than comparisons.
|
||||
* properties:
|
||||
* lt:
|
||||
* type: number
|
||||
* description: filter by inventory quantity less than this number
|
||||
* gt:
|
||||
* type: number
|
||||
* description: filter by inventory quantity greater than this number
|
||||
* lte:
|
||||
* type: number
|
||||
* description: filter by inventory quantity less than or equal to this number
|
||||
* gte:
|
||||
* type: number
|
||||
* description: filter by inventory quantity greater than or equal to this number
|
||||
* - in: query
|
||||
* name: created_at
|
||||
* description: Filter by a creation date range.
|
||||
* schema:
|
||||
@@ -189,6 +214,13 @@ export class AdminGetProductsVariantsParams {
|
||||
@IsOptional()
|
||||
order?: string
|
||||
|
||||
/**
|
||||
* Number filters to apply on product variants' `inventory_quantity` field.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsType([Number, NumericalComparisonOperator])
|
||||
inventory_quantity?: number | NumericalComparisonOperator
|
||||
|
||||
/**
|
||||
* Filter product variants by whether their inventory is managed or not.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
CartService,
|
||||
PricingService,
|
||||
@@ -5,16 +6,16 @@ import {
|
||||
RegionService,
|
||||
SalesChannelService,
|
||||
} from "../../../../services"
|
||||
import { IsInt, IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { AdminPriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
||||
import { PricedVariant } from "../../../../types/pricing"
|
||||
import ProductVariantService from "../../../../services/product-variant"
|
||||
import { Type } from "class-transformer"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import { omit } from "lodash"
|
||||
import ProductVariantService from "../../../../services/product-variant"
|
||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
||||
import { AdminPriceSelectionParams } from "../../../../types/price-selection"
|
||||
import { PricedVariant } from "../../../../types/pricing"
|
||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
|
||||
/**
|
||||
* @oas [get] /admin/variants
|
||||
@@ -40,6 +41,9 @@ import { omit } from "lodash"
|
||||
* - (query) fields {string} "Comma-separated fields that should be included in the returned product variants."
|
||||
* - (query) offset=0 {number} The number of product variants to skip when retrieving the product variants.
|
||||
* - (query) limit=100 {number} Limit the number of product variants returned.
|
||||
* - (query) order {string} The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
|
||||
* - (query) manage_inventory {boolean} Filter product variants by whether their inventory is managed or not.
|
||||
* - (query) allow_backorder {boolean} Filter product variants by whether they are allowed to be backordered or not.
|
||||
* - in: query
|
||||
* name: cart_id
|
||||
* style: form
|
||||
@@ -320,4 +324,27 @@ export class AdminGetVariantsParams extends AdminPriceSelectionParams {
|
||||
@IsOptional()
|
||||
@IsType([Number, NumericalComparisonOperator])
|
||||
inventory_quantity?: number | NumericalComparisonOperator
|
||||
|
||||
/**
|
||||
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
|
||||
/**
|
||||
* Filter product variants by whether their inventory is managed or not.
|
||||
*/
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
|
||||
manage_inventory?: boolean
|
||||
|
||||
/**
|
||||
* Filter product variants by whether they are allowed to be backordered or not.
|
||||
*/
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => optionalBooleanMapper.get(value.toLowerCase()))
|
||||
allow_backorder?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user