feat(dashboard): 3.0 product collection (#6120)
This commit is contained in:
committed by
GitHub
parent
b132ff7669
commit
e49b6944e3
5
.changeset/metal-turtles-cover.md
Normal file
5
.changeset/metal-turtles-cover.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/ui": patch
|
||||
---
|
||||
|
||||
fix(ui): Fix broken responsive style of Drawer between `sm` and `md`.
|
||||
@@ -16,6 +16,8 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@hookform/resolvers": "3.3.2",
|
||||
"@medusajs/icons": "workspace:^",
|
||||
"@medusajs/ui": "workspace:^",
|
||||
|
||||
@@ -56,7 +56,16 @@
|
||||
}
|
||||
},
|
||||
"collections": {
|
||||
"domain": "Collections"
|
||||
"domain": "Collections",
|
||||
"createCollection": "Create Collection",
|
||||
"createCollectionHint": "Create a new collection to organize your products.",
|
||||
"editCollection": "Edit Collection",
|
||||
"handleTooltip": "The handle is used to reference the collection in your storefront. If not specified, the handle will be generated from the collection title.",
|
||||
"deleteWarning_one": "You are about to delete {{count}} collection. This action cannot be undone.",
|
||||
"deleteWarning_other": "You are about to delete {{count}} collections. This action cannot be undone.",
|
||||
"removeSingleProductWarning": "You are about to remove the product {{title}} from the collection. This action cannot be undone.",
|
||||
"removeProductsWarning_one": "You are about to remove {{count}} product from the collection. This action cannot be undone.",
|
||||
"removeProductsWarning_other": "You are about to remove {{count}} products from the collection. This action cannot be undone."
|
||||
},
|
||||
"categories": {
|
||||
"domain": "Categories"
|
||||
@@ -78,7 +87,8 @@
|
||||
"changePasswordPromptDescription": "You are about to change the password for {{email}}. Make sure that you have communicated the new password to the customer before proceeding.",
|
||||
"guest": "Guest",
|
||||
"registered": "Registered",
|
||||
"firstSeen": "First seen"
|
||||
"firstSeen": "First seen",
|
||||
"viewOrder": "View order"
|
||||
},
|
||||
"customerGroups": {
|
||||
"domain": "Customer Groups",
|
||||
@@ -138,10 +148,13 @@
|
||||
"regions": {
|
||||
"domain": "Regions",
|
||||
"createRegion": "Create Region",
|
||||
"createRegionHint": "Manage tax rates and providers for a set of countries.",
|
||||
"editRegion": "Edit Region",
|
||||
"deleteRegionWarning": "You are about to delete the region {{name}}. This action cannot be undone.",
|
||||
"taxInclusiveHint": "When enabled all prices in the region will be tax inclusive.",
|
||||
"providersHint": "The providers that are available in the region."
|
||||
"providersHint": "The providers that are available in the region.",
|
||||
"shippingOptions": "Shipping Options",
|
||||
"returnShippingOptions": "Return Shipping Options"
|
||||
},
|
||||
"locations": {
|
||||
"domain": "Locations",
|
||||
@@ -150,7 +163,9 @@
|
||||
"addSalesChannels": "Add sales channels",
|
||||
"detailsHint": "Specify the details of the location.",
|
||||
"noLocationsFound": "No locations found",
|
||||
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone."
|
||||
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
|
||||
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.",
|
||||
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location."
|
||||
},
|
||||
"salesChannels": {
|
||||
"domain": "Sales Channels",
|
||||
@@ -215,6 +230,7 @@
|
||||
"phone": "Phone",
|
||||
"metadata": "Metadata",
|
||||
"selectCountry": "Select country",
|
||||
"products": "Products",
|
||||
"variants": "Variants",
|
||||
"orders": "Orders",
|
||||
"account": "Account",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DropdownMenu, IconButton } from "@medusajs/ui"
|
||||
import { ReactNode } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
type TableRowAction = {
|
||||
type Action = {
|
||||
icon: ReactNode
|
||||
label: string
|
||||
} & (
|
||||
@@ -17,15 +17,15 @@ type TableRowAction = {
|
||||
}
|
||||
)
|
||||
|
||||
type TableRowActionGroup = {
|
||||
actions: TableRowAction[]
|
||||
type ActionGroup = {
|
||||
actions: Action[]
|
||||
}
|
||||
|
||||
type TableRowActionsProps = {
|
||||
groups: TableRowActionGroup[]
|
||||
type ActionMenuProps = {
|
||||
groups: ActionGroup[]
|
||||
}
|
||||
|
||||
export const TableRowActions = ({ groups }: TableRowActionsProps) => {
|
||||
export const ActionMenu = ({ groups }: ActionMenuProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
@@ -42,7 +42,7 @@ export const TableRowActions = ({ groups }: TableRowActionsProps) => {
|
||||
const isLast = index === groups.length - 1
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<DropdownMenu.Group key={index}>
|
||||
{group.actions.map((action, index) => {
|
||||
if (action.onClick) {
|
||||
return (
|
||||
@@ -61,21 +61,18 @@ export const TableRowActions = ({ groups }: TableRowActionsProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={action.to} key={index}>
|
||||
<DropdownMenu.Item
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
|
||||
>
|
||||
{action.icon}
|
||||
<span>{action.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<div key={index}>
|
||||
<Link to={action.to} onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu.Item className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2">
|
||||
{action.icon}
|
||||
<span>{action.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{!isLast && <DropdownMenu.Separator />}
|
||||
</div>
|
||||
</DropdownMenu.Group>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./action-menu"
|
||||
@@ -1,5 +1,23 @@
|
||||
import { Combobox as Primitive } from "@headlessui/react"
|
||||
import { EllipseMiniSolid, TrianglesMini } from "@medusajs/icons"
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import { useAdminProducts } from "medusa-react"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
ElementRef,
|
||||
ReactNode,
|
||||
createContext,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
type ComboboxOption = {
|
||||
value: string
|
||||
@@ -13,25 +31,363 @@ type ComboboxProps = {
|
||||
}
|
||||
|
||||
export const Combobox = ({ size = "base" }: ComboboxProps) => {
|
||||
const [product, setProduct] = useState<Product | null>(null)
|
||||
const [query, setQuery] = useState("")
|
||||
const { products, count, isLoading } = useAdminProducts(
|
||||
{
|
||||
q: query,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Primitive by="id" value={product} onChange={setProduct}>
|
||||
<div className="relative">
|
||||
<div className="relative w-full">
|
||||
<Primitive.Input
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
displayValue={(value: Product) => value?.title}
|
||||
className={clx(
|
||||
"bg-ui-bg-field shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none",
|
||||
"placeholder:text-ui-fg-muted text-ui-fg-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||
"invalid:border-ui-border-error invalid:shadow-borders-error",
|
||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
{
|
||||
"h-8 px-2 py-1.5 txt-compact-small": size === "base",
|
||||
"h-7 px-2 py-1 txt-compact-small": size === "small",
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Primitive.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<TrianglesMini className="text-ui-fg-muted" aria-hidden="true" />
|
||||
</Primitive.Button>
|
||||
</div>
|
||||
<Primitive.Options className="absolute mt-2 max-h-[200px] w-full overflow-auto z-10 bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout rounded-lg p-1">
|
||||
{products?.map((p) => (
|
||||
<Primitive.Option
|
||||
key={p.id}
|
||||
value={p}
|
||||
className={clx(
|
||||
"bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 items-center rounded-md px-3 py-2 outline-none transition-colors",
|
||||
"ui-active:bg-ui-bg-base-hover",
|
||||
{
|
||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "base",
|
||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "small",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="w-5 h-5 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<EllipseMiniSolid className="ui-selected:block hidden" />
|
||||
</div>
|
||||
<span className="block truncate ui-selected:font-medium">
|
||||
{p.title}
|
||||
</span>
|
||||
</Primitive.Option>
|
||||
))}
|
||||
</Primitive.Options>
|
||||
</div>
|
||||
</Primitive>
|
||||
)
|
||||
}
|
||||
|
||||
type ComboboxContextValue = {
|
||||
size: "base" | "small"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
const ComboboxContext = createContext<ComboboxContextValue | null>(null)
|
||||
|
||||
const useComboboxContext = () => {
|
||||
const context = useContext(ComboboxContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"Combobox compound components cannot be rendered outside the Combobox component"
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const Root = forwardRef<
|
||||
ElementRef<typeof Primitive>,
|
||||
Omit<ComponentPropsWithoutRef<typeof Primitive>, "children"> & {
|
||||
className?: string
|
||||
size?: "base" | "small"
|
||||
children: ReactNode
|
||||
}
|
||||
>(({ children, className, size = "base", ...props }, ref) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const value = useMemo(() => ({ size, open, setOpen }), [size, open])
|
||||
|
||||
return (
|
||||
<ComboboxContext.Provider value={value}>
|
||||
<Primitive {...props} ref={ref}>
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
{children}
|
||||
</Popover.Root>
|
||||
</Primitive>
|
||||
</ComboboxContext.Provider>
|
||||
)
|
||||
})
|
||||
Root.displayName = "Combobox"
|
||||
|
||||
const Trigger = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<"div">>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { size } = useComboboxContext()
|
||||
|
||||
return (
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-field shadow-buttons-neutral transition-fg flex w-full select-none items-center justify-between rounded-md outline-none",
|
||||
"data-[placeholder]:text-ui-fg-muted text-ui-fg-base",
|
||||
"relative bg-ui-bg-field shadow-buttons-neutral transition-fg w-full select-none items-center justify-between rounded-md outline-none",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||
"focus-visible:shadow-borders-interactive-with-active data-[state=open]:!shadow-borders-interactive-with-active",
|
||||
"aria-[invalid=true]:border-ui-border-error aria-[invalid=true]:shadow-borders-error",
|
||||
"invalid::border-ui-border-error invalid:shadow-borders-error",
|
||||
"invalid:border-ui-border-error invalid:shadow-borders-error",
|
||||
"disabled:!bg-ui-bg-disabled disabled:!text-ui-fg-disabled",
|
||||
{
|
||||
"h-8 px-2 py-1.5 txt-compact-small": size === "base",
|
||||
"h-7 px-2 py-1 txt-compact-small": size === "small",
|
||||
}
|
||||
},
|
||||
className
|
||||
)}
|
||||
></button>
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
<Primitive.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<TrianglesMini className="text-ui-fg-muted" aria-hidden="true" />
|
||||
</Primitive.Button>
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
)
|
||||
Trigger.displayName = "Combobox.Trigger"
|
||||
|
||||
const Value = forwardRef<
|
||||
ElementRef<typeof Primitive.Input>,
|
||||
ComponentPropsWithoutRef<typeof Primitive.Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { size } = useComboboxContext()
|
||||
|
||||
return (
|
||||
<Primitive.Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"placeholder:text-ui-fg-muted text-ui-fg-base outline-none bg-transparent w-full",
|
||||
"disabled:!text-ui-fg-disabled",
|
||||
{
|
||||
" txt-compact-small": size === "base",
|
||||
"txt-compact-small": size === "small",
|
||||
},
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Value.displayName = "Combobox.Value"
|
||||
|
||||
const Item = forwardRef<
|
||||
ElementRef<typeof Primitive.Option>,
|
||||
Omit<ComponentPropsWithoutRef<typeof Primitive.Option>, "children"> & {
|
||||
children?: ReactNode
|
||||
}
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
const { size } = useComboboxContext()
|
||||
|
||||
return (
|
||||
<Primitive.Option
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 items-center rounded-md px-2 py-1.5 outline-none transition-colors",
|
||||
"ui-active:bg-ui-bg-base-hover",
|
||||
{
|
||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "base",
|
||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "small",
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="w-5 h-5 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<EllipseMiniSolid className="ui-selected:block hidden" />
|
||||
</div>
|
||||
{children}
|
||||
</Primitive.Option>
|
||||
)
|
||||
})
|
||||
Item.displayName = "Combobox.Item"
|
||||
|
||||
const NoResults = forwardRef<
|
||||
ElementRef<"span">,
|
||||
ComponentPropsWithoutRef<"span">
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
const { size } = useComboboxContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base items-center flex w-full justify-center rounded-md px-2 py-1.5 outline-none transition-colors",
|
||||
"ui-active:bg-ui-bg-base-hover",
|
||||
{
|
||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "base",
|
||||
"txt-compact-small data-[state=checked]:txt-compact-medium-plus":
|
||||
size === "small",
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children ?? t("general.noResultsTitle")}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
Item.displayName = "Combobox.NoResults"
|
||||
|
||||
const Content = forwardRef<
|
||||
ElementRef<typeof Popover.Content>,
|
||||
ComponentPropsWithoutRef<typeof Popover.Content>
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
side = "bottom",
|
||||
sideOffset = 8,
|
||||
collisionPadding = 24,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base text-ui-fg-base shadow-elevation-flyout relative max-h-[120px] h-full min-w-[var(--radix-popper-anchor-width)] overflow-hidden rounded-lg flex flex-col divide-y",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={collisionPadding}
|
||||
{...props}
|
||||
>
|
||||
<Primitive.Options
|
||||
static={true}
|
||||
className={clx("p-1 flex-1 overflow-auto")}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Options>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
Content.displayName = "Combobox.Content"
|
||||
|
||||
const Pagination = forwardRef<
|
||||
ElementRef<"div">,
|
||||
{
|
||||
isLoading?: boolean
|
||||
hasNext?: boolean
|
||||
onPaginate: () => void
|
||||
className?: string
|
||||
}
|
||||
>(({ isLoading, hasNext, onPaginate, className, ...props }, ref) => {
|
||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||
const innerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Merge innerRef and ref
|
||||
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => {
|
||||
return innerRef.current
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (innerRef.current) {
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
onPaginate()
|
||||
}
|
||||
})
|
||||
|
||||
observerRef.current.observe(innerRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observerRef.current && innerRef.current) {
|
||||
observerRef.current.unobserve(innerRef.current)
|
||||
}
|
||||
}
|
||||
}, [isLoading, hasNext, onPaginate, ref])
|
||||
|
||||
return <div ref={innerRef} className="bg-transparent w-px h-px" {...props} />
|
||||
})
|
||||
Pagination.displayName = "Combobox.Pagination"
|
||||
|
||||
const Combo = Object.assign(Root, {
|
||||
Trigger,
|
||||
Value,
|
||||
Item,
|
||||
NoResults,
|
||||
Pagination,
|
||||
Content,
|
||||
})
|
||||
|
||||
export const TestCombobox = () => {
|
||||
const [product, setProduct] = useState<Product[]>([])
|
||||
const [query, setQuery] = useState("")
|
||||
const { products, count, isLoading } = useAdminProducts(
|
||||
{
|
||||
q: query,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Combo value={product} onChange={setProduct}>
|
||||
<Combo.Trigger>
|
||||
<Combo.Value
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
displayValue={(value: Product[]) => `${value?.length}`}
|
||||
/>
|
||||
</Combo.Trigger>
|
||||
<Combo.Content>
|
||||
{!products?.length && <Combo.NoResults />}
|
||||
<Combo.Pagination isLoading onPaginate={() => console.log("Heyo!")} />
|
||||
{products?.map((p) => (
|
||||
<Combo.Item key={p.id} value={p}>
|
||||
{p.title}
|
||||
</Combo.Item>
|
||||
))}
|
||||
</Combo.Content>
|
||||
</Combo>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { InformationCircleSolid } from "@medusajs/icons"
|
||||
import {
|
||||
Hint as HintComponent,
|
||||
Label as LabelComponent,
|
||||
Text,
|
||||
Tooltip,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import * as LabelPrimitives from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { createContext, forwardRef, useContext, useId } from "react"
|
||||
import { ReactNode, createContext, forwardRef, useContext, useId } from "react"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
@@ -22,7 +24,7 @@ const Provider = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
@@ -33,7 +35,7 @@ const FormFieldContext = createContext<FormFieldContextValue>(
|
||||
|
||||
const Field = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
@@ -97,8 +99,9 @@ const Label = forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitives.Root> & {
|
||||
optional?: boolean
|
||||
tooltip?: ReactNode
|
||||
}
|
||||
>(({ className, optional = false, ...props }, ref) => {
|
||||
>(({ className, optional = false, tooltip, ...props }, ref) => {
|
||||
const { formItemId } = useFormField()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -112,6 +115,11 @@ const Label = forwardRef<
|
||||
weight="plus"
|
||||
{...props}
|
||||
/>
|
||||
{tooltip && (
|
||||
<Tooltip content={tooltip}>
|
||||
<InformationCircleSolid className="text-ui-fg-muted" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{optional && (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
({t("fields.optional")})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./table-row-actions"
|
||||
@@ -122,7 +122,7 @@ export const NavItem = ({
|
||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none first-of-type:mt-1 last-of-type:mb-2 md:py-1.5",
|
||||
{
|
||||
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
|
||||
location.pathname === item.to,
|
||||
location.pathname.startsWith(item.to),
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useRef, useState } from "react"
|
||||
|
||||
export const useHandleTableScroll = () => {
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Listen for if the table container that has overflow-y: auto is scrolled, and if true set some state
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
const handleScroll = () => {
|
||||
if (tableContainerRef.current) {
|
||||
setIsScrolled(
|
||||
tableContainerRef.current.scrollTop > 0 &&
|
||||
tableContainerRef.current.scrollTop <
|
||||
tableContainerRef.current.scrollHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tableContainerRef,
|
||||
isScrolled,
|
||||
handleScroll,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AdminCollectionsRes,
|
||||
AdminCustomerGroupsRes,
|
||||
AdminCustomersRes,
|
||||
AdminProductsRes,
|
||||
@@ -136,12 +137,37 @@ const router = createBrowserRouter([
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: () => import("../../routes/collections/list"),
|
||||
path: "",
|
||||
lazy: () => import("../../routes/collections/collection-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../routes/collections/collection-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/collections/details"),
|
||||
handle: {
|
||||
crumb: (data: AdminCollectionsRes) => data.collection.title,
|
||||
},
|
||||
lazy: () =>
|
||||
import("../../routes/collections/collection-detail"),
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import("../../routes/collections/collection-edit"),
|
||||
},
|
||||
{
|
||||
path: "add-products",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/collections/collection-add-products"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -62,7 +62,7 @@ export const CreatePublishableApiKeyForm = ({
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-10 px-2 pb-6 pt-[72px]">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading>
|
||||
{t("apiKeyManagement.createPublishableApiKey")}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { EllipsisHorizontal, Trash, XCircle } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||
import { PublishableApiKey } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
@@ -26,6 +17,7 @@ import {
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
@@ -212,27 +204,33 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item onClick={handleRevoke}>
|
||||
<div className="flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle">
|
||||
<XCircle />
|
||||
<span onClick={handleRevoke}>{t("apiKeyManagement.revoke")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onClick={handleDelete}>
|
||||
<div className="flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle">
|
||||
<Trash />
|
||||
<span>{t("general.delete")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/api-key-management/${apiKey.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <XCircle />,
|
||||
label: t("apiKeyManagement.revoke"),
|
||||
onClick: handleRevoke,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FocusModal } from "@medusajs/ui"
|
||||
import { useAdminCollection } from "medusa-react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
import { AddProductsToCollectionForm } from "./components/add-products-to-collection-form"
|
||||
|
||||
export const CollectionAddProducts = () => {
|
||||
const { id } = useParams()
|
||||
const { collection, isLoading, isError, error } = useAdminCollection(id!)
|
||||
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
const handleSuccessfulSubmit = () => {
|
||||
onOpenChange(false, true)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||
<FocusModal.Content>
|
||||
{!isLoading && collection && (
|
||||
<AddProductsToCollectionForm
|
||||
collection={collection}
|
||||
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||
subscribe={subscribe}
|
||||
/>
|
||||
)}
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import type { Product, ProductCollection } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FocusModal,
|
||||
Hint,
|
||||
Table,
|
||||
Tooltip,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminAddProductsToCollection,
|
||||
useAdminProducts,
|
||||
} from "medusa-react"
|
||||
import { Fragment, useEffect, useMemo, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
ProductAvailabilityCell,
|
||||
ProductCollectionCell,
|
||||
ProductStatusCell,
|
||||
ProductTitleCell,
|
||||
ProductVariantCell,
|
||||
} from "../../../../../components/common/product-table-cells"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
import { queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
type AddProductsToCollectionFormProps = {
|
||||
collection: ProductCollection
|
||||
subscribe: (state: boolean) => void
|
||||
onSuccessfulSubmit: () => void
|
||||
}
|
||||
|
||||
const AddProductsToSalesChannelSchema = zod.object({
|
||||
product_ids: zod.array(zod.string()).min(1),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const AddProductsToCollectionForm = ({
|
||||
collection,
|
||||
subscribe,
|
||||
onSuccessfulSubmit,
|
||||
}: AddProductsToCollectionFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const form = useForm<zod.infer<typeof AddProductsToSalesChannelSchema>>({
|
||||
defaultValues: {
|
||||
product_ids: [],
|
||||
},
|
||||
resolver: zodResolver(AddProductsToSalesChannelSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty])
|
||||
|
||||
const { mutateAsync, isLoading: isMutating } =
|
||||
useAdminAddProductsToCollection(collection.id)
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue(
|
||||
"product_ids",
|
||||
Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||
)
|
||||
}, [rowSelection])
|
||||
|
||||
const params = useQueryParams(["q", "order"])
|
||||
|
||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||
{
|
||||
expand: "variants,sales_channels",
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: (products ?? []) as Product[],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
enableRowSelection(row) {
|
||||
return row.original.collection_id !== collection.id
|
||||
},
|
||||
meta: {
|
||||
collectionId: collection.id,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
product_ids: values.product_ids.map((p) => p),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
/**
|
||||
* Invalidate the products list query to refetch products and
|
||||
* determine if they are added to the collection or not.
|
||||
*/
|
||||
queryClient.invalidateQueries(adminProductKeys.lists())
|
||||
onSuccessfulSubmit()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
products?.length === 0 &&
|
||||
!Object.values(params).filter((v) => v).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
{form.formState.errors.product_ids && (
|
||||
<Hint variant="error">
|
||||
{form.formState.errors.product_ids.message}
|
||||
</Hint>
|
||||
)}
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
{t("general.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||
{!noRecords && (
|
||||
<div className="flex items-center justify-between w-full px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["title"]} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!noRecords ? (
|
||||
<Fragment>
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
className="w-full flex-1 overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{!isLoading && !products?.length ? (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
<NoResults />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Header
|
||||
className={clx(
|
||||
"bg-ui-bg-base transition-fg sticky inset-x-0 top-0 z-10 border-t-0",
|
||||
{
|
||||
"shadow-elevation-card-hover": isScrolled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/5"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
},
|
||||
{
|
||||
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||
row.original.collection_id === collection.id,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full border-t">
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<NoRecords />
|
||||
{/* TODO: fix this, and add NoRecords as well */}
|
||||
</div>
|
||||
)}
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row, table }) => {
|
||||
const { collectionId } = table.options.meta as {
|
||||
collectionId: string
|
||||
}
|
||||
|
||||
const isAdded = row.original.collection_id === collectionId
|
||||
|
||||
const isSelected = row.getIsSelected() || isAdded
|
||||
|
||||
const Component = (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isAdded}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (isAdded) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={t("salesChannels.productAlreadyAdded")}
|
||||
side="right"
|
||||
>
|
||||
{Component}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return Component
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ row }) => {
|
||||
const product = row.original
|
||||
|
||||
return <ProductTitleCell product={product} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("collection", {
|
||||
header: t("fields.collection"),
|
||||
cell: ({ getValue }) => {
|
||||
const collection = getValue()
|
||||
|
||||
return <ProductCollectionCell collection={collection} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channels", {
|
||||
header: t("fields.availability"),
|
||||
cell: ({ getValue }) => {
|
||||
const salesChannels = getValue()
|
||||
|
||||
return <ProductAvailabilityCell salesChannels={salesChannels} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("variants", {
|
||||
header: t("fields.inventory"),
|
||||
cell: (cell) => {
|
||||
const variants = cell.getValue()
|
||||
|
||||
return <ProductVariantCell variants={variants} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue()
|
||||
|
||||
return <ProductStatusCell status={status} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./add-products-to-collection-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { CollectionAddProducts as Component } from "./collection-add-products"
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FocusModal } from "@medusajs/ui"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
import { CreateCollectionForm } from "./components/create-collection-form"
|
||||
|
||||
export const CollectionCreate = () => {
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
return (
|
||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||
<FocusModal.Content>
|
||||
<CreateCollectionForm subscribe={subscribe} />
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui"
|
||||
import { useAdminCreateCollection } from "medusa-react"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import * as zod from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
|
||||
type CreateCollectionFormProps = {
|
||||
subscribe: (state: boolean) => void
|
||||
}
|
||||
|
||||
const CreateCollectionSchema = zod.object({
|
||||
title: zod.string().min(1),
|
||||
handle: zod.string().optional(),
|
||||
})
|
||||
|
||||
export const CreateCollectionForm = ({
|
||||
subscribe,
|
||||
}: CreateCollectionFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateCollectionSchema>>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
handle: "",
|
||||
},
|
||||
resolver: zodResolver(CreateCollectionSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty])
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateCollection()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: ({ collection }) => {
|
||||
navigate(`/collections/${collection.id}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button
|
||||
size="small"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("general.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-col items-center py-16">
|
||||
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("collections.createCollection")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("collections.createCollectionHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input autoComplete="off" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="handle"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
optional
|
||||
tooltip={t("collections.handleTooltip")}
|
||||
>
|
||||
{t("fields.handle")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 inset-y-0 w-8 border-r z-10 flex items-center justify-center">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
/
|
||||
</Text>
|
||||
</div>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-collection-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { CollectionCreate as Component } from "./collection-create"
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useAdminCollection } from "medusa-react"
|
||||
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { CollectionGeneralSection } from "./components/collection-general-section"
|
||||
import { CollectionProductSection } from "./components/collection-product-section"
|
||||
import { collectionLoader } from "./loader"
|
||||
|
||||
export const CollectionDetail = () => {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof collectionLoader>
|
||||
>
|
||||
|
||||
const { id } = useParams()
|
||||
const { collection, isLoading, isError, error } = useAdminCollection(id!, {
|
||||
initialData,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !collection) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error occurred", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<CollectionGeneralSection collection={collection} />
|
||||
<CollectionProductSection collection={collection} />
|
||||
<JsonViewSection data={collection} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import type { ProductCollection } from "@medusajs/medusa"
|
||||
import { Container, Heading, Text, usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteCollection } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type CollectionGeneralSectionProps = {
|
||||
collection: ProductCollection
|
||||
}
|
||||
|
||||
export const CollectionGeneralSection = ({
|
||||
collection,
|
||||
}: CollectionGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteCollection(collection.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.deleteWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{collection.title}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/collections/${collection.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.handle")}
|
||||
</Text>
|
||||
<Text size="small">/{collection.handle}</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.products")}
|
||||
</Text>
|
||||
<Text size="small">{collection.products?.length || "-"}</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-general-section"
|
||||
@@ -0,0 +1,402 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import type { Product, ProductCollection } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminProducts,
|
||||
useAdminRemoveProductsFromCollection,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content"
|
||||
import {
|
||||
ProductAvailabilityCell,
|
||||
ProductCollectionCell,
|
||||
ProductStatusCell,
|
||||
ProductTitleCell,
|
||||
ProductVariantCell,
|
||||
} from "../../../../../components/common/product-table-cells"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
import { queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
type CollectionProductSectionProps = {
|
||||
collection: ProductCollection
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export const CollectionProductSection = ({
|
||||
collection,
|
||||
}: CollectionProductSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const params = useQueryParams(["q", "order"])
|
||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
collection_id: [collection.id],
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: (products ?? []) as Product[],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
getRowId: (row) => row.id,
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
meta: {
|
||||
collectionId: collection.id,
|
||||
},
|
||||
})
|
||||
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminRemoveProductsFromCollection(collection.id)
|
||||
|
||||
const handleRemove = async () => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.removeProductsWarning", {
|
||||
count: ids.length,
|
||||
}),
|
||||
confirmText: t("general.confirm"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
product_ids: ids,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(adminProductKeys.lists())
|
||||
setRowSelection({})
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
products?.length === 0 &&
|
||||
!Object.values(params).filter((v) => v).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("products.domain")}</Heading>
|
||||
<Link to={`/collections/${collection.id}/add-products`}>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.add")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{!noRecords && (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["title", "status", "created_at", "updated_at"]} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{noRecords ? (
|
||||
<NoRecords />
|
||||
) : (
|
||||
<div>
|
||||
{!isLoading && !products?.length ? (
|
||||
<div className="border-b">
|
||||
<NoResults />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/5"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap [&_td:first-of-type]:w-[1%] [&_td:first-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() => navigate(`/products/${row.original.id}`)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||
<CommandBar.Bar>
|
||||
<CommandBar.Value>
|
||||
{t("general.countSelected", {
|
||||
count: Object.keys(rowSelection).length,
|
||||
})}
|
||||
</CommandBar.Value>
|
||||
<CommandBar.Seperator />
|
||||
<CommandBar.Command
|
||||
action={handleRemove}
|
||||
shortcut="r"
|
||||
label={t("general.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const ProductActions = ({
|
||||
product,
|
||||
collectionId,
|
||||
}: {
|
||||
product: Product
|
||||
collectionId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminRemoveProductsFromCollection(collectionId)
|
||||
|
||||
const handleRemove = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.removeSingleProductWarning", {
|
||||
title: product.title,
|
||||
}),
|
||||
confirmText: t("general.confirm"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
product_ids: [product.id],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/products/${product.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.remove"),
|
||||
onClick: handleRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ row }) => {
|
||||
return <ProductTitleCell product={row.original} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("collection", {
|
||||
header: t("fields.collection"),
|
||||
cell: (cell) => {
|
||||
const collection = cell.getValue()
|
||||
|
||||
return <ProductCollectionCell collection={collection} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channels", {
|
||||
header: t("fields.availability"),
|
||||
cell: (cell) => {
|
||||
const salesChannels = cell.getValue()
|
||||
|
||||
return <ProductAvailabilityCell salesChannels={salesChannels ?? []} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("variants", {
|
||||
header: t("fields.variants"),
|
||||
cell: (cell) => {
|
||||
const variants = cell.getValue()
|
||||
|
||||
return <ProductVariantCell variants={variants} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: t("fields.status"),
|
||||
cell: (cell) => {
|
||||
const value = cell.getValue()
|
||||
|
||||
return <ProductStatusCell status={value} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
const { collectionId } = table.options.meta as {
|
||||
collectionId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<ProductActions
|
||||
product={row.original}
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-product-section"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CollectionDetail as Component } from "./collection-detail"
|
||||
export { collectionLoader as loader } from "./loader"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AdminCollectionsRes } from "@medusajs/medusa"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
import { adminProductKeys } from "medusa-react"
|
||||
import { LoaderFunctionArgs } from "react-router-dom"
|
||||
|
||||
import { medusa, queryClient } from "../../../lib/medusa"
|
||||
|
||||
const collectionDetailQuery = (id: string) => ({
|
||||
queryKey: adminProductKeys.detail(id),
|
||||
queryFn: async () => medusa.admin.collections.retrieve(id),
|
||||
})
|
||||
|
||||
export const collectionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = collectionDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminCollectionsRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Drawer, Heading } from "@medusajs/ui"
|
||||
import { useAdminCollection } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
import { EditCollectionForm } from "./components/edit-collection-form"
|
||||
|
||||
export const CollectionEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
const { collection, isLoading, isError, error } = useAdminCollection(id!)
|
||||
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
const handleSuccessfulSubmit = () => {
|
||||
onOpenChange(false, true)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Heading>{t("collections.editCollection")}</Heading>
|
||||
</Drawer.Header>
|
||||
{!isLoading && collection && (
|
||||
<EditCollectionForm
|
||||
collection={collection}
|
||||
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||
subscribe={subscribe}
|
||||
/>
|
||||
)}
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import type { ProductCollection } from "@medusajs/medusa"
|
||||
import { Button, Drawer, Input, Text } from "@medusajs/ui"
|
||||
import { useAdminUpdateCollection } from "medusa-react"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
|
||||
type EditCollectionFormProps = {
|
||||
collection: ProductCollection
|
||||
subscribe: (state: boolean) => void
|
||||
onSuccessfulSubmit: () => void
|
||||
}
|
||||
|
||||
const EditCollectionSchema = zod.object({
|
||||
title: zod.string().min(1),
|
||||
handle: zod.string().min(1),
|
||||
})
|
||||
|
||||
export const EditCollectionForm = ({
|
||||
collection,
|
||||
onSuccessfulSubmit,
|
||||
subscribe,
|
||||
}: EditCollectionFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditCollectionSchema>>({
|
||||
defaultValues: {
|
||||
title: collection.title,
|
||||
handle: collection.handle,
|
||||
},
|
||||
resolver: zodResolver(EditCollectionSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty])
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateCollection(collection.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
onSuccessfulSubmit()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||
<Drawer.Body>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="handle"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label tooltip={t("collections.handleTooltip")}>
|
||||
{t("fields.handle")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 inset-y-0 w-8 border-r z-10 flex items-center justify-center">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
>
|
||||
/
|
||||
</Text>
|
||||
</div>
|
||||
<Input {...field} className="pl-10" />
|
||||
</div>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Drawer.Body>
|
||||
<Drawer.Footer>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-collection-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { CollectionEdit as Component } from "./collection-add-edit"
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { CollectionListTable } from "./components/collection-list-table"
|
||||
|
||||
export const CollectionList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<CollectionListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { ProductCollection } from "@medusajs/medusa"
|
||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminCollections, useAdminDeleteCollection } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const CollectionListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const params = useQueryParams(["q"])
|
||||
const { collections, count, isError, error, isLoading } = useAdminCollections(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
data: collections ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
(!collections || collections.length === 0) &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("collections.domain")}</Heading>
|
||||
<Link to="/collections/create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{!noRecords && (
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{noRecords ? (
|
||||
<NoRecords />
|
||||
) : (
|
||||
<div>
|
||||
{!isLoading && !collections?.length ? (
|
||||
<div className="border-b">
|
||||
<NoResults />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() => navigate(`/collections/${row.original.id}`)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CollectionActions = ({
|
||||
collection,
|
||||
}: {
|
||||
collection: ProductCollection
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteCollection(collection.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.deleteWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/collections/${collection.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ProductCollection>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <CollectionActions collection={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-list-table"
|
||||
@@ -0,0 +1 @@
|
||||
export { CollectionList as Component } from "./collection-list"
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const CollectionDetails = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Collection</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { CollectionDetails as Component } from "./details";
|
||||
@@ -1 +0,0 @@
|
||||
export { CollectionsList as Component } from "./list";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const CollectionsList = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Collections</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -26,11 +26,11 @@ import {
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content"
|
||||
import { TableRowActions } from "../../../../../components/common/table-row-actions"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
@@ -279,7 +279,7 @@ const CustomerActions = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowActions
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import type { CustomerGroup } from "@medusajs/medusa"
|
||||
import { Container, DropdownMenu, Heading, IconButton } from "@medusajs/ui"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminDeleteCustomerGroup } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type CustomerGroupGeneralSectionProps = {
|
||||
group: CustomerGroup
|
||||
@@ -28,29 +29,28 @@ export const CustomerGroupGeneralSection = ({
|
||||
return (
|
||||
<Container className="px-6 py-4 flex items-center justify-between">
|
||||
<Heading>{group.name}</Heading>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/customer-groups/${group.id}/edit`}>
|
||||
<DropdownMenu.Item className="flex items-center gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/customer-groups/${group.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
} from "../../../../../components/common/empty-table-content"
|
||||
import { TableRowActions } from "../../../../../components/common/table-row-actions"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
@@ -205,7 +205,7 @@ const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowActions
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
|
||||
@@ -93,8 +93,8 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||
<div className="w-full max-w-[720px] flex flex-col gap-y-10">
|
||||
<FocusModal.Body className="flex flex-col items-center py-16">
|
||||
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("customers.createCustomer")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { EllipsisHorizontal, ReceiptPercent } from "@medusajs/icons"
|
||||
import { ReceiptPercent } from "@medusajs/icons"
|
||||
import { Customer, Order } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import { Button, Container, Heading, Table, clx } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
@@ -20,7 +12,8 @@ import {
|
||||
import { useAdminOrders } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import {
|
||||
OrderDateCell,
|
||||
@@ -184,22 +177,22 @@ export const CustomerOrderSection = ({
|
||||
}
|
||||
|
||||
const OrderActions = ({ order }: { order: Order }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/orders/${order.id}`}>
|
||||
<DropdownMenu.Item className="flex items-center gap-x-2">
|
||||
<ReceiptPercent />
|
||||
<span>Go to order</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <ReceiptPercent />,
|
||||
label: t("customers.viewOrder"),
|
||||
to: `/orders/${order.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { EllipsisHorizontal, PencilSquare } from "@medusajs/icons"
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
@@ -22,6 +19,8 @@ import { useAdminCustomers } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
@@ -45,13 +44,11 @@ export const CustomerListTable = () => {
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { q } = useQueryParams(["q"])
|
||||
const params = useQueryParams(["q"])
|
||||
const { customers, count, isLoading, isError, error } = useAdminCustomers({
|
||||
q,
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
})
|
||||
|
||||
const columns = useColumns()
|
||||
@@ -62,14 +59,17 @@ export const CustomerListTable = () => {
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
(!customers || customers.length === 0) &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
@@ -90,62 +90,69 @@ export const CustomerListTable = () => {
|
||||
<Query />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
{noRecords ? (
|
||||
<NoRecords />
|
||||
) : (
|
||||
<div>
|
||||
<Table>
|
||||
<Table.Header className="border-t-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={headerGroup.id}
|
||||
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() => navigate(`/customers/${row.original.id}`)}
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeaderCell key={header.id}>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeaderCell>
|
||||
)
|
||||
})}
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Header>
|
||||
<Table.Body className="border-b-0">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={clx(
|
||||
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
{
|
||||
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||
row.getIsSelected(),
|
||||
}
|
||||
)}
|
||||
onClick={() => navigate(`/customers/${row.original.id}`)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Table.Cell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<LocalizedTablePagination
|
||||
canNextPage={table.getCanNextPage()}
|
||||
canPreviousPage={table.getCanPreviousPage()}
|
||||
nextPage={table.nextPage}
|
||||
previousPage={table.previousPage}
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -154,24 +161,19 @@ const CustomerActions = ({ customer }: { customer: Customer }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/customers/${customer.id}/edit`}>
|
||||
<DropdownMenu.Item
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
{t("general.edit")}
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/customers/${customer.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export const CreateLocationForm = () => {
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-10 px-2 pb-6 pt-[72px]">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading className="capitalize">
|
||||
{t("locations.createLocation")}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import { StockLocationExpandedDTO } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
@@ -19,9 +18,11 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminRemoveLocationFromSalesChannel } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content/empty-table-content"
|
||||
|
||||
type LocationSalesChannelSectionProps = {
|
||||
@@ -67,6 +68,9 @@ export const LocationSalesChannelSection = ({
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
meta: {
|
||||
locationId: location.id,
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -143,28 +147,55 @@ export const LocationSalesChannelSection = ({
|
||||
)
|
||||
}
|
||||
|
||||
const SalesChannelActions = ({ id }: { id: string }) => {
|
||||
const SalesChannelActions = ({
|
||||
salesChannel,
|
||||
locationId,
|
||||
}: {
|
||||
salesChannel: SalesChannel
|
||||
locationId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminRemoveLocationFromSalesChannel()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("locations.removeSalesChannelsWarning", { count: 1 }),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync({
|
||||
location_id: locationId,
|
||||
sales_channel_id: salesChannel.id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -198,8 +229,17 @@ const useColumns = () => {
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <SalesChannelActions id={row.original.id} />
|
||||
cell: ({ row, table }) => {
|
||||
const { locationId } = table.options.meta as {
|
||||
locationId: string
|
||||
}
|
||||
|
||||
return (
|
||||
<SalesChannelActions
|
||||
salesChannel={row.original}
|
||||
locationId={locationId}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -61,7 +61,7 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Drawer.Body className="flex flex-col gap-y-10 overflow-y-auto">
|
||||
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto">
|
||||
<div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { StockLocationExpandedDTO } from "@medusajs/types"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
@@ -22,10 +13,11 @@ import {
|
||||
useAdminDeleteStockLocation,
|
||||
useAdminStockLocations,
|
||||
} from "medusa-react"
|
||||
import { MouseEvent, useMemo, useState } from "react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import {
|
||||
NoRecords,
|
||||
NoResults,
|
||||
@@ -176,9 +168,7 @@ const LocationActions = ({
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminDeleteStockLocation(location.id)
|
||||
|
||||
const handleDelete = async (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("locations.deleteLocationWarning", {
|
||||
@@ -198,30 +188,24 @@ const LocationActions = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/settings/locations/${location.id}`}>
|
||||
<DropdownMenu.Item onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onClick={handleDelete}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/locations/${location.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { EllipsisHorizontal, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import type { Product } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
@@ -32,6 +30,7 @@ import {
|
||||
ProductVariantCell,
|
||||
} from "../../../../../components/common/product-table-cells"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { productsLoader } from "../../loader"
|
||||
|
||||
@@ -172,6 +171,7 @@ export const ProductListTable = () => {
|
||||
}
|
||||
|
||||
const ProductActions = ({ id }: { id: string }) => {
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync } = useAdminDeleteProduct(id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -179,21 +179,28 @@ const ProductActions = ({ id }: { id: string }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton size="small" variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item onClick={handleDelete}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/products/${id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui"
|
||||
import { useAdminCreateRegion } from "medusa-react"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import * as zod from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
|
||||
type CreateRegionFormProps = {
|
||||
subscribe: (state: boolean) => void
|
||||
}
|
||||
|
||||
const CreateRegionSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
currency_code: zod.string(),
|
||||
includes_tax: zod.boolean(),
|
||||
countries: zod.array(zod.string()),
|
||||
fulfillment_providers: zod.array(zod.string()).min(1),
|
||||
payment_providers: zod.array(zod.string()).min(1),
|
||||
tax_rate: zod.number().min(0).max(1),
|
||||
tax_code: zod.string().optional(),
|
||||
})
|
||||
|
||||
export const CreateRegionForm = ({ subscribe }: CreateRegionFormProps) => {
|
||||
const form = useForm<zod.infer<typeof CreateRegionSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
currency_code: "",
|
||||
includes_tax: false,
|
||||
countries: [],
|
||||
fulfillment_providers: [],
|
||||
payment_providers: [],
|
||||
tax_code: "",
|
||||
},
|
||||
resolver: zodResolver(CreateRegionSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty])
|
||||
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateRegion()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
name: values.name,
|
||||
countries: values.countries,
|
||||
currency_code: values.currency_code,
|
||||
fulfillment_providers: values.fulfillment_providers,
|
||||
payment_providers: values.payment_providers,
|
||||
tax_rate: values.tax_rate,
|
||||
tax_code: values.tax_code,
|
||||
includes_tax: values.includes_tax,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ region }) => {
|
||||
navigate(`../${region.id}`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex h-full flex-col overflow-hidden"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
|
||||
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("regions.createRegion")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("regions.createRegionHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.name")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="currency_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.currency")}</Form.Label>
|
||||
<Form.Control></Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tax_rate"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.taxRate")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tax_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>{t("fields.taxCode")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="includes_tax"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<Form.Label>
|
||||
{t("fields.taxInclusivePricing")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.providers")}
|
||||
</Text>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("regions.providersHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-region-form"
|
||||
@@ -1,200 +1,15 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, FocusModal, Heading, Input, Switch, Text } from "@medusajs/ui"
|
||||
import { useAdminCreateRegion } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { FocusModal } from "@medusajs/ui"
|
||||
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Form } from "../../../components/common/form"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
|
||||
const CreateRegionFormSchema = zod.object({
|
||||
name: zod.string().min(1),
|
||||
currency_code: zod.string(),
|
||||
includes_tax: zod.boolean(),
|
||||
countries: zod.array(zod.string()),
|
||||
fulfillment_providers: zod.array(zod.string()).min(1),
|
||||
payment_providers: zod.array(zod.string()).min(1),
|
||||
tax_rate: zod.number().min(0).max(100),
|
||||
tax_code: zod.string().optional(),
|
||||
})
|
||||
import { CreateRegionForm } from "./components/create-region-form"
|
||||
|
||||
export const RegionCreate = () => {
|
||||
const [open, onOpenChange] = useRouteModalState()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateRegionFormSchema>>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
currency_code: "",
|
||||
includes_tax: false,
|
||||
countries: [],
|
||||
fulfillment_providers: [],
|
||||
payment_providers: [],
|
||||
tax_code: "",
|
||||
},
|
||||
resolver: zodResolver(CreateRegionFormSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateRegion()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
name: values.name,
|
||||
countries: values.countries,
|
||||
currency_code: values.currency_code,
|
||||
fulfillment_providers: values.fulfillment_providers,
|
||||
payment_providers: values.payment_providers,
|
||||
tax_rate: values.tax_rate,
|
||||
tax_code: values.tax_code,
|
||||
includes_tax: values.includes_tax,
|
||||
},
|
||||
{
|
||||
onSuccess: ({ region }) => {
|
||||
navigate(`../${region.id}`)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
return (
|
||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||
<FocusModal.Content>
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
|
||||
<Heading></Heading>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.name")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input size="small" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="currency_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.currency")}</Form.Label>
|
||||
<Form.Control></Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tax_rate"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.taxRate")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" size="small" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tax_code"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("fields.taxCode")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="number" size="small" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="includes_tax"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<Form.Label>
|
||||
{t("fields.taxInclusivePricing")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...field}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>{t("regions.taxInclusiveHint")}</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.providers")}
|
||||
</Text>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("regions.providersHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4"></div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.metadata")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</Form>
|
||||
<CreateRegionForm subscribe={subscribe} />
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import {
|
||||
BuildingTax,
|
||||
EllipsisHorizontal,
|
||||
PencilSquare,
|
||||
Trash,
|
||||
} from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Country, Region } from "@medusajs/medusa"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Container,
|
||||
Drawer,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Text,
|
||||
Tooltip,
|
||||
@@ -21,8 +14,8 @@ import {
|
||||
import { useAdminDeleteRegion, useAdminUpdateRegion } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import * as zod from "zod"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type RegionGeneralSectionProps = {
|
||||
region: Region
|
||||
@@ -113,38 +106,28 @@ const RegionActions = ({ region }: { region: Region }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to="edit" relative="route">
|
||||
<DropdownMenu.Item>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<Link to={`/settings/taxes/${region.id}`}>
|
||||
<DropdownMenu.Item>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<BuildingTax className="text-ui-fg-subtle" />
|
||||
<span>Tax settings</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onClick={handleDelete}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/regions/${region.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Region, ShippingOption } from "@medusajs/medusa"
|
||||
import { Container, StatusBadge, Table, clx } from "@medusajs/ui"
|
||||
import { Container, Heading, StatusBadge, Table, clx } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
@@ -17,15 +17,18 @@ type RegionShippingOptionSectionProps = {
|
||||
region: Region
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
// TODO: Need to fix pagination and search for shipping options
|
||||
export const RegionShippingOptionSection = ({
|
||||
region,
|
||||
}: RegionShippingOptionSectionProps) => {
|
||||
const { shipping_options, count, isError, error, isLoading } =
|
||||
useAdminShippingOptions({
|
||||
region_id: region.id,
|
||||
is_return: false,
|
||||
})
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: PAGE_SIZE,
|
||||
pageSize: count || 0,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
@@ -36,11 +39,6 @@ export const RegionShippingOptionSection = ({
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const { shipping_options, count, isError, error, isLoading } =
|
||||
useAdminShippingOptions({
|
||||
region_id: region.id,
|
||||
})
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const columns = useShippingOptionColumns()
|
||||
@@ -48,7 +46,7 @@ export const RegionShippingOptionSection = ({
|
||||
const table = useReactTable({
|
||||
data: shipping_options ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
pageCount: count ? 1 : 0,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
@@ -59,17 +57,17 @@ export const RegionShippingOptionSection = ({
|
||||
onRowSelectionChange: setRowSelection,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0">
|
||||
<div className="px-6 py-4">{/* Filters go here */}</div>
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4">
|
||||
<Heading level="h2">{t("regions.shippingOptions")}</Heading>
|
||||
</div>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
{table.getHeaderGroups().map((headerGroup) => {
|
||||
@@ -121,7 +119,7 @@ export const RegionShippingOptionSection = ({
|
||||
count={count ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
pageCount={table.getPageCount()}
|
||||
pageSize={PAGE_SIZE}
|
||||
pageSize={count ?? 0}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -14,8 +14,6 @@ export const regionLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = regionQuery(id!)
|
||||
|
||||
console.log("regionLoader", query)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminProductsRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAdminRegion } from "medusa-react"
|
||||
import { Outlet, useNavigate, useParams } from "react-router-dom"
|
||||
import { Outlet, json, useParams } from "react-router-dom"
|
||||
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { RegionGeneralSection } from "./components/region-general-section"
|
||||
@@ -8,28 +8,19 @@ import { RegionShippingOptionSection } from "./components/region-shipping-option
|
||||
export const RegionDetail = () => {
|
||||
const { id } = useParams()
|
||||
const { region, isLoading, isError, error } = useAdminRegion(id!)
|
||||
const navigate = useNavigate()
|
||||
|
||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||
if (isLoading) {
|
||||
return <div>Loading</div>
|
||||
}
|
||||
|
||||
// TODO: Move to error.tsx and set as ErrorBoundary for the route
|
||||
if (isError || !region) {
|
||||
const err = error ? JSON.parse(JSON.stringify(error)) : null
|
||||
return (
|
||||
<div>
|
||||
{(err as Error & { status: number })?.status === 404 ? (
|
||||
<div>Not found</div>
|
||||
) : (
|
||||
<div>Something went wrong!</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.log("RegionDetail")
|
||||
throw json("An unknown error occurred", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Region } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
Tooltip,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminRegions } from "medusa-react"
|
||||
import { useAdminDeleteRegion, useAdminRegions } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
@@ -43,8 +42,6 @@ export const RegionListTable = () => {
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const { regions, count, isLoading, isError, error } = useAdminRegions({
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
@@ -58,10 +55,8 @@ export const RegionListTable = () => {
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
})
|
||||
@@ -140,32 +135,52 @@ export const RegionListTable = () => {
|
||||
|
||||
const RegionActions = ({ region }: { region: Region }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteRegion(region.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("regions.deleteRegionWarning", {
|
||||
name: region.name,
|
||||
}),
|
||||
verificationText: region.name,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("general.confirm"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/settings/regions/${region.id}/edit`}>
|
||||
<DropdownMenu.Item onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("general.edit"),
|
||||
to: `/settings/regions/${region.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Outlet } from "react-router-dom"
|
||||
import { RegionListTable } from "./components/region-list-table"
|
||||
|
||||
export const RegionList = () => {
|
||||
console.log("RegionList")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<RegionListTable />
|
||||
|
||||
@@ -86,7 +86,7 @@ export const CreateSalesChannelForm = ({
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 flex-col items-center overflow-y-auto">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-10 px-2 pb-6 pt-[72px]">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
|
||||
<div>
|
||||
<Heading className="capitalize">
|
||||
{t("salesChannels.createSalesChannel")}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Text,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui"
|
||||
import { useAdminDeleteSalesChannel } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type SalesChannelGeneralSection = {
|
||||
salesChannel: SalesChannel
|
||||
@@ -33,6 +26,8 @@ export const SalesChannelGeneralSection = ({
|
||||
description: t("salesChannels.deleteSalesChannelWarning", {
|
||||
name: salesChannel.name,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: salesChannel.name,
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
@@ -61,26 +56,28 @@ export const SalesChannelGeneralSection = ({
|
||||
<StatusBadge color={salesChannel.is_disabled ? "red" : "green"}>
|
||||
{t(`general.${salesChannel.is_disabled ? "disabled" : "enabled"}`)}
|
||||
</StatusBadge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/settings/sales-channels/${salesChannel.id}/edit`}>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2" onClick={handleDelete}>
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
@@ -36,6 +34,7 @@ import {
|
||||
} from "../../../../../components/common/product-table-cells"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { FilterGroup } from "../../../../../components/filtering/filter-group"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
@@ -324,25 +323,27 @@ const ProductListCellActions = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<Link to={`/products/${productId}`}>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare />
|
||||
<span>Edit</span>
|
||||
</DropdownMenu.Item>
|
||||
</Link>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onClick={onRemove} className="gap-x-2">
|
||||
<Trash />
|
||||
<span>{t("general.remove")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/products/${productId}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.remove"),
|
||||
onClick: onRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
@@ -22,6 +21,7 @@ import { useAdminDeleteSalesChannel, useAdminSalesChannels } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { Query } from "../../../../../components/filtering/query"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
@@ -162,29 +162,57 @@ export const SalesChannelListTable = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const SalesChannelActions = ({ id }: { id: string }) => {
|
||||
const { mutateAsync } = useAdminDeleteSalesChannel(id)
|
||||
const SalesChannelActions = ({
|
||||
salesChannel,
|
||||
}: {
|
||||
salesChannel: SalesChannel
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminDeleteSalesChannel(salesChannel.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirm = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("salesChannels.deleteSalesChannelWarning", {
|
||||
name: salesChannel.name,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: salesChannel.name,
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<PencilSquare className="text-ui-fg-subtle" />
|
||||
<span>{t("general.edit")}</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item className="gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.delete")}</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -223,7 +251,7 @@ const useColumns = () => {
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <SalesChannelActions id={row.original.id} />
|
||||
return <SalesChannelActions salesChannel={row.original} />
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -19,11 +19,12 @@ import {
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { useAdminCurrencies, useAdminUpdateStore } from "medusa-react"
|
||||
import { FormEvent, useMemo, useRef, useState } from "react"
|
||||
import { FormEvent, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||
import { useHandleTableScroll } from "../../../../../hooks/use-handle-table-scroll"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
type AddCurrenciesFormProps = {
|
||||
@@ -86,20 +87,7 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
||||
|
||||
const { mutateAsync, isLoading: isMutating } = useAdminUpdateStore()
|
||||
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Listen for if the table container that has overflow-y: auto is scrolled, and if true set some state
|
||||
const [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
const handleScroll = () => {
|
||||
if (tableContainerRef.current) {
|
||||
setIsScrolled(
|
||||
tableContainerRef.current.scrollTop > 0 &&
|
||||
tableContainerRef.current.scrollTop <
|
||||
tableContainerRef.current.scrollHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
const { handleScroll, isScrolled, tableContainerRef } = useHandleTableScroll()
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
@@ -129,10 +117,6 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { BuildingTax, EllipsisHorizontal, Trash } from "@medusajs/icons"
|
||||
import { Trash } from "@medusajs/icons"
|
||||
import { Currency, Store } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
DropdownMenu,
|
||||
Heading,
|
||||
IconButton,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
@@ -25,6 +23,7 @@ import { useAdminUpdateStore } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../../components/common/action-menu"
|
||||
import { LocalizedTablePagination } from "../../../../../../components/localization/localized-table-pagination"
|
||||
|
||||
type StoreCurrencySectionProps = {
|
||||
@@ -180,12 +179,14 @@ const CurrencyActions = ({
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDeleteCurrency = async () => {
|
||||
const handleRemove = async () => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("store.removeCurrencyWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
verificationText: currency.name,
|
||||
confirmText: t("general.remove"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
@@ -200,28 +201,19 @@ const CurrencyActions = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<IconButton variant="transparent">
|
||||
<EllipsisHorizontal />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Item>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<BuildingTax className="text-ui-fg-subtle" />
|
||||
<span>{t("general.remove")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onClick={handleDeleteCurrency}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Trash className="text-ui-fg-subtle" />
|
||||
<span>{t("general.remove")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.remove"),
|
||||
onClick: handleRemove,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import preset from "@medusajs/ui-preset";
|
||||
import path from "path";
|
||||
import path from "path"
|
||||
|
||||
const rootProject = path.join(
|
||||
process.cwd(),
|
||||
"../../apps/server/src/admin/**/*.{js,jsx,ts,tsx}"
|
||||
);
|
||||
)
|
||||
|
||||
// get the path of the dependency "@medusajs/ui"
|
||||
const medusaUI = path.join(
|
||||
path.dirname(require.resolve("@medusajs/ui")),
|
||||
"**/*.{js,jsx,ts,tsx}"
|
||||
);
|
||||
)
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
presets: [preset],
|
||||
presets: [require("@medusajs/ui-preset")],
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
@@ -25,5 +24,5 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
plugins: [require("@headlessui/tailwindcss")],
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ const DrawerContent = React.forwardRef<
|
||||
<DrawerPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-modal border-ui-border-base fixed inset-y-2 flex flex-1 flex-col rounded-lg border focus-visible:outline-none max-sm:inset-x-2 sm:right-2 sm:max-w-[560px] md:w-full",
|
||||
"bg-ui-bg-base shadow-elevation-modal border-ui-border-base fixed inset-y-2 flex w-full flex-1 flex-col rounded-lg border focus:outline-none max-sm:inset-x-2 max-sm:w-[calc(100%-16px)] sm:right-2 sm:max-w-[560px]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-1/2 data-[state=open]:slide-in-from-right-1/2 duration-200",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -185,7 +185,7 @@ const Item = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"txt-compact-medium bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 rounded-md px-3 py-2 outline-none transition-colors",
|
||||
"bg-ui-bg-base grid cursor-pointer grid-cols-[20px_1fr] gap-x-2 rounded-md px-3 py-2 outline-none transition-colors",
|
||||
"hover:bg-ui-bg-base-hover focus-visible:bg-ui-bg-base-hover",
|
||||
{
|
||||
"txt-compact-medium data-[state=checked]:txt-compact-medium-plus":
|
||||
|
||||
50
yarn.lock
50
yarn.lock
@@ -6076,6 +6076,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@headlessui/react@npm:^1.7.18":
|
||||
version: 1.7.18
|
||||
resolution: "@headlessui/react@npm:1.7.18"
|
||||
dependencies:
|
||||
"@tanstack/react-virtual": ^3.0.0-beta.60
|
||||
client-only: ^0.0.1
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
checksum: 2d88d10874879182d4b9ed9a7779266032214034481129ba544e858d3624c8d12333e6a9d9d8263f2f116bc823bcfd43a2d1f69800fbf6a47b34d989370346e5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@headlessui/tailwindcss@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@headlessui/tailwindcss@npm:0.2.0"
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.0
|
||||
checksum: b641bef150e4ee18afd068c1af8e83c582a3131903ed1a310abbdb463b6150428a9b4c38e42cb296469fb20857f5542ae34f681cbff62f47d52332fbc5117479
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@hookform/error-message@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@hookform/error-message@npm:2.0.1"
|
||||
@@ -7942,6 +7964,8 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"
|
||||
dependencies:
|
||||
"@headlessui/react": ^1.7.18
|
||||
"@headlessui/tailwindcss": ^0.2.0
|
||||
"@hookform/resolvers": 3.3.2
|
||||
"@medusajs/icons": "workspace:^"
|
||||
"@medusajs/medusa": "workspace:^"
|
||||
@@ -16412,6 +16436,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-virtual@npm:^3.0.0-beta.60":
|
||||
version: 3.0.1
|
||||
resolution: "@tanstack/react-virtual@npm:3.0.1"
|
||||
dependencies:
|
||||
"@tanstack/virtual-core": 3.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 2b9464dab8a975734b651211402a4eaf10e4ae6d9570138576891db350cfabe11e444fea8f17ef0b26ef8ede0adc230bec9c8fe859e61ca66147831076b77bb2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/table-core@npm:8.10.7":
|
||||
version: 8.10.7
|
||||
resolution: "@tanstack/table-core@npm:8.10.7"
|
||||
@@ -16426,6 +16462,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/virtual-core@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@tanstack/virtual-core@npm:3.0.0"
|
||||
checksum: d0899fe470b43f80a2ccc567a01138cc015900f13d91b82abc784fc7bc46419e9c8b99c102a5c6ee76d15933db61aecc68c0ae8e92e9cb9ded9ae2f51a01cf4c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/dom@npm:^8.3.0, @testing-library/dom@npm:^8.5.0":
|
||||
version: 8.20.1
|
||||
resolution: "@testing-library/dom@npm:8.20.1"
|
||||
@@ -22640,6 +22683,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"client-only@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "client-only@npm:0.0.1"
|
||||
checksum: 9d6cfd0c19e1c96a434605added99dff48482152af791ec4172fb912a71cff9027ff174efd8cdb2160cc7f377543e0537ffc462d4f279bc4701de3f2a3c4b358
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"client-sessions@npm:^0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "client-sessions@npm:0.8.0"
|
||||
|
||||
Reference in New Issue
Block a user