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:
Kasper Fabricius Kristensen
2024-03-25 17:18:24 +00:00
committed by GitHub
parent 20132d7cea
commit 26531c5a38
54 changed files with 3414 additions and 536 deletions
+11
View File
@@ -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,
@@ -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"
@@ -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",
@@ -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",
}
@@ -0,0 +1,5 @@
import { createContext } from "react"
import { CreateDraftOrderContextValue } from "./types"
export const CreateDraftOrderContext =
createContext<CreateDraftOrderContextValue | null>(null)
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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
}
@@ -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]
)
}
@@ -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
}
}
@@ -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)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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
}
@@ -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>
)
}
@@ -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
}
@@ -0,0 +1 @@
export * from "./create-draft-order-form"
@@ -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
}
@@ -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"
@@ -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(
@@ -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,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
}
@@ -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,
@@ -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>
@@ -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
}