feat(admin-sdk,admin-bundler,admin-shared,medusa): Restructure admin packages (#8988)
**What** - Renames /admin-next -> /admin - Renames @medusajs/admin-sdk -> @medusajs/admin-bundler - Creates a new package called @medusajs/admin-sdk that will hold all tooling relevant to creating admin extensions. This is currently `defineRouteConfig` and `defineWidgetConfig`, but will eventually also export methods for adding custom fields, register translation, etc. - cc: @shahednasser we should update the examples in the docs so these functions are imported from `@medusajs/admin-sdk`. People will also need to install the package in their project, as it's no longer a transient dependency. - cc: @olivermrbl we might want to publish a changelog when this is merged, as it is a breaking change, and will require people to import the `defineXConfig` from the new package instead of `@medusajs/admin-shared`. - Updates CODEOWNERS so /admin packages does not require a review from the UI team.
This commit is contained in:
committed by
GitHub
parent
beaa851302
commit
0fe1201435
@@ -0,0 +1,191 @@
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { Badge, clx } from "@medusajs/ui"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import {
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
type ChipInputProps = {
|
||||
value?: string[]
|
||||
onChange?: (value: string[]) => void
|
||||
onBlur?: () => void
|
||||
name?: string
|
||||
disabled?: boolean
|
||||
allowDuplicates?: boolean
|
||||
showRemove?: boolean
|
||||
variant?: "base" | "contrast"
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ChipInput = forwardRef<HTMLInputElement, ChipInputProps>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
disabled,
|
||||
name,
|
||||
showRemove = true,
|
||||
variant = "base",
|
||||
allowDuplicates = false,
|
||||
placeholder,
|
||||
className,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const innerRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const isControlledRef = useRef(typeof value !== "undefined")
|
||||
const isControlled = isControlledRef.current
|
||||
|
||||
const [uncontrolledValue, setUncontrolledValue] = useState<string[]>([])
|
||||
|
||||
useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
|
||||
ref,
|
||||
() => innerRef.current
|
||||
)
|
||||
|
||||
const [duplicateIndex, setDuplicateIndex] = useState<number | null>(null)
|
||||
|
||||
const chips = isControlled ? (value as string[]) : uncontrolledValue
|
||||
|
||||
const handleAddChip = (chip: string) => {
|
||||
const cleanValue = chip.trim()
|
||||
|
||||
if (!cleanValue) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!allowDuplicates && chips.includes(cleanValue)) {
|
||||
setDuplicateIndex(chips.indexOf(cleanValue))
|
||||
|
||||
setTimeout(() => {
|
||||
setDuplicateIndex(null)
|
||||
}, 300)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onChange?.([...chips, cleanValue])
|
||||
|
||||
if (!isControlled) {
|
||||
setUncontrolledValue([...chips, cleanValue])
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveChip = (chip: string) => {
|
||||
onChange?.(chips.filter((v) => v !== chip))
|
||||
|
||||
if (!isControlled) {
|
||||
setUncontrolledValue(chips.filter((v) => v !== chip))
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
|
||||
onBlur?.()
|
||||
|
||||
if (e.target.value) {
|
||||
handleAddChip(e.target.value)
|
||||
e.target.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault()
|
||||
|
||||
if (!innerRef.current?.value) {
|
||||
return
|
||||
}
|
||||
|
||||
handleAddChip(innerRef.current?.value ?? "")
|
||||
innerRef.current.value = ""
|
||||
innerRef.current?.focus()
|
||||
}
|
||||
|
||||
if (e.key === "Backspace" && innerRef.current?.value === "") {
|
||||
handleRemoveChip(chips[chips.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
// create a shake animation using framer motion
|
||||
const shake = {
|
||||
x: [0, -2, 2, -2, 2, 0],
|
||||
transition: { duration: 0.3 },
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clx(
|
||||
"shadow-borders-base flex min-h-8 flex-wrap items-center gap-1 rounded-md px-2 py-1.5",
|
||||
"transition-fg focus-within:shadow-borders-interactive-with-active",
|
||||
"has-[input:disabled]:bg-ui-bg-disabled has-[input:disabled]:text-ui-fg-disabled has-[input:disabled]:cursor-not-allowed",
|
||||
{
|
||||
"bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover":
|
||||
variant === "contrast",
|
||||
"bg-ui-bg-field hover:bg-ui-bg-field-hover": variant === "base",
|
||||
},
|
||||
className
|
||||
)}
|
||||
tabIndex={-1}
|
||||
onClick={() => innerRef.current?.focus()}
|
||||
>
|
||||
{chips.map((v, index) => {
|
||||
return (
|
||||
<AnimatePresence key={`${v}-${index}`}>
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
className={clx("gap-x-0.5 pl-1.5 pr-1.5", {
|
||||
"transition-fg pr-1": showRemove,
|
||||
"shadow-borders-focus": index === duplicateIndex,
|
||||
})}
|
||||
asChild
|
||||
>
|
||||
<motion.div
|
||||
animate={index === duplicateIndex ? shake : undefined}
|
||||
>
|
||||
{v}
|
||||
{showRemove && (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
onClick={() => handleRemoveChip(v)}
|
||||
className={clx(
|
||||
"text-ui-fg-subtle transition-fg outline-none"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
</Badge>
|
||||
</AnimatePresence>
|
||||
)
|
||||
})}
|
||||
<input
|
||||
className={clx(
|
||||
"caret-ui-fg-base text-ui-fg-base txt-compact-small flex-1 appearance-none bg-transparent",
|
||||
"disabled:text-ui-fg-disabled disabled:cursor-not-allowed",
|
||||
"focus:outline-none",
|
||||
"placeholder:text-ui-fg-muted"
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
ref={innerRef}
|
||||
placeholder={chips.length === 0 ? placeholder : undefined}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ChipInput.displayName = "ChipInput"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./chip-input"
|
||||
@@ -0,0 +1,352 @@
|
||||
import {
|
||||
Combobox as PrimitiveCombobox,
|
||||
ComboboxDisclosure as PrimitiveComboboxDisclosure,
|
||||
ComboboxItem as PrimitiveComboboxItem,
|
||||
ComboboxItemCheck as PrimitiveComboboxItemCheck,
|
||||
ComboboxItemValue as PrimitiveComboboxItemValue,
|
||||
ComboboxPopover as PrimitiveComboboxPopover,
|
||||
ComboboxProvider as PrimitiveComboboxProvider,
|
||||
Separator as PrimitiveSeparator,
|
||||
} from "@ariakit/react"
|
||||
import {
|
||||
EllipseMiniSolid,
|
||||
PlusMini,
|
||||
TrianglesMini,
|
||||
XMarkMini,
|
||||
} from "@medusajs/icons"
|
||||
import { clx, Text } from "@medusajs/ui"
|
||||
import { matchSorter } from "match-sorter"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
ForwardedRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useDeferredValue,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { genericForwardRef } from "../../common/generic-forward-ref"
|
||||
|
||||
type ComboboxOption = {
|
||||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type Value = string[] | string
|
||||
|
||||
interface ComboboxProps<T extends Value = Value>
|
||||
extends Omit<ComponentPropsWithoutRef<"input">, "onChange" | "value"> {
|
||||
value?: T
|
||||
onChange?: (value?: T) => void
|
||||
searchValue?: string
|
||||
onSearchValueChange?: (value: string) => void
|
||||
options: ComboboxOption[]
|
||||
fetchNextPage?: () => void
|
||||
isFetchingNextPage?: boolean
|
||||
onCreateOption?: (value: string) => void
|
||||
}
|
||||
|
||||
const ComboboxImpl = <T extends Value = string>(
|
||||
{
|
||||
value: controlledValue,
|
||||
onChange,
|
||||
searchValue: controlledSearchValue,
|
||||
onSearchValueChange,
|
||||
options,
|
||||
className,
|
||||
placeholder,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
onCreateOption,
|
||||
...inputProps
|
||||
}: ComboboxProps<T>,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const comboboxRef = useRef<HTMLInputElement>(null)
|
||||
const listboxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useImperativeHandle(ref, () => comboboxRef.current!)
|
||||
|
||||
const isValueControlled = controlledValue !== undefined
|
||||
const isSearchControlled = controlledSearchValue !== undefined
|
||||
|
||||
const isArrayValue = Array.isArray(controlledValue)
|
||||
const emptyState = (isArrayValue ? [] : "") as T
|
||||
|
||||
const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState(
|
||||
controlledSearchValue || ""
|
||||
)
|
||||
const defferedSearchValue = useDeferredValue(uncontrolledSearchValue)
|
||||
|
||||
const [uncontrolledValue, setUncontrolledValue] = useState<T>(emptyState)
|
||||
|
||||
const searchValue = isSearchControlled
|
||||
? controlledSearchValue
|
||||
: uncontrolledSearchValue
|
||||
const selectedValues = isValueControlled ? controlledValue : uncontrolledValue
|
||||
|
||||
const handleValueChange = (newValues?: T) => {
|
||||
// check if the value already exists in options
|
||||
const exists = options
|
||||
.filter((o) => !o.disabled)
|
||||
.find((o) => {
|
||||
if (isArrayValue) {
|
||||
return newValues?.includes(o.value)
|
||||
}
|
||||
return o.value === newValues
|
||||
})
|
||||
|
||||
// If the value does not exist in the options, and the component has a handler
|
||||
// for creating new options, call it.
|
||||
if (!exists && onCreateOption && newValues) {
|
||||
onCreateOption(newValues as string)
|
||||
}
|
||||
|
||||
if (!isValueControlled) {
|
||||
setUncontrolledValue(newValues || emptyState)
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(newValues)
|
||||
}
|
||||
|
||||
setUncontrolledSearchValue("")
|
||||
}
|
||||
|
||||
const handleSearchChange = (query: string) => {
|
||||
setUncontrolledSearchValue(query)
|
||||
|
||||
if (onSearchValueChange) {
|
||||
onSearchValueChange(query)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter and sort the options based on the search value,
|
||||
* and whether the value is already selected.
|
||||
*
|
||||
* This is only used when the search value is uncontrolled.
|
||||
*/
|
||||
const matches = useMemo(() => {
|
||||
if (isSearchControlled) {
|
||||
return []
|
||||
}
|
||||
|
||||
return matchSorter(options, defferedSearchValue, {
|
||||
keys: ["label"],
|
||||
})
|
||||
}, [options, defferedSearchValue, isSearchControlled])
|
||||
|
||||
const observer = useRef(
|
||||
new IntersectionObserver(
|
||||
(entries) => {
|
||||
const first = entries[0]
|
||||
if (first.isIntersecting) {
|
||||
fetchNextPage?.()
|
||||
}
|
||||
},
|
||||
{ threshold: 1 }
|
||||
)
|
||||
)
|
||||
|
||||
const lastOptionRef = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
if (isFetchingNextPage) {
|
||||
return
|
||||
}
|
||||
if (observer.current) {
|
||||
observer.current.disconnect()
|
||||
}
|
||||
if (node) {
|
||||
observer.current.observe(node)
|
||||
}
|
||||
},
|
||||
[isFetchingNextPage]
|
||||
)
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setUncontrolledSearchValue("")
|
||||
}
|
||||
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
const hasValue = selectedValues.length > 0
|
||||
|
||||
const showTag = hasValue && isArrayValue
|
||||
const showSelected = showTag && !searchValue && !open
|
||||
|
||||
const hideInput = !isArrayValue && !open
|
||||
const selectedLabel = options.find((o) => o.value === selectedValues)?.label
|
||||
|
||||
const hidePlaceholder = showSelected || open
|
||||
|
||||
const results = useMemo(() => {
|
||||
return isSearchControlled ? options : matches
|
||||
}, [matches, options, isSearchControlled])
|
||||
|
||||
return (
|
||||
<PrimitiveComboboxProvider
|
||||
open={open}
|
||||
setOpen={handleOpenChange}
|
||||
selectedValue={selectedValues}
|
||||
setSelectedValue={(value) => handleValueChange(value as T)}
|
||||
value={uncontrolledSearchValue}
|
||||
setValue={(query) => {
|
||||
startTransition(() => handleSearchChange(query))
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"relative flex cursor-pointer items-center gap-x-2 overflow-hidden",
|
||||
"h-8 w-full rounded-md px-2 py-0.5",
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||
"has-[:invalid]:shadow-borders-error has-[[aria-invalid=true]]:shadow-borders-error",
|
||||
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||
{
|
||||
"pl-0.5": hasValue && isArrayValue,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showTag && (
|
||||
<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">
|
||||
<span>{selectedValues.length}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="size-fit outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleValueChange(undefined)
|
||||
}}
|
||||
>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex size-full items-center">
|
||||
{showSelected && (
|
||||
<Text size="small" leading="compact">
|
||||
{t("general.selected")}
|
||||
</Text>
|
||||
)}
|
||||
{hideInput && (
|
||||
<div className="absolute inset-y-0 left-0 flex size-full items-center overflow-hidden">
|
||||
<Text size="small" leading="compact" className="truncate">
|
||||
{selectedLabel}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<PrimitiveCombobox
|
||||
autoSelect
|
||||
ref={comboboxRef}
|
||||
className={clx(
|
||||
"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",
|
||||
{
|
||||
"opacity-0": hideInput,
|
||||
}
|
||||
)}
|
||||
placeholder={hidePlaceholder ? undefined : placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
<PrimitiveComboboxDisclosure
|
||||
render={() => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="text-ui-fg-muted pointer-events-none absolute right-2 size-fit outline-none"
|
||||
>
|
||||
<TrianglesMini />
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<PrimitiveComboboxPopover
|
||||
gutter={4}
|
||||
ref={listboxRef}
|
||||
role="listbox"
|
||||
className={clx(
|
||||
"shadow-elevation-flyout bg-ui-bg-base -left-2 z-50 w-[calc(var(--popover-anchor-width)+16px)] 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"
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: open ? "auto" : "none",
|
||||
}}
|
||||
aria-busy={isPending}
|
||||
>
|
||||
{results.map(({ value, label, disabled }) => (
|
||||
<PrimitiveComboboxItem
|
||||
key={value}
|
||||
value={value}
|
||||
focusOnHover
|
||||
setValueOnClick={false}
|
||||
disabled={disabled}
|
||||
className={clx(
|
||||
"transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group flex cursor-pointer items-center gap-x-2 rounded-[4px] px-2 py-1.5",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
"bg-ui-bg-component": disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<PrimitiveComboboxItemCheck className="flex !size-5 items-center justify-center">
|
||||
<EllipseMiniSolid />
|
||||
</PrimitiveComboboxItemCheck>
|
||||
<PrimitiveComboboxItemValue className="txt-compact-small">
|
||||
{label}
|
||||
</PrimitiveComboboxItemValue>
|
||||
</PrimitiveComboboxItem>
|
||||
))}
|
||||
{!!fetchNextPage && <div ref={lastOptionRef} className="w-px" />}
|
||||
{isFetchingNextPage && (
|
||||
<div className="transition-fg bg-ui-bg-base flex items-center rounded-[4px] px-2 py-1.5">
|
||||
<div className="bg-ui-bg-component size-full h-5 w-full animate-pulse rounded-[4px]" />
|
||||
</div>
|
||||
)}
|
||||
{!results.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>
|
||||
)}
|
||||
{!results.length && onCreateOption && (
|
||||
<Fragment>
|
||||
<PrimitiveSeparator className="bg-ui-border-base -mx-1" />
|
||||
<PrimitiveComboboxItem
|
||||
value={uncontrolledSearchValue}
|
||||
focusOnHover
|
||||
setValueOnClick={false}
|
||||
className="transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group mt-1 flex cursor-pointer items-center gap-x-2 rounded-[4px] px-2 py-1.5"
|
||||
>
|
||||
<PlusMini className="text-ui-fg-subtle" />
|
||||
<Text size="small" leading="compact">
|
||||
{t("actions.create")} "{searchValue}"
|
||||
</Text>
|
||||
</PrimitiveComboboxItem>
|
||||
</Fragment>
|
||||
)}
|
||||
</PrimitiveComboboxPopover>
|
||||
</PrimitiveComboboxProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const Combobox = genericForwardRef(ComboboxImpl)
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./combobox"
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react"
|
||||
|
||||
import { TrianglesMini } from "@medusajs/icons"
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { countries } from "../../../lib/data/countries"
|
||||
|
||||
export const CountrySelect = forwardRef<
|
||||
HTMLSelectElement,
|
||||
ComponentPropsWithoutRef<"select"> & {
|
||||
placeholder?: string
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, disabled, placeholder, value, defaultValue, ...props },
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const innerRef = useRef<HTMLSelectElement>(null)
|
||||
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLSelectElement)
|
||||
|
||||
const isPlaceholder = innerRef.current?.value === ""
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<TrianglesMini
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg pointer-events-none absolute right-2 top-1/2 -translate-y-1/2",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<select
|
||||
value={value !== undefined ? value.toLowerCase() : undefined}
|
||||
defaultValue={defaultValue ? defaultValue.toLowerCase() : undefined}
|
||||
disabled={disabled}
|
||||
className={clx(
|
||||
"bg-ui-bg-field shadow-buttons-neutral transition-fg txt-compact-small flex w-full select-none appearance-none items-center justify-between rounded-md px-2 py-1.5 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",
|
||||
{
|
||||
"text-ui-fg-muted": isPlaceholder,
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={innerRef}
|
||||
>
|
||||
{/* Add an empty option so the first option is preselected */}
|
||||
<option value="" disabled className="text-ui-fg-muted">
|
||||
{placeholder || t("fields.selectCountry")}
|
||||
</option>
|
||||
{countries.map((country) => {
|
||||
return (
|
||||
<option key={country.iso_2} value={country.iso_2.toLowerCase()}>
|
||||
{country.display_name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
CountrySelect.displayName = "CountrySelect"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./country-select"
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Input, Text } from "@medusajs/ui"
|
||||
import { ComponentProps, ElementRef, forwardRef } from "react"
|
||||
|
||||
export const HandleInput = forwardRef<
|
||||
ElementRef<typeof Input>,
|
||||
ComponentProps<typeof Input>
|
||||
>((props, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
/
|
||||
</Text>
|
||||
</div>
|
||||
<Input ref={ref} {...props} className="pl-10" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
HandleInput.displayName = "HandleInput"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./handle-input"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./percentage-input"
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Input, Text, clx } from "@medusajs/ui"
|
||||
import { ComponentProps, ElementRef, forwardRef } from "react"
|
||||
import Primitive from "react-currency-input-field"
|
||||
|
||||
/**
|
||||
* @deprecated Use `PercentageInput` instead
|
||||
*/
|
||||
export const DeprecatedPercentageInput = forwardRef<
|
||||
ElementRef<typeof Input>,
|
||||
Omit<ComponentProps<typeof Input>, "type">
|
||||
>(({ min = 0, max = 100, step = 0.0001, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
%
|
||||
</Text>
|
||||
</div>
|
||||
<Input
|
||||
ref={ref}
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
{...props}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
DeprecatedPercentageInput.displayName = "PercentageInput"
|
||||
|
||||
export const PercentageInput = forwardRef<
|
||||
ElementRef<"input">,
|
||||
ComponentProps<typeof Primitive>
|
||||
>(({ min = 0, decimalScale = 2, className, ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Primitive
|
||||
ref={ref as any} // dependency is typed incorrectly
|
||||
min={min}
|
||||
autoComplete="off"
|
||||
decimalScale={decimalScale}
|
||||
decimalsLimit={decimalScale}
|
||||
{...props}
|
||||
className={clx(
|
||||
"caret-ui-fg-base bg-ui-bg-field shadow-buttons-neutral transition-fg txt-compact-small flex w-full select-none appearance-none items-center justify-between rounded-md px-2 py-1.5 pr-10 text-right 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",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 z-10 flex w-8 items-center justify-center border-l">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
%
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
PercentageInput.displayName = "PercentageInput"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./province-select"
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react"
|
||||
|
||||
import { TrianglesMini } from "@medusajs/icons"
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getCountryProvinceObjectByIso2 } from "../../../lib/data/country-states"
|
||||
|
||||
interface ProvinceSelectProps extends ComponentPropsWithoutRef<"select"> {
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 country code
|
||||
*/
|
||||
country_code: string
|
||||
/**
|
||||
* Whether to use the ISO 3166-1 alpha-2 code or the name of the province as the value
|
||||
*
|
||||
* @default "iso_2"
|
||||
*/
|
||||
valueAs?: "iso_2" | "name"
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const ProvinceSelect = forwardRef<
|
||||
HTMLSelectElement,
|
||||
ProvinceSelectProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
disabled,
|
||||
placeholder,
|
||||
country_code,
|
||||
valueAs = "iso_2",
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const innerRef = useRef<HTMLSelectElement>(null)
|
||||
|
||||
useImperativeHandle(ref, () => innerRef.current as HTMLSelectElement)
|
||||
|
||||
const isPlaceholder = innerRef.current?.value === ""
|
||||
|
||||
const provinceObject = getCountryProvinceObjectByIso2(country_code)
|
||||
|
||||
if (!provinceObject) {
|
||||
disabled = true
|
||||
}
|
||||
|
||||
const options = Object.entries(provinceObject?.options ?? {}).map(
|
||||
([iso2, name]) => {
|
||||
return (
|
||||
<option key={iso2} value={valueAs === "iso_2" ? iso2 : name}>
|
||||
{name}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const placeholderText = provinceObject
|
||||
? t(`taxRegions.fields.sublevels.placeholders.${provinceObject.type}`)
|
||||
: ""
|
||||
|
||||
const placeholderOption = provinceObject ? (
|
||||
<option value="" disabled className="text-ui-fg-muted">
|
||||
{placeholder || placeholderText}
|
||||
</option>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<TrianglesMini
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg pointer-events-none absolute right-2 top-1/2 -translate-y-1/2",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<select
|
||||
disabled={disabled}
|
||||
className={clx(
|
||||
"bg-ui-bg-field shadow-buttons-neutral transition-fg txt-compact-small flex w-full select-none appearance-none items-center justify-between rounded-md px-2 py-1.5 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",
|
||||
{
|
||||
"text-ui-fg-muted": isPlaceholder,
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={innerRef}
|
||||
>
|
||||
{/* Add an empty option so the first option is preselected */}
|
||||
{placeholderOption}
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ProvinceSelect.displayName = "CountrySelect"
|
||||
Reference in New Issue
Block a user