feat(dashboard): Regions domain (#6534)
**What** - Implements new Region domain design - Adds new SplitView component for managing adding nested relations in FocusModals, eg. adding countries to a region. - Adds new Combobox component for multi select fields in forms **medusajs/ui** - Fix styling of RadioGroup.Choicebox component CLOSES CORE-1650, CORE-1671
This commit is contained in:
1 parent
0b9fcb6324
commit
44a5567d0d
46 files changed
+3013
-735
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/ui": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(ui): Left aligns text in RadioGroup.Choicebox component.
|
||||||
@@ -16,8 +16,7 @@
|
|||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.18",
|
"@ariakit/react": "^0.4.1",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
|
||||||
"@hookform/resolvers": "3.3.2",
|
"@hookform/resolvers": "3.3.2",
|
||||||
"@medusajs/icons": "workspace:^",
|
"@medusajs/icons": "workspace:^",
|
||||||
"@medusajs/ui": "workspace:^",
|
"@medusajs/ui": "workspace:^",
|
||||||
@@ -33,9 +32,11 @@
|
|||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
"i18next-http-backend": "2.4.2",
|
"i18next-http-backend": "2.4.2",
|
||||||
|
"match-sorter": "^6.3.4",
|
||||||
"medusa-react": "workspace:^",
|
"medusa-react": "workspace:^",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-focus-lock": "^2.11.1",
|
||||||
"react-hook-form": "7.49.1",
|
"react-hook-form": "7.49.1",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"prev": "Prev",
|
"prev": "Prev",
|
||||||
"is": "is",
|
"is": "is",
|
||||||
|
"select": "Select",
|
||||||
|
"selected": "Selected",
|
||||||
"extensions": "Extensions",
|
"extensions": "Extensions",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -38,7 +40,8 @@
|
|||||||
"noRecordsTitle": "No records",
|
"noRecordsTitle": "No records",
|
||||||
"noRecordsMessage": "There are no records to show",
|
"noRecordsMessage": "There are no records to show",
|
||||||
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
||||||
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page."
|
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
|
||||||
|
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved."
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
@@ -49,7 +52,8 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"clearAll": "Clear all"
|
||||||
},
|
},
|
||||||
"errorBoundary": {
|
"errorBoundary": {
|
||||||
"badRequestTitle": "Bad request",
|
"badRequestTitle": "Bad request",
|
||||||
@@ -252,18 +256,48 @@
|
|||||||
"domain": "Regions",
|
"domain": "Regions",
|
||||||
"createRegion": "Create Region",
|
"createRegion": "Create Region",
|
||||||
"createRegionHint": "Manage tax rates and providers for a set of countries.",
|
"createRegionHint": "Manage tax rates and providers for a set of countries.",
|
||||||
|
"addCountries": "Add countries",
|
||||||
"editRegion": "Edit Region",
|
"editRegion": "Edit Region",
|
||||||
|
"countriesHint": "Add the countries that should be included in this region.",
|
||||||
"deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.",
|
"deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.",
|
||||||
|
"removeCountriesWarning_one": "You are about to remove {{count}} country from the region. This action cannot be undone.",
|
||||||
|
"removeCountriesWarning_other": "You are about to remove {{count}} countries from the region. This action cannot be undone.",
|
||||||
|
"removeCountryWarning": "You are about to remove the country {{name}} from the region. This action cannot be undone.",
|
||||||
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
|
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
|
||||||
"providersHint": "The providers that are available in the region.",
|
"providersHint": " Add which fulfillment and payment providers should be available in this region.",
|
||||||
"shippingOptions": "Shipping Options",
|
"shippingOptions": "Shipping Options",
|
||||||
"returnShippingOptions": "Return Shipping Options",
|
"deleteShippingOptionWarning": "You are about to delete the shipping option {{name}}. This action cannot be undone.",
|
||||||
"return": "Return",
|
"return": "Return",
|
||||||
"outbound": "Outbound",
|
"outbound": "Outbound",
|
||||||
"priceType": "Price Type",
|
"priceType": "Price Type",
|
||||||
"flatRate": "Flat Rate",
|
"flatRate": "Flat Rate",
|
||||||
|
"calculated": "Calculated",
|
||||||
|
"shippingOption": {
|
||||||
|
"createShippingOption": "Create Shipping Option",
|
||||||
|
"createShippingOptionHint": "Create a new shipping option for the region.",
|
||||||
|
"editShippingOption": "Edit Shipping Option",
|
||||||
|
"fulfillmentMethod": "Fulfillment Method",
|
||||||
|
"type": {
|
||||||
|
"outbound": "Outbound",
|
||||||
|
"outboundHint": "Use this if you are creating a shipping option for sending products to the customer.",
|
||||||
|
"return": "Return",
|
||||||
|
"returnHint": "Use this if you are creating a shipping option for the customer to return products to you."
|
||||||
|
},
|
||||||
|
"priceType": {
|
||||||
|
"label": "Price Type",
|
||||||
|
"flatRate": "Flat rate",
|
||||||
"calculated": "Calculated"
|
"calculated": "Calculated"
|
||||||
},
|
},
|
||||||
|
"availability": {
|
||||||
|
"adminOnly": "Admin only",
|
||||||
|
"adminOnlyHint": "When enabled the shipping option will only be available in the admin dashboard, and not in the storefront."
|
||||||
|
},
|
||||||
|
"requirements": {
|
||||||
|
"label": "Requirements",
|
||||||
|
"hint": "Specify the requirements for the shipping option."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"locations": {
|
"locations": {
|
||||||
"domain": "Locations",
|
"domain": "Locations",
|
||||||
"createLocation": "Create location",
|
"createLocation": "Create location",
|
||||||
@@ -370,6 +404,7 @@
|
|||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"paymentProviders": "Payment Providers",
|
"paymentProviders": "Payment Providers",
|
||||||
"fulfillmentProviders": "Fulfillment Providers",
|
"fulfillmentProviders": "Fulfillment Providers",
|
||||||
|
"fulfillmentProvider": "Fulfillment Provider",
|
||||||
"providers": "Providers",
|
"providers": "Providers",
|
||||||
"availability": "Availability",
|
"availability": "Availability",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
@@ -430,6 +465,9 @@
|
|||||||
"material": "Material",
|
"material": "Material",
|
||||||
"thumbnail": "Thumbnail",
|
"thumbnail": "Thumbnail",
|
||||||
"sku": "SKU",
|
"sku": "SKU",
|
||||||
"managedInventory": "Managed inventory"
|
"managedInventory": "Managed inventory",
|
||||||
|
"minSubtotal": "Min. Subtotal",
|
||||||
|
"maxSubtotal": "Max. Subtotal",
|
||||||
|
"shippingProfile": "Shipping Profile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import { Combobox as Primitive } from "@headlessui/react"
|
import {
|
||||||
import { EllipseMiniSolid, TrianglesMini } from "@medusajs/icons"
|
Combobox as PrimitiveCombobox,
|
||||||
import { Product } from "@medusajs/medusa"
|
ComboboxItem as PrimitiveComboboxItem,
|
||||||
import { clx } from "@medusajs/ui"
|
ComboboxItemCheck as PrimitiveComboboxItemCheck,
|
||||||
|
ComboboxItemValue as PrimitiveComboboxItemValue,
|
||||||
|
ComboboxList as PrimitiveComboboxList,
|
||||||
|
ComboboxProvider as PrimitiveComboboxProvider,
|
||||||
|
} from "@ariakit/react"
|
||||||
|
import { EllipseMiniSolid, TrianglesMini, XMarkMini } from "@medusajs/icons"
|
||||||
|
import { Text, clx } from "@medusajs/ui"
|
||||||
import * as Popover from "@radix-ui/react-popover"
|
import * as Popover from "@radix-ui/react-popover"
|
||||||
import { useAdminProducts } from "medusa-react"
|
import { matchSorter } from "match-sorter"
|
||||||
import {
|
import {
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
ElementRef,
|
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -24,370 +25,204 @@ type ComboboxOption = {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComboboxProps = {
|
interface ComboboxProps
|
||||||
size?: "base" | "small"
|
extends Omit<ComponentPropsWithoutRef<"input">, "onChange" | "value"> {
|
||||||
|
value?: string[]
|
||||||
|
onChange?: (value?: string[]) => void
|
||||||
options: ComboboxOption[]
|
options: ComboboxOption[]
|
||||||
value: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Combobox = ({ size = "base" }: ComboboxProps) => {
|
export const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||||
const [product, setProduct] = useState<Product | null>(null)
|
|
||||||
const [query, setQuery] = useState("")
|
|
||||||
const { products, count, isLoading } = useAdminProducts(
|
|
||||||
{
|
|
||||||
q: query,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Primitive by="id" value={product} onChange={setProduct}>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Primitive.Input
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
displayValue={(value: Product) => value?.title}
|
|
||||||
className={clx(
|
|
||||||
"bg-ui-bg-field shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none",
|
|
||||||
"placeholder:text-ui-fg-muted text-ui-fg-base",
|
|
||||||
"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",
|
|
||||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
|
||||||
{
|
|
||||||
"h-8 px-2 py-1.5 txt-compact-small": size === "base",
|
|
||||||
"h-7 px-2 py-1 txt-compact-small": size === "small",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Primitive.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
||||||
<TrianglesMini className="text-ui-fg-muted" aria-hidden="true" />
|
|
||||||
</Primitive.Button>
|
|
||||||
</div>
|
|
||||||
<Primitive.Options className="absolute mt-2 max-h-[200px] w-full overflow-auto z-10 bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout rounded-lg p-1">
|
|
||||||
{products?.map((p) => (
|
|
||||||
<Primitive.Option
|
|
||||||
key={p.id}
|
|
||||||
value={p}
|
|
||||||
className={clx(
|
|
||||||
"bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 items-center rounded-md px-3 py-2 outline-none transition-colors",
|
|
||||||
"ui-active:bg-ui-bg-base-hover",
|
|
||||||
{
|
|
||||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "base",
|
|
||||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "small",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-5 h-5 flex items-center justify-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<EllipseMiniSolid className="ui-selected:block hidden" />
|
|
||||||
</div>
|
|
||||||
<span className="block truncate ui-selected:font-medium">
|
|
||||||
{p.title}
|
|
||||||
</span>
|
|
||||||
</Primitive.Option>
|
|
||||||
))}
|
|
||||||
</Primitive.Options>
|
|
||||||
</div>
|
|
||||||
</Primitive>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ComboboxContextValue = {
|
|
||||||
size: "base" | "small"
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ComboboxContext = createContext<ComboboxContextValue | null>(null)
|
|
||||||
|
|
||||||
const useComboboxContext = () => {
|
|
||||||
const context = useContext(ComboboxContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"Combobox compound components cannot be rendered outside the Combobox component"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
const Root = forwardRef<
|
|
||||||
ElementRef<typeof Primitive>,
|
|
||||||
Omit<ComponentPropsWithoutRef<typeof Primitive>, "children"> & {
|
|
||||||
className?: string
|
|
||||||
size?: "base" | "small"
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
>(({ children, className, size = "base", ...props }, ref) => {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const value = useMemo(() => ({ size, open, setOpen }), [size, open])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComboboxContext.Provider value={value}>
|
|
||||||
<Primitive {...props} ref={ref}>
|
|
||||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
||||||
{children}
|
|
||||||
</Popover.Root>
|
|
||||||
</Primitive>
|
|
||||||
</ComboboxContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Root.displayName = "Combobox"
|
|
||||||
|
|
||||||
const Trigger = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<"div">>(
|
|
||||||
({ className, children, ...props }, ref) => {
|
|
||||||
const { size } = useComboboxContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover.Trigger asChild>
|
|
||||||
<div
|
|
||||||
className={clx(
|
|
||||||
"relative bg-ui-bg-field shadow-buttons-neutral transition-fg w-full select-none items-center justify-between rounded-md outline-none",
|
|
||||||
"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",
|
|
||||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
|
||||||
{
|
|
||||||
"h-8 px-2 py-1.5 txt-compact-small": size === "base",
|
|
||||||
"h-7 px-2 py-1 txt-compact-small": size === "small",
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Primitive.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
||||||
<TrianglesMini className="text-ui-fg-muted" aria-hidden="true" />
|
|
||||||
</Primitive.Button>
|
|
||||||
</div>
|
|
||||||
</Popover.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Trigger.displayName = "Combobox.Trigger"
|
|
||||||
|
|
||||||
const Value = forwardRef<
|
|
||||||
ElementRef<typeof Primitive.Input>,
|
|
||||||
ComponentPropsWithoutRef<typeof Primitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const { size } = useComboboxContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Primitive.Input
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
className={clx(
|
|
||||||
"placeholder:text-ui-fg-muted text-ui-fg-base outline-none bg-transparent w-full",
|
|
||||||
"disabled:!text-ui-fg-disabled",
|
|
||||||
{
|
|
||||||
" txt-compact-small": size === "base",
|
|
||||||
"txt-compact-small": size === "small",
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Value.displayName = "Combobox.Value"
|
|
||||||
|
|
||||||
const Item = forwardRef<
|
|
||||||
ElementRef<typeof Primitive.Option>,
|
|
||||||
Omit<ComponentPropsWithoutRef<typeof Primitive.Option>, "children"> & {
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
>(({ children, className, ...props }, ref) => {
|
|
||||||
const { size } = useComboboxContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Primitive.Option
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
className={clx(
|
|
||||||
"bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 items-center rounded-md px-2 py-1.5 outline-none transition-colors",
|
|
||||||
"ui-active:bg-ui-bg-base-hover",
|
|
||||||
{
|
|
||||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "base",
|
|
||||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "small",
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-5 h-5 flex items-center justify-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<EllipseMiniSolid className="ui-selected:block hidden" />
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</Primitive.Option>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Item.displayName = "Combobox.Item"
|
|
||||||
|
|
||||||
const NoResults = forwardRef<
|
|
||||||
ElementRef<"span">,
|
|
||||||
ComponentPropsWithoutRef<"span">
|
|
||||||
>(({ children, className, ...props }, ref) => {
|
|
||||||
const { size } = useComboboxContext()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
className={clx(
|
|
||||||
"bg-ui-bg-base items-center flex w-full justify-center rounded-md px-2 py-1.5 outline-none transition-colors",
|
|
||||||
"ui-active:bg-ui-bg-base-hover",
|
|
||||||
{
|
|
||||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "base",
|
|
||||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
|
||||||
size === "small",
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children ?? t("general.noResultsTitle")}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
Item.displayName = "Combobox.NoResults"
|
|
||||||
|
|
||||||
const Content = forwardRef<
|
|
||||||
ElementRef<typeof Popover.Content>,
|
|
||||||
ComponentPropsWithoutRef<typeof Popover.Content>
|
|
||||||
>(
|
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
children,
|
value: controlledValue,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
className,
|
className,
|
||||||
side = "bottom",
|
placeholder,
|
||||||
sideOffset = 8,
|
...inputProps
|
||||||
collisionPadding = 24,
|
|
||||||
...props
|
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const comboboxRef = useRef<HTMLInputElement>(null)
|
||||||
|
const listboxRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => comboboxRef.current!)
|
||||||
|
|
||||||
|
const isControlled = controlledValue !== undefined
|
||||||
|
const [searchValue, setSearchValue] = useState("")
|
||||||
|
const [uncontrolledValue, setUncontrolledValue] = useState<string[]>([])
|
||||||
|
|
||||||
|
const selectedValues = isControlled ? controlledValue : uncontrolledValue
|
||||||
|
|
||||||
|
const handleValueChange = (newValues?: string[]) => {
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledValue(newValues || [])
|
||||||
|
}
|
||||||
|
if (onChange) {
|
||||||
|
onChange(newValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter and sort the options based on the search value,
|
||||||
|
* and whether the value is already selected.
|
||||||
|
*/
|
||||||
|
const matches = useMemo(() => {
|
||||||
|
return matchSorter(options, searchValue, {
|
||||||
|
keys: ["label"],
|
||||||
|
baseSort: (a, b) => {
|
||||||
|
const aIndex = selectedValues.indexOf(a.item.value)
|
||||||
|
const bIndex = selectedValues.indexOf(b.item.value)
|
||||||
|
|
||||||
|
if (aIndex === -1 && bIndex === -1) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aIndex === -1) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bIndex === -1) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return aIndex - bIndex
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [options, searchValue, selectedValues])
|
||||||
|
|
||||||
|
const hasValues = selectedValues.length > 0
|
||||||
|
const showSelected = hasValues && !searchValue && !open
|
||||||
|
const hidePlaceholder = showSelected || open
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Portal>
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||||
<Popover.Content
|
<PrimitiveComboboxProvider
|
||||||
ref={ref}
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
selectedValue={selectedValues}
|
||||||
|
setSelectedValue={handleValueChange}
|
||||||
|
setValue={(value) => {
|
||||||
|
setSearchValue(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover.Anchor asChild>
|
||||||
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout relative max-h-[120px] h-full min-w-[var(--radix-popper-anchor-width)] overflow-hidden rounded-lg flex flex-col divide-y",
|
"relative flex cursor-pointer items-center gap-x-2 overflow-hidden",
|
||||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
"h-8 w-full rounded-md px-2 py-0.5",
|
||||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"hover:bg-ui-bg-field-hover",
|
||||||
|
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||||
|
"has-[:invalid]:shadow-borders-error",
|
||||||
|
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||||
|
{
|
||||||
|
"pl-0.5": hasValues,
|
||||||
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
collisionPadding={collisionPadding}
|
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<Primitive.Options
|
{hasValues && (
|
||||||
static={true}
|
<div className="bg-ui-bg-base txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive relative flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1">
|
||||||
className={clx("p-1 flex-1 overflow-auto")}
|
<span>{selectedValues.length}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="size-fit outline-none"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleValueChange(undefined)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
<XMarkMini className="text-ui-fg-muted" />
|
||||||
</Primitive.Options>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative flex size-full items-center">
|
||||||
|
{showSelected && (
|
||||||
|
<Text size="small" leading="compact">
|
||||||
|
{t("general.selected")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<PrimitiveCombobox
|
||||||
|
ref={comboboxRef}
|
||||||
|
className="txt-compact-small text-ui-fg-base placeholder:text-ui-fg-subtle size-full cursor-pointer bg-transparent pr-7 outline-none focus:cursor-text"
|
||||||
|
placeholder={hidePlaceholder ? undefined : placeholder}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="text-ui-fg-muted pointer-events-none absolute right-2 size-fit outline-none"
|
||||||
|
>
|
||||||
|
<TrianglesMini />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Popover.Anchor>
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content
|
||||||
|
asChild
|
||||||
|
align="center"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={8}
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
onInteractOutside={(event) => {
|
||||||
|
const target = event.target as Element | null
|
||||||
|
const isCombobox = target === comboboxRef.current
|
||||||
|
const inListbox = target && listboxRef.current?.contains(target)
|
||||||
|
|
||||||
|
if (isCombobox || inListbox) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PrimitiveComboboxList
|
||||||
|
ref={listboxRef}
|
||||||
|
role="listbox"
|
||||||
|
className={clx(
|
||||||
|
"shadow-elevation-flyout bg-ui-bg-base w-[var(--radix-popper-anchor-width)] rounded-[8px] p-1",
|
||||||
|
"max-h-[200px] overflow-y-auto",
|
||||||
|
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||||
|
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{matches.map(({ value, label }) => (
|
||||||
|
<PrimitiveComboboxItem
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
focusOnHover
|
||||||
|
className="transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group flex items-center gap-x-2 rounded-[4px] px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<PrimitiveComboboxItemCheck className="flex !size-5 items-center justify-center">
|
||||||
|
<EllipseMiniSolid />
|
||||||
|
</PrimitiveComboboxItemCheck>
|
||||||
|
<PrimitiveComboboxItemValue className="txt-compact-small group-aria-selected:txt-compact-small-plus">
|
||||||
|
{label}
|
||||||
|
</PrimitiveComboboxItemValue>
|
||||||
|
</PrimitiveComboboxItem>
|
||||||
|
))}
|
||||||
|
{!matches.length && (
|
||||||
|
<div className="flex items-center gap-x-2 rounded-[4px] px-2 py-1.5">
|
||||||
|
<Text
|
||||||
|
size="small"
|
||||||
|
leading="compact"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
>
|
||||||
|
{t("general.noResultsTitle")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PrimitiveComboboxList>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
|
</PrimitiveComboboxProvider>
|
||||||
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Content.displayName = "Combobox.Content"
|
Combobox.displayName = "Combobox"
|
||||||
|
|
||||||
const Pagination = forwardRef<
|
|
||||||
ElementRef<"div">,
|
|
||||||
{
|
|
||||||
isLoading?: boolean
|
|
||||||
hasNext?: boolean
|
|
||||||
onPaginate: () => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
>(({ isLoading, hasNext, onPaginate, className, ...props }, ref) => {
|
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
|
||||||
const innerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// Merge innerRef and ref
|
|
||||||
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => {
|
|
||||||
return innerRef.current
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (innerRef.current) {
|
|
||||||
observerRef.current = new IntersectionObserver((entries) => {
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
onPaginate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
observerRef.current.observe(innerRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (observerRef.current && innerRef.current) {
|
|
||||||
observerRef.current.unobserve(innerRef.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isLoading, hasNext, onPaginate, ref])
|
|
||||||
|
|
||||||
return <div ref={innerRef} className="bg-transparent w-px h-px" {...props} />
|
|
||||||
})
|
|
||||||
Pagination.displayName = "Combobox.Pagination"
|
|
||||||
|
|
||||||
const Combo = Object.assign(Root, {
|
|
||||||
Trigger,
|
|
||||||
Value,
|
|
||||||
Item,
|
|
||||||
NoResults,
|
|
||||||
Pagination,
|
|
||||||
Content,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const TestCombobox = () => {
|
|
||||||
const [product, setProduct] = useState<Product[]>([])
|
|
||||||
const [query, setQuery] = useState("")
|
|
||||||
const { products, count, isLoading } = useAdminProducts(
|
|
||||||
{
|
|
||||||
q: query,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Combo value={product} onChange={setProduct}>
|
|
||||||
<Combo.Trigger>
|
|
||||||
<Combo.Value
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
displayValue={(value: Product[]) => `${value?.length}`}
|
|
||||||
/>
|
|
||||||
</Combo.Trigger>
|
|
||||||
<Combo.Content>
|
|
||||||
{!products?.length && <Combo.NoResults />}
|
|
||||||
<Combo.Pagination isLoading onPaginate={() => console.log("Heyo!")} />
|
|
||||||
{products?.map((p) => (
|
|
||||||
<Combo.Item key={p.id} value={p}>
|
|
||||||
{p.title}
|
|
||||||
</Combo.Item>
|
|
||||||
))}
|
|
||||||
</Combo.Content>
|
|
||||||
</Combo>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./combobox"
|
||||||
@@ -100,8 +100,9 @@ const Label = forwardRef<
|
|||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitives.Root> & {
|
React.ComponentPropsWithoutRef<typeof LabelPrimitives.Root> & {
|
||||||
optional?: boolean
|
optional?: boolean
|
||||||
tooltip?: ReactNode
|
tooltip?: ReactNode
|
||||||
|
icon?: ReactNode
|
||||||
}
|
}
|
||||||
>(({ className, optional = false, tooltip, ...props }, ref) => {
|
>(({ className, optional = false, tooltip, icon, ...props }, ref) => {
|
||||||
const { formItemId } = useFormField()
|
const { formItemId } = useFormField()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ const Label = forwardRef<
|
|||||||
<InformationCircleSolid className="text-ui-fg-muted" />
|
<InformationCircleSolid className="text-ui-fg-muted" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{icon}
|
||||||
{optional && (
|
{optional && (
|
||||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||||
({t("fields.optional")})
|
({t("fields.optional")})
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { BuildingTax } from "@medusajs/icons"
|
||||||
|
import { Tooltip } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
type IncludesTaxTooltipProps = {
|
||||||
|
includesTax?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IncludesTaxTooltip = ({
|
||||||
|
includesTax,
|
||||||
|
}: IncludesTaxTooltipProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!includesTax) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={t("general.includesTaxTooltip")}>
|
||||||
|
<BuildingTax className="text-ui-fg-muted" />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./split-view"
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { Button, clx } from "@medusajs/ui"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
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
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SplitViewContext = createContext<SplitViewContextValue | null>(null)
|
||||||
|
|
||||||
|
const useSplitViewContext = () => {
|
||||||
|
const context = useContext(SplitViewContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSplitViewContext must be used within a SplitView")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitViewProps = PropsWithChildren<{
|
||||||
|
open?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}>
|
||||||
|
|
||||||
|
const Root = ({
|
||||||
|
open: controlledOpen,
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</SplitViewContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = ({ children }: PropsWithChildren) => {
|
||||||
|
const { open, onOpenChange } = useSplitViewContext()
|
||||||
|
const isLargeScreenSize = useMediaQuery("(min-width: 1024px)")
|
||||||
|
|
||||||
|
const contentWidth = !isLargeScreenSize ? "100%" : open ? "50%" : "100%"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: "100%" }}
|
||||||
|
animate={{ width: contentWidth }}
|
||||||
|
transition={isLargeScreenSize ? { duration: 0.3 } : undefined}
|
||||||
|
className="relative h-full overflow-y-auto"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SplitView is a layout component that allows you to create a split view layout within a FocusModal.
|
||||||
|
*/
|
||||||
|
export const SplitView = Object.assign(Root, {
|
||||||
|
Content,
|
||||||
|
Drawer,
|
||||||
|
Close,
|
||||||
|
Open,
|
||||||
|
})
|
||||||
+4
@@ -181,6 +181,7 @@ export const DataTableRoot = <TData,>({
|
|||||||
<Table.Body className="border-b-0">
|
<Table.Body className="border-b-0">
|
||||||
{table.getRowModel().rows.map((row) => {
|
{table.getRowModel().rows.map((row) => {
|
||||||
const to = navigateTo ? navigateTo(row) : undefined
|
const to = navigateTo ? navigateTo(row) : undefined
|
||||||
|
const isRowDisabled = hasSelect && !row.getCanSelect()
|
||||||
return (
|
return (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={row.id}
|
key={row.id}
|
||||||
@@ -192,6 +193,7 @@ export const DataTableRoot = <TData,>({
|
|||||||
"cursor-pointer": !!to,
|
"cursor-pointer": !!to,
|
||||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
row.getIsSelected(),
|
row.getIsSelected(),
|
||||||
|
"bg-ui-bg-subtle hover:bg-ui-bg-subtle": isRowDisabled,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onClick={to ? () => navigate(to) : undefined}
|
onClick={to ? () => navigate(to) : undefined}
|
||||||
@@ -220,6 +222,8 @@ export const DataTableRoot = <TData,>({
|
|||||||
isStickyCell && hasSelect && !isSelectCell,
|
isStickyCell && hasSelect && !isSelectCell,
|
||||||
"after:bg-ui-border-base":
|
"after:bg-ui-border-base":
|
||||||
showStickyBorder && isStickyCell && !isSelectCell,
|
showStickyBorder && isStickyCell && !isSelectCell,
|
||||||
|
"bg-ui-bg-subtle hover:bg-ui-bg-subtle":
|
||||||
|
isRowDisabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
|
|||||||
+13
-21
@@ -1,5 +1,4 @@
|
|||||||
import { Country } from "@medusajs/medusa"
|
import { Country } from "@medusajs/medusa"
|
||||||
import { Tooltip } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
@@ -19,28 +18,21 @@ export const CountriesCell = ({ countries }: CountriesCellProps) => {
|
|||||||
.map((c) => c.display_name)
|
.map((c) => c.display_name)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|
||||||
const additionalCountries = countries.slice(2).map((c) => c.display_name)
|
const additionalCountries = countries
|
||||||
|
.slice(2)
|
||||||
|
.map((c) => c.display_name).length
|
||||||
|
|
||||||
|
const text = `${displayValue}${
|
||||||
|
additionalCountries > 0
|
||||||
|
? ` ${t("general.plusCountMore", {
|
||||||
|
count: additionalCountries,
|
||||||
|
})}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center gap-x-1">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span>{displayValue}</span>
|
<span className="truncate">{text}</span>
|
||||||
{additionalCountries.length > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<ul>
|
|
||||||
{additionalCountries.map((c) => (
|
|
||||||
<li key={c}>{c}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: additionalCountries.length,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-24
@@ -1,6 +1,6 @@
|
|||||||
import { FulfillmentProvider } from "@medusajs/medusa"
|
import { FulfillmentProvider } from "@medusajs/medusa"
|
||||||
import { Tooltip } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
type FulfillmentProvidersCellProps = {
|
type FulfillmentProvidersCellProps = {
|
||||||
@@ -18,31 +18,22 @@ export const FulfillmentProvidersCell = ({
|
|||||||
|
|
||||||
const displayValue = fulfillmentProviders
|
const displayValue = fulfillmentProviders
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((p) => p.id)
|
.map((p) => formatProvider(p.id))
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|
||||||
const additionalProviders = fulfillmentProviders.slice(2).map((c) => c.id)
|
const additionalProviders = fulfillmentProviders.slice(2).length
|
||||||
|
|
||||||
|
const text = `${displayValue}${
|
||||||
|
additionalProviders > 0
|
||||||
|
? ` ${t("general.plusCountMore", {
|
||||||
|
count: additionalProviders,
|
||||||
|
})}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center gap-x-1">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span>{displayValue}</span>
|
<span className="truncate">{text}</span>
|
||||||
{additionalProviders.length > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<ul>
|
|
||||||
{additionalProviders.map((c) => (
|
|
||||||
<li key={c}>{c}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: additionalProviders.length,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,8 +42,8 @@ export const FulfillmentProvidersHeader = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span>{t("fields.fulfillmentProviders")}</span>
|
<span className="truncate">{t("fields.fulfillmentProviders")}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+15
-24
@@ -1,6 +1,6 @@
|
|||||||
import { PaymentProvider } from "@medusajs/medusa"
|
import { PaymentProvider } from "@medusajs/medusa"
|
||||||
import { Tooltip } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||||
|
|
||||||
type PaymentProvidersCellProps = {
|
type PaymentProvidersCellProps = {
|
||||||
@@ -18,31 +18,22 @@ export const PaymentProvidersCell = ({
|
|||||||
|
|
||||||
const displayValue = paymentProviders
|
const displayValue = paymentProviders
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((p) => p.id)
|
.map((p) => formatProvider(p.id))
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
|
||||||
const additionalProviders = paymentProviders.slice(2).map((c) => c.id)
|
const additionalProviders = paymentProviders.slice(2).length
|
||||||
|
|
||||||
|
const text = `${displayValue}${
|
||||||
|
additionalProviders > 0
|
||||||
|
? ` ${t("general.plusCountMore", {
|
||||||
|
count: additionalProviders,
|
||||||
|
})}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center gap-x-1">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span>{displayValue}</span>
|
<span className="truncate">{text}</span>
|
||||||
{additionalProviders.length > 0 && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<ul>
|
|
||||||
{additionalProviders.map((c) => (
|
|
||||||
<li key={c}>{c}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: additionalProviders.length,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,8 +42,8 @@ export const PaymentProvidersHeader = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center">
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
<span>{t("fields.paymentProviders")}</span>
|
<span className="truncate">{t("fields.paymentProviders")}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export const useMediaQuery = (query: string) => {
|
||||||
|
const mediaQuery = window.matchMedia(query)
|
||||||
|
const [matches, setMatches] = useState(mediaQuery.matches)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
|
||||||
|
mediaQuery.addEventListener("change", handler)
|
||||||
|
return () => mediaQuery.removeEventListener("change", handler)
|
||||||
|
}, [mediaQuery])
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Providers only have an ID to identify them. This function formats the ID
|
||||||
|
* into a human-readable string.
|
||||||
|
* @param id - The ID of the provider
|
||||||
|
* @returns A formatted string
|
||||||
|
*/
|
||||||
|
export const formatProvider = (id: string) => {
|
||||||
|
return id
|
||||||
|
.split("-")
|
||||||
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
@@ -477,6 +477,25 @@ const router = createBrowserRouter([
|
|||||||
path: "edit",
|
path: "edit",
|
||||||
lazy: () => import("../../routes/regions/region-edit"),
|
lazy: () => import("../../routes/regions/region-edit"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "countries/add",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/regions/region-add-countries"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shipping-options/:so_id/edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/regions/region-edit-shipping-option"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "shipping-options/create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/regions/region-create-shipping-option"
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Country, Region } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Button, Checkbox } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateRegion } from "medusa-react"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { countries as staticCountries } from "../../../../../lib/countries"
|
||||||
|
import { useCountries } from "../../../shared/hooks/use-countries"
|
||||||
|
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
|
||||||
|
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
|
||||||
|
|
||||||
|
type AddCountriesFormProps = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddCountriesSchema = zod.object({
|
||||||
|
countries: zod.array(zod.string()).min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
const PREFIX = "ac"
|
||||||
|
|
||||||
|
export const AddCountriesForm = ({ region }: AddCountriesFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof AddCountriesSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
countries: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AddCountriesSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { setValue } = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ids = Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
|
||||||
|
setValue("countries", ids, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
}, [rowSelection, setValue])
|
||||||
|
|
||||||
|
const { searchParams, raw } = useCountryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { countries, count } = useCountries({
|
||||||
|
countries: staticCountries.map((c, i) => ({
|
||||||
|
display_name: c.display_name,
|
||||||
|
name: c.name,
|
||||||
|
id: i,
|
||||||
|
iso_2: c.iso_2,
|
||||||
|
iso_3: c.iso_3,
|
||||||
|
num_code: c.num_code,
|
||||||
|
region_id: null,
|
||||||
|
region: {} as Region,
|
||||||
|
})),
|
||||||
|
...searchParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: countries || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: (row) => {
|
||||||
|
return (
|
||||||
|
region.countries?.findIndex((c) => c.iso_2 === row.original.iso_2) ===
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.iso_2,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater: setRowSelection,
|
||||||
|
},
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
const payload = [
|
||||||
|
...region.countries.map((c) => c.iso_2),
|
||||||
|
...values.countries,
|
||||||
|
]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
countries: payload,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" isLoading={isLoading} type="submit">
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
layout="fill"
|
||||||
|
orderBy={["name", "code"]}
|
||||||
|
queryObject={raw}
|
||||||
|
prefix={PREFIX}
|
||||||
|
/>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Country>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useCountryTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isPreselected = !row.getCanSelect()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected() || isPreselected}
|
||||||
|
disabled={isPreselected}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
) as ColumnDef<Country>[]
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-countries-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { RegionAddCountries as Component } from "./region-add-countries"
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { useAdminRegion } from "medusa-react"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { AddCountriesForm } from "./components/add-countries-form"
|
||||||
|
|
||||||
|
export const RegionAddCountries = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{!isLoading && region && <AddCountriesForm region={region} />}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+558
@@ -0,0 +1,558 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import {
|
||||||
|
FulfillmentOption,
|
||||||
|
Region,
|
||||||
|
ShippingOptionRequirement,
|
||||||
|
ShippingProfile,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CurrencyInput,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
RadioGroup,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useAdminCreateShippingOption } from "medusa-react"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { IncludesTaxTooltip } from "../../../../../components/common/tax-badge/tax-badge"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { ShippingOptionPriceType } from "../../../shared/constants"
|
||||||
|
|
||||||
|
type CreateShippingOptionProps = {
|
||||||
|
region: Region
|
||||||
|
shippingProfiles: ShippingProfile[]
|
||||||
|
fulfillmentOptions: FulfillmentOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ShippingOptionType {
|
||||||
|
OUTBOUND = "outbound",
|
||||||
|
RETURN = "return",
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateShippingOptionSchema = zod
|
||||||
|
.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
type: zod.nativeEnum(ShippingOptionType),
|
||||||
|
admin_only: zod.boolean(),
|
||||||
|
provider_id: zod.string().min(1),
|
||||||
|
profile_id: zod.string().min(1),
|
||||||
|
includes_tax: zod.boolean(),
|
||||||
|
price_type: zod.nativeEnum(ShippingOptionPriceType),
|
||||||
|
amount: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Amount must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
min_subtotal: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Min. subtotal must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
max_subtotal: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Max. subtotal must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.price_type === ShippingOptionPriceType.FLAT_RATE) {
|
||||||
|
if (typeof data.amount === "string" && data.amount === "") {
|
||||||
|
return ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Amount is required",
|
||||||
|
path: ["amount"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateShippingOptionForm = ({
|
||||||
|
region,
|
||||||
|
fulfillmentOptions,
|
||||||
|
shippingProfiles,
|
||||||
|
}: CreateShippingOptionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof CreateShippingOptionSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
type: ShippingOptionType.OUTBOUND,
|
||||||
|
admin_only: false,
|
||||||
|
provider_id: "",
|
||||||
|
profile_id: "",
|
||||||
|
price_type: ShippingOptionPriceType.FLAT_RATE,
|
||||||
|
includes_tax: false,
|
||||||
|
amount: "",
|
||||||
|
min_subtotal: "",
|
||||||
|
max_subtotal: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateShippingOptionSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedPriceType = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "price_type",
|
||||||
|
defaultValue: ShippingOptionPriceType.FLAT_RATE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlatRate = watchedPriceType === ShippingOptionPriceType.FLAT_RATE
|
||||||
|
|
||||||
|
const includesTax = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "includes_tax",
|
||||||
|
defaultValue: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminCreateShippingOption()
|
||||||
|
|
||||||
|
const getPricePayload = (amount?: string | number) => {
|
||||||
|
if (!amount) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountValue = typeof amount === "string" ? Number(amount) : amount
|
||||||
|
return getDbAmount(amountValue, region.currency_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
const { type, amount, min_subtotal, max_subtotal, ...rest } = values
|
||||||
|
|
||||||
|
const amountPayload = getPricePayload(amount)
|
||||||
|
|
||||||
|
const minSubtotalPayload = getPricePayload(min_subtotal)
|
||||||
|
const maxSubtotalPayload = getPricePayload(max_subtotal)
|
||||||
|
|
||||||
|
const minSubtotalRequirement = minSubtotalPayload
|
||||||
|
? {
|
||||||
|
amount: minSubtotalPayload,
|
||||||
|
type: "min_subtotal",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const maxSubtotalRequirement = maxSubtotalPayload
|
||||||
|
? {
|
||||||
|
amount: maxSubtotalPayload,
|
||||||
|
type: "max_subtotal",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const requirements = [
|
||||||
|
minSubtotalRequirement,
|
||||||
|
maxSubtotalRequirement,
|
||||||
|
].filter(Boolean) as ShippingOptionRequirement[]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
region_id: region.id,
|
||||||
|
data: {},
|
||||||
|
is_return: type === ShippingOptionType.RETURN,
|
||||||
|
amount: isFlatRate ? amountPayload : undefined,
|
||||||
|
requirements: requirements.length ? requirements : undefined,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<RouteFocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
|
||||||
|
)}
|
||||||
|
id="form-section"
|
||||||
|
>
|
||||||
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
|
<div>
|
||||||
|
<Heading>
|
||||||
|
{t("regions.shippingOption.createShippingOption")}
|
||||||
|
</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("regions.shippingOption.createShippingOptionHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.type")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<RadioGroup
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="grid grid-cols-1 gap-4 md:grid-cols-2"
|
||||||
|
>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={ShippingOptionType.OUTBOUND}
|
||||||
|
label={t("regions.shippingOption.type.outbound")}
|
||||||
|
description={t(
|
||||||
|
"regions.shippingOption.type.outboundHint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<RadioGroup.ChoiceBox
|
||||||
|
value={ShippingOptionType.RETURN}
|
||||||
|
label={t("regions.shippingOption.type.return")}
|
||||||
|
description={t(
|
||||||
|
"regions.shippingOption.type.returnHint"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="admin_only"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Form.Label>
|
||||||
|
{t("regions.shippingOption.availability.adminOnly")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>
|
||||||
|
{t(
|
||||||
|
"regions.shippingOption.availability.adminOnlyHint"
|
||||||
|
)}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="includes_tax"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.taxInclusivePricing")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="price_type"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("regions.shippingOption.priceType.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select onValueChange={onChange} {...field}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item
|
||||||
|
value={ShippingOptionPriceType.FLAT_RATE}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"regions.shippingOption.priceType.flatRate"
|
||||||
|
)}
|
||||||
|
</Select.Item>
|
||||||
|
<Select.Item
|
||||||
|
value={ShippingOptionPriceType.CALCULATED}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"regions.shippingOption.priceType.calculated"
|
||||||
|
)}
|
||||||
|
</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isFlatRate && (
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="amount"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={
|
||||||
|
<IncludesTaxTooltip includesTax={includesTax} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("fields.price")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="profile_id"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.shippingProfile")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select onValueChange={onChange} {...field}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{shippingProfiles.map((profile) => (
|
||||||
|
<Select.Item
|
||||||
|
key={profile.id}
|
||||||
|
value={profile.id}
|
||||||
|
>
|
||||||
|
{profile.name}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="provider_id"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.fulfillmentProvider")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select onValueChange={onChange} {...field}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{fulfillmentOptions.map((option) => (
|
||||||
|
<Select.Item
|
||||||
|
key={option.provider_id}
|
||||||
|
value={option.provider_id}
|
||||||
|
>
|
||||||
|
{formatProvider(option.provider_id)}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("regions.shippingOption.requirements.label")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("regions.shippingOption.requirements.hint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="min_subtotal"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={
|
||||||
|
<IncludesTaxTooltip includesTax={includesTax} />
|
||||||
|
}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{t("fields.minSubtotal")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="max_subtotal"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={
|
||||||
|
<IncludesTaxTooltip includesTax={includesTax} />
|
||||||
|
}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{t("fields.maxSubtotal")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-shipping-option-form"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { RegionCreateShippingOption as Component } from "./region-create-shipping-option"
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
useAdminRegion,
|
||||||
|
useAdminRegionFulfillmentOptions,
|
||||||
|
useAdminShippingProfiles,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
|
import { CreateShippingOptionForm } from "./components/create-shipping-option-form"
|
||||||
|
|
||||||
|
export const RegionCreateShippingOption = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const {
|
||||||
|
region,
|
||||||
|
isLoading: isLoadingRegion,
|
||||||
|
isError: isRegionError,
|
||||||
|
error: regionError,
|
||||||
|
} = useAdminRegion(id!)
|
||||||
|
|
||||||
|
const {
|
||||||
|
shipping_profiles,
|
||||||
|
isLoading: isLoadingProfiles,
|
||||||
|
isError: isProfilesError,
|
||||||
|
error: profileError,
|
||||||
|
} = useAdminShippingProfiles()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fulfillment_options,
|
||||||
|
isLoading: isLoadingOptions,
|
||||||
|
isError: isOptionsError,
|
||||||
|
error: optionsError,
|
||||||
|
} = useAdminRegionFulfillmentOptions(id!)
|
||||||
|
|
||||||
|
const isLoading = isLoadingProfiles || isLoadingOptions || isLoadingRegion
|
||||||
|
|
||||||
|
if (isRegionError) {
|
||||||
|
throw regionError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProfilesError) {
|
||||||
|
throw profileError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOptionsError) {
|
||||||
|
throw optionsError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
{!isLoading && region && shipping_profiles && fulfillment_options && (
|
||||||
|
<CreateShippingOptionForm
|
||||||
|
region={region}
|
||||||
|
fulfillmentOptions={fulfillment_options}
|
||||||
|
shippingProfiles={shipping_profiles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+361
-25
@@ -1,25 +1,82 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Button, Heading, Input, Select, Switch, Text } from "@medusajs/ui"
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
import { useAdminCreateRegion, useAdminStore } from "medusa-react"
|
import {
|
||||||
import { useForm } from "react-hook-form"
|
Country,
|
||||||
|
Currency,
|
||||||
|
FulfillmentProvider,
|
||||||
|
PaymentProvider,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useAdminCreateRegion } from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { SplitView } from "../../../../../components/layout/split-view"
|
||||||
|
import { useRouteModal } from "../../../../../components/route-modal"
|
||||||
import { RouteFocusModal } from "../../../../../components/route-modal/route-focus-modal"
|
import { RouteFocusModal } from "../../../../../components/route-modal/route-focus-modal"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { countries as staticCountries } from "../../../../../lib/countries"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
import { useCountries } from "../../../shared/hooks/use-countries"
|
||||||
|
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
|
||||||
|
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
|
||||||
|
|
||||||
|
type CreateRegionFormProps = {
|
||||||
|
currencies: Currency[]
|
||||||
|
paymentProviders: PaymentProvider[]
|
||||||
|
fulfillmentProviders: FulfillmentProvider[]
|
||||||
|
}
|
||||||
|
|
||||||
const CreateRegionSchema = zod.object({
|
const CreateRegionSchema = zod.object({
|
||||||
name: zod.string().min(1),
|
name: zod.string().min(1),
|
||||||
currency_code: zod.string(),
|
currency_code: zod.string().min(2, "Select a currency"),
|
||||||
includes_tax: zod.boolean(),
|
includes_tax: zod.boolean(),
|
||||||
countries: zod.array(zod.string()),
|
countries: zod.array(zod.object({ code: zod.string(), name: zod.string() })),
|
||||||
fulfillment_providers: zod.array(zod.string()).min(1),
|
fulfillment_providers: zod.array(zod.string()).min(1),
|
||||||
payment_providers: zod.array(zod.string()).min(1),
|
payment_providers: zod.array(zod.string()).min(1),
|
||||||
tax_rate: zod.number().min(0).max(1),
|
tax_rate: zod.union([zod.string(), zod.number()]).refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (num >= 0 && num <= 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, "Tax rate must be a number between 0 and 100"),
|
||||||
tax_code: zod.string().optional(),
|
tax_code: zod.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const CreateRegionForm = () => {
|
const PREFIX = "cr"
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const CreateRegionForm = ({
|
||||||
|
currencies,
|
||||||
|
paymentProviders,
|
||||||
|
fulfillmentProviders,
|
||||||
|
}: CreateRegionFormProps) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateRegionSchema>>({
|
const form = useForm<zod.infer<typeof CreateRegionSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -29,39 +86,129 @@ export const CreateRegionForm = () => {
|
|||||||
fulfillment_providers: [],
|
fulfillment_providers: [],
|
||||||
payment_providers: [],
|
payment_providers: [],
|
||||||
tax_code: "",
|
tax_code: "",
|
||||||
tax_rate: 0,
|
tax_rate: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateRegionSchema),
|
resolver: zodResolver(CreateRegionSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedCountries = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "countries",
|
||||||
|
defaultValue: [],
|
||||||
|
})
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminCreateRegion()
|
const { mutateAsync, isLoading } = useAdminCreateRegion()
|
||||||
|
|
||||||
const { store } = useAdminStore()
|
|
||||||
const storeCurrencies = store?.currencies ?? []
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
countries: values.countries,
|
countries: values.countries.map((c) => c.code),
|
||||||
currency_code: values.currency_code,
|
currency_code: values.currency_code,
|
||||||
fulfillment_providers: values.fulfillment_providers,
|
fulfillment_providers: values.fulfillment_providers,
|
||||||
payment_providers: values.payment_providers,
|
payment_providers: values.payment_providers,
|
||||||
tax_rate: values.tax_rate,
|
tax_rate:
|
||||||
|
typeof values.tax_rate === "string"
|
||||||
|
? Number(values.tax_rate)
|
||||||
|
: values.tax_rate,
|
||||||
tax_code: values.tax_code,
|
tax_code: values.tax_code,
|
||||||
includes_tax: values.includes_tax,
|
includes_tax: values.includes_tax,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: ({ region }) => {
|
onSuccess: ({ region }) => {
|
||||||
navigate(`../${region.id}`)
|
handleSuccess(`../${region.id}`)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { searchParams, raw } = useCountryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { countries, count } = useCountries({
|
||||||
|
countries: staticCountries.map((c, i) => ({
|
||||||
|
display_name: c.display_name,
|
||||||
|
name: c.name,
|
||||||
|
id: i,
|
||||||
|
iso_2: c.iso_2,
|
||||||
|
iso_3: c.iso_3,
|
||||||
|
num_code: c.num_code,
|
||||||
|
region_id: null,
|
||||||
|
region: {} as any,
|
||||||
|
})),
|
||||||
|
...searchParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: countries || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater: setRowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.iso_2,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveCountries = () => {
|
||||||
|
const selected = Object.keys(rowSelection).filter(
|
||||||
|
(key) => rowSelection[key]
|
||||||
|
)
|
||||||
|
|
||||||
|
form.setValue(
|
||||||
|
"countries",
|
||||||
|
selected.map((key) => ({
|
||||||
|
code: key,
|
||||||
|
name: staticCountries.find((c) => c.iso_2 === key)!.display_name,
|
||||||
|
})),
|
||||||
|
{ shouldDirty: true, shouldTouch: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
setOpen(open)
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
const ids = selectedCountries.reduce((acc, c) => {
|
||||||
|
acc[c.code] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState)
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setRowSelection(ids)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCountry = (code: string) => {
|
||||||
|
const update = selectedCountries.filter((c) => c.code !== code)
|
||||||
|
const ids = update
|
||||||
|
.map((c) => c.code)
|
||||||
|
.reduce((acc, c) => {
|
||||||
|
acc[c] = true
|
||||||
|
return acc
|
||||||
|
}, {} as RowSelectionState)
|
||||||
|
|
||||||
|
form.setValue("countries", update, { shouldDirty: true, shouldTouch: true })
|
||||||
|
setRowSelection(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCountries = () => {
|
||||||
|
form.setValue("countries", [], { shouldDirty: true, shouldTouch: true })
|
||||||
|
setRowSelection({})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
<form
|
<form
|
||||||
@@ -80,7 +227,15 @@ export const CreateRegionForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
|
<RouteFocusModal.Body className="flex overflow-hidden">
|
||||||
|
<SplitView open={open} onOpenChange={onOpenChange}>
|
||||||
|
<SplitView.Content>
|
||||||
|
<div
|
||||||
|
className={clx(
|
||||||
|
"flex h-full w-full flex-col items-center overflow-y-auto p-16"
|
||||||
|
)}
|
||||||
|
id="form-section"
|
||||||
|
>
|
||||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||||
<div>
|
<div>
|
||||||
<Heading>{t("regions.createRegion")}</Heading>
|
<Heading>{t("regions.createRegion")}</Heading>
|
||||||
@@ -89,7 +244,7 @@ export const CreateRegionForm = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -118,7 +273,7 @@ export const CreateRegionForm = () => {
|
|||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{storeCurrencies.map((currency) => (
|
{currencies.map((currency) => (
|
||||||
<Select.Item
|
<Select.Item
|
||||||
value={currency.code}
|
value={currency.code}
|
||||||
key={currency.code}
|
key={currency.code}
|
||||||
@@ -135,16 +290,16 @@ export const CreateRegionForm = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="tax_rate"
|
name="tax_rate"
|
||||||
render={({ field }) => {
|
render={({ field: { value, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label>{t("fields.taxRate")}</Form.Label>
|
<Form.Label>{t("fields.taxRate")}</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input type="number" {...field} />
|
<Input type="number" value={value} {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -157,7 +312,9 @@ export const CreateRegionForm = () => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>{t("fields.taxCode")}</Form.Label>
|
<Form.Label optional>
|
||||||
|
{t("fields.taxCode")}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Input type="number" {...field} />
|
<Input type="number" {...field} />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -187,13 +344,51 @@ export const CreateRegionForm = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
<Form.Hint>
|
||||||
|
{t("regions.taxInclusiveHint")}
|
||||||
|
</Form.Hint>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.countries")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("regions.countriesHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{selectedCountries.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedCountries.map((country) => (
|
||||||
|
<CountryTag
|
||||||
|
key={country.code}
|
||||||
|
country={country}
|
||||||
|
onRemove={removeCountry}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="transparent"
|
||||||
|
size="small"
|
||||||
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
|
onClick={clearCountries}
|
||||||
|
>
|
||||||
|
{t("actions.clearAll")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<SplitView.Open type="button">
|
||||||
|
{t("regions.addCountries")}
|
||||||
|
</SplitView.Open>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
@@ -203,11 +398,152 @@ export const CreateRegionForm = () => {
|
|||||||
{t("regions.providersHint")}
|
{t("regions.providersHint")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4"></div>
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="payment_providers"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.paymentProviders")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
options={paymentProviders.map((pp) => ({
|
||||||
|
label: formatProvider(pp.id),
|
||||||
|
value: pp.id,
|
||||||
|
}))}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="fulfillment_providers"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.fulfillmentProviders")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
options={fulfillmentProviders.map((fp) => ({
|
||||||
|
label: formatProvider(fp.id),
|
||||||
|
value: fp.id,
|
||||||
|
}))}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Content>
|
||||||
|
<SplitView.Drawer>
|
||||||
|
<div className="flex size-full flex-col overflow-hidden">
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
count={count}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
orderBy={["name", "code"]}
|
||||||
|
pagination
|
||||||
|
search
|
||||||
|
layout="fill"
|
||||||
|
queryObject={raw}
|
||||||
|
prefix={PREFIX}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-x-2 border-t p-4">
|
||||||
|
<SplitView.Close type="button">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</SplitView.Close>
|
||||||
|
<Button size="small" type="button" onClick={saveCountries}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SplitView.Drawer>
|
||||||
|
</SplitView>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</form>
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Country>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useCountryTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isPreselected = !row.getCanSelect()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected() || isPreselected}
|
||||||
|
disabled={isPreselected}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountryTag = ({
|
||||||
|
country,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
country: { code: string; name: string }
|
||||||
|
onRemove: (code: string) => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-field shadow-borders-base transition-fg hover:bg-ui-bg-field-hover flex h-7 items-center overflow-hidden rounded-md">
|
||||||
|
<div className="txt-compact-small-plus flex h-full select-none items-center justify-center px-2 py-0.5">
|
||||||
|
{country.name}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemove(country.code)}
|
||||||
|
className="focus-visible:bg-ui-bg-field-hover transition-fg hover:bg-ui-bg-field-hover flex h-full w-7 items-center justify-center border-l outline-none"
|
||||||
|
>
|
||||||
|
<XMarkMini className="text-ui-fg-muted" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
|
import { useAdminStore } from "medusa-react"
|
||||||
import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
|
import { RouteFocusModal } from "../../../components/route-modal/route-focus-modal"
|
||||||
import { CreateRegionForm } from "./components/create-region-form"
|
import { CreateRegionForm } from "./components/create-region-form"
|
||||||
|
|
||||||
export const RegionCreate = () => {
|
export const RegionCreate = () => {
|
||||||
|
const { store, isLoading, isError, error } = useAdminStore()
|
||||||
|
|
||||||
|
const currencies = store?.currencies ?? []
|
||||||
|
const paymentProviders = store?.payment_providers ?? []
|
||||||
|
const fulfillmentProviders = store?.fulfillment_providers ?? []
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<CreateRegionForm />
|
{!isLoading && store && (
|
||||||
|
<CreateRegionForm
|
||||||
|
currencies={currencies}
|
||||||
|
fulfillmentProviders={fulfillmentProviders}
|
||||||
|
paymentProviders={paymentProviders}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./region-country-section"
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
import { PlusMini, Trash } from "@medusajs/icons"
|
||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import { Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import { useAdminUpdateRegion } from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useCountries } from "../../../shared/hooks/use-countries"
|
||||||
|
import { useCountryTableColumns } from "../../../shared/hooks/use-country-table-columns"
|
||||||
|
import { useCountryTableQuery } from "../../../shared/hooks/use-country-table-query"
|
||||||
|
|
||||||
|
type RegionCountrySectionProps = {
|
||||||
|
region: Region
|
||||||
|
}
|
||||||
|
|
||||||
|
type Country = {
|
||||||
|
id: number
|
||||||
|
iso_2: string
|
||||||
|
iso_3: string
|
||||||
|
num_code: number
|
||||||
|
name: string
|
||||||
|
display_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX = "c"
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const RegionCountrySection = ({ region }: RegionCountrySectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const { searchParams, raw } = useCountryTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
const { countries, count } = useCountries({
|
||||||
|
countries: region.countries || [],
|
||||||
|
...searchParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: countries || [],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
getRowId: (row) => row.iso_2,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
rowSelection: {
|
||||||
|
state: rowSelection,
|
||||||
|
updater: setRowSelection,
|
||||||
|
},
|
||||||
|
prefix: PREFIX,
|
||||||
|
meta: {
|
||||||
|
region,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminUpdateRegion(region.id)
|
||||||
|
|
||||||
|
const handleRemoveCountries = async () => {
|
||||||
|
const ids = Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("regions.removeCountriesWarning", {
|
||||||
|
count: ids.length,
|
||||||
|
}),
|
||||||
|
verificationText: t("actions.remove"),
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = region.countries
|
||||||
|
.filter((c) => !ids.includes(c.iso_2))
|
||||||
|
.map((c) => c.iso_2)
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
countries: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("fields.countries")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("regions.addCountries"),
|
||||||
|
icon: <PlusMini />,
|
||||||
|
to: "countries/add",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
count={count}
|
||||||
|
orderBy={["name", "code"]}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
queryObject={raw}
|
||||||
|
prefix={PREFIX}
|
||||||
|
commands={[
|
||||||
|
{
|
||||||
|
action: handleRemoveCountries,
|
||||||
|
label: t("actions.remove"),
|
||||||
|
shortcut: "r",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountryActions = ({
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
}: {
|
||||||
|
country: Country
|
||||||
|
region: Region
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
const { mutateAsync } = useAdminUpdateRegion(region.id)
|
||||||
|
|
||||||
|
const payload = region.countries
|
||||||
|
?.filter((c) => c.iso_2 !== country.iso_2)
|
||||||
|
.map((c) => c.iso_2)
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("regions.removeCountryWarning", {
|
||||||
|
name: country.display_name,
|
||||||
|
}),
|
||||||
|
verificationText: country.display_name,
|
||||||
|
verificationInstruction: t("general.typeToConfirm"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
countries: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.remove"),
|
||||||
|
onClick: handleRemove,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Country>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = useCountryTableColumns()
|
||||||
|
|
||||||
|
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)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...base,
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const { region } = table.options.meta as { region: Region }
|
||||||
|
|
||||||
|
return <CountryActions country={row.original} region={region} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
) as ColumnDef<Country>[]
|
||||||
|
}
|
||||||
+16
-120
@@ -1,21 +1,10 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { BuildingTax, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { Country, Region } from "@medusajs/medusa"
|
import { Region } from "@medusajs/medusa"
|
||||||
import {
|
import { Badge, Container, Heading, Text, usePrompt } from "@medusajs/ui"
|
||||||
Badge,
|
import { useAdminDeleteRegion } from "medusa-react"
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Drawer,
|
|
||||||
Heading,
|
|
||||||
StatusBadge,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
usePrompt,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useAdminDeleteRegion, useAdminUpdateRegion } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
|
||||||
type RegionGeneralSectionProps = {
|
type RegionGeneralSectionProps = {
|
||||||
region: Region
|
region: Region
|
||||||
@@ -28,15 +17,8 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
|
|||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading>{region.name}</Heading>
|
<Heading>{region.name}</Heading>
|
||||||
|
|
||||||
<RegionActions region={region} />
|
<RegionActions region={region} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{t("fields.countries")}
|
|
||||||
</Text>
|
|
||||||
<RegionCountries countries={region.countries} />
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{t("fields.currency")}
|
{t("fields.currency")}
|
||||||
@@ -50,24 +32,15 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" leading="compact" weight="plus">
|
|
||||||
{t("fields.taxInclusivePricing")}
|
|
||||||
</Text>
|
|
||||||
<StatusBadge
|
|
||||||
color={region.includes_tax ? "green" : "red"}
|
|
||||||
className="w-fit"
|
|
||||||
>
|
|
||||||
{region.includes_tax ? t("general.enabled") : t("general.disabled")}
|
|
||||||
</StatusBadge>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{t("fields.paymentProviders")}
|
{t("fields.paymentProviders")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{region.payment_providers.length > 0
|
{region.payment_providers.length > 0
|
||||||
? region.payment_providers.map((p) => p.id).join(", ")
|
? region.payment_providers
|
||||||
|
.map((p) => formatProvider(p.id))
|
||||||
|
.join(", ")
|
||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +50,9 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{region.fulfillment_providers.length > 0
|
{region.fulfillment_providers.length > 0
|
||||||
? region.fulfillment_providers.map((p) => p.id).join(", ")
|
? region.fulfillment_providers
|
||||||
|
.map((p) => formatProvider(p.id))
|
||||||
|
.join(", ")
|
||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,6 +94,11 @@ const RegionActions = ({ region }: { region: Region }) => {
|
|||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
to: `/settings/regions/${region.id}/edit`,
|
to: `/settings/regions/${region.id}/edit`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <BuildingTax />,
|
||||||
|
label: "Tax settings",
|
||||||
|
to: `/settings/taxes/${region.id}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -134,87 +114,3 @@ const RegionActions = ({ region }: { region: Region }) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegionCountries = ({ countries }: { countries: Country[] }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const countIsGreaterThanTwo = countries.length > 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-ui-fg-subtle flex items-center gap-x-2">
|
|
||||||
<Text leading="compact" size="small">
|
|
||||||
{countries
|
|
||||||
.slice(0, 2)
|
|
||||||
.map((c) => c.display_name)
|
|
||||||
.join(", ")}
|
|
||||||
{countIsGreaterThanTwo && ", "}
|
|
||||||
</Text>
|
|
||||||
{countIsGreaterThanTwo && (
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<ul>
|
|
||||||
{countries.slice(2).map((c) => (
|
|
||||||
<li key={c.id}>{c.display_name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
leading="compact"
|
|
||||||
size="small"
|
|
||||||
weight="plus"
|
|
||||||
className="cursor-default"
|
|
||||||
>
|
|
||||||
{t("general.plusCountMore", {
|
|
||||||
count: countries.length - 2,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditRegionSchema = zod.object({
|
|
||||||
name: zod.string().min(1),
|
|
||||||
includes_tax: zod.boolean(),
|
|
||||||
currency_code: zod.string(),
|
|
||||||
countries: zod.array(zod.string()),
|
|
||||||
})
|
|
||||||
|
|
||||||
const EditRegionDrawer = ({ region }: { region: Region }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
|
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditRegionSchema>>({
|
|
||||||
defaultValues: {
|
|
||||||
name: region.name,
|
|
||||||
currency_code: region.currency_code,
|
|
||||||
includes_tax: region.includes_tax,
|
|
||||||
countries: region.countries.map((c) => c.iso_2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
|
||||||
await mutateAsync({
|
|
||||||
name: values.name,
|
|
||||||
currency_code: values.currency_code,
|
|
||||||
includes_tax: values.includes_tax,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer>
|
|
||||||
<Drawer.Content>
|
|
||||||
<Drawer.Header>
|
|
||||||
<Heading>{t("regions.editRegion")}</Heading>
|
|
||||||
</Drawer.Header>
|
|
||||||
<Drawer.Body></Drawer.Body>
|
|
||||||
<Drawer.Footer className="flex items-center justify-end gap-x-2">
|
|
||||||
<Button variant="secondary">{t("actions.cancel")}</Button>
|
|
||||||
<Button>{t("actions.save")}</Button>
|
|
||||||
</Drawer.Footer>
|
|
||||||
</Drawer.Content>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+17
-2
@@ -3,7 +3,9 @@ import { Container, Heading } from "@medusajs/ui"
|
|||||||
import { useAdminShippingOptions } from "medusa-react"
|
import { useAdminShippingOptions } from "medusa-react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { PlusMini } from "@medusajs/icons"
|
||||||
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
import { useShippingOptionColumns } from "./use-shipping-option-table-columns"
|
import { useShippingOptionColumns } from "./use-shipping-option-table-columns"
|
||||||
@@ -53,8 +55,21 @@ export const RegionShippingOptionSection = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">{t("regions.shippingOptions")}</Heading>
|
<Heading level="h2">{t("regions.shippingOptions")}</Heading>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.create"),
|
||||||
|
icon: <PlusMini />,
|
||||||
|
to: "shipping-options/create",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DataTable
|
<DataTable
|
||||||
table={table}
|
table={table}
|
||||||
@@ -71,7 +86,7 @@ export const RegionShippingOptionSection = ({
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]}
|
]}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rowCount={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
pagination
|
pagination
|
||||||
search
|
search
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
|
|||||||
+81
-18
@@ -1,7 +1,11 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
import { PricedShippingOption } from "@medusajs/medusa/dist/types/pricing"
|
||||||
|
import { usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useAdminDeleteShippingOption } from "medusa-react"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||||
@@ -21,18 +25,22 @@ export const useShippingOptionColumns = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor("is_return", {
|
||||||
|
header: t("fields.type"),
|
||||||
|
cell: (cell) => {
|
||||||
|
const value = cell.getValue()
|
||||||
|
|
||||||
|
return value ? t("regions.return") : t("regions.outbound")
|
||||||
|
},
|
||||||
|
}),
|
||||||
columnHelper.accessor("price_type", {
|
columnHelper.accessor("price_type", {
|
||||||
header: t("regions.priceType"),
|
header: t("regions.priceType"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
const type = getValue()
|
const type = getValue()
|
||||||
|
|
||||||
return (
|
return type === "flat_rate"
|
||||||
<StatusCell color={type === "flat_rate" ? "green" : "blue"}>
|
|
||||||
{type === "flat_rate"
|
|
||||||
? t("regions.flatRate")
|
? t("regions.flatRate")
|
||||||
: t("regions.calculated")}
|
: t("regions.calculated")
|
||||||
</StatusCell>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("price_incl_tax", {
|
columnHelper.accessor("price_incl_tax", {
|
||||||
@@ -52,7 +60,11 @@ export const useShippingOptionColumns = () => {
|
|||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "min_amount",
|
id: "min_amount",
|
||||||
header: "Min.",
|
header: () => (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.minSubtotal")}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const minAmountReq = row.original.requirements?.find(
|
const minAmountReq = row.original.requirements?.find(
|
||||||
(r) => r.type === "min_subtotal"
|
(r) => r.type === "min_subtotal"
|
||||||
@@ -70,7 +82,11 @@ export const useShippingOptionColumns = () => {
|
|||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "max_amount",
|
id: "max_amount",
|
||||||
header: "Max.",
|
header: () => (
|
||||||
|
<div className="flex size-full items-center overflow-hidden">
|
||||||
|
<span className="truncate">{t("fields.maxSubtotal")}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const maxAmountReq = row.original.requirements?.find(
|
const maxAmountReq = row.original.requirements?.find(
|
||||||
(r) => r.type === "max_subtotal"
|
(r) => r.type === "max_subtotal"
|
||||||
@@ -98,19 +114,66 @@ export const useShippingOptionColumns = () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("is_return", {
|
columnHelper.display({
|
||||||
header: t("fields.type"),
|
id: "actions",
|
||||||
cell: (cell) => {
|
cell: ({ row }) => {
|
||||||
const value = cell.getValue()
|
return <ShippingOptionActions shippingOption={row.original} />
|
||||||
|
|
||||||
return (
|
|
||||||
<StatusCell color={value ? "blue" : "green"}>
|
|
||||||
{value ? t("regions.return") : t("regions.outbound")}
|
|
||||||
</StatusCell>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[t]
|
[t]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShippingOptionActions = ({
|
||||||
|
shippingOption,
|
||||||
|
}: {
|
||||||
|
shippingOption: PricedShippingOption
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminDeleteShippingOption(shippingOption.id!)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("regions.deleteShippingOptionWarning", {
|
||||||
|
name: shippingOption.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `shipping-options/${shippingOption.id}/edit`,
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
import { useAdminRegion } from "medusa-react"
|
import { useAdminRegion } from "medusa-react"
|
||||||
import { Outlet, json, useParams } from "react-router-dom"
|
import { Outlet, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { RegionCountrySection } from "./components/region-country-section"
|
||||||
import { RegionGeneralSection } from "./components/region-general-section"
|
import { RegionGeneralSection } from "./components/region-general-section"
|
||||||
import { RegionShippingOptionSection } from "./components/region-shipping-option-section/region-shipping-option-section"
|
import { RegionShippingOptionSection } from "./components/region-shipping-option-section"
|
||||||
|
|
||||||
export const RegionDetail = () => {
|
export const RegionDetail = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
||||||
|
|
||||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||||
if (isLoading) {
|
if (isLoading || !region) {
|
||||||
return <div>Loading</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !region) {
|
if (isError) {
|
||||||
if (error) {
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
throw json("An unknown error occurred", 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
<RegionGeneralSection region={region} />
|
<RegionGeneralSection region={region} />
|
||||||
|
<RegionCountrySection region={region} />
|
||||||
<RegionShippingOptionSection region={region} />
|
<RegionShippingOptionSection region={region} />
|
||||||
<JsonViewSection data={region} />
|
<JsonViewSection data={region} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
+425
@@ -0,0 +1,425 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import {
|
||||||
|
Region,
|
||||||
|
ShippingOption,
|
||||||
|
ShippingOptionRequirement,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CurrencyInput,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateShippingOption } from "medusa-react"
|
||||||
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { IncludesTaxTooltip } from "../../../../../components/common/tax-badge/tax-badge"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import {
|
||||||
|
getDbAmount,
|
||||||
|
getPresentationalAmount,
|
||||||
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { ShippingOptionPriceType } from "../../../shared/constants"
|
||||||
|
|
||||||
|
type EditShippingOptionFormProps = {
|
||||||
|
region: Region
|
||||||
|
shippingOption: ShippingOption
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditShippingOptionSchema = zod
|
||||||
|
.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
admin_only: zod.boolean(),
|
||||||
|
price_type: zod.nativeEnum(ShippingOptionPriceType),
|
||||||
|
includes_tax: zod.boolean(),
|
||||||
|
amount: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Amount must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
min_subtotal: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Min. subtotal must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
max_subtotal: zod
|
||||||
|
.union([zod.string(), zod.number()])
|
||||||
|
.refine((value) => {
|
||||||
|
if (value === "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return num >= 0
|
||||||
|
}, "Max. subtotal must be a positive number")
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.price_type === ShippingOptionPriceType.FLAT_RATE) {
|
||||||
|
if (typeof data.amount === "string" && data.amount === "") {
|
||||||
|
return ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Amount is required",
|
||||||
|
path: ["amount"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditShippingOptionForm = ({
|
||||||
|
region,
|
||||||
|
shippingOption,
|
||||||
|
}: EditShippingOptionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const defaultAmount = shippingOption.amount
|
||||||
|
? getPresentationalAmount(shippingOption.amount, region.currency_code)
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const defaultMinSubtotal = shippingOption.requirements.find(
|
||||||
|
(r) => r.type === "min_subtotal"
|
||||||
|
)
|
||||||
|
const defaultMinSubtotalAmount = defaultMinSubtotal
|
||||||
|
? getPresentationalAmount(defaultMinSubtotal.amount, region.currency_code)
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const defaultMaxSubtotal = shippingOption.requirements.find(
|
||||||
|
(r) => r.type === "max_subtotal"
|
||||||
|
)
|
||||||
|
const defaultMaxSubtotalAmount = defaultMaxSubtotal
|
||||||
|
? getPresentationalAmount(defaultMaxSubtotal.amount, region.currency_code)
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditShippingOptionSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
admin_only: shippingOption.admin_only,
|
||||||
|
name: shippingOption.name,
|
||||||
|
amount: defaultAmount,
|
||||||
|
max_subtotal: defaultMaxSubtotalAmount,
|
||||||
|
min_subtotal: defaultMinSubtotalAmount,
|
||||||
|
price_type: shippingOption.price_type,
|
||||||
|
includes_tax: shippingOption.includes_tax,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditShippingOptionSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedPriceType = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "price_type",
|
||||||
|
defaultValue: ShippingOptionPriceType.FLAT_RATE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFlatRate = watchedPriceType === ShippingOptionPriceType.FLAT_RATE
|
||||||
|
|
||||||
|
const includesTax = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: "includes_tax",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateShippingOption(
|
||||||
|
shippingOption.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const getPricePayload = (amount?: string | number) => {
|
||||||
|
if (!amount) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountValue = typeof amount === "string" ? Number(amount) : amount
|
||||||
|
return getDbAmount(amountValue, region.currency_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
const { amount, min_subtotal, max_subtotal, ...rest } = values
|
||||||
|
|
||||||
|
const amountPayload = getPricePayload(amount)
|
||||||
|
|
||||||
|
const minSubtotalPayload = getPricePayload(min_subtotal)
|
||||||
|
const maxSubtotalPayload = getPricePayload(max_subtotal)
|
||||||
|
|
||||||
|
const minSubtotalRequirement = minSubtotalPayload
|
||||||
|
? {
|
||||||
|
amount: minSubtotalPayload,
|
||||||
|
type: "min_subtotal",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const maxSubtotalRequirement = maxSubtotalPayload
|
||||||
|
? {
|
||||||
|
amount: maxSubtotalPayload,
|
||||||
|
type: "max_subtotal",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const requirements = [
|
||||||
|
minSubtotalRequirement,
|
||||||
|
maxSubtotalRequirement,
|
||||||
|
].filter(Boolean) as ShippingOptionRequirement[]
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
amount: amountPayload,
|
||||||
|
requirements,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-1 flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="admin_only"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Form.Label>
|
||||||
|
{t("regions.shippingOption.availability.adminOnly")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>
|
||||||
|
{t("regions.shippingOption.availability.adminOnlyHint")}
|
||||||
|
</Form.Hint>
|
||||||
|
</div>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="includes_tax"
|
||||||
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<Form.Label>{t("fields.taxInclusivePricing")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="price_type"
|
||||||
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("regions.shippingOption.priceType.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Select onValueChange={onChange} {...field}>
|
||||||
|
<Select.Trigger ref={ref}>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item
|
||||||
|
value={ShippingOptionPriceType.FLAT_RATE}
|
||||||
|
>
|
||||||
|
{t("regions.shippingOption.priceType.flatRate")}
|
||||||
|
</Select.Item>
|
||||||
|
<Select.Item
|
||||||
|
value={ShippingOptionPriceType.CALCULATED}
|
||||||
|
>
|
||||||
|
{t("regions.shippingOption.priceType.calculated")}
|
||||||
|
</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isFlatRate && (
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="amount"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={<IncludesTaxTooltip includesTax={includesTax} />}
|
||||||
|
>
|
||||||
|
{t("fields.price")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-border-base h-px w-full" />
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("regions.shippingOption.requirements.label")}
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("regions.shippingOption.requirements.hint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="min_subtotal"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={<IncludesTaxTooltip includesTax={includesTax} />}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{t("fields.minSubtotal")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="max_subtotal"
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label
|
||||||
|
icon={<IncludesTaxTooltip includesTax={includesTax} />}
|
||||||
|
optional
|
||||||
|
>
|
||||||
|
{t("fields.maxSubtotal")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<CurrencyInput
|
||||||
|
code={region.currency_code}
|
||||||
|
symbol={region.currency.symbol_native}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<RouteDrawer.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteDrawer.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Footer>
|
||||||
|
</form>
|
||||||
|
</RouteDrawer.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-shipping-option-form"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { RegionEditShippingOption as Component } from "./region-edit-shipping-option"
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminRegion, useAdminShippingOption } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
|
import { EditShippingOptionForm } from "./components/edit-shipping-option-form"
|
||||||
|
|
||||||
|
export const RegionEditShippingOption = () => {
|
||||||
|
const { id, so_id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
region,
|
||||||
|
isLoading: isLoadingRegion,
|
||||||
|
isError: isRegionError,
|
||||||
|
error: regionError,
|
||||||
|
} = useAdminRegion(id!)
|
||||||
|
|
||||||
|
const {
|
||||||
|
shipping_option,
|
||||||
|
isLoading: isLoadingOption,
|
||||||
|
isError: isOptionError,
|
||||||
|
error: optionError,
|
||||||
|
} = useAdminShippingOption(so_id!)
|
||||||
|
|
||||||
|
const isLoading = isLoadingRegion || isLoadingOption
|
||||||
|
|
||||||
|
if (isRegionError) {
|
||||||
|
throw regionError
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOptionError) {
|
||||||
|
throw optionError
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteDrawer>
|
||||||
|
<RouteDrawer.Header>
|
||||||
|
<Heading>{t("regions.shippingOption.editShippingOption")}</Heading>
|
||||||
|
</RouteDrawer.Header>
|
||||||
|
{!isLoading && region && shipping_option && (
|
||||||
|
<EditShippingOptionForm
|
||||||
|
region={region}
|
||||||
|
shippingOption={shipping_option}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
+120
-35
@@ -1,48 +1,78 @@
|
|||||||
import { Region } from "@medusajs/medusa"
|
import {
|
||||||
import { Button, Drawer, Input, Switch } from "@medusajs/ui"
|
Currency,
|
||||||
|
FulfillmentProvider,
|
||||||
|
PaymentProvider,
|
||||||
|
Region,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { Button, Input, Select, Text } from "@medusajs/ui"
|
||||||
import { useAdminUpdateRegion } from "medusa-react"
|
import { useAdminUpdateRegion } from "medusa-react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Combobox } from "../../../../../components/common/combobox"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import {
|
||||||
|
RouteDrawer,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/route-modal"
|
||||||
|
import { formatProvider } from "../../../../../lib/format-provider"
|
||||||
|
|
||||||
type EditRegionFormProps = {
|
type EditRegionFormProps = {
|
||||||
region: Region
|
region: Region
|
||||||
|
currencies: Currency[]
|
||||||
|
paymentProviders: PaymentProvider[]
|
||||||
|
fulfillmentProviders: FulfillmentProvider[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditRegionSchema = zod.object({
|
const EditRegionSchema = zod.object({
|
||||||
name: zod.string().min(1),
|
name: zod.string().min(1),
|
||||||
includes_tax: zod.boolean(),
|
|
||||||
currency_code: zod.string(),
|
currency_code: zod.string(),
|
||||||
countries: zod.array(zod.string()),
|
payment_providers: zod.array(zod.string()),
|
||||||
|
fulfillment_providers: zod.array(zod.string()),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const EditRegionForm = ({ region }: EditRegionFormProps) => {
|
export const EditRegionForm = ({
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
|
region,
|
||||||
|
currencies,
|
||||||
|
paymentProviders,
|
||||||
|
fulfillmentProviders,
|
||||||
|
}: EditRegionFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof EditRegionSchema>>({
|
const form = useForm<zod.infer<typeof EditRegionSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: region.name,
|
name: region.name,
|
||||||
currency_code: region.currency_code,
|
currency_code: region.currency_code,
|
||||||
includes_tax: region.includes_tax,
|
fulfillment_providers: region.fulfillment_providers.map((fp) => fp.id),
|
||||||
countries: region.countries.map((c) => c.iso_2),
|
payment_providers: region.payment_providers.map((pp) => pp.id),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { mutateAsync, isLoading } = useAdminUpdateRegion(region.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync({
|
await mutateAsync(
|
||||||
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
currency_code: values.currency_code,
|
currency_code: values.currency_code,
|
||||||
includes_tax: values.includes_tax,
|
fulfillment_providers: values.fulfillment_providers,
|
||||||
})
|
payment_providers: values.payment_providers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<RouteDrawer.Form form={form}>
|
||||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
<Drawer.Body>
|
<RouteDrawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -61,45 +91,100 @@ export const EditRegionForm = ({ region }: EditRegionFormProps) => {
|
|||||||
/>
|
/>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="includes_tax"
|
name="currency_code"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field: { onChange, ref, ...field } }) => {
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<div className="flex items-start justify-between">
|
<Form.Label>{t("fields.currency")}</Form.Label>
|
||||||
<div>
|
|
||||||
<Form.Label>
|
|
||||||
{t("fields.taxInclusivePricing")}
|
|
||||||
</Form.Label>
|
|
||||||
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
|
||||||
</div>
|
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
<Switch
|
<Select onValueChange={onChange} {...field}>
|
||||||
{...field}
|
<Select.Trigger ref={ref}>
|
||||||
checked={value}
|
<Select.Value />
|
||||||
onCheckedChange={onChange}
|
</Select.Trigger>
|
||||||
/>
|
<Select.Content>
|
||||||
|
{currencies.map((c) => (
|
||||||
|
<Select.Item key={c.code} value={c.code}>
|
||||||
|
{c.code.toUpperCase()}
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Drawer.Body>
|
<div className="flex flex-col gap-y-4">
|
||||||
<Drawer.Footer>
|
<div>
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
Providers
|
||||||
|
</Text>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("regions.providersHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="payment_providers"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.paymentProviders")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
options={paymentProviders.map((pp) => ({
|
||||||
|
label: formatProvider(pp.id),
|
||||||
|
value: pp.id,
|
||||||
|
}))}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="fulfillment_providers"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("fields.fulfillmentProviders")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Combobox
|
||||||
|
options={fulfillmentProviders.map((fp) => ({
|
||||||
|
label: formatProvider(fp.id),
|
||||||
|
value: fp.id,
|
||||||
|
}))}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteDrawer.Body>
|
||||||
|
<RouteDrawer.Footer>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Drawer.Close asChild>
|
<RouteDrawer.Close asChild>
|
||||||
<Button size="small" variant="secondary">
|
<Button size="small" variant="secondary">
|
||||||
{t("actions.cancel")}
|
{t("actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</Drawer.Close>
|
</RouteDrawer.Close>
|
||||||
<Button size="small" type="submit" isLoading={isLoading}>
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
{t("actions.save")}
|
{t("actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Drawer.Footer>
|
</RouteDrawer.Footer>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</RouteDrawer.Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,49 +1,56 @@
|
|||||||
import { Drawer, Heading } from "@medusajs/ui"
|
import { Heading } from "@medusajs/ui"
|
||||||
import { useAdminRegion } from "medusa-react"
|
import { useAdminRegion, useAdminStore } from "medusa-react"
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { json, useNavigate, useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
|
|
||||||
|
import { RouteDrawer } from "../../../components/route-modal"
|
||||||
import { EditRegionForm } from "./components/edit-region-form"
|
import { EditRegionForm } from "./components/edit-region-form"
|
||||||
|
|
||||||
export const RegionEdit = () => {
|
export const RegionEdit = () => {
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const { id } = useParams()
|
|
||||||
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOpen(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onOpenChange = (open: boolean) => {
|
|
||||||
if (!open) {
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate(`/settings/regions/${id}`, { replace: true })
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
if (isError) {
|
const {
|
||||||
throw error
|
region,
|
||||||
|
isLoading: isRegionLoading,
|
||||||
|
isError: isRegionError,
|
||||||
|
error: regionError,
|
||||||
|
} = useAdminRegion(id!)
|
||||||
|
|
||||||
|
const {
|
||||||
|
store,
|
||||||
|
isLoading: isStoreLoading,
|
||||||
|
isError: isStoreError,
|
||||||
|
error: storeError,
|
||||||
|
} = useAdminStore()
|
||||||
|
|
||||||
|
const isLoading = isRegionLoading || isStoreLoading
|
||||||
|
|
||||||
|
const currencies = store?.currencies || []
|
||||||
|
const paymentProviders = store?.payment_providers || []
|
||||||
|
const fulfillmentProviders = store?.fulfillment_providers || []
|
||||||
|
|
||||||
|
if (isRegionError) {
|
||||||
|
throw regionError
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!region && !isLoading) {
|
if (isStoreError) {
|
||||||
throw json("An unknown error has occured", 500)
|
throw storeError
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
<RouteDrawer>
|
||||||
<Drawer.Content>
|
<RouteDrawer.Header>
|
||||||
<Drawer.Header>
|
|
||||||
<Heading>{t("regions.editRegion")}</Heading>
|
<Heading>{t("regions.editRegion")}</Heading>
|
||||||
</Drawer.Header>
|
</RouteDrawer.Header>
|
||||||
{region && <EditRegionForm region={region} />}
|
{!isLoading && region && (
|
||||||
</Drawer.Content>
|
<EditRegionForm
|
||||||
</Drawer>
|
region={region}
|
||||||
|
currencies={currencies}
|
||||||
|
paymentProviders={paymentProviders}
|
||||||
|
fulfillmentProviders={fulfillmentProviders}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteDrawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+3
-2
@@ -6,6 +6,7 @@ import { useAdminDeleteRegion, useAdminRegions } from "medusa-react"
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
|
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
|
||||||
@@ -32,7 +33,7 @@ export const RegionListTable = () => {
|
|||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
|
|
||||||
const { table } = useDataTable({
|
const { table } = useDataTable({
|
||||||
data: regions ?? [],
|
data: (regions ?? []) as Region[],
|
||||||
columns,
|
columns,
|
||||||
count,
|
count,
|
||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
@@ -58,7 +59,7 @@ export const RegionListTable = () => {
|
|||||||
table={table}
|
table={table}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
count={count}
|
count={count}
|
||||||
rowCount={PAGE_SIZE}
|
pageSize={PAGE_SIZE}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
orderBy={["name", "created_at", "updated_at"]}
|
orderBy={["name", "created_at", "updated_at"]}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum ShippingOptionPriceType {
|
||||||
|
FLAT_RATE = "flat_rate",
|
||||||
|
CALCULATED = "calculated",
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Country } from "@medusajs/medusa"
|
||||||
|
import { json } from "react-router-dom"
|
||||||
|
|
||||||
|
const acceptedOrderKeys = ["name", "code"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since countries cannot be retrieved from the API, we need to create a hook
|
||||||
|
* that can be used to filter and sort the static list of countries.
|
||||||
|
*/
|
||||||
|
export const useCountries = ({
|
||||||
|
countries,
|
||||||
|
q,
|
||||||
|
order = "name",
|
||||||
|
limit,
|
||||||
|
offset = 0,
|
||||||
|
}: {
|
||||||
|
countries: Country[]
|
||||||
|
limit: number
|
||||||
|
offset?: number
|
||||||
|
order?: string
|
||||||
|
q?: string
|
||||||
|
}) => {
|
||||||
|
const data = countries.slice(offset, offset + limit)
|
||||||
|
|
||||||
|
if (order) {
|
||||||
|
const direction = order.startsWith("-") ? -1 : 1
|
||||||
|
const key = order.replace("-", "")
|
||||||
|
|
||||||
|
if (!acceptedOrderKeys.includes(key)) {
|
||||||
|
console.log("The key ${key} is not a valid order key")
|
||||||
|
throw json(`The key ${key} is not a valid order key`, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortKey: keyof Country = key === "code" ? "iso_2" : "name"
|
||||||
|
|
||||||
|
data.sort((a, b) => {
|
||||||
|
if (a[sortKey] === null && b[sortKey] === null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (a[sortKey] === null) {
|
||||||
|
return direction
|
||||||
|
}
|
||||||
|
if (b[sortKey] === null) {
|
||||||
|
return -direction
|
||||||
|
}
|
||||||
|
return a[sortKey]! > b[sortKey]! ? direction : -direction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
const query = q.toLowerCase()
|
||||||
|
const results = countries.filter(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase().includes(query) ||
|
||||||
|
c.iso_2.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
countries: results,
|
||||||
|
count: results.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
countries: data,
|
||||||
|
count: countries.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { Country } from "@medusajs/medusa"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Country>()
|
||||||
|
|
||||||
|
export const useCountryTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("display_name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("iso_2", {
|
||||||
|
header: t("fields.code"),
|
||||||
|
cell: ({ getValue }) => <span className="uppercase">{getValue()}</span>,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useCountryTableQuery = ({
|
||||||
|
pageSize,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const raw = useQueryParams(["order", "q", "offset"], prefix)
|
||||||
|
|
||||||
|
const { offset, order, q } = raw
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: offset ? parseInt(offset, 10) : 0,
|
||||||
|
order,
|
||||||
|
q,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,5 +24,5 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require("@headlessui/tailwindcss")],
|
plugins: [],
|
||||||
}
|
}
|
||||||
@@ -110,14 +110,14 @@ const ChoiceBox = React.forwardRef<
|
|||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
size="base"
|
size="small"
|
||||||
weight="plus"
|
weight="plus"
|
||||||
className="group-disabled:text-ui-fg-disabled cursor-pointer group-disabled:cursor-not-allowed"
|
className="group-disabled:text-ui-fg-disabled cursor-pointer group-disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
<Hint
|
<Hint
|
||||||
className="txt-compact-medium text-ui-fg-subtle group-disabled:text-ui-fg-disabled"
|
className="txt-compact-medium text-ui-fg-subtle group-disabled:text-ui-fg-disabled text-left"
|
||||||
id={descriptionId}
|
id={descriptionId}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@@ -233,6 +233,39 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ariakit/core@npm:0.4.1":
|
||||||
|
version: 0.4.1
|
||||||
|
resolution: "@ariakit/core@npm:0.4.1"
|
||||||
|
checksum: 2402e054f888d67b44bed81e80757a915ea0f943c3ac76e65f3a1f96973eaa291a344fb6edeeee09f007ede372ef745e43a058e1404ee745d2caf55b7290a8b8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@ariakit/react-core@npm:0.4.1":
|
||||||
|
version: 0.4.1
|
||||||
|
resolution: "@ariakit/react-core@npm:0.4.1"
|
||||||
|
dependencies:
|
||||||
|
"@ariakit/core": 0.4.1
|
||||||
|
"@floating-ui/dom": ^1.0.0
|
||||||
|
use-sync-external-store: ^1.2.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0
|
||||||
|
checksum: a6388f8c46dd49f0a117639950261fdf7d38c0b395b5e2d87edd1cf9aa479ea0cab338b716a915b7d13142e88ad0430b85a95c9558dbc892c9a7b51bbc702092
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@ariakit/react@npm:^0.4.1":
|
||||||
|
version: 0.4.1
|
||||||
|
resolution: "@ariakit/react@npm:0.4.1"
|
||||||
|
dependencies:
|
||||||
|
"@ariakit/react-core": 0.4.1
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0
|
||||||
|
checksum: 31a2a8f63a8e3ab5037445669da4005ab61d5f7957c39780b32ef3ac77db478ac39b940a16a252c24f2babf95c44d363fc904667b7ddf86bb30332845080cddc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@atomico/rollup-plugin-sizes@npm:^1.1.4":
|
"@atomico/rollup-plugin-sizes@npm:^1.1.4":
|
||||||
version: 1.1.4
|
version: 1.1.4
|
||||||
resolution: "@atomico/rollup-plugin-sizes@npm:1.1.4"
|
resolution: "@atomico/rollup-plugin-sizes@npm:1.1.4"
|
||||||
@@ -3821,6 +3854,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.23.8":
|
||||||
|
version: 7.23.9
|
||||||
|
resolution: "@babel/runtime@npm:7.23.9"
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime: ^0.14.0
|
||||||
|
checksum: e71205fdd7082b2656512cc98e647d9ea7e222e4fe5c36e9e5adc026446fcc3ba7b3cdff8b0b694a0b78bb85db83e7b1e3d4c56ef90726682b74f13249cf952d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.6":
|
"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.6":
|
||||||
version: 7.22.11
|
version: 7.22.11
|
||||||
resolution: "@babel/runtime@npm:7.22.11"
|
resolution: "@babel/runtime@npm:7.22.11"
|
||||||
@@ -3848,15 +3890,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@babel/runtime@npm:^7.23.7":
|
|
||||||
version: 7.23.9
|
|
||||||
resolution: "@babel/runtime@npm:7.23.9"
|
|
||||||
dependencies:
|
|
||||||
regenerator-runtime: ^0.14.0
|
|
||||||
checksum: e71205fdd7082b2656512cc98e647d9ea7e222e4fe5c36e9e5adc026446fcc3ba7b3cdff8b0b694a0b78bb85db83e7b1e3d4c56ef90726682b74f13249cf952d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@babel/template@npm:^7.12.13, @babel/template@npm:^7.12.7, @babel/template@npm:^7.16.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
|
"@babel/template@npm:^7.12.13, @babel/template@npm:^7.12.7, @babel/template@npm:^7.16.7, @babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
|
||||||
version: 7.22.5
|
version: 7.22.5
|
||||||
resolution: "@babel/template@npm:7.22.5"
|
resolution: "@babel/template@npm:7.22.5"
|
||||||
@@ -5670,6 +5703,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/core@npm:^1.0.0":
|
||||||
|
version: 1.6.0
|
||||||
|
resolution: "@floating-ui/core@npm:1.6.0"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/utils": ^0.2.1
|
||||||
|
checksum: 667a68036f7dd5ed19442c7792a6002ca02d1799221c4396691bbe0b6008b48f6ccad581225e81fa266bb91232f6c66838a5f825f554217e1ec886178b93381b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@floating-ui/core@npm:^1.3.1":
|
"@floating-ui/core@npm:^1.3.1":
|
||||||
version: 1.3.1
|
version: 1.3.1
|
||||||
resolution: "@floating-ui/core@npm:1.3.1"
|
resolution: "@floating-ui/core@npm:1.3.1"
|
||||||
@@ -5677,6 +5719,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/dom@npm:^1.0.0":
|
||||||
|
version: 1.6.3
|
||||||
|
resolution: "@floating-ui/dom@npm:1.6.3"
|
||||||
|
dependencies:
|
||||||
|
"@floating-ui/core": ^1.0.0
|
||||||
|
"@floating-ui/utils": ^0.2.0
|
||||||
|
checksum: d6cac10877918ce5a8d1a24b21738d2eb130a0191043d7c0dd43bccac507844d3b4dc5d4107d3891d82f6007945ca8fb4207a1252506e91c37e211f0f73cf77e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.3.0":
|
"@floating-ui/dom@npm:^1.0.1, @floating-ui/dom@npm:^1.3.0":
|
||||||
version: 1.4.3
|
version: 1.4.3
|
||||||
resolution: "@floating-ui/dom@npm:1.4.3"
|
resolution: "@floating-ui/dom@npm:1.4.3"
|
||||||
@@ -5698,6 +5750,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.1":
|
||||||
|
version: 0.2.1
|
||||||
|
resolution: "@floating-ui/utils@npm:0.2.1"
|
||||||
|
checksum: ee77756712cf5b000c6bacf11992ffb364f3ea2d0d51cc45197a7e646a17aeb86ea4b192c0b42f3fbb29487aee918a565e84f710b8c3645827767f406a6b4cc9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@formatjs/ecma402-abstract@npm:1.17.0":
|
"@formatjs/ecma402-abstract@npm:1.17.0":
|
||||||
version: 1.17.0
|
version: 1.17.0
|
||||||
resolution: "@formatjs/ecma402-abstract@npm:1.17.0"
|
resolution: "@formatjs/ecma402-abstract@npm:1.17.0"
|
||||||
@@ -6135,28 +6194,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@headlessui/react@npm:^1.7.18":
|
|
||||||
version: 1.7.18
|
|
||||||
resolution: "@headlessui/react@npm:1.7.18"
|
|
||||||
dependencies:
|
|
||||||
"@tanstack/react-virtual": ^3.0.0-beta.60
|
|
||||||
client-only: ^0.0.1
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16 || ^17 || ^18
|
|
||||||
react-dom: ^16 || ^17 || ^18
|
|
||||||
checksum: 2d88d10874879182d4b9ed9a7779266032214034481129ba544e858d3624c8d12333e6a9d9d8263f2f116bc823bcfd43a2d1f69800fbf6a47b34d989370346e5
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@headlessui/tailwindcss@npm:^0.2.0":
|
|
||||||
version: 0.2.0
|
|
||||||
resolution: "@headlessui/tailwindcss@npm:0.2.0"
|
|
||||||
peerDependencies:
|
|
||||||
tailwindcss: ^3.0
|
|
||||||
checksum: b641bef150e4ee18afd068c1af8e83c582a3131903ed1a310abbdb463b6150428a9b4c38e42cb296469fb20857f5542ae34f681cbff62f47d52332fbc5117479
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@hookform/error-message@npm:^2.0.1":
|
"@hookform/error-message@npm:^2.0.1":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "@hookform/error-message@npm:2.0.1"
|
resolution: "@hookform/error-message@npm:2.0.1"
|
||||||
@@ -8076,8 +8113,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"
|
resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@headlessui/react": ^1.7.18
|
"@ariakit/react": ^0.4.1
|
||||||
"@headlessui/tailwindcss": ^0.2.0
|
|
||||||
"@hookform/resolvers": 3.3.2
|
"@hookform/resolvers": 3.3.2
|
||||||
"@medusajs/icons": "workspace:^"
|
"@medusajs/icons": "workspace:^"
|
||||||
"@medusajs/medusa": "workspace:^"
|
"@medusajs/medusa": "workspace:^"
|
||||||
@@ -8102,11 +8138,13 @@ __metadata:
|
|||||||
i18next: 23.7.11
|
i18next: 23.7.11
|
||||||
i18next-browser-languagedetector: 7.2.0
|
i18next-browser-languagedetector: 7.2.0
|
||||||
i18next-http-backend: 2.4.2
|
i18next-http-backend: 2.4.2
|
||||||
|
match-sorter: ^6.3.4
|
||||||
medusa-react: "workspace:^"
|
medusa-react: "workspace:^"
|
||||||
postcss: ^8.4.33
|
postcss: ^8.4.33
|
||||||
prettier: ^3.1.1
|
prettier: ^3.1.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
|
react-focus-lock: ^2.11.1
|
||||||
react-hook-form: 7.49.1
|
react-hook-form: 7.49.1
|
||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
@@ -16828,18 +16866,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@tanstack/react-virtual@npm:^3.0.0-beta.60":
|
|
||||||
version: 3.0.1
|
|
||||||
resolution: "@tanstack/react-virtual@npm:3.0.1"
|
|
||||||
dependencies:
|
|
||||||
"@tanstack/virtual-core": 3.0.0
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
checksum: 2b9464dab8a975734b651211402a4eaf10e4ae6d9570138576891db350cfabe11e444fea8f17ef0b26ef8ede0adc230bec9c8fe859e61ca66147831076b77bb2
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@tanstack/react-virtual@npm:^3.0.4":
|
"@tanstack/react-virtual@npm:^3.0.4":
|
||||||
version: 3.0.4
|
version: 3.0.4
|
||||||
resolution: "@tanstack/react-virtual@npm:3.0.4"
|
resolution: "@tanstack/react-virtual@npm:3.0.4"
|
||||||
@@ -23127,13 +23153,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"client-only@npm:^0.0.1":
|
|
||||||
version: 0.0.1
|
|
||||||
resolution: "client-only@npm:0.0.1"
|
|
||||||
checksum: 9d6cfd0c19e1c96a434605added99dff48482152af791ec4172fb912a71cff9027ff174efd8cdb2160cc7f377543e0537ffc462d4f279bc4701de3f2a3c4b358
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"client-sessions@npm:^0.8.0":
|
"client-sessions@npm:^0.8.0":
|
||||||
version: 0.8.0
|
version: 0.8.0
|
||||||
resolution: "client-sessions@npm:0.8.0"
|
resolution: "client-sessions@npm:0.8.0"
|
||||||
@@ -28610,6 +28629,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"focus-lock@npm:^1.3.2":
|
||||||
|
version: 1.3.3
|
||||||
|
resolution: "focus-lock@npm:1.3.3"
|
||||||
|
dependencies:
|
||||||
|
tslib: ^2.0.3
|
||||||
|
checksum: 38b978ab30f5be6f061689c747b05193217e9cb9c5d5a41b9b322454c6bb14e84cbd6c928fa245c62e744f070cfa939820a0ae2dd51011da233de7c912cea412
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"follow-redirects@npm:1.5.10":
|
"follow-redirects@npm:1.5.10":
|
||||||
version: 1.5.10
|
version: 1.5.10
|
||||||
resolution: "follow-redirects@npm:1.5.10"
|
resolution: "follow-redirects@npm:1.5.10"
|
||||||
@@ -37250,6 +37278,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"match-sorter@npm:^6.3.4":
|
||||||
|
version: 6.3.4
|
||||||
|
resolution: "match-sorter@npm:6.3.4"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.23.8
|
||||||
|
remove-accents: 0.5.0
|
||||||
|
checksum: 35d2a6b6df003c677d9ec87ecd4683657638f5bce856f43f9cf90b03e357ed2f09813ebbac759defa7e7438706936dd34dc2bfe1a18771f7d2541f14d639b4ad
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"matcher-collection@npm:^2.0.0":
|
"matcher-collection@npm:^2.0.0":
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
resolution: "matcher-collection@npm:2.0.1"
|
resolution: "matcher-collection@npm:2.0.1"
|
||||||
@@ -43427,6 +43465,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-clientside-effect@npm:^1.2.6":
|
||||||
|
version: 1.2.6
|
||||||
|
resolution: "react-clientside-effect@npm:1.2.6"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.13
|
||||||
|
peerDependencies:
|
||||||
|
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
checksum: aba0adb666018e5c64657c31f4914a8558be73f71d6e2210fa871ebfcab94d786c83082868d7c7fa66feddc2aec19e375745cf0903e0619f2efffef08b92d941
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-collapsible@npm:^2.8.3":
|
"react-collapsible@npm:^2.8.3":
|
||||||
version: 2.10.0
|
version: 2.10.0
|
||||||
resolution: "react-collapsible@npm:2.10.0"
|
resolution: "react-collapsible@npm:2.10.0"
|
||||||
@@ -43697,6 +43746,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-focus-lock@npm:^2.11.1":
|
||||||
|
version: 2.11.1
|
||||||
|
resolution: "react-focus-lock@npm:2.11.1"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.0.0
|
||||||
|
focus-lock: ^1.3.2
|
||||||
|
prop-types: ^15.6.2
|
||||||
|
react-clientside-effect: ^1.2.6
|
||||||
|
use-callback-ref: ^1.3.0
|
||||||
|
use-sidecar: ^1.1.2
|
||||||
|
peerDependencies:
|
||||||
|
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@types/react":
|
||||||
|
optional: true
|
||||||
|
checksum: b4905ec18bb6bd6732cbd2d3bd70853007dcc31363fa8d0700ea645eec2e2e685f2fe09443d070983c2077fc2d6c06279f2838fc9046c6218facc3e310e50279
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-helmet-async@npm:^1.3.0":
|
"react-helmet-async@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "react-helmet-async@npm:1.3.0"
|
resolution: "react-helmet-async@npm:1.3.0"
|
||||||
@@ -44759,6 +44828,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"remove-accents@npm:0.5.0":
|
||||||
|
version: 0.5.0
|
||||||
|
resolution: "remove-accents@npm:0.5.0"
|
||||||
|
checksum: a75321aa1b53d9abe82637115a492770bfe42bb38ed258be748bf6795871202bc8b4badff22013494a7029f5a241057ad8d3f72adf67884dbe15a9e37e87adc4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"remove-bom-buffer@npm:^3.0.0":
|
"remove-bom-buffer@npm:^3.0.0":
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
resolution: "remove-bom-buffer@npm:3.0.0"
|
resolution: "remove-bom-buffer@npm:3.0.0"
|
||||||
|
|||||||
Reference in new issue
Block a user