feat(dashboard,medusa,ui): Manual gift cards + cleanup (#6380)
**Dashboard** - Adds different views for managing manual/custom gift cards (not associated with a product gift card). - Cleans up several table implementations to use new DataTable component. - Minor cleanup of translation file. **Medusa** - Adds missing query params for list endpoints in the following admin domains: /customers, /customer-groups, /collections, and /gift-cards. **UI** - Adds new sizes for Badge component. **Note for review** Since this PR contains updates to the translation keys, it touches a lot of files. For the review the parts that are relevant are: the /gift-cards domain of admin, the table overview of collections, customers, and customer groups. And the changes to the list endpoints in the core.
This commit is contained in:
committed by
GitHub
parent
bc2a63782b
commit
d37ff8024d
@@ -13,10 +13,10 @@ import { NoResults } from "../../../common/empty-table-content"
|
||||
type BulkCommand = {
|
||||
label: string
|
||||
shortcut: string
|
||||
action: (selection: Record<string, boolean>) => void
|
||||
action: (selection: Record<string, boolean>) => Promise<void>
|
||||
}
|
||||
|
||||
export interface DataTableRootProps<TData, TValue> {
|
||||
export interface DataTableRootProps<TData> {
|
||||
/**
|
||||
* The table instance to render
|
||||
*/
|
||||
@@ -24,7 +24,7 @@ export interface DataTableRootProps<TData, TValue> {
|
||||
/**
|
||||
* The columns to render
|
||||
*/
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
columns: ColumnDef<TData, any>[]
|
||||
/**
|
||||
* Function to generate a link to navigate to when clicking on a row
|
||||
*/
|
||||
@@ -61,7 +61,7 @@ export interface DataTableRootProps<TData, TValue> {
|
||||
/**
|
||||
* Table component for rendering a table with pagination, filtering and ordering.
|
||||
*/
|
||||
export const DataTableRoot = <TData, TValue>({
|
||||
export const DataTableRoot = <TData,>({
|
||||
table,
|
||||
columns,
|
||||
pagination,
|
||||
@@ -69,7 +69,7 @@ export const DataTableRoot = <TData, TValue>({
|
||||
commands,
|
||||
count = 0,
|
||||
noResults = false,
|
||||
}: DataTableRootProps<TData, TValue>) => {
|
||||
}: DataTableRootProps<TData>) => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [showStickyBorder, setShowStickyBorder] = useState(false)
|
||||
@@ -94,6 +94,12 @@ export const DataTableRoot = <TData, TValue>({
|
||||
}
|
||||
}
|
||||
|
||||
const handleAction = async (action: BulkCommand["action"]) => {
|
||||
await action(rowSelection).then(() => {
|
||||
table.resetRowSelection()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div onScroll={handleHorizontalScroll} className="w-full overflow-x-auto">
|
||||
@@ -159,6 +165,7 @@ export const DataTableRoot = <TData, TValue>({
|
||||
return (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
data-selected={row.getIsSelected()}
|
||||
className={clx(
|
||||
"transition-fg group/row [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||
"[&:has(td_a:focus-visible)_td]:bg-ui-bg-base-pressed",
|
||||
@@ -188,7 +195,7 @@ export const DataTableRoot = <TData, TValue>({
|
||||
<Table.Cell
|
||||
key={cell.id}
|
||||
className={clx("has-[a]:cursor-pointer", {
|
||||
"bg-ui-bg-base group-[:has(td_a:focus)]/row:bg-ui-bg-base-pressed group-hover/row:bg-ui-bg-base-hover transition-fg sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
|
||||
"bg-ui-bg-base group-data-[selected=true]/row:bg-ui-bg-highlight group-data-[selected=true]/row:group-hover/row:bg-ui-bg-highlight-hover group-[:has(td_a:focus)]/row:bg-ui-bg-base-pressed group-hover/row:bg-ui-bg-base-hover transition-fg sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
|
||||
isStickyCell,
|
||||
"after:bg-ui-border-base":
|
||||
showStickyBorder && isStickyCell,
|
||||
@@ -239,7 +246,7 @@ export const DataTableRoot = <TData, TValue>({
|
||||
<CommandBar.Command
|
||||
label={command.label}
|
||||
shortcut={command.shortcut}
|
||||
action={() => command.action(rowSelection)}
|
||||
action={() => handleAction(command.action)}
|
||||
/>
|
||||
{index < commands.length - 1 && <CommandBar.Seperator />}
|
||||
</Fragment>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { DataTableQuery, DataTableQueryProps } from "./data-table-query"
|
||||
import { DataTableRoot, DataTableRootProps } from "./data-table-root"
|
||||
import { DataTableSkeleton } from "./data-table-skeleton"
|
||||
|
||||
interface DataTableProps<TData, TValue>
|
||||
extends DataTableRootProps<TData, TValue>,
|
||||
interface DataTableProps<TData>
|
||||
extends DataTableRootProps<TData>,
|
||||
DataTableQueryProps {
|
||||
isLoading?: boolean
|
||||
rowCount: number
|
||||
@@ -15,7 +15,7 @@ interface DataTableProps<TData, TValue>
|
||||
const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot
|
||||
const MemoizedDataTableQuery = memo(DataTableQuery)
|
||||
|
||||
export const DataTable = <TData, TValue>({
|
||||
export const DataTable = <TData,>({
|
||||
table,
|
||||
columns,
|
||||
pagination,
|
||||
@@ -29,7 +29,7 @@ export const DataTable = <TData, TValue>({
|
||||
queryObject = {},
|
||||
rowCount,
|
||||
isLoading = false,
|
||||
}: DataTableProps<TData, TValue>) => {
|
||||
}: DataTableProps<TData>) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<DataTableSkeleton
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PlaceholderCell } from "../placeholder-cell"
|
||||
|
||||
type EmailCellProps = {
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export const EmailCell = ({ email }: EmailCellProps) => {
|
||||
if (!email) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">{email}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const EmailHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.email")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./email-cell"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./money-amount-cell"
|
||||
@@ -0,0 +1,45 @@
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { PlaceholderCell } from "../placeholder-cell"
|
||||
|
||||
type MoneyAmountCellProps = {
|
||||
currencyCode: string
|
||||
amount?: number | null
|
||||
align?: "left" | "right"
|
||||
}
|
||||
|
||||
export const MoneyAmountCell = ({
|
||||
currencyCode,
|
||||
amount,
|
||||
align = "left",
|
||||
}: MoneyAmountCellProps) => {
|
||||
if (!amount) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
const formatted = new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
currencyDisplay: "narrowSymbol",
|
||||
}).format(0)
|
||||
|
||||
const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim()
|
||||
|
||||
const presentationAmount = getPresentationalAmount(amount, currencyCode)
|
||||
const formattedTotal = new Intl.NumberFormat(undefined, {
|
||||
style: "decimal",
|
||||
}).format(presentationAmount)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clx("flex h-full w-full items-center overflow-hidden", {
|
||||
"justify-start text-left": align === "left",
|
||||
"justify-end text-right": align === "right",
|
||||
})}
|
||||
>
|
||||
<span className="truncate">
|
||||
{symbol} {formattedTotal} {currencyCode.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./name-cell"
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PlaceholderCell } from "../placeholder-cell"
|
||||
|
||||
type NameCellProps = {
|
||||
firstName?: string | null
|
||||
lastName?: string | null
|
||||
}
|
||||
|
||||
export const NameCell = ({ firstName, lastName }: NameCellProps) => {
|
||||
if (!firstName && !lastName) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
const name = [firstName, lastName].filter(Boolean).join(" ")
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">{name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NameHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.name")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./placeholder-cell"
|
||||
@@ -0,0 +1,7 @@
|
||||
export const PlaceholderCell = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="text-ui-fg-muted">-</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { StatusCell } from "../../common/status-cell"
|
||||
|
||||
type AccountCellProps = {
|
||||
hasAccount: boolean
|
||||
}
|
||||
|
||||
export const AccountCell = ({ hasAccount }: AccountCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const color = hasAccount ? "green" : ("orange" as const)
|
||||
const text = hasAccount ? t("customers.registered") : t("customers.guest")
|
||||
|
||||
return <StatusCell color={color}>{text}</StatusCell>
|
||||
}
|
||||
|
||||
export const AccountHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("fields.account")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { DateCell } from "../../common/date-cell"
|
||||
|
||||
type FirstSeenCellProps = {
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export const FirstSeenCell = ({ createdAt }: FirstSeenCellProps) => {
|
||||
return <DateCell date={createdAt} />
|
||||
}
|
||||
|
||||
export const FirstSeenHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span className="truncate">{t("customers.firstSeen")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./first-seen-cell"
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
export const DisplayIdCell = ({ displayId }: { displayId?: number | null }) => {
|
||||
if (!displayId) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
export const DisplayIdCell = ({ displayId }: { displayId: number }) => {
|
||||
return (
|
||||
<div className="text-ui-fg-subtle txt-compact-small flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">#{displayId}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { MoneyAmountCell } from "../../common/money-amount-cell"
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type TotalCellProps = {
|
||||
currencyCode: string
|
||||
@@ -8,28 +9,11 @@ type TotalCellProps = {
|
||||
|
||||
export const TotalCell = ({ currencyCode, total }: TotalCellProps) => {
|
||||
if (!total) {
|
||||
return <span className="text-ui-fg-muted">-</span>
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
const formatted = new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
currencyDisplay: "narrowSymbol",
|
||||
}).format(0)
|
||||
|
||||
const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim()
|
||||
|
||||
const presentationAmount = getPresentationalAmount(total, currencyCode)
|
||||
const formattedTotal = new Intl.NumberFormat(undefined, {
|
||||
style: "decimal",
|
||||
}).format(presentationAmount)
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-end overflow-hidden">
|
||||
<span className="truncate">
|
||||
{symbol} {formattedTotal} {currencyCode.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<MoneyAmountCell currencyCode={currencyCode} amount={total} align="right" />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ProductCollection } from "@medusajs/medusa"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type CollectionCellProps = {
|
||||
collection?: ProductCollection | null
|
||||
}
|
||||
|
||||
export const CollectionCell = ({ collection }: CollectionCellProps) => {
|
||||
if (!collection) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">{collection.title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CollectionHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span>{t("fields.collection")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-cell"
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./product-cell"
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Product } from "@medusajs/medusa"
|
||||
import type { PricedProduct } from "@medusajs/medusa/dist/types/pricing"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { Thumbnail } from "../../../../common/thumbnail"
|
||||
|
||||
type ProductCellProps = {
|
||||
product: Product | PricedProduct
|
||||
}
|
||||
|
||||
export const ProductCell = ({ product }: ProductCellProps) => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||
<div className="w-fit flex-shrink-0">
|
||||
<Thumbnail src={product.thumbnail} />
|
||||
</div>
|
||||
<span className="truncate">{product.title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ProductHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span>{t("fields.product")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./product-status-cell"
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ProductStatus } from "@medusajs/types"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { StatusCell } from "../../common/status-cell"
|
||||
|
||||
type ProductStatusCellProps = {
|
||||
status: ProductStatus
|
||||
}
|
||||
|
||||
export const ProductStatusCell = ({ status }: ProductStatusCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [color, text] = {
|
||||
draft: ["grey", t("products.productStatus.draft")],
|
||||
proposed: ["orange", t("products.productStatus.proposed")],
|
||||
published: ["green", t("products.productStatus.published")],
|
||||
rejected: ["red", t("products.productStatus.rejected")],
|
||||
}[status] as ["grey" | "orange" | "green" | "red", string]
|
||||
|
||||
return <StatusCell color={color}>{text}</StatusCell>
|
||||
}
|
||||
|
||||
export const ProductStatusHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span>{t("fields.status")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./sales-channels-cell"
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { SalesChannel } from "@medusajs/medusa"
|
||||
import { Tooltip } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type SalesChannelsCellProps = {
|
||||
salesChannels?: SalesChannel[] | null
|
||||
}
|
||||
|
||||
export const SalesChannelsCell = ({
|
||||
salesChannels,
|
||||
}: SalesChannelsCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!salesChannels || !salesChannels.length) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
if (salesChannels.length > 2) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-x-1 overflow-hidden">
|
||||
<span className="truncate">
|
||||
{salesChannels
|
||||
.slice(0, 2)
|
||||
.map((sc) => sc.name)
|
||||
.join(", ")}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{salesChannels.slice(2).map((sc) => (
|
||||
<li key={sc.id}>{sc.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">
|
||||
{t("general.plusCountMore", {
|
||||
count: salesChannels.length - 2,
|
||||
})}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">
|
||||
{salesChannels.map((sc) => sc.name).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SalesChannelHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span>{t("fields.salesChannels")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./variant-cell"
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ProductVariant } from "@medusajs/medusa"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { PlaceholderCell } from "../../common/placeholder-cell"
|
||||
|
||||
type VariantCellProps = {
|
||||
variants?: ProductVariant[] | null
|
||||
}
|
||||
|
||||
export const VariantCell = ({ variants }: VariantCellProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!variants || !variants.length) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center overflow-hidden">
|
||||
<span className="truncate">
|
||||
{t("products.variantCount", { count: variants.length })}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const VariantHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center">
|
||||
<span>{t("fields.variants")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user