feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)
This commit is contained in:
committed by
GitHub
parent
d6b1ad1ccd
commit
40de54b010
@@ -0,0 +1,96 @@
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||
import clsx from "clsx"
|
||||
import React, { useState } from "react"
|
||||
import CheckIcon from "../../fundamentals/icons/check-icon"
|
||||
import ChevronDownIcon from "../../fundamentals/icons/chevron-down"
|
||||
|
||||
export type FilteringOptionProps = {
|
||||
title: string
|
||||
options: {
|
||||
title: string
|
||||
count?: number
|
||||
onClick: () => void
|
||||
}[]
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const FilteringOptions: React.FC<FilteringOptionProps> = ({
|
||||
title,
|
||||
options,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const [selected, setSelected] = useState(options?.[0]?.title || "All")
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"inter-small-regular flex text-grey-50 mr-6 last:mr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="">{title}:</span>
|
||||
<DropdownMenu.Root onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
asChild
|
||||
className={clsx(
|
||||
"inter-small-regular text-grey-50 flex items-center pl-1.5 pr-0.5 rounded active:bg-grey-5 hover:bg-grey-5",
|
||||
{ "bg-grey-5": open }
|
||||
)}
|
||||
>
|
||||
<div className="flex align-center">
|
||||
{selected}
|
||||
<div className="text-grey-40 h-min">
|
||||
<ChevronDownIcon size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
sideOffset={8}
|
||||
className="bg-grey-0 p-2 border border-grey-20 rounded-rounded shadow-dropdown"
|
||||
>
|
||||
{options.map((opt, idx) => (
|
||||
<DropdownMenu.DropdownMenuItem
|
||||
key={`${idx}-${opt.title}`}
|
||||
onSelect={() => {
|
||||
opt.onClick()
|
||||
setSelected(opt.title)
|
||||
}}
|
||||
disabled={typeof opt.count !== "undefined" && opt.count < 1}
|
||||
className={clsx(
|
||||
"py-1.5 my-1 w-48 px-3 flex items-center rounded text-grey-90 hover:border-0 hover:outline-none inter-small-semibold",
|
||||
{
|
||||
"cursor-pointer hover:bg-grey-10":
|
||||
typeof opt.count === "undefined" || opt.count > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{selected === opt.title && (
|
||||
<span className="w-4">
|
||||
<CheckIcon size={16} />
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={clsx("ml-3 w-full flex justify-between", {
|
||||
"ml-7": selected !== opt.title,
|
||||
"text-grey-30": (opt.count || 0) < 1,
|
||||
})}
|
||||
>
|
||||
{opt.title}
|
||||
<span
|
||||
className={clsx("inter-small-regular text-grey-40 ml-3", {
|
||||
"text-grey-30": (opt.count || 0) < 1,
|
||||
})}
|
||||
>
|
||||
{opt.count}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilteringOptions
|
||||
250
packages/admin-ui/ui/src/components/molecules/table/index.tsx
Normal file
250
packages/admin-ui/ui/src/components/molecules/table/index.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import Spinner from "../../atoms/spinner"
|
||||
import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon"
|
||||
import ArrowRightIcon from "../../fundamentals/icons/arrow-right-icon"
|
||||
import SortingIcon from "../../fundamentals/icons/sorting-icon"
|
||||
import Actionables, { ActionType } from "../../molecules/actionables"
|
||||
import FilteringOptions, { FilteringOptionProps } from "./filtering-option"
|
||||
import TableSearch from "./table-search"
|
||||
|
||||
type TableRowProps = React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
forceDropdown?: boolean
|
||||
actions?: ActionType[]
|
||||
linkTo?: string
|
||||
}
|
||||
|
||||
type TableCellProps = React.TdHTMLAttributes<HTMLTableCellElement> & {
|
||||
linkTo?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
type SortingHeadCellProps = {
|
||||
onSortClicked: () => void
|
||||
sortDirection?: "ASC" | "DESC"
|
||||
setSortDirection: (string) => void
|
||||
} & React.HTMLAttributes<HTMLTableCellElement>
|
||||
|
||||
export type TableProps = {
|
||||
filteringOptions?: FilteringOptionProps[] | React.ReactNode
|
||||
tableActions?: React.ReactNode
|
||||
enableSearch?: boolean
|
||||
immediateSearchFocus?: boolean
|
||||
searchPlaceholder?: string
|
||||
searchValue?: string
|
||||
containerClassName?: string
|
||||
handleSearch?: (searchTerm: string) => void
|
||||
} & React.HTMLAttributes<HTMLTableElement>
|
||||
|
||||
type TableElement<T> = React.ForwardRefExoticComponent<T> &
|
||||
React.RefAttributes<unknown>
|
||||
|
||||
type TableType = {
|
||||
Head: TableElement<React.HTMLAttributes<HTMLTableSectionElement>>
|
||||
HeadRow: TableElement<React.HTMLAttributes<HTMLTableRowElement>>
|
||||
HeadCell: TableElement<React.ThHTMLAttributes<HTMLTableCellElement>>
|
||||
SortingHeadCell: TableElement<SortingHeadCellProps>
|
||||
Body: TableElement<React.HTMLAttributes<HTMLTableSectionElement>>
|
||||
Row: TableElement<TableRowProps>
|
||||
Cell: TableElement<TableCellProps>
|
||||
} & TableElement<TableProps>
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
tableActions,
|
||||
enableSearch,
|
||||
immediateSearchFocus,
|
||||
searchPlaceholder,
|
||||
searchValue,
|
||||
handleSearch,
|
||||
filteringOptions,
|
||||
containerClassName,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
if (enableSearch && !handleSearch) {
|
||||
throw new Error("Table cannot enable search without a search handler")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col ${containerClassName}`}>
|
||||
<div className="w-full flex justify-between mb-2">
|
||||
{filteringOptions ? (
|
||||
<div className="flex mb-2 self-end">
|
||||
{Array.isArray(filteringOptions)
|
||||
? filteringOptions.map((fo) => <FilteringOptions {...fo} />)
|
||||
: filteringOptions}
|
||||
</div>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
{tableActions && <div>{tableActions}</div>}
|
||||
{enableSearch && (
|
||||
<TableSearch
|
||||
autoFocus={immediateSearchFocus}
|
||||
placeholder={searchPlaceholder}
|
||||
searchValue={searchValue}
|
||||
onSearch={handleSearch!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<table
|
||||
ref={ref}
|
||||
className={clsx("w-full table-auto", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
) as TableType
|
||||
|
||||
Table.Head = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"whitespace-nowrap inter-small-semibold text-grey-50 border-t border-b border-grey-20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
))
|
||||
|
||||
Table.HeadRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<tr ref={ref} className={clsx(className)} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
))
|
||||
|
||||
Table.HeadCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.HTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<th ref={ref} className={clsx("text-left h-[40px]", className)} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
))
|
||||
|
||||
Table.SortingHeadCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
SortingHeadCellProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
onSortClicked,
|
||||
sortDirection,
|
||||
setSortDirection,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SortingHeadCellProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<th ref={ref} className={clsx("text-left py-2.5", className)} {...props}>
|
||||
<div
|
||||
className="flex items-center cursor-pointer select-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (!sortDirection) {
|
||||
setSortDirection("ASC")
|
||||
} else {
|
||||
if (sortDirection === "ASC") {
|
||||
setSortDirection("DESC")
|
||||
} else {
|
||||
setSortDirection(undefined)
|
||||
}
|
||||
}
|
||||
onSortClicked()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<SortingIcon
|
||||
size={16}
|
||||
ascendingColor={sortDirection === "ASC" ? "#111827" : undefined}
|
||||
descendingColor={sortDirection === "DESC" ? "#111827" : undefined}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Table.Body = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<tbody ref={ref} className={clsx(className)} {...props}>
|
||||
{children}
|
||||
</tbody>
|
||||
))
|
||||
|
||||
Table.Cell = React.forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
({ className, linkTo, children, ...props }, ref) => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<td
|
||||
ref={ref}
|
||||
className={clsx("inter-small-regular h-[40px]", className)}
|
||||
{...props}
|
||||
{...(linkTo && {
|
||||
onClick: (e) => {
|
||||
navigate(linkTo)
|
||||
e.stopPropagation()
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Table.Row = React.forwardRef<HTMLTableRowElement, TableRowProps>(
|
||||
({ className, actions, children, linkTo, forceDropdown, ...props }, ref) => {
|
||||
const navigate = useNavigate()
|
||||
return (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"inter-small-regular border-t border-b border-grey-20 text-grey-90",
|
||||
className,
|
||||
{ "cursor-pointer hover:bg-grey-5": linkTo !== undefined }
|
||||
)}
|
||||
{...props}
|
||||
{...(linkTo && {
|
||||
onClick: () => {
|
||||
navigate(linkTo)
|
||||
},
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{actions && (
|
||||
<Table.Cell onClick={(e) => e.stopPropagation()} className="w-[32px]">
|
||||
<Actionables forceDropdown={forceDropdown} actions={actions} />
|
||||
</Table.Cell>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default Table
|
||||
@@ -0,0 +1,47 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import SearchIcon from "../../fundamentals/icons/search-icon"
|
||||
|
||||
type TableSearchProps = {
|
||||
autoFocus?: boolean
|
||||
onSearch: (term: string) => void
|
||||
placeholder?: string
|
||||
searchValue?: string
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TableSearch: React.FC<TableSearchProps> = ({
|
||||
autoFocus,
|
||||
onSearch,
|
||||
placeholder = "Search",
|
||||
searchValue,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"inter-small-regular mt-1 transition-color flex text-grey-50 items-center mb-1 pl-1 py-1.5 rounded-rounded border border-grey-20 min-w-content w-60 focus-within:shadow-input focus-within:border-violet-60 bg-grey-5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="px-2.5 py-0.5">
|
||||
<SearchIcon size={16} />
|
||||
</span>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
type="text"
|
||||
value={searchValue}
|
||||
className={clsx(
|
||||
"focus:outline-none focus:border-none inter-small-regular w-full bg-transparent focus:text-grey-90 caret-violet-60 placeholder:inter-small-regular placeholder-grey-40"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => {
|
||||
onSearch(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableSearch
|
||||
Reference in New Issue
Block a user