feat(dashboard,types,js-sdk,ui): Tax Regions UI (#7935)

This commit is contained in:
Kasper Fabricius Kristensen
2024-07-10 11:26:43 +02:00
committed by GitHub
parent 75e7047243
commit 046a34bdfc
232 changed files with 7555 additions and 3818 deletions

View File

@@ -1,59 +0,0 @@
import { PropsWithChildren, createContext } from "react"
type ConditionOperator =
| "eq"
| "ne"
| "gt"
| "lt"
| "gte"
| "lte"
| "in"
| "nin"
type ConditionBlockValue<TValue> = {
attribute: string
operator: ConditionOperator
value: TValue
}
type ConditionBlockState<TValue> = {
defaultValue?: ConditionBlockValue<TValue>
value?: ConditionBlockValue<TValue>
onChange: (value: ConditionBlockValue<TValue>) => void
}
const ConditionBlockContext = createContext<ConditionBlockState<any> | null>(
null
)
const useConditionBlock = () => {
const context = ConditionBlockContext
if (!context) {
throw new Error("useConditionBlock must be used within a ConditionBlock")
}
return context
}
type ConditionBlockProps<TValue> = PropsWithChildren<
ConditionBlockState<TValue>
>
const Root = <TValue,>({ children, ...props }: ConditionBlockProps<TValue>) => {
return (
<ConditionBlockContext.Provider value={props}>
{children}
</ConditionBlockContext.Provider>
)
}
const Divider = () => {}
const Operator = () => {}
const Item = () => {}
export const ConditionBlock = Object.assign(Root, {
Divider,
})

View File

@@ -3,6 +3,7 @@ import { PropsWithChildren } from "react"
type IconAvatarProps = PropsWithChildren<{
className?: string
size?: "small" | "large" | "xlarge"
}>
/**
@@ -10,12 +11,24 @@ type IconAvatarProps = PropsWithChildren<{
*
* The `<Avatar/>` component from `@medusajs/ui` does not support passing an icon as a child.
*/
export const IconAvatar = ({ children, className }: IconAvatarProps) => {
export const IconAvatar = ({
size = "small",
children,
className,
}: IconAvatarProps) => {
return (
<div
className={clx(
"shadow-borders-base bg-ui-bg-base flex size-7 items-center justify-center rounded-md",
"[&>div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle [&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center [&>div]:rounded-[4px]",
"shadow-borders-base flex size-7 items-center justify-center",
"[&>div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle [&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center",
{
"size-7 rounded-md [&>div]:size-6 [&>div]:rounded-[4px]":
size === "small",
"size-10 rounded-lg [&>div]:size-9 [&>div]:rounded-[6px]":
size === "large",
"size-12 rounded-xl [&>div]:size-11 [&>div]:rounded-[10px]":
size === "xlarge",
},
className
)}
>

View File

@@ -136,6 +136,23 @@ export const GeneralSectionSkeleton = ({
)
}
export const TableFooterSkeleton = ({ layout }: { layout: "fill" | "fit" }) => {
return (
<div
className={clx("flex items-center justify-between p-4", {
"border-t": layout === "fill",
})}
>
<Skeleton className="h-7 w-[138px]" />
<div className="flex items-center gap-x-2">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-7 w-11" />
<Skeleton className="h-7 w-11" />
</div>
</div>
)
}
type TableSkeletonProps = {
rowCount?: number
search?: boolean
@@ -182,20 +199,7 @@ export const TableSkeleton = ({
<Skeleton key={row} className="h-10 w-full rounded-none" />
))}
</div>
{pagination && (
<div
className={clx("flex items-center justify-between p-4", {
"border-t": layout === "fill",
})}
>
<Skeleton className="h-7 w-[138px]" />
<div className="flex items-center gap-x-2">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-7 w-11" />
<Skeleton className="h-7 w-11" />
</div>
</div>
)}
{pagination && <TableFooterSkeleton layout={layout} />}
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { clx } from "@medusajs/ui"
import { ComponentPropsWithoutRef, forwardRef, memo } from "react"
import { Controller } from "react-hook-form"
import { countries } from "../../../lib/countries"
import { countries } from "../../../lib/data/countries"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"

View File

@@ -1,7 +1,7 @@
import CurrencyInput from "react-currency-input-field"
import { Controller } from "react-hook-form"
import { currencies } from "../../../lib/currencies"
import { currencies } from "../../../lib/data/currencies"
import { useDataGridCell } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridCellContainer } from "./data-grid-cell-container"

View File

@@ -8,7 +8,7 @@ import {
import { TrianglesMini } from "@medusajs/icons"
import { clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { countries } from "../../../lib/countries"
import { countries } from "../../../lib/data/countries"
export const CountrySelect = forwardRef<
HTMLSelectElement,

View File

@@ -1,7 +1,11 @@
import { Input, Text } from "@medusajs/ui"
import { Input, Text, clx } from "@medusajs/ui"
import { ComponentProps, ElementRef, forwardRef } from "react"
import Primitive from "react-currency-input-field"
export const PercentageInput = forwardRef<
/**
* @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) => {
@@ -29,4 +33,43 @@ export const PercentageInput = forwardRef<
</div>
)
})
PercentageInput.displayName = "HandleInput"
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"

View File

@@ -0,0 +1 @@
export * from "./province-select"

View File

@@ -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"

View File

@@ -107,10 +107,7 @@ export const NavItem = ({
<ul>
{items.map((item) => {
return (
<li
key={item.to}
className="flex h-[32px] items-center gap-x-1 pl-2"
>
<li key={item.to} className="flex h-[32px] items-center pl-1">
<div
role="presentation"
className="flex h-full w-5 items-center justify-center"

View File

@@ -0,0 +1,2 @@
export * from "./single-column-page"
export * from "./two-column-page"

View File

@@ -0,0 +1 @@
export * from "./single-column-page"

View File

@@ -0,0 +1,38 @@
import { Outlet } from "react-router-dom"
import { JsonViewSection } from "../../../common/json-view-section"
import { PageProps } from "../types"
export const SingleColumnPage = <TData,>({
children,
widgets,
data,
hasOutlet,
showJSON,
}: PageProps<TData>) => {
const { before, after } = widgets
const widgetProps = { data }
if (showJSON && !data) {
if (process.env.NODE_ENV === "development") {
console.warn(
"`showJSON` is true but no data is provided. To display JSON, provide data prop."
)
}
showJSON = false
}
return (
<div className="flex flex-col gap-y-3">
{before.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
{children}
{after.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
{showJSON && <JsonViewSection data={data!} />}
{hasOutlet && <Outlet />}
</div>
)
}

View File

@@ -0,0 +1 @@
export * from "./two-column-page"

View File

@@ -0,0 +1,123 @@
import { clx } from "@medusajs/ui"
import { Children, ComponentPropsWithoutRef } from "react"
import { Outlet } from "react-router-dom"
import { JsonViewSection } from "../../../common/json-view-section"
import { PageProps, WidgetImport, WidgetProps } from "../types"
interface TwoColumnWidgetProps extends WidgetProps {
sideBefore: WidgetImport
sideAfter: WidgetImport
}
interface TwoColumnPageProps<TData> extends PageProps<TData> {
widgets: TwoColumnWidgetProps
}
const Root = <TData,>({
children,
/**
* Widgets to be rendered in the main content area and sidebar.
*/
widgets,
/**
* Data to be passed to widgets and the JSON view.
*/
data,
/**
* Whether to show JSON view of the data. Defaults to true.
*/
showJSON = false,
/**
* Whether to render an outlet for children routes. Defaults to true.
*/
hasOutlet = true,
}: TwoColumnPageProps<TData>) => {
const widgetProps = { data }
const { before, after, sideBefore, sideAfter } = widgets
if (showJSON && !data) {
if (process.env.NODE_ENV === "development") {
console.warn(
"`showJSON` is true but no data is provided. To display JSON, provide data prop."
)
}
showJSON = false
}
const childrenArray = Children.toArray(children)
if (childrenArray.length !== 2) {
throw new Error("TwoColumnPage expects exactly two children")
}
const [main, sidebar] = childrenArray
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-3">
{main}
{after.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
{showJSON && (
<div className="hidden xl:block">
<JsonViewSection data={data!} root="product" />
</div>
)}
</div>
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
{sideBefore.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
{sidebar}
{sideAfter.widgets.map((w, i) => {
return <w.Component {...widgetProps} key={i} />
})}
{showJSON && (
<div className="xl:hidden">
<JsonViewSection data={data!} />
</div>
)}
</div>
</div>
{hasOutlet && <Outlet />}
</div>
)
}
const Main = ({
children,
className,
...props
}: ComponentPropsWithoutRef<"div">) => {
return (
<div className={clx("flex w-full flex-col gap-y-3", className)} {...props}>
{children}
</div>
)
}
const Sidebar = ({
children,
className,
...props
}: ComponentPropsWithoutRef<"div">) => {
return (
<div
className={clx(
"flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]",
className
)}
{...props}
>
{children}
</div>
)
}
export const TwoColumnPage = Object.assign(Root, { Main, Sidebar })

View File

@@ -0,0 +1,22 @@
import { ReactNode } from "react"
export type Widget = {
Component: React.ComponentType<any>
}
export type WidgetImport = {
widgets: Widget[]
}
export interface WidgetProps {
before: WidgetImport
after: WidgetImport
}
export interface PageProps<TData> {
children: ReactNode
widgets: WidgetProps
data?: TData
showJSON?: boolean
hasOutlet?: boolean
}

View File

@@ -42,8 +42,8 @@ const useSettingRoutes = (): NavItemProps[] => {
to: "/settings/regions",
},
{
label: "Taxes",
to: "/settings/taxes",
label: t("taxRegions.domain"),
to: "/settings/tax-regions",
},
{
label: t("salesChannels.domain"),

View File

@@ -10,6 +10,7 @@ import {
Keyboard,
MagnifyingGlass,
SidebarLeft,
TriangleRightMini,
User as UserIcon,
} from "@medusajs/icons"
import {
@@ -92,18 +93,17 @@ const Breadcrumbs = () => {
})
return (
<ol className={clx("text-ui-fg-muted flex select-none items-center")}>
<ol
className={clx(
"text-ui-fg-muted txt-compact-small-plus flex select-none items-center"
)}
>
{crumbs.map((crumb, index) => {
const isLast = index === crumbs.length - 1
const isSingle = crumbs.length === 1
return (
<li
key={index}
className={clx("txt-compact-small-plus flex items-center", {
"text-ui-fg-subtle": isLast,
})}
>
<li key={index} className={clx("flex items-center")}>
{!isLast ? (
<Link
className="transition-fg hover:text-ui-fg-subtle"
@@ -124,7 +124,11 @@ const Breadcrumbs = () => {
</span>
</div>
)}
{!isLast && <span className="mx-2 -mt-0.5"></span>}
{!isLast && (
<span className="mx-2">
<TriangleRightMini />
</span>
)}
</li>
)
})}

View File

@@ -66,7 +66,7 @@ const Content = forwardRef<
return (
<FocusModal.Content
ref={ref}
className={clx("top-6", className)}
className={clx("!top-6", className)}
overlayProps={{
className: "bg-transparent",
}}

View File

@@ -1,4 +1,4 @@
import { ArrowUpDown } from "@medusajs/icons"
import { DescendingSorting } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"
import { useState } from "react"
import { useTranslation } from "react-i18next"
@@ -107,7 +107,7 @@ export const DataTableOrderBy = <TData,>({
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="small">
<ArrowUpDown />
<DescendingSorting />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="z-[1]" align="end">

View File

@@ -1,10 +1,10 @@
import { Tooltip } from "@medusajs/ui"
import format from "date-fns/format"
import { format } from "date-fns/format"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../placeholder-cell"
type DateCellProps = {
date: Date | string | null
date?: Date | string | null
}
export const DateCell = ({ date }: DateCellProps) => {

View File

@@ -1,28 +0,0 @@
import type { Discount } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
type DiscountCellProps = {
discount: Discount
}
export const CodeCell = ({ discount }: DiscountCellProps) => {
return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
{/* // TODO: border color inversion*/}
<span className="bg-ui-tag-neutral-bg truncate rounded-md border border-neutral-200 p-1 text-xs">
{discount.code}
</span>
</div>
)
}
export const CodeHeader = () => {
const { t } = useTranslation()
return (
<div className=" flex h-full w-full items-center ">
<span>{t("fields.code")}</span>
</div>
)
}

View File

@@ -1,25 +0,0 @@
import type { Discount } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
type DiscountCellProps = {
discount: Discount
}
export const DescriptionCell = ({ discount }: DiscountCellProps) => {
return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
<span className="truncate">{discount.rule.description}</span>
</div>
)
}
export const DescriptionHeader = () => {
const { t } = useTranslation()
return (
<div className=" flex h-full w-full items-center ">
<span>{t("fields.description")}</span>
</div>
)
}

View File

@@ -1 +0,0 @@
export * from "./redemption-cell.tsx"

View File

@@ -1,23 +0,0 @@
import { useTranslation } from "react-i18next"
type DiscountCellProps = {
redemptions: number
}
export const RedemptionCell = ({ redemptions }: DiscountCellProps) => {
return (
<div className="flex h-full w-full items-center justify-end gap-x-3 text-right">
<span>{redemptions}</span>
</div>
)
}
export const RedemptionHeader = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full items-center justify-end text-right">
<span className="truncate">{t("fields.totalRedemptions")}</span>
</div>
)
}

View File

@@ -1,45 +0,0 @@
import { Discount } from "@medusajs/medusa"
import { StatusCell as StatusCell_ } from "../../common/status-cell"
import { useTranslation } from "react-i18next"
import {
getDiscountStatus,
PromotionStatus,
} from "../../../../../lib/discounts.ts"
type DiscountCellProps = {
discount: Discount
}
export const StatusCell = ({ discount }: DiscountCellProps) => {
const { t } = useTranslation()
const [color, text] = {
[PromotionStatus.DISABLED]: [
"grey",
t("discounts.discountStatus.disabled"),
],
[PromotionStatus.ACTIVE]: ["green", t("discounts.discountStatus.active")],
[PromotionStatus.SCHEDULED]: [
"orange",
t("discounts.discountStatus.scheduled"),
],
[PromotionStatus.EXPIRED]: ["red", t("discounts.discountStatus.expired")],
}[getDiscountStatus(discount)] as [
"grey" | "orange" | "green" | "red",
string
]
return <StatusCell_ color={color}>{text}</StatusCell_>
}
export const StatusHeader = () => {
const { t } = useTranslation()
return (
<div className=" flex h-full w-full items-center ">
<span>{t("fields.status")}</span>
</div>
)
}

View File

@@ -1 +0,0 @@
export * from "./value-cell.tsx"

View File

@@ -1,35 +0,0 @@
import { DiscountRule } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { MoneyAmountCell } from "../../common/money-amount-cell"
type DiscountCellProps = {
rule: DiscountRule
currencyCode: string
}
export const ValueCell = ({ currencyCode, rule }: DiscountCellProps) => {
const isFixed = rule.type === "fixed"
const isPercentage = rule.type === "percentage"
const isFreeShipping = rule.type === "free_shipping"
return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
{isFreeShipping && <span>Free shipping</span>}
{isPercentage && <span className="">{rule.value}%</span>}
{isFixed && (
<MoneyAmountCell currencyCode={currencyCode} amount={rule.value} />
)}
</div>
)
}
export const ValueHeader = () => {
const { t } = useTranslation()
return (
<div className=" flex h-full w-full items-center ">
<span>{t("fields.value")}</span>
</div>
)
}

View File

@@ -1,9 +1,9 @@
import { useTranslation } from "react-i18next"
import { RegionCountryDTO } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../common/placeholder-cell"
import { countries as COUNTRIES } from "../../../../../lib/data/countries"
import { ListSummary } from "../../../../common/list-summary"
import { countries as COUNTRIES } from "../../../../../lib/countries"
import { PlaceholderCell } from "../../common/placeholder-cell"
type CountriesCellProps = {
countries?: RegionCountryDTO[] | null

View File

@@ -21,3 +21,5 @@ export const I18n = () => {
return null
}
export { i18n }