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

This commit is contained in:
Kasper Fabricius Kristensen
2024-07-10 09:26:43 +00:00
committed by GitHub
parent 75e7047243
commit 046a34bdfc
232 changed files with 7555 additions and 3818 deletions
@@ -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,
})
@@ -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
)}
>
@@ -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>
)
}
@@ -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"
@@ -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"
@@ -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,
@@ -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"
@@ -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"
@@ -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"
@@ -0,0 +1,2 @@
export * from "./single-column-page"
export * from "./two-column-page"
@@ -0,0 +1 @@
export * from "./single-column-page"
@@ -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>
)
}
@@ -0,0 +1 @@
export * from "./two-column-page"
@@ -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 })
@@ -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
}
@@ -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"),
@@ -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>
)
})}
@@ -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",
}}
@@ -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">
@@ -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) => {
@@ -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>
)
}
@@ -1 +0,0 @@
export * from "./code-cell"
@@ -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>
)
}
@@ -1 +0,0 @@
export * from "./description-cell"
@@ -1 +0,0 @@
export * from "./redemption-cell.tsx"
@@ -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>
)
}
@@ -1 +0,0 @@
export * from "./status-cell"
@@ -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>
)
}
@@ -1 +0,0 @@
export * from "./value-cell.tsx"
@@ -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>
)
}
@@ -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
@@ -21,3 +21,5 @@ export const I18n = () => {
return null
}
export { i18n }
@@ -1,3 +1,4 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes, PaginatedResponse } from "@medusajs/types"
import {
QueryKey,
@@ -25,7 +26,7 @@ export const useCustomerGroup = (
options?: Omit<
UseQueryOptions<
{ customer_group: HttpTypes.AdminCustomerGroup },
Error,
FetchError,
{ customer_group: HttpTypes.AdminCustomerGroup },
QueryKey
>,
@@ -46,7 +47,7 @@ export const useCustomerGroups = (
options?: Omit<
UseQueryOptions<
PaginatedResponse<{ customer_groups: HttpTypes.AdminCustomerGroup[] }>,
Error,
FetchError,
PaginatedResponse<{ customer_groups: HttpTypes.AdminCustomerGroup[] }>,
QueryKey
>,
@@ -65,7 +66,7 @@ export const useCustomerGroups = (
export const useCreateCustomerGroup = (
options?: UseMutationOptions<
{ customer_group: HttpTypes.AdminCustomerGroup },
Error,
FetchError,
z.infer<typeof CreateCustomerGroupSchema>
>
) => {
@@ -85,7 +86,7 @@ export const useUpdateCustomerGroup = (
id: string,
options?: UseMutationOptions<
{ customer_group: HttpTypes.AdminCustomerGroup },
Error,
FetchError,
z.infer<typeof EditCustomerGroupSchema>
>
) => {
@@ -109,7 +110,7 @@ export const useDeleteCustomerGroup = (
id: string,
options?: UseMutationOptions<
{ id: string; object: "customer-group"; deleted: boolean },
Error,
FetchError,
void
>
) => {
@@ -133,7 +134,7 @@ export const useAddCustomersToGroup = (
id: string,
options?: UseMutationOptions<
{ customer_group: HttpTypes.AdminCustomerGroup },
Error,
FetchError,
{ customer_ids: string[] }
>
) => {
@@ -160,7 +161,7 @@ export const useRemoveCustomersFromGroup = (
id: string,
options?: UseMutationOptions<
{ customer_group: HttpTypes.AdminCustomerGroup },
Error,
FetchError,
{ customer_ids: string[] }
>
) => {
@@ -0,0 +1,31 @@
export * from "./api-keys"
export * from "./auth"
export * from "./campaigns"
export * from "./categories"
export * from "./collections"
export * from "./currencies"
export * from "./customer-groups"
export * from "./customers"
export * from "./fulfillment"
export * from "./fulfillment-providers"
export * from "./fulfillment-sets"
export * from "./inventory"
export * from "./invites"
export * from "./orders"
export * from "./payments"
export * from "./price-lists"
export * from "./product-types"
export * from "./products"
export * from "./promotions"
export * from "./regions"
export * from "./reservations"
export * from "./sales-channels"
export * from "./shipping-options"
export * from "./shipping-profiles"
export * from "./stock-locations"
export * from "./store"
export * from "./tags"
export * from "./tax-rates"
export * from "./tax-regions"
export * from "./users"
export * from "./workflow-executions"
@@ -1,7 +1,8 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { sdk } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { TagsListRes, TagRes } from "../../types/api-responses"
const TAGS_QUERY_KEY = "tags" as const
export const tagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY)
@@ -9,13 +10,18 @@ export const tagsQueryKeys = queryKeysFactory(TAGS_QUERY_KEY)
export const useTag = (
id: string,
options?: Omit<
UseQueryOptions<TagRes, Error, TagRes, QueryKey>,
UseQueryOptions<
HttpTypes.AdminProductTagResponse,
FetchError,
HttpTypes.AdminProductTagResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: tagsQueryKeys.detail(id),
queryFn: async () => client.tags.retrieve(id),
queryFn: async () => sdk.admin.productTag.retrieve(id),
...options,
})
@@ -23,15 +29,20 @@ export const useTag = (
}
export const useTags = (
query?: Record<string, any>,
query?: HttpTypes.AdminProductTagListParams,
options?: Omit<
UseQueryOptions<TagsListRes, Error, TagsListRes, QueryKey>,
UseQueryOptions<
HttpTypes.AdminProductTagListResponse,
FetchError,
HttpTypes.AdminProductTagListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryKey: tagsQueryKeys.list(query),
queryFn: async () => client.tags.list(query),
queryFn: async () => sdk.admin.productTag.list(query),
...options,
})
@@ -1,3 +1,4 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
@@ -9,6 +10,7 @@ import {
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { taxRegionsQueryKeys } from "./tax-regions"
const TAX_RATES_QUERY_KEY = "tax_rates" as const
export const taxRatesQueryKeys = queryKeysFactory(TAX_RATES_QUERY_KEY)
@@ -19,7 +21,7 @@ export const useTaxRate = (
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTaxRateResponse,
Error,
FetchError,
HttpTypes.AdminTaxRateResponse,
QueryKey
>,
@@ -72,6 +74,8 @@ export const useUpdateTaxRate = (
queryKey: taxRatesQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context)
},
...options,
@@ -81,7 +85,7 @@ export const useUpdateTaxRate = (
export const useCreateTaxRate = (
options?: UseMutationOptions<
HttpTypes.AdminTaxRateResponse,
Error,
FetchError,
HttpTypes.AdminCreateTaxRate
>
) => {
@@ -89,6 +93,9 @@ export const useCreateTaxRate = (
mutationFn: (payload) => sdk.admin.taxRate.create(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: taxRatesQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context)
},
...options,
@@ -111,6 +118,8 @@ export const useDeleteTaxRate = (
queryKey: taxRatesQueryKeys.detail(id),
})
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context)
},
...options,
@@ -1,3 +1,4 @@
import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
@@ -15,11 +16,11 @@ export const taxRegionsQueryKeys = queryKeysFactory(TAX_REGIONS_QUERY_KEY)
export const useTaxRegion = (
id: string,
query?: Record<string, any>,
query?: HttpTypes.AdminTaxRegionParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTaxRegionResponse,
Error,
FetchError,
HttpTypes.AdminTaxRegionResponse,
QueryKey
>,
@@ -36,11 +37,11 @@ export const useTaxRegion = (
}
export const useTaxRegions = (
query?: Record<string, any>,
query?: HttpTypes.AdminTaxRegionListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminTaxRegionListResponse,
Error,
FetchError,
HttpTypes.AdminTaxRegionListResponse,
QueryKey
>,
@@ -59,7 +60,7 @@ export const useTaxRegions = (
export const useCreateTaxRegion = (
options?: UseMutationOptions<
HttpTypes.AdminTaxRegionResponse,
Error,
FetchError,
HttpTypes.AdminCreateTaxRegion
>
) => {
@@ -77,7 +78,7 @@ export const useDeleteTaxRegion = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminTaxRegionDeleteResponse,
Error,
FetchError,
void
>
) => {
@@ -89,6 +90,9 @@ export const useDeleteTaxRegion = (
queryKey: taxRegionsQueryKeys.detail(id),
})
// Invalidate all detail queries, as the deleted tax region may have been a sublevel region
queryClient.invalidateQueries({ queryKey: taxRegionsQueryKeys.details() })
options?.onSuccess?.(data, variables, context)
},
...options,
@@ -0,0 +1,12 @@
export * from "./use-campaign-table-columns"
export * from "./use-collection-table-columns"
export * from "./use-customer-group-table-columns"
export * from "./use-customer-table-columns"
export * from "./use-order-table-columns"
export * from "./use-product-table-columns"
export * from "./use-product-tag-table-columns"
export * from "./use-product-type-table-columns"
export * from "./use-region-table-columns"
export * from "./use-sales-channel-table-columns"
export * from "./use-shipping-option-table-columns"
export * from "./use-tax-rates-table-columns"
@@ -2,8 +2,7 @@ import { HttpTypes } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { CollectionRowActions } from "./collection-row-actions"
import { TextCell } from "../../../components/table/table-cells/common/text-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminCollection>()
@@ -14,23 +13,20 @@ export const useCollectionTableColumns = () => {
() => [
columnHelper.accessor("title", {
header: t("fields.title"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("handle", {
header: t("fields.handle"),
cell: ({ getValue }) => `/${getValue()}`,
cell: ({ getValue }) => <TextCell text={`/${getValue()}`} />,
}),
columnHelper.accessor("products", {
header: t("fields.products"),
cell: ({ getValue }) => {
const count = getValue()?.length
const count = getValue()?.length || undefined
return <span>{count || "-"}</span>
return <TextCell text={count} />
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <CollectionRowActions collection={row.original} />,
}),
],
[t]
)
@@ -1,65 +0,0 @@
import { useMemo } from "react"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import type { Discount } from "@medusajs/medusa"
import {
CodeCell,
CodeHeader,
} from "../../../components/table/table-cells/discount/code-cell"
import {
DescriptionHeader,
DescriptionCell,
} from "../../../components/table/table-cells/discount/description-cell"
import {
ValueCell,
ValueHeader,
} from "../../../components/table/table-cells/discount/value-cell"
import {
RedemptionCell,
RedemptionHeader,
} from "../../../components/table/table-cells/discount/redemption-cell"
import {
StatusHeader,
StatusCell,
} from "../../../components/table/table-cells/discount/status-cell"
const columnHelper = createColumnHelper<Discount>()
export const useDiscountTableColumns = () => {
return useMemo(
() => [
columnHelper.display({
id: "discount",
header: () => <CodeHeader />,
cell: ({ row }) => <CodeCell discount={row.original} />,
}),
columnHelper.accessor("rule.description", {
header: () => <DescriptionHeader />,
cell: ({ row }) => <DescriptionCell discount={row.original} />,
}),
columnHelper.accessor("rule.value", {
header: () => <ValueHeader />,
cell: ({ row }) => (
<ValueCell
rule={row.original.rule}
currencyCode={row.original.regions[0]?.currency_code}
/>
),
}),
columnHelper.display({
id: "status",
header: () => <StatusHeader />,
cell: ({ row }) => <StatusCell discount={row.original} />,
}),
columnHelper.accessor("usage_count", {
header: () => <RedemptionHeader />,
cell: ({ row }) => (
<RedemptionCell redemptions={row.original.usage_count} />
),
}),
],
[]
) as ColumnDef<Discount>[]
}
@@ -0,0 +1,35 @@
import { HttpTypes } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { DateCell } from "../../../components/table/table-cells/common/date-cell"
import { TextCell } from "../../../components/table/table-cells/common/text-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminProductTag>()
export const useProductTagTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("value", {
header: () => t("fields.value"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("created_at", {
header: () => t("fields.createdAt"),
cell: ({ getValue }) => {
return <DateCell date={getValue()} />
},
}),
columnHelper.accessor("updated_at", {
header: () => t("fields.updatedAt"),
cell: ({ getValue }) => {
return <DateCell date={getValue()} />
},
}),
],
[t]
)
}
@@ -2,8 +2,9 @@ import { HttpTypes } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ProductTypeRowActions } from "./product-table-row-actions"
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
import { DateCell } from "../../../components/table/table-cells/common/date-cell"
import { TextCell } from "../../../components/table/table-cells/common/text-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminProductType>()
@@ -13,28 +14,20 @@ export const useProductTypeTableColumns = () => {
return useMemo(
() => [
columnHelper.accessor("value", {
header: () => t("productTypes.fields.value"),
cell: ({ getValue }) => getValue(),
header: () => t("fields.value"),
cell: ({ getValue }) => <TextCell text={getValue()} />,
}),
columnHelper.accessor("created_at", {
header: () => t("fields.createdAt"),
cell: ({ getValue }) => {
const date = new Date(getValue())
return <DateCell date={date} />
return <DateCell date={getValue()} />
},
}),
columnHelper.accessor("updated_at", {
header: () => t("fields.updatedAt"),
cell: ({ getValue }) => {
const date = new Date(getValue())
return <DateCell date={date} />
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <ProductTypeRowActions productType={row.original} />
return <DateCell date={getValue()} />
},
}),
],
@@ -0,0 +1,13 @@
export * from "./use-collection-table-filters"
export * from "./use-customer-group-table-filters"
export * from "./use-customer-table-filters"
export * from "./use-date-table-filters"
export * from "./use-order-table-filters"
export * from "./use-product-table-filters"
export * from "./use-product-tag-table-filters"
export * from "./use-product-type-table-filters"
export * from "./use-promotion-table-filters"
export * from "./use-region-table-filters"
export * from "./use-sales-channel-table-filters"
export * from "./use-shipping-option-table-filters"
export * from "./use-tax-rate-table-filters"
@@ -0,0 +1,7 @@
import { useDateTableFilters } from "./use-date-table-filters"
export const useCollectionTableFilters = () => {
const dateFilters = useDateTableFilters()
return dateFilters
}
@@ -1,51 +0,0 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
export const useDiscountTableFilters = () => {
const { t } = useTranslation()
let filters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
const isDisabledFilter: Filter = {
key: "is_disabled",
label: t("fields.disabled"),
type: "select",
options: [
{
label: t("fields.true"),
value: "true",
},
{
label: t("fields.false"),
value: "false",
},
],
}
const isDynamicFilter: Filter = {
key: "is_dynamic",
label: t("fields.type"),
type: "select",
options: [
{
label: t("fields.dynamic"),
value: "true",
},
{
label: t("fields.normal"),
value: "false",
},
],
}
filters = [...filters, isDisabledFilter, isDynamicFilter]
return filters
}
@@ -0,0 +1,7 @@
import { useDateTableFilters } from "./use-date-table-filters"
export const useProductTagTableFilters = () => {
const dateFilters = useDateTableFilters()
return dateFilters
}
@@ -1,4 +1,4 @@
import { useDateTableFilters } from "../../../../../hooks/table/filters/use-date-table-filters"
import { useDateTableFilters } from "./use-date-table-filters"
export const useProductTypeTableFilters = () => {
const dateFilters = useDateTableFilters()
@@ -0,0 +1,14 @@
export * from "./use-campaign-table-query"
export * from "./use-collection-table-query"
export * from "./use-customer-group-table-query"
export * from "./use-customer-table-query"
export * from "./use-order-table-query"
export * from "./use-product-table-query"
export * from "./use-product-tag-table-query"
export * from "./use-product-type-table-query"
export * from "./use-region-table-query"
export * from "./use-sales-channel-table-query"
export * from "./use-shipping-option-table-query"
export * from "./use-tax-rate-table-query"
export * from "./use-tax-region-table-query"
export * from "./use-user-invite-table-query"
@@ -1,5 +1,5 @@
import { AdminCollectionFilters, FindParams } from "@medusajs/types"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseCollectionTableQueryProps = {
prefix?: string
@@ -17,7 +17,7 @@ export const useCollectionTableQuery = ({
const { offset, created_at, updated_at, q, order } = queryObject
const searchParams: FindParams & AdminCollectionFilters = {
const searchParams: HttpTypes.AdminCollectionListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
@@ -1,45 +0,0 @@
import { AdminGetDiscountsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../use-query-params"
type UseDiscountTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useDiscountTableQuery = ({
prefix,
pageSize = 20,
}: UseDiscountTableQueryProps) => {
const queryObject = useQueryParams(
[
"offset",
"order",
"q",
"is_dynamic",
"is_disabled",
"created_at",
"updated_at",
],
prefix
)
const { offset, order, q, is_dynamic, is_disabled, created_at, updated_at } =
queryObject
const searchParams: AdminGetDiscountsParams = {
limit: pageSize,
is_disabled: is_disabled ? is_disabled === "true" : undefined,
is_dynamic: is_dynamic ? is_dynamic === "true" : undefined,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
offset: offset ? Number(offset) : 0,
order,
q,
}
return {
searchParams,
raw: queryObject,
}
}
@@ -0,0 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseProductTagTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useProductTagTableQuery = ({
prefix,
pageSize = 20,
}: UseProductTagTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams: HttpTypes.AdminProductTagListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}
@@ -1,4 +1,5 @@
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseProductTypeTableQueryProps = {
prefix?: string
@@ -15,7 +16,7 @@ export const useProductTypeTableQuery = ({
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams = {
const searchParams: HttpTypes.AdminProductTypeListParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,
@@ -98,7 +98,8 @@
"add": "Add",
"select": "Select",
"browse": "Browse",
"logout": "Logout"
"logout": "Logout",
"hide": "Hide"
},
"operators": {
"in": "In"
@@ -1049,127 +1050,239 @@
"type": "Enter shipping profile type, for example: Heavy, Oversized, Freight-only, etc."
}
},
"discounts": {
"domain": "Discounts",
"startDate": "Start date",
"createDiscountTitle": "Create Discount",
"validDuration": "Duration of the discount",
"redemptionsLimit": "Redemptions limit",
"endDate": "End date",
"type": "Discount type",
"percentageDiscount": "Percentage discount",
"freeShipping": "Free shipping",
"fixedDiscount": "Fixed discount",
"fixedAmount": "Fixed amount",
"validRegions": "Valid regions",
"deleteWarning": "You are about to delete the discount {{code}}. This action cannot be undone.",
"editDiscountDetails": "Edit Discount Details",
"editDiscountConfiguration": "Edit Discount Configuration",
"hasStartDate": "Discount has a start date",
"hasEndDate": "Discount has an expiry date",
"startDateHint": "Schedule the discount to activate in the future.",
"endDateHint": "Schedule the discount to deactivate in the future.",
"codeHint": "Discount code applies from when you hit the publish button and forever if left untouched.",
"hasUsageLimit": "Limit the number of redemptions?",
"usageLimitHint": "Limit applies across all customers, not per customer.",
"titleHint": "The code your customers will enter during checkout. This will appear on your customer's invoice.\nUppercase letters and numbers only.",
"hasDurationLimit": "Availability duration",
"durationHint": "Set the duration of the discount",
"chooseValidRegions": "Choose valid regions",
"conditionsHint": "Create conditions to apply on the discount",
"isTemplateDiscount": "Is this a template discount?",
"percentageDescription": "Discount applied in %",
"fixedDescription": "Amount discount",
"shippingDescription": "Override delivery amount",
"selectRegionFirst": "Select a region first",
"templateHint": "Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same.",
"conditions": {
"editHeader": "Edit Discount Conditions",
"editHint": "Specify conditions for when the discount can be applied to a cart.",
"manageTypesAction": "Manage condition types",
"including": {
"products_one": "Discount applies to <0/> product",
"products_other": "Discount applies to <0/> products",
"customer_groups_one": "Discount applies to <0/> customer group",
"customer_groups_other": "Discount applies to <0/> customer groups",
"product_tags_one": "Discount applies to <0/> tag",
"product_tags_other": "Discount applies to <0/> tags",
"product_collections_one": "Discount applies to <0/> product collection",
"product_collections_other": "Discount applies to <0/> product collections",
"product_types_one": "Discount applies to <0/> product type",
"product_types_other": "Discount applies to <0/> product types"
},
"excluding": {
"products": "Discount applies to <1>all</1> products except <0/>",
"customer_groups": "Discount applies to <1>all</1> customer groups except <0/>",
"product_tags": "Discount applies to <1>all</1> product tags except <0/>",
"product_collections": "Discount applies to <1>all</1> product collections except <0/>",
"product_types": "Discount applies to <1>all</1> product types except <0/>"
},
"edit": {
"appliesTo": "Discount applies to",
"except": {
"products_one": "product, except",
"products_other": "products, except",
"product_tags_one": "product tag, except",
"product_tags_other": "product tags, except",
"product_types_one": "product type, except",
"product_types_other": "product types, except",
"product_collections_one": "product collection, except",
"product_collections_other": "product collections, except",
"customer_groups_one": "customer group, except",
"customer_groups_other": "customer groups, except"
}
}
},
"discountStatus": {
"scheduled": "Scheduled",
"expired": "Expired",
"active": "Active",
"disabled": "Disabled"
}
},
"taxRates": {
"domain": "Tax Rates",
"fields": {
"isCombinable": "Is combinable?",
"appliesTo": "Tax Rate applies to",
"customer_groups": "Customer Group",
"product_collections": "Product Collection",
"product_tags": "Product Tag",
"product_types": "Product Type",
"products": "Product"
},
"edit": {
"title": "Edit Tax Rate",
"description": "Edits the tax rate of a tax region"
},
"create": {
"title": "Create Tax Rate Override",
"description": "Create a tax rate that overrides the default tax rates for selected conditions."
}
},
"taxRegions": {
"domain": "Tax Regions",
"subtitle": "Manage tax rates and settings for each region.",
"description": "Manage your region's tax structure",
"list": {
"hint": "Manage what you charge your customers when they shop from different countries and regions."
},
"delete": {
"confirmation": "You are about to delete a tax region. This action cannot be undone.",
"successToast": "The tax region was successfully deleted."
},
"create": {
"title": "Create Tax Region",
"description": "Creates a tax region with a default tax rate."
},
"create-child": {
"title": "Create Default Rate for Province",
"description": "Creates a tax region for a province with default tax rate"
},
"removeWarning": "You are about to remove {{tax_region_name}}. This action cannot be undone.",
"fields": {
"rate": {
"name": "Rate",
"hint": "Tax rate to apply for a region or province"
"header": "Create Tax Region",
"hint": "Create a new tax region to define tax rates for a specific country.",
"errors": {
"rateIsRequired": "Tax rate is required when creating a default tax rate.",
"nameIsRequired": "Name is required when creating a default tax rate."
},
"is_combinable": {
"name": "Is combinable",
"hint": "Whether this tax rate can be combined with the default rate from province or parent tax region."
"successToast": "The tax region was successfully created."
},
"province": {
"header": "Provinces",
"create": {
"header": "Create Province Tax Region",
"hint": "Create a new tax region to define tax rates for a specific province."
}
},
"state": {
"header": "States",
"create": {
"header": "Create State Tax Region",
"hint": "Create a new tax region to define tax rates for a specific state."
}
},
"stateOrTerritory": {
"header": "States or Territories",
"create": {
"header": "Create State/Territory Tax Region",
"hint": "Create a new tax region to define tax rates for a specific state/territory."
}
},
"county": {
"header": "Counties",
"create": {
"header": "Create County Tax Region",
"hint": "Create a new tax region to define tax rates for a specific county."
}
},
"region": {
"header": "Regions",
"create": {
"header": "Create Region Tax Region",
"hint": "Create a new tax region to define tax rates for a specific region."
}
},
"department": {
"header": "Departments",
"create": {
"header": "Create Department Tax Region",
"hint": "Create a new tax region to define tax rates for a specific department."
}
},
"territory": {
"header": "Territories",
"create": {
"header": "Create Territory Tax Region",
"hint": "Create a new tax region to define tax rates for a specific territory."
}
},
"prefecture": {
"header": "Prefectures",
"create": {
"header": "Create Prefecture Tax Region",
"hint": "Create a new tax region to define tax rates for a specific prefecture."
}
},
"district": {
"header": "Districts",
"create": {
"header": "Create District Tax Region",
"hint": "Create a new tax region to define tax rates for a specific district."
}
},
"governorate": {
"header": "Governorates",
"create": {
"header": "Create Governorate Tax Region",
"hint": "Create a new tax region to define tax rates for a specific governorate."
}
},
"canton": {
"header": "Cantons",
"create": {
"header": "Create Canton Tax Region",
"hint": "Create a new tax region to define tax rates for a specific canton."
}
},
"emirate": {
"header": "Emirates",
"create": {
"header": "Create Emirate Tax Region",
"hint": "Create a new tax region to define tax rates for a specific emirate."
}
},
"sublevel": {
"header": "Sublevels",
"create": {
"header": "Create Sublevel Tax Region",
"hint": "Create a new tax region to define tax rates for a specific sublevel."
}
},
"taxOverrides": {
"header": "Overrides",
"create": {
"header": "Create Override",
"hint": "Create a tax rate that overrides the default tax rates for selected conditions."
},
"edit": {
"header": "Edit Override",
"hint": "Edit the tax rate that overrides the default tax rates for selected conditions."
}
},
"taxRates": {
"create": {
"header": "Create Tax Rate",
"hint": "Create a new tax rate to define the tax rate for a region.",
"successToast": "Tax rate was successfully created."
},
"edit": {
"header": "Edit Tax Rate",
"hint": "Edit the tax rate to define the tax rate for a region.",
"successToast": "Tax rate was successfully updated."
},
"delete": {
"confirmation": "You are about to delete the tax rate {{name}}. This action cannot be undone.",
"successToast": "Tax rate was successfully deleted."
}
},
"fields": {
"isCombinable": {
"label": "Combinable",
"hint": "Whether this tax rate can be combined with the default rate from the tax region.",
"true": "Combinable",
"false": "Not combinable"
},
"defaultTaxRate": {
"label": "Default tax rate",
"tooltip": "The default tax rate for this region. An example is the standard VAT rate for a country or region.",
"action": "Create default tax rate"
},
"taxRate": "Tax rate",
"taxCode": "Tax code",
"targets": {
"label": "Targets",
"hint": "Select the targets that this tax rate will apply to.",
"options": {
"product": "Products",
"productCollection": "Product collections",
"productTag": "Product tags",
"productType": "Product types",
"customerGroup": "Customer groups"
},
"operators": {
"in": "in",
"on": "on",
"and": "and"
},
"placeholders": {
"product": "Search for products",
"productCollection": "Search for product collections",
"productTag": "Search for product tags",
"productType": "Search for product types",
"customerGroup": "Search for customer groups"
},
"tags": {
"product": "Product",
"productCollection": "Product collection",
"productTag": "Product tag",
"productType": "Product type",
"customerGroup": "Customer group"
},
"modal": {
"header": "Add targets"
},
"values_one": "{{count}} value",
"values_other": "{{count}} values",
"numberOfTargets_one": "{{count}} target",
"numberOfTargets_other": "{{count}} targets",
"additionalValues_one": "and {{count}} more value",
"additionalValues_other": "and {{count}} more values",
"action": "Add target"
},
"sublevels": {
"labels": {
"province": "Province",
"state": "State",
"region": "Region",
"stateOrTerritory": "State/Territory",
"department": "Department",
"county": "County",
"territory": "Territory",
"prefecture": "Prefecture",
"district": "District",
"governorate": "Governorate",
"emirate": "Emirate",
"canton": "Canton",
"sublevel": "Sublevel code"
},
"placeholders": {
"province": "Select province",
"state": "Select state",
"region": "Select region",
"stateOrTerritory": "Select state/territory",
"department": "Select department",
"county": "Select county",
"territory": "Select territory",
"prefecture": "Select prefecture",
"district": "Select district",
"governorate": "Select governorate",
"emirate": "Select emirate",
"canton": "Select canton"
},
"tooltips": {
"sublevel": "Enter the ISO 3166-2 code for the sublevel tax region.",
"notPartOfCountry": "{{province}} does not appear to be part of {{country}}. Please double-check if this is correct."
},
"alert": {
"header": "Sublevel regions are disabled for this tax region",
"description": "Sublevel regions are disabled for this region by default. You can enable them to create sublevel regions like provinces, states, or territories.",
"action": "Enable sublevel regions"
}
},
"noDefaultRate": {
"label": "No default rate",
"tooltip": "This tax region does not have a default tax rate. If there is a standard rate, such as a country's VAT, please add it to this region."
}
}
},
@@ -1985,8 +2098,6 @@
"note": "Note",
"automaticTaxes": "Automatic Taxes",
"taxInclusivePricing": "Tax inclusive pricing",
"taxRate": "Tax Rate",
"taxCode": "Tax Code",
"currency": "Currency",
"address": "Address",
"address2": "Apartment, suite, etc.",
@@ -1,6 +1,6 @@
import { AddressDTO } from "@medusajs/types"
import { countries } from "./countries"
import { countries } from "./data/countries"
export const isSameAddress = (a: AddressDTO | null, b: AddressDTO | null) => {
if (!a || !b) {
File diff suppressed because it is too large Load Diff
@@ -1,32 +0,0 @@
import { Discount } from "@medusajs/medusa"
import { end, parse } from "iso8601-duration"
export enum PromotionStatus {
SCHEDULED = "SCHEDULED",
EXPIRED = "EXPIRED",
ACTIVE = "ACTIVE",
DISABLED = "DISABLED",
}
export const getDiscountStatus = (discount: Discount) => {
if (discount.is_disabled) {
return PromotionStatus.DISABLED
}
const date = new Date()
if (new Date(discount.starts_at) > date) {
return PromotionStatus.SCHEDULED
}
if (
(discount.ends_at && new Date(discount.ends_at) < date) ||
(discount.valid_duration &&
date >
end(parse(discount.valid_duration), new Date(discount.starts_at))) ||
discount.usage_count === discount.usage_limit
) {
return PromotionStatus.EXPIRED
}
return PromotionStatus.ACTIVE
}
@@ -1,4 +1,4 @@
import { currencies } from "./currencies"
import { currencies } from "./data/currencies"
export const getDecimalDigits = (currency: string) => {
return currencies[currency.toUpperCase()]?.decimal_digits ?? 0
@@ -15,7 +15,7 @@ export const getDecimalDigits = (currency: string) => {
* getFormattedAmount(10, "usd") // '10,00 $' if the browser's locale is fr-FR
*/
export const getLocaleAmount = (amount: number, currencyCode: string) => {
const formatter = new Intl.NumberFormat(undefined, {
const formatter = new Intl.NumberFormat([], {
style: "currency",
currencyDisplay: "narrowSymbol",
currency: currencyCode,
@@ -25,7 +25,7 @@ export const getLocaleAmount = (amount: number, currencyCode: string) => {
}
export const getNativeSymbol = (currencyCode: string) => {
const formatted = new Intl.NumberFormat(undefined, {
const formatted = new Intl.NumberFormat([], {
style: "currency",
currency: currencyCode,
currencyDisplay: "narrowSymbol",
@@ -1,4 +1,4 @@
const formatter = new Intl.NumberFormat(undefined, {
const formatter = new Intl.NumberFormat([], {
style: "percent",
minimumFractionDigits: 2,
})
@@ -220,7 +220,7 @@ export const useGlobalShortcuts = () => {
},
label: t("app.keyboardShortcuts.goToTaxRegions"),
type: "settingShortcut",
callback: () => navigate("/settings/taxes"),
callback: () => navigate("/settings/tax-regions"),
},
{
keys: {
@@ -1,11 +1,9 @@
import {
AdminApiKeyResponse,
AdminProductCategoryResponse,
AdminTaxRateResponse,
AdminTaxRegionResponse,
HttpTypes,
SalesChannelDTO,
UserDTO,
} from "@medusajs/types"
import { Outlet, RouteObject } from "react-router-dom"
@@ -15,6 +13,12 @@ import { SettingsLayout } from "../../components/layout/settings-layout"
import { ErrorBoundary } from "../../components/utilities/error-boundary"
import { PriceListRes } from "../../types/api-responses"
import { getCountryByIso2 } from "../../lib/data/countries"
import {
getProvinceByIso2,
isProvinceInCountry,
} from "../../lib/data/country-states"
import { taxRegionLoader } from "../../routes/tax-regions/tax-region-detail/loader"
import { RouteExtensions } from "./route-extensions"
import { SettingsExtensions } from "./settings-extensions"
@@ -1074,60 +1078,116 @@ export const RouteMap: RouteObject[] = [
],
},
{
path: "taxes",
path: "tax-regions",
element: <Outlet />,
handle: {
crumb: () => "Taxes",
crumb: () => "Tax Regions",
},
children: [
{
path: "",
lazy: () => import("../../routes/taxes/tax-region-list"),
lazy: () => import("../../routes/tax-regions/tax-region-list"),
children: [
{
path: "create",
lazy: () => import("../../routes/taxes/tax-region-create"),
children: [],
lazy: () =>
import("../../routes/tax-regions/tax-region-create"),
},
],
},
{
path: ":id",
lazy: () => import("../../routes/taxes/tax-region-detail"),
Component: Outlet,
loader: taxRegionLoader,
handle: {
crumb: (data: AdminTaxRegionResponse) => {
return data.tax_region.country_code
return (
getCountryByIso2(data.tax_region.country_code)
?.display_name ||
data.tax_region.country_code?.toUpperCase()
)
},
},
children: [
{
path: "create-default",
path: "",
lazy: () =>
import("../../routes/taxes/tax-province-create"),
children: [],
},
{
path: "create-override",
lazy: () => import("../../routes/taxes/tax-rate-create"),
children: [],
},
{
path: "tax-rates",
import("../../routes/tax-regions/tax-region-detail"),
children: [
{
path: ":taxRateId",
children: [
{
path: "edit",
lazy: () =>
import("../../routes/taxes/tax-rate-edit"),
handle: {
crumb: (data: AdminTaxRateResponse) => {
return data.tax_rate.code
},
},
},
],
path: "provinces/create",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-province-create"
),
},
{
path: "overrides/create",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-override-create"
),
},
{
path: "overrides/:tax_rate_id/edit",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-override-edit"
),
},
{
path: "tax-rates/create",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-rate-create"
),
},
{
path: "tax-rates/:tax_rate_id/edit",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-rate-edit"
),
},
],
},
{
path: "provinces/:province_id",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-province-detail"
),
handle: {
crumb: (data: AdminTaxRegionResponse) => {
const countryCode =
data.tax_region.country_code?.toUpperCase()
const provinceCode =
data.tax_region.province_code?.toUpperCase()
const isValid = isProvinceInCountry(
countryCode,
provinceCode
)
return isValid
? getProvinceByIso2(provinceCode)
: provinceCode
},
},
children: [
{
path: "tax-rates/create",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-rate-create"
),
},
{
path: "tax-rates/:tax_rate_id/edit",
lazy: () =>
import(
"../../routes/tax-regions/tax-region-tax-rate-edit"
),
},
],
},
@@ -7,7 +7,7 @@ import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { useUpdateCampaign } from "../../../../../hooks/api/campaigns"
import { getCurrencySymbol } from "../../../../../lib/currencies"
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
type EditCampaignBudgetFormProps = {
campaign: CampaignResponse
@@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { formatDate } from "../../../../../components/common/date"
import { useDeleteCampaign } from "../../../../../hooks/api/campaigns"
import { currencies } from "../../../../../lib/currencies"
import { currencies } from "../../../../../lib/data/currencies"
import {
campaignStatus,
statusColor,
@@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import { useStore } from "../../../../../hooks/api/store"
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies"
import { currencies, getCurrencySymbol } from "../../../../../lib/data/currencies"
export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
const { t } = useTranslation()
@@ -2,13 +2,17 @@ import { Button, Container, Heading, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { HttpTypes } from "@medusajs/types"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { DataTable } from "../../../../../components/table/data-table"
import { useCollections } from "../../../../../hooks/api/collections"
import { useCollectionTableColumns } from "../../../../../hooks/table/columns/use-collection-table-columns"
import { useCollectionTableFilters } from "../../../../../hooks/table/filters"
import { useCollectionTableQuery } from "../../../../../hooks/table/query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCollectionTableColumns } from "./use-collection-table-columns"
import { useCollectionTableFilters } from "./use-collection-table-filters"
import { useCollectionTableQuery } from "./use-collection-table-query"
import { CollectionRowActions } from "./collection-row-actions"
const PAGE_SIZE = 20
@@ -26,7 +30,7 @@ export const CollectionListTable = () => {
)
const filters = useCollectionTableFilters()
const columns = useCollectionTableColumns()
const columns = useColumns()
const { table } = useDataTable({
data: collections ?? [],
@@ -71,3 +75,20 @@ export const CollectionListTable = () => {
</Container>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminCollection>()
const useColumns = () => {
const base = useCollectionTableColumns()
return useMemo(
() => [
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => <CollectionRowActions collection={row.original} />,
}),
],
[base]
)
}
@@ -1,21 +0,0 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../../../components/table/data-table"
export const useCollectionTableFilters = () => {
const { t } = useTranslation()
let filters: Filter[] = []
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
filters = [...filters, ...dateFilters]
return filters
}
@@ -17,7 +17,7 @@ import { useDataTable } from "../../../../../hooks/use-data-table"
import {
StaticCountry,
countries as staticCountries,
} from "../../../../../lib/countries"
} from "../../../../../lib/data/countries"
import { useCountries } from "../../../../regions/common/hooks/use-countries"
import { useCountryTableColumns } from "../../../../regions/common/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../../regions/common/hooks/use-country-table-query"
@@ -41,7 +41,7 @@ import { getFormattedAddress } from "../../../../../lib/addresses"
import {
StaticCountry,
countries as staticCountries,
} from "../../../../../lib/countries"
} from "../../../../../lib/data/countries"
import { formatProvider } from "../../../../../lib/format-provider"
import {
isOptionEnabledInStore,
@@ -12,7 +12,7 @@ import {
useRouteModal,
} from "../../../../../components/modals"
import { useUpdateFulfillmentSetServiceZone } from "../../../../../hooks/api/fulfillment-sets"
import { countries } from "../../../../../lib/countries"
import { countries } from "../../../../../lib/data/countries"
import { GeoZoneForm } from "../../../common/components/geo-zone-form"
const EditeServiceZoneSchema = z.object({
@@ -42,7 +42,7 @@ export const PriceListEditForm = ({ priceList }: PriceListEditFormProps) => {
resolver: zodResolver(PriceListEditSchema),
})
const { mutateAsync } = useUpdatePriceList(priceList.id)
const { mutateAsync, isPending } = useUpdatePriceList(priceList.id)
const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, {
@@ -172,7 +172,7 @@ export const PriceListEditForm = ({ priceList }: PriceListEditFormProps) => {
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit">
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
@@ -1,13 +1,18 @@
import { HttpTypes } from "@medusajs/types"
import { Button, Container, Heading, Text } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { useProductTypes } from "../../../../../hooks/api/product-types"
import { useProductTypeTableColumns } from "../../../../../hooks/table/columns/use-product-type-table-columns"
import { useProductTypeTableFilters } from "../../../../../hooks/table/filters/use-product-type-table-filters"
import { useProductTypeTableQuery } from "../../../../../hooks/table/query/use-product-type-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useProductTypeTableColumns } from "./use-product-type-table-columns"
import { useProductTypeTableFilters } from "./use-product-type-table-filters"
import { useProductTypeTableQuery } from "./use-product-type-table-query"
import { ProductTypeRowActions } from "./product-table-row-actions"
const PAGE_SIZE = 20
@@ -25,7 +30,7 @@ export const ProductTypeListTable = () => {
)
const filters = useProductTypeTableFilters()
const columns = useProductTypeTableColumns()
const columns = useColumns()
const { table } = useDataTable({
columns,
@@ -68,3 +73,22 @@ export const ProductTypeListTable = () => {
</Container>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminProductType>()
const useColumns = () => {
const base = useProductTypeTableColumns()
return useMemo(
() => [
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <ProductTypeRowActions productType={row.original} />
},
}),
],
[base]
)
}
@@ -26,14 +26,14 @@ import {
} from "@medusajs/types"
import { Divider } from "../../../../../components/common/divider"
import { Form } from "../../../../../components/common/form"
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { useCampaigns } from "../../../../../hooks/api/campaigns"
import { useCreatePromotion } from "../../../../../hooks/api/promotions"
import { getCurrencySymbol } from "../../../../../lib/currencies"
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
import { defaultCampaignValues } from "../../../../campaigns/campaign-create/components/create-campaign-form"
import { RulesFormField } from "../../../common/edit-rules/components/rules-form-field"
import { AddCampaignPromotionFields } from "../../../promotion-add-campaign/components/add-campaign-promotion-form"
@@ -661,7 +661,7 @@ export const CreatePromotionForm = () => {
disabled={!currencyCode}
/>
) : (
<PercentageInput
<DeprecatedPercentageInput
key="amount"
className="text-right"
min={0}
@@ -13,13 +13,13 @@ import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { PercentageInput } from "../../../../../components/inputs/percentage-input"
import { DeprecatedPercentageInput } from "../../../../../components/inputs/percentage-input"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/modals"
import { useUpdatePromotion } from "../../../../../hooks/api/promotions"
import { getCurrencySymbol } from "../../../../../lib/currencies"
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
type EditPromotionFormProps = {
promotion: PromotionDTO
@@ -99,7 +99,7 @@ export const EditPromotionDetailsForm = ({
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"false" === field.value,
})}
value={"false"}
@@ -110,7 +110,7 @@ export const EditPromotionDetailsForm = ({
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"true" === field.value,
})}
value={"true"}
@@ -172,7 +172,7 @@ export const EditPromotionDetailsForm = ({
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"fixed" === field.value,
})}
value={"fixed"}
@@ -184,7 +184,7 @@ export const EditPromotionDetailsForm = ({
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"percentage" === field.value,
})}
value={"percentage"}
@@ -227,7 +227,7 @@ export const EditPromotionDetailsForm = ({
value={field.value}
/>
) : (
<PercentageInput
<DeprecatedPercentageInput
key="amount"
min={0}
max={100}
@@ -264,7 +264,7 @@ export const EditPromotionDetailsForm = ({
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"each" === field.value,
})}
value={"each"}
@@ -276,7 +276,7 @@ export const EditPromotionDetailsForm = ({
<RadioGroup.ChoiceBox
className={clx("basis-1/2", {
"border-2 border-ui-border-interactive":
"border-ui-border-interactive border-2":
"across" === field.value,
})}
value={"across"}
@@ -1,6 +1,6 @@
import { RegionCountryDTO } from "@medusajs/types"
import { json } from "react-router-dom"
import { StaticCountry } from "../../../../lib/countries"
import { StaticCountry } from "../../../../lib/data/countries"
const acceptedOrderKeys = ["name", "code"]
@@ -2,7 +2,7 @@ import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { StaticCountry } from "../../../../lib/countries"
import { StaticCountry } from "../../../../lib/data/countries"
const columnHelper = createColumnHelper<StaticCountry>()
@@ -18,7 +18,7 @@ import {
import { DataTable } from "../../../../../components/table/data-table"
import { useUpdateRegion } from "../../../../../hooks/api/regions"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { countries as staticCountries } from "../../../../../lib/countries"
import { countries as staticCountries } from "../../../../../lib/data/countries"
import { useCountries } from "../../../common/hooks/use-countries"
import { useCountryTableColumns } from "../../../common/hooks/use-country-table-columns"
import { useCountryTableQuery } from "../../../common/hooks/use-country-table-query"
@@ -29,8 +29,8 @@ import {
import { DataTable } from "../../../../../components/table/data-table"
import { useCreateRegion } from "../../../../../hooks/api/regions"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { countries as staticCountries } from "../../../../../lib/countries"
import { CurrencyInfo } from "../../../../../lib/currencies"
import { countries as staticCountries } from "../../../../../lib/data/countries"
import { CurrencyInfo } from "../../../../../lib/data/currencies"
import { formatProvider } from "../../../../../lib/format-provider"
import { useCountries } from "../../../common/hooks/use-countries"
import { useCountryTableColumns } from "../../../common/hooks/use-country-table-columns"
@@ -1,7 +1,7 @@
import { RouteFocusModal } from "../../../components/modals/route-focus-modal"
import { usePaymentProviders } from "../../../hooks/api/payments"
import { useStore } from "../../../hooks/api/store"
import { currencies } from "../../../lib/currencies"
import { currencies } from "../../../lib/data/currencies"
import { CreateRegionForm } from "./components/create-region-form"
export const RegionCreate = () => {
@@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu/index.ts"
import { ListSummary } from "../../../../../components/common/list-summary/index.ts"
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { currencies } from "../../../../../lib/currencies.ts"
import { currencies } from "../../../../../lib/data/currencies.ts"
import { formatProvider } from "../../../../../lib/format-provider.ts"
import { SectionRow } from "../../../../../components/common/section/section-row.tsx"
@@ -11,7 +11,7 @@ import {
useRouteModal,
} from "../../../../../components/modals/index.ts"
import { useUpdateRegion } from "../../../../../hooks/api/regions.tsx"
import { CurrencyInfo } from "../../../../../lib/currencies.ts"
import { CurrencyInfo } from "../../../../../lib/data/currencies.ts"
import { formatProvider } from "../../../../../lib/format-provider.ts"
type EditRegionFormProps = {
@@ -6,7 +6,7 @@ import { RouteDrawer } from "../../../components/modals"
import { usePaymentProviders } from "../../../hooks/api/payments"
import { useRegion } from "../../../hooks/api/regions"
import { useStore } from "../../../hooks/api/store"
import { currencies } from "../../../lib/currencies"
import { currencies } from "../../../lib/data/currencies"
import { EditRegionForm } from "./components/edit-region-form"
import { usePricePreferences } from "../../../hooks/api/price-preferences"
@@ -0,0 +1 @@
export * from "./target-form"
@@ -0,0 +1,789 @@
import { HttpTypes } from "@medusajs/types"
import { Button, Checkbox } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import {
OnChangeFn,
RowSelectionState,
createColumnHelper,
} from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useSearchParams } from "react-router-dom"
import {
StackedDrawer,
StackedFocusModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import {
useCollections,
useCustomerGroups,
useProductTypes,
useProducts,
useTags,
} from "../../../../../hooks/api"
import {
useCollectionTableColumns,
useCustomerGroupTableColumns,
useProductTableColumns,
useProductTagTableColumns,
useProductTypeTableColumns,
} from "../../../../../hooks/table/columns"
import {
useCollectionTableFilters,
useCustomerGroupTableFilters,
useProductTableFilters,
useProductTagTableFilters,
useProductTypeTableFilters,
} from "../../../../../hooks/table/filters"
import {
useCollectionTableQuery,
useCustomerGroupTableQuery,
useProductTableQuery,
useProductTagTableQuery,
useProductTypeTableQuery,
} from "../../../../../hooks/table/query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { TaxRateRuleReferenceType } from "../../constants"
import { TaxRateRuleReference } from "../../schemas"
type TargetFormProps = {
referenceType: TaxRateRuleReferenceType
type: "focus" | "drawer"
state: TaxRateRuleReference[]
setState: (state: TaxRateRuleReference[]) => void
}
function initRowSelection(state: TaxRateRuleReference[]) {
return state.reduce((acc, reference) => {
acc[reference.value] = true
return acc
}, {} as RowSelectionState)
}
export const TargetForm = ({
referenceType,
type,
setState,
state,
}: TargetFormProps) => {
const { t } = useTranslation()
const Component = type === "focus" ? StackedFocusModal : StackedDrawer
const [intermediate, setIntermediate] =
useState<TaxRateRuleReference[]>(state)
const handleSave = () => {
setState(intermediate)
}
return (
<div className="flex size-full flex-col overflow-hidden">
<Component.Body className="min-h-0 p-0">
<Table
referenceType={referenceType}
initialRowState={initRowSelection(state)}
intermediate={intermediate}
setIntermediate={setIntermediate}
/>
</Component.Body>
<Component.Footer>
<Component.Close asChild>
<Button variant="secondary" size="small" type="button">
{t("actions.cancel")}
</Button>
</Component.Close>
<Button type="button" size="small" onClick={handleSave}>
{t("actions.save")}
</Button>
</Component.Footer>
</div>
)
}
type TableProps = {
referenceType: TaxRateRuleReferenceType
initialRowState: RowSelectionState
intermediate: TaxRateRuleReference[]
setIntermediate: (state: TaxRateRuleReference[]) => void
}
const Table = ({ referenceType, ...props }: TableProps) => {
switch (referenceType) {
case TaxRateRuleReferenceType.CUSTOMER_GROUP:
return <CustomerGroupTable {...props} />
case TaxRateRuleReferenceType.PRODUCT:
return <ProductTable {...props} />
case TaxRateRuleReferenceType.PRODUCT_COLLECTION:
return <ProductCollectionTable {...props} />
case TaxRateRuleReferenceType.PRODUCT_TYPE:
return <ProductTypeTable {...props} />
case TaxRateRuleReferenceType.PRODUCT_TAG:
return <ProductTagTable {...props} />
default:
return null
}
}
type TableImplementationProps = {
initialRowState: RowSelectionState
intermediate: TaxRateRuleReference[]
setIntermediate: (state: TaxRateRuleReference[]) => void
}
const PAGE_SIZE = 50
const PREFIX_CUSTOMER_GROUP = "cg"
const CustomerGroupTable = ({
initialRowState,
intermediate,
setIntermediate,
}: TableImplementationProps) => {
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialRowState)
useCleanupSearchParams()
const { searchParams, raw } = useCustomerGroupTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX_CUSTOMER_GROUP,
})
const { customer_groups, count, isLoading, isError, error } =
useCustomerGroups(searchParams, {
placeholderData: keepPreviousData,
})
const updater: OnChangeFn<RowSelectionState> = (value) => {
const state = typeof value === "function" ? value(rowSelection) : value
const currentIds = Object.keys(rowSelection)
const ids = Object.keys(state)
const newIds = ids.filter((id) => !currentIds.includes(id))
const removedIds = currentIds.filter((id) => !ids.includes(id))
const newCustomerGroups =
customer_groups
?.filter((cg) => newIds.includes(cg.id))
.map((cg) => ({ value: cg.id, label: cg.name! })) || []
const filteredIntermediate = intermediate.filter(
(cg) => !removedIds.includes(cg.value)
)
setIntermediate([...filteredIntermediate, ...newCustomerGroups])
setRowSelection(state)
}
const filters = useCustomerGroupTableFilters()
const columns = useGroupColumns()
const { table } = useDataTable({
data: customer_groups || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
rowSelection: {
state: rowSelection,
updater,
},
pageSize: PAGE_SIZE,
prefix: PREFIX_CUSTOMER_GROUP,
})
if (isError) {
throw error
}
return (
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
isLoading={isLoading}
filters={filters}
orderBy={["name", "created_at", "updated_at"]}
layout="fill"
pagination
search
prefix={PREFIX_CUSTOMER_GROUP}
queryObject={raw}
/>
)
}
const cgColumnHelper = createColumnHelper<HttpTypes.AdminCustomerGroup>()
const useGroupColumns = () => {
const base = useCustomerGroupTableColumns()
return useMemo(
() => [
cgColumnHelper.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,
],
[base]
)
}
const PREFIX_PRODUCT = "p"
const ProductTable = ({
initialRowState,
intermediate,
setIntermediate,
}: TableImplementationProps) => {
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialRowState)
useCleanupSearchParams()
const { searchParams, raw } = useProductTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT,
})
const { products, count, isLoading, isError, error } = useProducts(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const updater: OnChangeFn<RowSelectionState> = (value) => {
const state = typeof value === "function" ? value(rowSelection) : value
const currentIds = Object.keys(rowSelection)
const ids = Object.keys(state)
const newIds = ids.filter((id) => !currentIds.includes(id))
const removedIds = currentIds.filter((id) => !ids.includes(id))
const newProducts =
products
?.filter((p) => newIds.includes(p.id))
.map((p) => ({
value: p.id,
label: p.title!,
})) || []
const filteredIntermediate = intermediate.filter(
(p) => !removedIds.includes(p.value)
)
setIntermediate([...filteredIntermediate, ...newProducts])
setRowSelection(state)
}
const filters = useProductTableFilters()
const columns = useProductColumns()
const { table } = useDataTable({
data: products || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
rowSelection: {
state: rowSelection,
updater,
},
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT,
})
if (isError) {
throw error
}
return (
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
isLoading={isLoading}
filters={filters}
orderBy={["title", "created_at", "updated_at"]}
layout="fill"
pagination
search
prefix={PREFIX_PRODUCT}
queryObject={raw}
/>
)
}
const pColumnHelper = createColumnHelper<HttpTypes.AdminProduct>()
const useProductColumns = () => {
const base = useProductTableColumns()
return useMemo(
() => [
pColumnHelper.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,
],
[base]
)
}
const PREFIX_PRODUCT_COLLECTION = "pc"
const ProductCollectionTable = ({
initialRowState,
intermediate,
setIntermediate,
}: TableImplementationProps) => {
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialRowState)
useCleanupSearchParams()
const { searchParams, raw } = useCollectionTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_COLLECTION,
})
const { collections, count, isLoading, isError, error } = useCollections(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const updater: OnChangeFn<RowSelectionState> = (value) => {
const state = typeof value === "function" ? value(rowSelection) : value
const currentIds = Object.keys(rowSelection)
const ids = Object.keys(state)
const newIds = ids.filter((id) => !currentIds.includes(id))
const removedIds = currentIds.filter((id) => !ids.includes(id))
const newCollections =
collections
?.filter((p) => newIds.includes(p.id))
.map((p) => ({
value: p.id,
label: p.title,
})) || []
const filteredIntermediate = intermediate.filter(
(p) => !removedIds.includes(p.value)
)
setIntermediate([...filteredIntermediate, ...newCollections])
setRowSelection(state)
}
const filters = useCollectionTableFilters()
const columns = useCollectionColumns()
const { table } = useDataTable({
data: collections || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
rowSelection: {
state: rowSelection,
updater,
},
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_COLLECTION,
})
if (isError) {
throw error
}
return (
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
isLoading={isLoading}
filters={filters}
orderBy={["title", "created_at", "updated_at"]}
layout="fill"
pagination
search
prefix={PREFIX_PRODUCT_COLLECTION}
queryObject={raw}
/>
)
}
const pcColumnHelper = createColumnHelper<HttpTypes.AdminCollection>()
const useCollectionColumns = () => {
const base = useCollectionTableColumns()
return useMemo(
() => [
pcColumnHelper.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,
],
[base]
)
}
const PREFIX_PRODUCT_TYPE = "pt"
const ProductTypeTable = ({
initialRowState,
intermediate,
setIntermediate,
}: TableImplementationProps) => {
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialRowState)
useCleanupSearchParams()
const { searchParams, raw } = useProductTypeTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_TYPE,
})
const { product_types, count, isLoading, isError, error } = useProductTypes(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const updater: OnChangeFn<RowSelectionState> = (value) => {
const state = typeof value === "function" ? value(rowSelection) : value
const currentIds = Object.keys(rowSelection)
const ids = Object.keys(state)
const newIds = ids.filter((id) => !currentIds.includes(id))
const removedIds = currentIds.filter((id) => !ids.includes(id))
const newTypes =
product_types
?.filter((p) => newIds.includes(p.id))
.map((p) => ({
value: p.id,
label: p.value,
})) || []
const filteredIntermediate = intermediate.filter(
(p) => !removedIds.includes(p.value)
)
setIntermediate([...filteredIntermediate, ...newTypes])
setRowSelection(state)
}
const filters = useProductTypeTableFilters()
const columns = useProductTypeColumns()
const { table } = useDataTable({
data: product_types || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
rowSelection: {
state: rowSelection,
updater,
},
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_TYPE,
})
if (isError) {
throw error
}
return (
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
isLoading={isLoading}
filters={filters}
orderBy={["value", "created_at", "updated_at"]}
layout="fill"
pagination
search
prefix={PREFIX_PRODUCT_TYPE}
queryObject={raw}
/>
)
}
const ptColumnHelper = createColumnHelper<HttpTypes.AdminProductType>()
const useProductTypeColumns = () => {
const base = useProductTypeTableColumns()
return useMemo(
() => [
ptColumnHelper.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,
],
[base]
)
}
const PREFIX_PRODUCT_TAG = "ptag"
const ProductTagTable = ({
initialRowState,
intermediate,
setIntermediate,
}: TableImplementationProps) => {
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialRowState)
useCleanupSearchParams()
const { searchParams, raw } = useProductTagTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_TAG,
})
const { product_tags, count, isLoading, isError, error } = useTags(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const updater: OnChangeFn<RowSelectionState> = (value) => {
const state = typeof value === "function" ? value(rowSelection) : value
const currentIds = Object.keys(rowSelection)
const ids = Object.keys(state)
const newIds = ids.filter((id) => !currentIds.includes(id))
const removedIds = currentIds.filter((id) => !ids.includes(id))
const newTags =
product_tags
?.filter((p) => newIds.includes(p.id))
.map((p) => ({
value: p.id,
label: p.value,
})) || []
const filteredIntermediate = intermediate.filter(
(p) => !removedIds.includes(p.value)
)
setIntermediate([...filteredIntermediate, ...newTags])
setRowSelection(state)
}
const filters = useProductTagTableFilters()
const columns = useProductTagColumns()
const { table } = useDataTable({
data: product_tags || [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
rowSelection: {
state: rowSelection,
updater,
},
pageSize: PAGE_SIZE,
prefix: PREFIX_PRODUCT_TAG,
})
if (isError) {
throw error
}
return (
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
isLoading={isLoading}
filters={filters}
orderBy={["value", "created_at", "updated_at"]}
layout="fill"
pagination
search
prefix={PREFIX_PRODUCT_TAG}
queryObject={raw}
/>
)
}
const ptagColumnHelper = createColumnHelper<HttpTypes.AdminProductTag>()
const useProductTagColumns = () => {
const base = useProductTagTableColumns()
return useMemo(
() => [
ptagColumnHelper.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,
],
[base]
)
}
const useCleanupSearchParams = () => {
const [_, setSearchParams] = useSearchParams()
useEffect(() => {
return () => {
setSearchParams({})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}
@@ -0,0 +1 @@
export * from "./target-item"
@@ -0,0 +1,26 @@
import { XMarkMini } from "@medusajs/icons"
import { IconButton, Text } from "@medusajs/ui"
type TargetItemProps = {
index: number
onRemove: (index: number) => void
label: string
}
export const TargetItem = ({ index, label, onRemove }: TargetItemProps) => {
return (
<div className="bg-ui-bg-field-component shadow-borders-base flex items-center justify-between gap-2 rounded-md px-2 py-0.5">
<Text size="small" leading="compact">
{label}
</Text>
<IconButton
size="small"
variant="transparent"
type="button"
onClick={() => onRemove(index)}
>
<XMarkMini />
</IconButton>
</div>
)
}
@@ -0,0 +1 @@
export * from "./tax-override-card"
@@ -0,0 +1,378 @@
import {
ArrowDownRightMini,
PencilSquare,
Trash,
TriangleRightMini,
} from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Badge, IconButton, StatusBadge, Text, Tooltip } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { ComponentPropsWithoutRef } from "react"
import { useTranslation } from "react-i18next"
import { FetchError } from "@medusajs/js-sdk"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { Divider } from "../../../../../components/common/divider"
import { useCollections } from "../../../../../hooks/api/collections"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import { useProductTypes } from "../../../../../hooks/api/product-types"
import { useProducts } from "../../../../../hooks/api/products"
import { useTags } from "../../../../../hooks/api/tags"
import { formatPercentage } from "../../../../../lib/percentage-helpers"
import { TaxRateRuleReferenceType } from "../../constants"
import { useDeleteTaxRateAction } from "../../hooks"
interface TaxOverrideCardProps extends ComponentPropsWithoutRef<"div"> {
taxRate: HttpTypes.AdminTaxRate
}
export const TaxOverrideCard = ({ taxRate }: TaxOverrideCardProps) => {
const { t } = useTranslation()
const handleDelete = useDeleteTaxRateAction(taxRate)
if (taxRate.is_default) {
return null
}
const groupedRules = taxRate.rules.reduce((acc, rule) => {
if (!acc[rule.reference]) {
acc[rule.reference] = []
}
acc[rule.reference].push(rule.reference_id)
return acc
}, {} as Record<string, string[]>)
const validKeys = Object.values(TaxRateRuleReferenceType)
const numberOfTargets = Object.keys(groupedRules).map((key) =>
validKeys.includes(key as TaxRateRuleReferenceType)
).length
return (
<Collapsible.Root>
<div className="flex items-center justify-between px-6 py-3">
<div className="flex items-center gap-x-2">
<Collapsible.Trigger asChild>
<IconButton size="2xsmall" variant="transparent" className="group">
<TriangleRightMini className="text-ui-fg-muted transition-transform group-data-[state='open']:rotate-90" />
</IconButton>
</Collapsible.Trigger>
<div className="flex items-center gap-x-1.5">
<Text size="small" weight="plus" leading="compact">
{taxRate.name}
</Text>
{taxRate.code && (
<div className="text-ui-fg-subtle flex items-center gap-x-1.5">
<Text size="small" leading="compact">
·
</Text>
<Text size="small" leading="compact">
{taxRate.code}
</Text>
</div>
)}
</div>
</div>
<div className="flex items-center gap-x-3">
<Text size="small" leading="compact" className="text-ui-fg-subtle">
{t("taxRegions.fields.targets.numberOfTargets", {
count: numberOfTargets,
})}
</Text>
<div className="bg-ui-border-base h-3 w-px" />
<StatusBadge color={taxRate.is_combinable ? "green" : "grey"}>
{taxRate.is_combinable
? t("taxRegions.fields.isCombinable.true")
: t("taxRegions.fields.isCombinable.false")}
</StatusBadge>
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `overrides/${taxRate.id}/edit`,
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
</div>
</div>
<Collapsible.Content>
<div className="bg-ui-bg-subtle">
<Divider variant="dashed" />
<div className="px-6 py-3">
<div className="flex items-center gap-x-3">
<div className="text-ui-fg-muted flex size-5 items-center justify-center">
<ArrowDownRightMini />
</div>
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-2">
<Badge size="2xsmall">{formatPercentage(taxRate.rate)}</Badge>
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{t("taxRegions.fields.targets.operators.on")}
</Text>
{Object.entries(groupedRules).map(([reference, ids], index) => {
return (
<div
key={reference}
className="flex items-center gap-x-1.5"
>
<Reference
key={reference}
reference={reference as TaxRateRuleReferenceType}
ids={ids}
/>
{index < Object.keys(groupedRules).length - 1 && (
<Text
size="small"
leading="compact"
className="text-ui-fg-subtle"
>
{t("taxRegions.fields.targets.operators.and")}
</Text>
)}
</div>
)
})}
</div>
</div>
</div>
</div>
</Collapsible.Content>
</Collapsible.Root>
)
}
const Reference = ({
reference,
ids,
}: {
reference: TaxRateRuleReferenceType
ids: string[]
}) => {
return (
<div className="flex items-center gap-x-1.5">
<ReferenceBadge reference={reference} />
<ReferenceValues type={reference} ids={ids} />
</div>
)
}
const ReferenceBadge = ({
reference,
}: {
reference: TaxRateRuleReferenceType
}) => {
const { t } = useTranslation()
let label: string | null = null
switch (reference) {
case TaxRateRuleReferenceType.PRODUCT:
label = t("taxRegions.fields.targets.tags.product")
break
case TaxRateRuleReferenceType.PRODUCT_COLLECTION:
label = t("taxRegions.fields.targets.tags.productCollection")
break
case TaxRateRuleReferenceType.PRODUCT_TAG:
label = t("taxRegions.fields.targets.tags.productTag")
break
case TaxRateRuleReferenceType.PRODUCT_TYPE:
label = t("taxRegions.fields.targets.tags.productType")
break
case TaxRateRuleReferenceType.CUSTOMER_GROUP:
label = t("taxRegions.fields.targets.tags.customerGroup")
break
}
if (!label) {
return null
}
return <Badge size="2xsmall">{label}</Badge>
}
const ReferenceValues = ({
type,
ids,
}: {
type: TaxRateRuleReferenceType
ids: string[]
}) => {
const { t } = useTranslation()
const { isPending, additional, labels, isError, error } = useReferenceValues(
type,
ids
)
if (isError) {
throw error
}
if (isPending) {
return (
<div className="bg-ui-tag-neutral-bg border-ui-tag-neutral-border h-5 w-14 animate-pulse rounded-md" />
)
}
return (
<Tooltip
content={
<ul>
{labels?.map((label: string, index) => (
<li key={index}>{label}</li>
))}
{additional > 0 && (
<li>
{t("taxRegions.fields.targets.additionalValues", {
count: additional,
})}
</li>
)}
</ul>
}
>
<Badge size="2xsmall">
{t("taxRegions.fields.targets.values", {
count: ids.length,
})}
</Badge>
</Tooltip>
)
}
const useReferenceValues = (
type: TaxRateRuleReferenceType,
ids: string[]
): {
labels: string[] | undefined
isPending: boolean
additional: number
isError: boolean
error: FetchError | null
} => {
const products = useProducts(
{
id: ids,
limit: 10,
},
{
enabled: !!ids.length && type === TaxRateRuleReferenceType.PRODUCT,
}
)
const tags = useTags(
{
id: ids,
limit: 10,
},
{
enabled: !!ids.length && type === TaxRateRuleReferenceType.PRODUCT_TAG,
}
)
const productTypes = useProductTypes(
{
id: ids,
limit: 10,
},
{
enabled: !!ids.length && type === TaxRateRuleReferenceType.PRODUCT_TYPE,
}
)
const collections = useCollections(
{
id: ids,
limit: 10,
},
{
enabled:
!!ids.length && type === TaxRateRuleReferenceType.PRODUCT_COLLECTION,
}
)
const customerGroups = useCustomerGroups(
{
id: ids,
limit: 10,
},
{
enabled: !!ids.length && type === TaxRateRuleReferenceType.CUSTOMER_GROUP,
}
)
switch (type) {
case TaxRateRuleReferenceType.PRODUCT:
return {
labels: products.products?.map((product) => product.title),
isPending: products.isPending,
additional:
products.products && products.count
? products.count - products.products.length
: 0,
isError: products.isError,
error: products.error,
}
case TaxRateRuleReferenceType.PRODUCT_TAG:
return {
labels: tags.product_tags?.map((tag: any) => tag.value),
isPending: tags.isPending,
additional:
tags.product_tags && tags.count
? tags.count - tags.product_tags.length
: 0,
isError: tags.isError,
error: tags.error,
}
case TaxRateRuleReferenceType.PRODUCT_TYPE:
return {
labels: productTypes.product_types?.map((type) => type.value),
isPending: productTypes.isPending,
additional:
productTypes.product_types && productTypes.count
? productTypes.count - productTypes.product_types.length
: 0,
isError: productTypes.isError,
error: productTypes.error,
}
case TaxRateRuleReferenceType.PRODUCT_COLLECTION:
return {
labels: collections.collections?.map((collection) => collection.title!),
isPending: collections.isPending,
additional:
collections.collections && collections.count
? collections.count - collections.collections.length
: 0,
isError: collections.isError,
error: collections.error,
}
case TaxRateRuleReferenceType.CUSTOMER_GROUP:
return {
labels: customerGroups.customer_groups?.map((group) => group.name!),
isPending: customerGroups.isPending,
additional:
customerGroups.customer_groups && customerGroups.count
? customerGroups.count - customerGroups.customer_groups.length
: 0,
isError: customerGroups.isError,
error: customerGroups.error,
}
}
}
@@ -0,0 +1 @@
export * from "./tax-override-table"
@@ -0,0 +1,119 @@
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import { Table } from "@tanstack/react-table"
import { ReactNode } from "react"
import { Link } from "react-router-dom"
import {
NoRecords,
NoResults,
} from "../../../../../components/common/empty-table-content"
import { TableFooterSkeleton } from "../../../../../components/common/skeleton"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { DataTableOrderBy } from "../../../../../components/table/data-table/data-table-order-by"
import { DataTableSearch } from "../../../../../components/table/data-table/data-table-search"
import { TaxOverrideCard } from "../tax-override-card"
type TaxOverrideTableProps = {
isPending: boolean
queryObject: Record<string, any>
count?: number
table: Table<HttpTypes.AdminTaxRate>
action: { label: string; to: string }
prefix?: string
children?: ReactNode
}
export const TaxOverrideTable = ({
isPending,
action,
count = 0,
table,
queryObject,
prefix,
children,
}: TaxOverrideTableProps) => {
if (isPending) {
return (
<div className="flex flex-col divide-y">
{Array.from({ length: 3 }).map((_, index) => {
return (
<div
key={index}
className="bg-ui-bg-field-component h-[52px] w-full animate-pulse"
/>
)
})}
<TableFooterSkeleton layout="fit" />
</div>
)
}
const noQuery =
Object.values(queryObject).filter((v) => Boolean(v)).length === 0
const noResults = !isPending && count === 0 && !noQuery
const noRecords = !isPending && count === 0 && noQuery
const { pageIndex, pageSize } = table.getState().pagination
return (
<div className="flex flex-col divide-y">
<div className="flex flex-col justify-between gap-x-4 gap-y-3 px-6 py-4 md:flex-row md:items-center">
<div>{children}</div>
<div className="flex items-center gap-x-2">
{!noRecords && (
<div className="flex w-full items-center gap-x-2 md:w-fit">
<div className="w-full md:w-fit">
<DataTableSearch prefix={prefix} />
</div>
<DataTableOrderBy
keys={["name", "rate", "code", "updated_at", "created_at"]}
prefix={prefix}
/>
</div>
)}
<Link to={action.to}>
<Button size="small" variant="secondary">
{action.label}
</Button>
</Link>
</div>
</div>
{noResults && <NoResults />}
{noRecords && <NoRecords />}
{!noRecords && !noResults
? !isPending
? table.getRowModel().rows.map((row) => {
return (
<TaxOverrideCard
key={row.id}
taxRate={row.original}
role="row"
aria-rowindex={row.index}
/>
)
})
: Array.from({ length: 3 }).map((_, index) => {
return (
<div
key={index}
className="bg-ui-bg-field-component h-[60px] w-full animate-pulse"
/>
)
})
: null}
{!noRecords && (
<LocalizedTablePagination
prefix={prefix}
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
count={count}
nextPage={table.nextPage}
previousPage={table.previousPage}
pageCount={table.getPageCount()}
pageIndex={pageIndex}
pageSize={pageSize}
/>
)}
</div>
)
}
@@ -0,0 +1 @@
export * from "./tax-rate-line"
@@ -0,0 +1,82 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { StatusBadge, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { formatPercentage } from "../../../../../lib/percentage-helpers"
import { useDeleteTaxRateAction } from "../../hooks"
type TaxRateLineProps = {
taxRate: HttpTypes.AdminTaxRate
isSublevelTaxRate?: boolean
}
export const TaxRateLine = ({
taxRate,
isSublevelTaxRate,
}: TaxRateLineProps) => {
const { t } = useTranslation()
return (
<div className="text-ui-fg-subtle grid grid-cols-[1fr_1fr_auto] items-center gap-4 px-6 py-4">
<div className="flex items-center gap-x-1.5">
<Text size="small" weight="plus" leading="compact">
{taxRate.name}
</Text>
{taxRate.code && (
<div className="flex items-center gap-x-1.5">
<Text size="small" leading="compact">
·
</Text>
<Text size="small" leading="compact">
{taxRate.code}
</Text>
</div>
)}
</div>
<Text size="small" leading="compact">
{formatPercentage(taxRate.rate)}
</Text>
<div className="flex items-center justify-end gap-x-2">
{isSublevelTaxRate && (
<StatusBadge color={taxRate.is_combinable ? "green" : "grey"}>
{taxRate.is_combinable
? t("taxRegions.fields.isCombinable.true")
: t("taxRegions.fields.isCombinable.false")}
</StatusBadge>
)}
<TaxRateActions taxRate={taxRate} />
</div>
</div>
)
}
const TaxRateActions = ({ taxRate }: { taxRate: HttpTypes.AdminTaxRate }) => {
const { t } = useTranslation()
const handleDelete = useDeleteTaxRateAction(taxRate)
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `tax-rates/${taxRate.id}/edit`,
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
)
}
@@ -0,0 +1 @@
export * from "./tax-region-card"
@@ -0,0 +1,200 @@
import { HttpTypes } from "@medusajs/types"
import { Heading, Text, Tooltip, clx } from "@medusajs/ui"
import ReactCountryFlag from "react-country-flag"
import { ExclamationCircle, MapPin, Plus, Trash } from "@medusajs/icons"
import { ComponentPropsWithoutRef, ReactNode } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { IconAvatar } from "../../../../../components/common/icon-avatar"
import { getCountryByIso2 } from "../../../../../lib/data/countries"
import {
getProvinceByIso2,
isProvinceInCountry,
} from "../../../../../lib/data/country-states"
import { useDeleteTaxRegionAction } from "../../hooks"
interface TaxRegionCardProps extends ComponentPropsWithoutRef<"div"> {
taxRegion: HttpTypes.AdminTaxRegion
type?: "header" | "list"
variant?: "country" | "province"
asLink?: boolean
badge?: ReactNode
}
export const TaxRegionCard = ({
taxRegion,
type = "list",
variant = "country",
asLink = true,
badge,
}: TaxRegionCardProps) => {
const { t } = useTranslation()
const { id, country_code, province_code } = taxRegion
const country = getCountryByIso2(country_code)
const province = getProvinceByIso2(province_code)
let name = "N/A"
let misconfiguredSublevelTooltip: string | null = null
if (province || province_code) {
name = province ? province : province_code!.toUpperCase()
} else if (country || country_code) {
name = country ? country.display_name : country_code!.toUpperCase()
}
if (
country_code &&
province_code &&
!isProvinceInCountry(country_code, province_code)
) {
name = province_code.toUpperCase()
misconfiguredSublevelTooltip = t(
"taxRegions.fields.sublevels.tooltips.notPartOfCountry",
{
country: country?.display_name,
province: province_code.toUpperCase(),
}
)
}
const showCreateDefaultTaxRate =
!taxRegion.tax_rates.filter((tr) => tr.is_default).length &&
type === "header"
const Component = (
<div
className={clx(
"group-data-[link=true]:hover:bg-ui-bg-base-hover transition-fg flex flex-col justify-between gap-y-4 px-6 md:flex-row md:items-center md:gap-y-0",
{
"py-4": type === "header",
"py-3": type === "list",
}
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
<IconAvatar size={type === "list" ? "small" : "large"}>
{country_code && !province_code ? (
<div
className={clx(
"flex size-fit items-center justify-center overflow-hidden rounded-[1px]",
{
"rounded-sm": type === "header",
}
)}
>
<ReactCountryFlag
countryCode={country_code}
svg
style={
type === "list"
? { width: "12px", height: "9px" }
: { width: "16px", height: "12px" }
}
aria-label={country?.display_name}
/>
</div>
) : (
<MapPin className="text-ui-fg-subtle" />
)}
</IconAvatar>
<div>
{type === "list" ? (
<Text size="small" weight="plus" leading="compact">
{name}
</Text>
) : (
<Heading>{name}</Heading>
)}
</div>
</div>
<div className="flex size-fit items-center gap-x-2 md:hidden">
{misconfiguredSublevelTooltip && (
<Tooltip content={misconfiguredSublevelTooltip}>
<ExclamationCircle className="text-ui-tag-orange-icon" />
</Tooltip>
)}
{badge}
<TaxRegionCardActions
taxRegion={taxRegion}
showCreateDefaultTaxRate={showCreateDefaultTaxRate}
/>
</div>
</div>
<div className="hidden size-fit items-center gap-x-2 md:flex">
{misconfiguredSublevelTooltip && (
<Tooltip content={misconfiguredSublevelTooltip}>
<ExclamationCircle className="text-ui-tag-orange-icon" />
</Tooltip>
)}
{badge}
<TaxRegionCardActions
taxRegion={taxRegion}
showCreateDefaultTaxRate={showCreateDefaultTaxRate}
/>
</div>
</div>
)
if (asLink) {
return (
<Link
to={variant === "country" ? `${id}` : `provinces/${id}`}
data-link="true"
className="group block"
>
{Component}
</Link>
)
}
return Component
}
const TaxRegionCardActions = ({
taxRegion,
showCreateDefaultTaxRate,
}: {
taxRegion: HttpTypes.AdminTaxRegion
showCreateDefaultTaxRate?: boolean
}) => {
const { t } = useTranslation()
const to = taxRegion.parent_id
? `/settings/tax-regions/${taxRegion.parent_id}`
: undefined
const handleDelete = useDeleteTaxRegionAction({ taxRegion, to })
return (
<ActionMenu
groups={[
...(showCreateDefaultTaxRate
? [
{
actions: [
{
icon: <Plus />,
label: t("taxRegions.fields.defaultTaxRate.action"),
to: `tax-rates/create`,
},
],
},
]
: []),
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
},
]}
/>
)
}
@@ -0,0 +1 @@
export * from "./tax-region-table"
@@ -0,0 +1,122 @@
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import { Table } from "@tanstack/react-table"
import { ReactNode } from "react"
import { Link } from "react-router-dom"
import {
NoRecords,
NoResults,
} from "../../../../../components/common/empty-table-content"
import { TableFooterSkeleton } from "../../../../../components/common/skeleton"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { DataTableOrderBy } from "../../../../../components/table/data-table/data-table-order-by"
import { TaxRegionCard } from "../tax-region-card"
type TaxRegionTableProps = {
variant?: "country" | "province"
isPending: boolean
queryObject: Record<string, any>
count?: number
table: Table<HttpTypes.AdminTaxRegion>
action: { label: string; to: string }
prefix?: string
children?: ReactNode
}
export const TaxRegionTable = ({
variant = "country",
isPending,
action,
count = 0,
table,
queryObject,
prefix,
children,
}: TaxRegionTableProps) => {
if (isPending) {
return (
<div className="flex flex-col divide-y">
{Array.from({ length: 3 }).map((_, index) => {
return (
<div
key={index}
className="bg-ui-bg-field-component h-[52px] w-full animate-pulse"
/>
)
})}
<TableFooterSkeleton layout="fit" />
</div>
)
}
const noQuery =
Object.values(queryObject).filter((v) => Boolean(v)).length === 0
const noResults = !isPending && count === 0 && !noQuery
const noRecords = !isPending && count === 0 && noQuery
const { pageIndex, pageSize } = table.getState().pagination
return (
<div className="flex flex-col divide-y">
<div className="flex flex-col justify-between gap-x-4 gap-y-3 px-6 py-4 md:flex-row md:items-center">
<div>{children}</div>
<div className="flex items-center gap-x-2">
{!noRecords && (
<div className="flex w-full items-center gap-x-2 md:w-fit">
{/* Re-enable when we allow searching tax regions by country name rather than country_code */}
{/* <div className="w-full md:w-fit">
<DataTableSearch prefix={prefix} />
</div> */}
<DataTableOrderBy
keys={["updated_at", "created_at"]}
prefix={prefix}
/>
</div>
)}
<Link to={action.to}>
<Button size="small" variant="secondary">
{action.label}
</Button>
</Link>
</div>
</div>
{noResults && <NoResults />}
{noRecords && <NoRecords />}
{!noRecords && !noResults
? !isPending
? table.getRowModel().rows.map((row) => {
return (
<TaxRegionCard
variant={variant}
key={row.id}
taxRegion={row.original}
role="row"
aria-rowindex={row.index}
/>
)
})
: Array.from({ length: 3 }).map((_, index) => {
return (
<div
key={index}
className="bg-ui-bg-field-component h-[60px] w-full animate-pulse"
/>
)
})
: null}
{!noRecords && (
<LocalizedTablePagination
prefix={prefix}
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
count={count}
nextPage={table.nextPage}
previousPage={table.previousPage}
pageCount={table.getPageCount()}
pageIndex={pageIndex}
pageSize={pageSize}
/>
)}
</div>
)
}
@@ -1,11 +1,7 @@
export enum ConditionEntities {
export enum TaxRateRuleReferenceType {
PRODUCT = "products",
PRODUCT_TYPE = "product_types",
PRODUCT_COLLECTION = "product_collections",
PRODUCT_TAG = "product_tags",
PRODUCT_TYPE = "product_types",
CUSTOMER_GROUP = "customer_groups",
}
export enum Operators {
IN = "in",
}
@@ -0,0 +1,87 @@
import { HttpTypes } from "@medusajs/types"
import { toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useDeleteTaxRate } from "../../../hooks/api/tax-rates"
import { useDeleteTaxRegion } from "../../../hooks/api/tax-regions"
export const useDeleteTaxRegionAction = ({
taxRegion,
to = "/settings/tax-regions",
}: {
taxRegion: HttpTypes.AdminTaxRegion
to?: string
}) => {
const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt()
const { mutateAsync } = useDeleteTaxRegion(taxRegion.id)
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("taxRegions.delete.confirmation"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("taxRegions.delete.successToast"),
dismissable: true,
dismissLabel: t("actions.close"),
})
navigate(to, { replace: true })
},
onError: (e) => {
toast.error(t("general.error"), {
description: e.message,
dismissable: true,
dismissLabel: t("actions.close"),
})
},
})
}
return handleDelete
}
export const useDeleteTaxRateAction = (taxRate: HttpTypes.AdminTaxRate) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteTaxRate(taxRate.id)
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("taxRegions.taxRates.delete.confirmation", {
name: taxRate.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("taxRegions.taxRates.delete.successToast"))
},
onError: (e) => {
toast.error(e.message)
},
})
}
return handleDelete
}

Some files were not shown because too many files have changed in this diff Show More