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
@@ -3,16 +3,7 @@
|
||||
"general": {
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"invite": "Invite",
|
||||
"edit": "Edit",
|
||||
"confirm": "Confirm",
|
||||
"add": "Add",
|
||||
"continue": "Continue",
|
||||
"start": "Start",
|
||||
"end": "End",
|
||||
"apply": "Apply",
|
||||
@@ -30,10 +21,9 @@
|
||||
"details": "Details",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"expired": "Expired",
|
||||
"active": "Active",
|
||||
"revoke": "Revoke",
|
||||
"revoked": "Revoked",
|
||||
"remove": "Remove",
|
||||
"admin": "Admin",
|
||||
"store": "Store",
|
||||
"items_one": "{{count}} item",
|
||||
@@ -50,6 +40,16 @@
|
||||
"unsavedChangesTitle": "Are you sure you want to leave this page?",
|
||||
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page."
|
||||
},
|
||||
"actions": {
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"revoke": "Revoke",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"continue": "Continue",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"products": {
|
||||
"domain": "Products",
|
||||
"variants": "Variants",
|
||||
@@ -69,8 +69,7 @@
|
||||
"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.",
|
||||
"deleteWarning": "You are about to delete the collection {{title}}. 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."
|
||||
@@ -82,7 +81,22 @@
|
||||
"domain": "Inventory"
|
||||
},
|
||||
"giftCards": {
|
||||
"domain": "Gift Cards"
|
||||
"domain": "Gift Cards",
|
||||
"editGiftCard": "Edit Gift Card",
|
||||
"createGiftCard": "Create Gift Card",
|
||||
"createGiftCardHint": "Manually create a gift card that can be used as a payment method in your store.",
|
||||
"selectRegionFirst": "Select a region first",
|
||||
"deleteGiftCardWarning": "You are about to delete the gift card {{code}}. This action cannot be undone.",
|
||||
"balanceHigherThanValue": "The balance cannot be higher than the original amount.",
|
||||
"balanceLowerThanZero": "The balance cannot be negative.",
|
||||
"expiryDateHint": "Countries have different laws regarding gift card expiry dates. Make sure to check local regulations before setting an expiry date.",
|
||||
"regionHint": "Changing the region of the gift card will also change its currency, potentially affecting its monetary value.",
|
||||
"enabledHint": "Specify if the gift card is enabled or disabled.",
|
||||
"balance": "Balance",
|
||||
"currentBalance": "Current balance",
|
||||
"initialBalance": "Initial balance",
|
||||
"personalMessage": "Personal message",
|
||||
"recipient": "Recipient"
|
||||
},
|
||||
"customers": {
|
||||
"domain": "Customers",
|
||||
@@ -96,7 +110,8 @@
|
||||
"guest": "Guest",
|
||||
"registered": "Registered",
|
||||
"firstSeen": "First seen",
|
||||
"viewOrder": "View order"
|
||||
"viewOrder": "View order",
|
||||
"groups": "Groups"
|
||||
},
|
||||
"customerGroups": {
|
||||
"domain": "Customer Groups",
|
||||
@@ -174,7 +189,9 @@
|
||||
"admin": "Admin",
|
||||
"developer": "Developer",
|
||||
"member": "Member"
|
||||
}
|
||||
},
|
||||
"deleteUserWarning": "You are about to delete the user {{name}}. This action cannot be undone.",
|
||||
"invite": "Invite"
|
||||
},
|
||||
"store": {
|
||||
"domain": "Store",
|
||||
@@ -337,6 +354,17 @@
|
||||
"salesChannel": "Sales Channel",
|
||||
"region": "Region",
|
||||
"role": "Role",
|
||||
"sent": "Sent"
|
||||
"sent": "Sent",
|
||||
"salesChannels": "Sales Channels",
|
||||
"product": "Product",
|
||||
"createdAt": "Created at",
|
||||
"updatedAt": "Updated at",
|
||||
"true": "True",
|
||||
"false": "False",
|
||||
"giftCard": "Gift Card",
|
||||
"tag": "Tag",
|
||||
"dateIssued": "Date issued",
|
||||
"issuedDate": "Issued date",
|
||||
"expiryDate": "Expiry date"
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => {
|
||||
<Container className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Heading level="h2">JSON</Heading>
|
||||
<Badge>{numberOfKeys} keys</Badge>
|
||||
<Badge size="2xsmall">{numberOfKeys} keys</Badge>
|
||||
</div>
|
||||
<Drawer>
|
||||
<Drawer.Trigger asChild>
|
||||
@@ -44,7 +44,7 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => {
|
||||
<div className="bg-ui-code-bg-header border-ui-code-border flex items-center justify-between border-b px-6 py-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Heading>JSON</Heading>
|
||||
<Badge>{numberOfKeys} keys</Badge>
|
||||
<Badge size="2xsmall">{numberOfKeys} keys</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Kbd>esc</Kbd>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const LocalizedDatePicker = ({
|
||||
?.date_locale
|
||||
|
||||
const translations = {
|
||||
cancel: t("general.cancel"),
|
||||
cancel: t("actions.cancel"),
|
||||
apply: t("general.apply"),
|
||||
end: t("general.end"),
|
||||
start: t("general.start"),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
EmailCell,
|
||||
EmailHeader,
|
||||
} from "../../../components/table/table-cells/common/email-cell"
|
||||
import {
|
||||
NameCell,
|
||||
NameHeader,
|
||||
} from "../../../components/table/table-cells/common/name-cell"
|
||||
import {
|
||||
AccountCell,
|
||||
AccountHeader,
|
||||
} from "../../../components/table/table-cells/customer/account-cell/account-cell"
|
||||
import {
|
||||
FirstSeenCell,
|
||||
FirstSeenHeader,
|
||||
} from "../../../components/table/table-cells/customer/first-seen-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<Customer>()
|
||||
|
||||
export const useCustomerTableColumns = () => {
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("email", {
|
||||
header: () => <EmailHeader />,
|
||||
cell: ({ getValue }) => <EmailCell email={getValue()} />,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "name",
|
||||
header: () => <NameHeader />,
|
||||
cell: ({
|
||||
row: {
|
||||
original: { first_name, last_name },
|
||||
},
|
||||
}) => <NameCell firstName={first_name} lastName={last_name} />,
|
||||
}),
|
||||
columnHelper.accessor("has_account", {
|
||||
header: () => <AccountHeader />,
|
||||
cell: ({ getValue }) => <AccountCell hasAccount={getValue()} />,
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: () => <FirstSeenHeader />,
|
||||
cell: ({ getValue }) => <FirstSeenCell createdAt={getValue()} />,
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
CollectionCell,
|
||||
CollectionHeader,
|
||||
} from "../../../components/table/table-cells/product/collection-cell/collection-cell"
|
||||
import {
|
||||
ProductCell,
|
||||
ProductHeader,
|
||||
} from "../../../components/table/table-cells/product/product-cell"
|
||||
import {
|
||||
ProductStatusCell,
|
||||
ProductStatusHeader,
|
||||
} from "../../../components/table/table-cells/product/product-status-cell"
|
||||
import {
|
||||
SalesChannelHeader,
|
||||
SalesChannelsCell,
|
||||
} from "../../../components/table/table-cells/product/sales-channels-cell"
|
||||
import {
|
||||
VariantCell,
|
||||
VariantHeader,
|
||||
} from "../../../components/table/table-cells/product/variant-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
export const useProductTableColumns = () => {
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "product",
|
||||
header: () => <ProductHeader />,
|
||||
cell: ({ row }) => <ProductCell product={row.original} />,
|
||||
}),
|
||||
columnHelper.accessor("collection", {
|
||||
header: () => <CollectionHeader />,
|
||||
cell: ({ row }) => (
|
||||
<CollectionCell collection={row.original.collection} />
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("sales_channels", {
|
||||
header: () => <SalesChannelHeader />,
|
||||
cell: ({ row }) => (
|
||||
<SalesChannelsCell salesChannels={row.original.sales_channels} />
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("variants", {
|
||||
header: () => <VariantHeader />,
|
||||
cell: ({ row }) => <VariantCell variants={row.original.variants} />,
|
||||
}),
|
||||
columnHelper.accessor("status", {
|
||||
header: () => <ProductStatusHeader />,
|
||||
cell: ({ row }) => <ProductStatusCell status={row.original.status} />,
|
||||
}),
|
||||
],
|
||||
[]
|
||||
) as ColumnDef<Product>[]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useAdminCustomerGroups } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../components/table/data-table"
|
||||
|
||||
const excludeableFields = ["groups"] as const
|
||||
|
||||
export const useCustomerTableFilters = (
|
||||
exclude?: (typeof excludeableFields)[number][]
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isGroupsExcluded = exclude?.includes("groups")
|
||||
|
||||
const { customer_groups } = useAdminCustomerGroups(
|
||||
{
|
||||
limit: 1000,
|
||||
expand: "",
|
||||
},
|
||||
{
|
||||
enabled: !isGroupsExcluded,
|
||||
}
|
||||
)
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
if (customer_groups && !isGroupsExcluded) {
|
||||
const customerGroupFilter: Filter = {
|
||||
key: "groups",
|
||||
label: t("customers.groups"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: customer_groups.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, customerGroupFilter]
|
||||
}
|
||||
|
||||
const hasAccountFilter: Filter = {
|
||||
key: "has_account",
|
||||
label: t("fields.account"),
|
||||
type: "select",
|
||||
options: [
|
||||
{
|
||||
label: t("customers.registered"),
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
label: t("customers.guest"),
|
||||
value: "false",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, hasAccountFilter, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
useAdminCollections,
|
||||
useAdminProductCategories,
|
||||
useAdminProductTags,
|
||||
useAdminProductTypes,
|
||||
useAdminSalesChannels,
|
||||
} from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../components/table/data-table"
|
||||
|
||||
const excludeableFields = ["sales_channel_id", "collections"] as const
|
||||
|
||||
export const useProductTableFilters = (
|
||||
exclude?: (typeof excludeableFields)[number][]
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product_types } = useAdminProductTypes({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { product_tags } = useAdminProductTags({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const isSalesChannelExcluded = exclude?.includes("sales_channel_id")
|
||||
|
||||
const { sales_channels } = useAdminSalesChannels(
|
||||
{
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
expand: "",
|
||||
},
|
||||
{
|
||||
enabled: !isSalesChannelExcluded,
|
||||
}
|
||||
)
|
||||
|
||||
const { product_categories } = useAdminProductCategories({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
fields: "id,name",
|
||||
expand: "",
|
||||
})
|
||||
|
||||
const isCollectionExcluded = exclude?.includes("collections")
|
||||
|
||||
const { collections } = useAdminCollections(
|
||||
{
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
enabled: !isCollectionExcluded,
|
||||
}
|
||||
)
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
if (product_types) {
|
||||
const typeFilter: Filter = {
|
||||
key: "type_id",
|
||||
label: t("fields.type"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: product_types.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, typeFilter]
|
||||
}
|
||||
|
||||
if (product_tags) {
|
||||
const tagFilter: Filter = {
|
||||
key: "tags",
|
||||
label: t("fields.tag"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: product_tags.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, tagFilter]
|
||||
}
|
||||
|
||||
if (sales_channels) {
|
||||
const salesChannelFilter: Filter = {
|
||||
key: "sales_channel_id",
|
||||
label: t("fields.salesChannel"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: sales_channels.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, salesChannelFilter]
|
||||
}
|
||||
|
||||
if (product_categories) {
|
||||
const categoryFilter: Filter = {
|
||||
key: "category_id",
|
||||
label: t("fields.category"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: product_categories.map((c) => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, categoryFilter]
|
||||
}
|
||||
|
||||
if (collections) {
|
||||
const collectionFilter: Filter = {
|
||||
key: "collection_id",
|
||||
label: t("fields.collection"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: collections.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id,
|
||||
})),
|
||||
}
|
||||
|
||||
filters = [...filters, collectionFilter]
|
||||
}
|
||||
|
||||
const giftCardFilter: Filter = {
|
||||
key: "is_giftcard",
|
||||
label: t("fields.giftCard"),
|
||||
type: "select",
|
||||
options: [
|
||||
{
|
||||
label: t("fields.true"),
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
label: t("fields.false"),
|
||||
value: "false",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const statusFilter: Filter = {
|
||||
key: "status",
|
||||
label: t("fields.status"),
|
||||
type: "select",
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
label: t("products.productStatus.draft"),
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.proposed"),
|
||||
value: "proposed",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.published"),
|
||||
value: "published",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.rejected"),
|
||||
value: "rejected",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, statusFilter, giftCardFilter, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AdminGetCustomersParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseCustomerTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useCustomerTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseCustomerTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"q",
|
||||
"has_account",
|
||||
"groups",
|
||||
"order",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, groups, has_account, q, order } = queryObject
|
||||
|
||||
const searchParams: AdminGetCustomersParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
groups: groups?.split(","),
|
||||
has_account: has_account ? has_account === "true" : undefined,
|
||||
order,
|
||||
created_at: queryObject.created_at
|
||||
? JSON.parse(queryObject.created_at)
|
||||
: undefined,
|
||||
updated_at: queryObject.updated_at
|
||||
? JSON.parse(queryObject.updated_at)
|
||||
: undefined,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,9 @@ type UseOrderTableQueryProps = {
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Enable `order` query param when staging is updated
|
||||
*/
|
||||
|
||||
export const useOrderTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 50,
|
||||
pageSize = 20,
|
||||
}: UseOrderTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
[
|
||||
@@ -24,6 +20,7 @@ export const useOrderTableQuery = ({
|
||||
"sales_channel_id",
|
||||
"payment_status",
|
||||
"fulfillment_status",
|
||||
"order",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { AdminGetProductsParams } from "@medusajs/medusa"
|
||||
import { ProductStatus } from "@medusajs/types"
|
||||
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseProductTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useProductTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseProductTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
[
|
||||
"offset",
|
||||
"order",
|
||||
"q",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"sales_channel_id",
|
||||
"category_id",
|
||||
"collection_id",
|
||||
"is_giftcard",
|
||||
"tags",
|
||||
"type_id",
|
||||
"status",
|
||||
],
|
||||
prefix
|
||||
)
|
||||
|
||||
const {
|
||||
offset,
|
||||
sales_channel_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
category_id,
|
||||
collection_id,
|
||||
tags,
|
||||
type_id,
|
||||
is_giftcard,
|
||||
status,
|
||||
order,
|
||||
q,
|
||||
} = queryObject
|
||||
|
||||
const searchParams: AdminGetProductsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
sales_channel_id: sales_channel_id?.split(","),
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
category_id: category_id?.split(","),
|
||||
collection_id: collection_id?.split(","),
|
||||
is_giftcard: is_giftcard ? is_giftcard === "true" : undefined,
|
||||
order: order,
|
||||
tags: tags?.split(","),
|
||||
type_id: type_id?.split(","),
|
||||
status: status?.split(",") as ProductStatus[],
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -10,27 +10,29 @@ import {
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
type UseDataTableProps<TData, TValue> = {
|
||||
type UseDataTableProps<TData> = {
|
||||
data?: TData[]
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
columns: ColumnDef<TData, any>[]
|
||||
count?: number
|
||||
pageSize?: number
|
||||
enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
|
||||
enablePagination?: boolean
|
||||
getRowId?: (original: TData, index: number) => string
|
||||
meta?: Record<string, unknown>
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export const useDataTable = <TData, TValue>({
|
||||
export const useDataTable = <TData,>({
|
||||
data = [],
|
||||
columns,
|
||||
count = 0,
|
||||
pageSize: _pageSize = 50,
|
||||
pageSize: _pageSize = 20,
|
||||
enablePagination = true,
|
||||
enableRowSelection = false,
|
||||
getRowId,
|
||||
meta,
|
||||
prefix,
|
||||
}: UseDataTableProps<TData, TValue>) => {
|
||||
}: UseDataTableProps<TData>) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const offsetKey = `${prefix ? `${prefix}_` : ""}offset`
|
||||
const offset = searchParams.get(offsetKey)
|
||||
@@ -106,6 +108,7 @@ export const useDataTable = <TData, TValue>({
|
||||
? getPaginationRowModel()
|
||||
: undefined,
|
||||
manualPagination: enablePagination ? true : undefined,
|
||||
meta,
|
||||
})
|
||||
|
||||
return { table }
|
||||
|
||||
@@ -8,8 +8,8 @@ export const useFormPrompt = () => {
|
||||
const promptValues = {
|
||||
title: t("general.unsavedChangesTitle"),
|
||||
description: t("general.unsavedChangesDescription"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("general.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
confirmText: t("actions.continue"),
|
||||
}
|
||||
|
||||
const prompt = async () => {
|
||||
|
||||
@@ -3,13 +3,6 @@ import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
type Prompt = {
|
||||
title: string
|
||||
description: string
|
||||
cancelText: string
|
||||
confirmText: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing the state of route modals.
|
||||
*/
|
||||
@@ -30,11 +23,12 @@ export const useRouteModalState = (): [
|
||||
const prompt = usePrompt()
|
||||
const { t } = useTranslation()
|
||||
|
||||
let promptValues: Prompt = {
|
||||
const promptValues = {
|
||||
title: t("general.unsavedChangesTitle"),
|
||||
description: t("general.unsavedChangesDescription"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("general.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
confirmText: t("actions.continue"),
|
||||
variant: "confirmation" as const,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,22 +6,27 @@ import { initReactI18next } from "react-i18next"
|
||||
|
||||
import { Language } from "./types"
|
||||
|
||||
i18n
|
||||
void i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init<HttpBackendOptions>({
|
||||
fallbackLng: "en",
|
||||
fallbackLng: "en-US",
|
||||
load: "languageOnly",
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
backend: {
|
||||
// for all available options read the backend's repository readme file
|
||||
loadPath: "/locales/{{lng}}/{{ns}}.json",
|
||||
},
|
||||
})
|
||||
|
||||
export const languages: Language[] = [
|
||||
{
|
||||
code: "en",
|
||||
display_name: "English",
|
||||
code: "en-US",
|
||||
display_name: "English (US)",
|
||||
ltr: true,
|
||||
date_locale: enUS,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Locale } from "date-fns"
|
||||
import en from "../../public/locales/en/translation.json"
|
||||
import en from "../../public/locales/en-US/translation.json"
|
||||
|
||||
const resources = {
|
||||
translation: en,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Resources } from "./i18n/types";
|
||||
import { Resources } from "./i18n/types"
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
resources: Resources;
|
||||
resources: Resources
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AdminCollectionsRes,
|
||||
AdminCustomerGroupsRes,
|
||||
AdminCustomersRes,
|
||||
AdminGiftCardsRes,
|
||||
AdminProductsRes,
|
||||
AdminPublishableApiKeysRes,
|
||||
AdminRegionsRes,
|
||||
@@ -273,12 +274,29 @@ const router = createBrowserRouter([
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: () => import("../../routes/gift-cards/list"),
|
||||
path: "",
|
||||
lazy: () => import("../../routes/gift-cards/gift-card-list"),
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
lazy: () =>
|
||||
import("../../routes/gift-cards/gift-card-create"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
lazy: () => import("../../routes/gift-cards/details"),
|
||||
lazy: () => import("../../routes/gift-cards/gift-card-detail"),
|
||||
handle: {
|
||||
crumb: (data: AdminGiftCardsRes) => data.gift_card.code,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "edit",
|
||||
lazy: () =>
|
||||
import("../../routes/gift-cards/gift-card-edit"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -156,16 +156,16 @@ export const AddSalesChannelsToApiKeyForm = ({
|
||||
)}
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||
<div className="flex items-center justify-between w-full px-6 py-4">
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
|
||||
@@ -57,14 +57,14 @@ export const CreatePublishableApiKeyForm = ({
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center gap-x-2 justify-end">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
|
||||
@@ -39,8 +39,8 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -57,7 +57,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("apiKeyManagement.revoke"),
|
||||
cancelText: t("general.cancel"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -70,7 +70,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
const dangerousActions = [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
]
|
||||
@@ -84,8 +84,8 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{apiKey.title}</Heading>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={apiKey.revoked_at ? "red" : "green"}>
|
||||
@@ -96,7 +96,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
icon: <PencilSquare />,
|
||||
to: `/settings/api-key-management/${apiKey.id}/edit`,
|
||||
},
|
||||
@@ -109,11 +109,11 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.key")}
|
||||
</Text>
|
||||
<div className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden">
|
||||
<div className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1">
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
{apiKey.id}
|
||||
</Text>
|
||||
@@ -124,14 +124,14 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("apiKeyManagement.createdBy")}
|
||||
</Text>
|
||||
<ActionBy userId={apiKey.created_by} />
|
||||
</div>
|
||||
{apiKey.revoked_at && (
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("apiKeyManagement.revokedBy")}
|
||||
</Text>
|
||||
@@ -162,8 +162,8 @@ const ActionBy = ({ userId }: { userId: string | null }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-[20px_1fr]">
|
||||
<Skeleton className="w-5 h-5 rounded-full" />
|
||||
<Skeleton className="max-w-[220px] w-full" />
|
||||
<Skeleton className="h-5 w-5 rounded-full" />
|
||||
<Skeleton className="w-full max-w-[220px]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,8 +109,8 @@ export const ApiKeySalesChannelSection = ({
|
||||
description: t("apiKeyManagement.removeSalesChannelsWarning", {
|
||||
count: keys.length,
|
||||
}),
|
||||
confirmText: t("general.continue"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -136,15 +136,15 @@ export const ApiKeySalesChannelSection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("salesChannels.domain")}</Heading>
|
||||
<Button variant="secondary" size="small" asChild>
|
||||
<Link to="add-sales-channels">{t("general.add")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{!noRecords && (
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
@@ -162,7 +162,7 @@ export const ApiKeySalesChannelSection = ({
|
||||
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/3"
|
||||
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
@@ -229,7 +229,7 @@ export const ApiKeySalesChannelSection = ({
|
||||
<CommandBar.Command
|
||||
action={handleRemove}
|
||||
shortcut="r"
|
||||
label={t("general.remove")}
|
||||
label={t("actions.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
@@ -255,8 +255,8 @@ const SalesChannelActions = ({
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("apiKeyManagement.removeSalesChannelWarning"),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -275,7 +275,7 @@ const SalesChannelActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -284,7 +284,7 @@ const SalesChannelActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -76,11 +76,11 @@ export const EditApiKeyForm = ({
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
|
||||
@@ -161,7 +161,7 @@ const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit">
|
||||
|
||||
@@ -82,12 +82,12 @@ export const ApiKeyManagementListTable = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("apiKeyManagement.domain")}</Heading>
|
||||
<Link to="create">
|
||||
<Button variant="secondary" size="small">
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -186,8 +186,8 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -204,7 +204,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
title: apiKey.title,
|
||||
}),
|
||||
confirmText: t("apiKeyManagement.revoke"),
|
||||
cancelText: t("general.cancel"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -221,7 +221,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/api-key-management/${apiKey.id}`,
|
||||
},
|
||||
],
|
||||
@@ -235,7 +235,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
@@ -263,7 +263,7 @@ const useColumns = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit max-w-[220px] rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden"
|
||||
className="bg-ui-bg-subtle border-ui-border-base box-border flex w-fit max-w-[220px] cursor-default items-center gap-x-0.5 overflow-hidden rounded-full border pl-2 pr-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Text size="xsmall" leading="compact" className="truncate">
|
||||
|
||||
@@ -183,17 +183,17 @@ export const AddProductsToCollectionForm = ({
|
||||
)}
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isMutating}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
|
||||
{!noRecords && (
|
||||
<div className="flex items-center justify-between w-full px-6 py-4">
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
@@ -209,7 +209,7 @@ export const AddProductsToCollectionForm = ({
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{!isLoading && !products?.length ? (
|
||||
<div className="flex-1 flex items-center justify-center h-full">
|
||||
<div className="flex h-full flex-1 items-center justify-center">
|
||||
<NoResults />
|
||||
</div>
|
||||
) : (
|
||||
@@ -286,7 +286,7 @@ export const AddProductsToCollectionForm = ({
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<NoRecords />
|
||||
{/* TODO: fix this, and add NoRecords as well */}
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CreateCollectionForm = ({
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button
|
||||
@@ -65,12 +65,12 @@ export const CreateCollectionForm = ({
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("general.create")}
|
||||
{t("actions.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 className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("collections.createCollection")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
@@ -107,7 +107,7 @@ export const CreateCollectionForm = ({
|
||||
</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">
|
||||
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
|
||||
@@ -33,7 +33,7 @@ export const CollectionGeneralSection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{collection.title}</Heading>
|
||||
<ActionMenu
|
||||
@@ -42,7 +42,7 @@ export const CollectionGeneralSection = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/collections/${collection.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -51,7 +51,7 @@ export const CollectionGeneralSection = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
@@ -59,18 +59,12 @@ export const CollectionGeneralSection = ({
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,47 +1,21 @@
|
||||
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 { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminProducts,
|
||||
useAdminRemoveProductsFromCollection,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { Link } 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 { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
|
||||
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
|
||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { queryClient } from "../../../../../lib/medusa"
|
||||
|
||||
type CollectionProductSectionProps = {
|
||||
@@ -54,51 +28,30 @@ 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 { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
|
||||
const { products, count, isLoading, isError, error } = useAdminProducts(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...searchParams,
|
||||
collection_id: [collection.id],
|
||||
...params,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const filters = useProductTableFilters(["collections"])
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
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,
|
||||
count,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
meta: {
|
||||
collectionId: collection.id,
|
||||
},
|
||||
@@ -107,16 +60,16 @@ export const CollectionProductSection = ({
|
||||
const prompt = usePrompt()
|
||||
const { mutateAsync } = useAdminRemoveProductsFromCollection(collection.id)
|
||||
|
||||
const handleRemove = async () => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
const handleRemove = async (selection: Record<string, boolean>) => {
|
||||
const ids = Object.keys(selection)
|
||||
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.removeProductsWarning", {
|
||||
count: ids.length,
|
||||
}),
|
||||
confirmText: t("general.confirm"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -130,23 +83,17 @@ export const CollectionProductSection = ({
|
||||
{
|
||||
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">
|
||||
<Container className="divide-y p-0">
|
||||
<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`}>
|
||||
@@ -155,99 +102,26 @@ export const CollectionProductSection = ({
|
||||
</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>
|
||||
)}
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
search
|
||||
pagination
|
||||
rowCount={PAGE_SIZE}
|
||||
navigateTo={({ original }) => `/products/${original.id}`}
|
||||
count={count}
|
||||
filters={filters}
|
||||
isLoading={isLoading}
|
||||
orderBy={["title", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
commands={[
|
||||
{
|
||||
action: handleRemove,
|
||||
label: t("actions.remove"),
|
||||
shortcut: "r",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -269,8 +143,8 @@ const ProductActions = ({
|
||||
description: t("collections.removeSingleProductWarning", {
|
||||
title: product.title,
|
||||
}),
|
||||
confirmText: t("general.confirm"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -289,7 +163,7 @@ const ProductActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/products/${product.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -298,7 +172,7 @@ const ProductActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.remove"),
|
||||
label: t("actions.remove"),
|
||||
onClick: handleRemove,
|
||||
},
|
||||
],
|
||||
@@ -311,7 +185,7 @@ const ProductActions = ({
|
||||
const columnHelper = createColumnHelper<Product>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const columns = useProductTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -343,44 +217,7 @@ const useColumns = () => {
|
||||
)
|
||||
},
|
||||
}),
|
||||
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} />
|
||||
},
|
||||
}),
|
||||
...columns,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
@@ -397,6 +234,6 @@ const useColumns = () => {
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
[columns]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export const EditCollectionForm = ({
|
||||
</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">
|
||||
<div className="absolute inset-y-0 left-0 z-10 flex w-8 items-center justify-center border-r">
|
||||
<Text
|
||||
className="text-ui-fg-muted"
|
||||
size="small"
|
||||
@@ -107,11 +107,11 @@ export const EditCollectionForm = ({
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
|
||||
@@ -1,239 +1,66 @@
|
||||
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 { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminCollections } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { Link } 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"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useCollectionTableColumns } from "./use-collection-table-columns"
|
||||
import { useCollectionTableFilters } from "./use-collection-table-filters"
|
||||
import { useCollectionTableQuery } from "./use-collection-table-query"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
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 { searchParams, raw } = useCollectionTableQuery({ pageSize: PAGE_SIZE })
|
||||
const { collections, count, isError, error, isLoading } = useAdminCollections(
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
const filters = useCollectionTableFilters()
|
||||
const columns = useCollectionTableColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
data: collections ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
(!collections || collections.length === 0) &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<Container className="divide-y p-0">
|
||||
<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")}
|
||||
{t("actions.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>
|
||||
)}
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
rowCount={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
orderBy={["title", "handle", "created_at", "updated_at"]}
|
||||
search
|
||||
navigateTo={(row) => `/collections/${row.original.id}`}
|
||||
queryObject={raw}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</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,94 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { ProductCollection } from "@medusajs/medusa"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useAdminDeleteCollection } from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductCollection>()
|
||||
|
||||
export const useCollectionTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("title", {
|
||||
header: t("fields.title"),
|
||||
}),
|
||||
columnHelper.accessor("handle", {
|
||||
header: t("fields.handle"),
|
||||
cell: ({ getValue }) => `/${getValue()}`,
|
||||
}),
|
||||
columnHelper.accessor("products", {
|
||||
header: t("fields.products"),
|
||||
cell: ({ getValue }) => {
|
||||
const count = getValue()?.length
|
||||
|
||||
return <span>{count || "-"}</span>
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <CollectionActions collection={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const CollectionActions = ({
|
||||
collection,
|
||||
}: {
|
||||
collection: ProductCollection
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteCollection(collection.id)
|
||||
|
||||
const handleDeleteCollection = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("collections.deleteWarning", {
|
||||
title: collection.title,
|
||||
}),
|
||||
verificationText: collection.title,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `/collections/${collection.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDeleteCollection,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useCollectionTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AdminGetCollectionsParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
type UseCollectionTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useCollectionTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseCollectionTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, q, order } = queryObject
|
||||
|
||||
const searchParams: AdminGetCollectionsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ export const AddCustomersForm = ({
|
||||
)}
|
||||
<FocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button
|
||||
@@ -174,14 +174,14 @@ export const AddCustomersForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
|
||||
{noRecords ? (
|
||||
<div className="w-full flex-1 flex items-center justify-center">
|
||||
<div className="flex w-full flex-1 items-center justify-center">
|
||||
<NoRecords />
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y w-full flex-1 flex flex-col">
|
||||
<div className="flex items-center justify-between w-full px-6 py-4">
|
||||
<div className="flex w-full flex-1 flex-col divide-y">
|
||||
<div className="flex w-full items-center justify-between px-6 py-4">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
@@ -242,7 +242,7 @@ export const AddCustomersForm = ({
|
||||
</Table.Body>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center min-h-full">
|
||||
<div className="flex min-h-full flex-1 items-center justify-center">
|
||||
<NoResults />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,10 +56,10 @@ export const CreateCustomerGroupForm = ({
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FocusModal.Header>
|
||||
<div className="flex items-center gap-x-2 justify-end">
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button
|
||||
@@ -68,12 +68,12 @@ export const CreateCustomerGroupForm = ({
|
||||
size="small"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</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">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("customerGroups.createCustomerGroup")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { Customer, CustomerGroup } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
CommandBar,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import {
|
||||
useAdminCustomerGroupCustomers,
|
||||
useAdminRemoveCustomersFromCustomerGroup,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
import { Link } from "react-router-dom"
|
||||
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"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
||||
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
||||
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
type CustomerGroupCustomerSectionProps = {
|
||||
group: CustomerGroup
|
||||
@@ -44,53 +25,31 @@ const PAGE_SIZE = 10
|
||||
export const CustomerGroupCustomerSection = ({
|
||||
group,
|
||||
}: CustomerGroupCustomerSectionProps) => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
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"])
|
||||
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
|
||||
const { customers, count, isLoading, isError, error } =
|
||||
useAdminCustomerGroupCustomers(
|
||||
group.id,
|
||||
{
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const filters = useCustomerTableFilters(["groups"])
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
data: customers ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
count,
|
||||
getRowId: (row) => row.id,
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
pageSize: PAGE_SIZE,
|
||||
meta: {
|
||||
customerGroupId: group.id,
|
||||
},
|
||||
@@ -99,46 +58,34 @@ export const CustomerGroupCustomerSection = ({
|
||||
const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id)
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleRemoveCustomers = async () => {
|
||||
const selected = Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||
const handleRemoveCustomers = async (selection: Record<string, boolean>) => {
|
||||
const selected = Object.keys(selection).filter((k) => selection[k])
|
||||
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("customerGroups.removeCustomersWarning", {
|
||||
count: selected.length,
|
||||
}),
|
||||
confirmText: t("general.continue"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
customer_ids: selected.map((s) => ({ id: s })),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRowSelection({})
|
||||
},
|
||||
}
|
||||
)
|
||||
await mutateAsync({
|
||||
customer_ids: selected.map((s) => ({ id: s })),
|
||||
})
|
||||
}
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
!customers?.length &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("customers.domain")}</Heading>
|
||||
<Link to={`/customer-groups/${group.id}/add-customers`}>
|
||||
<Button variant="secondary" size="small">
|
||||
@@ -146,102 +93,33 @@ export const CustomerGroupCustomerSection = ({
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{noRecords ? (
|
||||
<NoRecords />
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{(customers?.length || 0) > 0 ? (
|
||||
<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"
|
||||
>
|
||||
{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(`/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>
|
||||
) : (
|
||||
<div className="border-b">
|
||||
<NoResults />
|
||||
</div>
|
||||
)}
|
||||
<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={handleRemoveCustomers}
|
||||
shortcut="r"
|
||||
label={t("general.remove")}
|
||||
/>
|
||||
</CommandBar.Bar>
|
||||
</CommandBar>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
rowCount={PAGE_SIZE}
|
||||
isLoading={isLoading}
|
||||
count={count}
|
||||
navigateTo={(row) => `/customers/${row.id}`}
|
||||
filters={filters}
|
||||
search
|
||||
pagination
|
||||
orderBy={[
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"has_account",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]}
|
||||
commands={[
|
||||
{
|
||||
action: handleRemoveCustomers,
|
||||
label: t("actions.remove"),
|
||||
shortcut: "r",
|
||||
},
|
||||
]}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -265,8 +143,8 @@ const CustomerActions = ({
|
||||
description: t("customerGroups.removeCustomersWarning", {
|
||||
count: 1,
|
||||
}),
|
||||
confirmText: t("general.continue"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.continue"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -285,7 +163,7 @@ const CustomerActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/customers/${customer.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -294,7 +172,7 @@ const CustomerActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.remove"),
|
||||
label: t("actions.remove"),
|
||||
onClick: handleRemove,
|
||||
},
|
||||
],
|
||||
@@ -307,7 +185,7 @@ const CustomerActions = ({
|
||||
const columnHelper = createColumnHelper<Customer>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const columns = useCustomerTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -339,33 +217,7 @@ const useColumns = () => {
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("email", {
|
||||
header: t("fields.email"),
|
||||
cell: ({ getValue }) => <span>{getValue()}</span>,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "name",
|
||||
header: t("fields.name"),
|
||||
cell: ({ row }) => {
|
||||
const name = [row.original.first_name, row.original.last_name]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return name || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("has_account", {
|
||||
header: t("fields.account"),
|
||||
cell: ({ getValue }) => {
|
||||
const hasAccount = getValue()
|
||||
|
||||
return (
|
||||
<StatusBadge color={hasAccount ? "green" : "blue"}>
|
||||
{hasAccount ? t("customers.registered") : t("customers.guest")}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...columns,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row, table }) => {
|
||||
@@ -382,6 +234,6 @@ const useColumns = () => {
|
||||
},
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
[columns]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const CustomerGroupGeneralSection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{group.name}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
@@ -35,7 +35,7 @@ export const CustomerGroupGeneralSection = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/customer-groups/${group.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -44,7 +44,7 @@ export const CustomerGroupGeneralSection = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -54,9 +54,9 @@ export const EditCustomerGroupForm = ({
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col overflow-hidden flex-1"
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto flex-1 max-w-full">
|
||||
<Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -77,11 +77,11 @@ export const EditCustomerGroupForm = ({
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
|
||||
@@ -1,251 +1,72 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
RowSelectionState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import {
|
||||
useAdminCustomerGroups,
|
||||
useAdminDeleteCustomerGroup,
|
||||
} from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminCustomerGroups } from "medusa-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 { 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 { Link } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useCustomerGroupTableColumns } from "./use-customer-group-table-columns"
|
||||
import { useCustomerGroupTableFilters } from "./use-customer-group-table-filters"
|
||||
import { useCustomerGroupTableQuery } from "./use-customer-group-table-query"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const CustomerGroupListTable = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
const { searchParams, raw } = useCustomerGroupTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
)
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const params = useQueryParams(["q", "order"])
|
||||
const { customer_groups, count, isLoading, isError, error } =
|
||||
useAdminCustomerGroups({
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
})
|
||||
useAdminCustomerGroups(
|
||||
{
|
||||
...searchParams,
|
||||
expand: "customers",
|
||||
fields: "id,name,customers,customers.id",
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useColumns()
|
||||
const filters = useCustomerGroupTableFilters()
|
||||
const columns = useCustomerGroupTableColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
data: customer_groups ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
enablePagination: true,
|
||||
count,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
!customer_groups?.length &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("customerGroups.domain")}</Heading>
|
||||
<Link to="/customer-groups/create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{noRecords ? (
|
||||
<NoRecords
|
||||
action={{
|
||||
label: t("customerGroups.createGroup"),
|
||||
to: "/customer-groups/create",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
<div className="flex items-center px-6 py-2 justify-between">
|
||||
<div className="flex items-center gap-x-2"></div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Query />
|
||||
<OrderBy keys={["name", "created_at", "updated_at"]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{(customer_groups?.length || 0) > 0 ? (
|
||||
<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"
|
||||
>
|
||||
{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(`/customer-groups/${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>
|
||||
) : (
|
||||
<div className="border-b">
|
||||
<NoResults />
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
rowCount={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
search
|
||||
pagination
|
||||
navigateTo={(row) => `/customer-groups/${row.original.id}`}
|
||||
orderBy={["name", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("customerGroups.deleteCustomerGroupWarning", {
|
||||
name: group.name,
|
||||
}),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
to: `/customer-groups/${group.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <CustomerGroupActions group={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import { usePrompt } from "@medusajs/ui"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useAdminDeleteCustomerGroup } from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||
|
||||
export const useCustomerGroupTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => getValue(),
|
||||
}),
|
||||
columnHelper.accessor("customers", {
|
||||
header: t("customers.domain"),
|
||||
cell: ({ getValue }) => {
|
||||
const count = getValue()?.length ?? 0
|
||||
|
||||
return count
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <CustomerGroupActions group={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("customerGroups.deleteCustomerGroupWarning", {
|
||||
name: group.name,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: `/customer-groups/${group.id}/edit`,
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
icon: <Trash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useCustomerGroupTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AdminGetCustomerGroupsParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
type UseCustomerGroupTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useCustomerGroupTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseCustomerGroupTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "order", "created_at", "updated_at"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, created_at, updated_at, q, order } = queryObject
|
||||
|
||||
const searchParams: AdminGetCustomerGroupsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
order,
|
||||
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button
|
||||
@@ -89,12 +89,12 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t("general.create")}
|
||||
{t("actions.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 className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("customers.createCustomer")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||
import format from "date-fns/format"
|
||||
import { ReactNode } from "react"
|
||||
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
|
||||
type CustomerGeneralSectionProps = {
|
||||
customer: Customer
|
||||
@@ -14,60 +13,52 @@ export const CustomerGeneralSection = ({
|
||||
}: CustomerGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const name = [customer.first_name, customer.last_name]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
const statusColor = customer.has_account ? "green" : "orange"
|
||||
const statusText = customer.has_account
|
||||
? t("customers.registered")
|
||||
: t("customers.guest")
|
||||
|
||||
return (
|
||||
<Container className="px-6 py-4 flex flex-col gap-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{customer.email}</Heading>
|
||||
<Link to={`/customers/${customer.id}/edit`}>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.edit")}
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={statusColor}>{statusText}</StatusBadge>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
icon: <PencilSquare />,
|
||||
to: "edit",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<Bulletpoint
|
||||
title={t("fields.name")}
|
||||
value={
|
||||
customer.first_name && customer.last_name
|
||||
? `${customer.first_name} ${customer.last_name}`
|
||||
: customer.last_name
|
||||
? customer.last_name
|
||||
: customer.first_name
|
||||
? customer.first_name
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Bulletpoint
|
||||
title={t("customers.firstSeen")}
|
||||
value={format(new Date(customer.created_at), "MMM d, yyyy")}
|
||||
/>
|
||||
<Bulletpoint title="Phone" value={customer.phone} />
|
||||
<Bulletpoint title="Orders" value={customer.orders.length} />
|
||||
<Bulletpoint
|
||||
title={t("fields.account")}
|
||||
value={
|
||||
customer.has_account
|
||||
? t("customers.registered")
|
||||
: t("customers.guest")
|
||||
}
|
||||
/>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.name")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{name ?? "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.phone")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{customer.phone ?? "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Bulletpoint = ({ title, value }: { title: string; value: ReactNode }) => {
|
||||
return (
|
||||
<div className="flex flex-col flex-1">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<div className="text-ui-fg-subtle txt-small-plus">{value ?? "-"}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CustomerOrderSection = ({
|
||||
<Heading level="h2">{t("orders.domain")}</Heading>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ export const EditCustomerForm = ({
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button
|
||||
@@ -144,7 +144,7 @@ export const EditCustomerForm = ({
|
||||
variant="primary"
|
||||
size="small"
|
||||
>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
|
||||
@@ -1,158 +1,79 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Table,
|
||||
clx,
|
||||
} from "@medusajs/ui"
|
||||
import {
|
||||
PaginationState,
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useAdminCustomers } from "medusa-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useMemo } 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"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
|
||||
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
|
||||
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const CustomerListTable = () => {
|
||||
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 { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
|
||||
const { customers, count, isLoading, isError, error } = useAdminCustomers(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const params = useQueryParams(["q"])
|
||||
const { customers, count, isLoading, isError, error } = useAdminCustomers({
|
||||
limit: PAGE_SIZE,
|
||||
offset: pageIndex * PAGE_SIZE,
|
||||
...params,
|
||||
})
|
||||
|
||||
const filters = useCustomerTableFilters()
|
||||
const columns = useColumns()
|
||||
|
||||
const table = useReactTable({
|
||||
const { table } = useDataTable({
|
||||
data: customers ?? [],
|
||||
columns,
|
||||
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
count,
|
||||
enablePagination: true,
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const noRecords =
|
||||
!isLoading &&
|
||||
(!customers || customers.length === 0) &&
|
||||
!Object.values(params).filter(Boolean).length
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="p-0 divide-y">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("customers.domain")}</Heading>
|
||||
<Link to="/customers/create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<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>
|
||||
<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(`/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>
|
||||
)}
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
rowCount={PAGE_SIZE}
|
||||
count={count}
|
||||
filters={filters}
|
||||
orderBy={[
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"has_account",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
navigateTo={(row) => row.original.id}
|
||||
search
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -167,7 +88,7 @@ const CustomerActions = ({ customer }: { customer: Customer }) => {
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/customers/${customer.id}/edit`,
|
||||
},
|
||||
],
|
||||
@@ -180,42 +101,16 @@ const CustomerActions = ({ customer }: { customer: Customer }) => {
|
||||
const columnHelper = createColumnHelper<Customer>()
|
||||
|
||||
const useColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const columns = useCustomerTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("email", {
|
||||
header: t("fields.email"),
|
||||
cell: ({ getValue }) => <span>{getValue()}</span>,
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "name",
|
||||
header: t("fields.name"),
|
||||
cell: ({ row }) => {
|
||||
const name = [row.original.first_name, row.original.last_name]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return name || "-"
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("has_account", {
|
||||
header: t("fields.account"),
|
||||
cell: ({ getValue }) => {
|
||||
const hasAccount = getValue()
|
||||
|
||||
return (
|
||||
<StatusBadge color={hasAccount ? "green" : "blue"}>
|
||||
{hasAccount ? t("customers.registered") : t("customers.guest")}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...columns,
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <CustomerActions customer={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
[columns]
|
||||
) as ColumnDef<Customer>[]
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const GiftCardDetails = () => {
|
||||
return <div>Gift Card Details</div>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { GiftCardDetails as Component } from "./details";
|
||||
@@ -0,0 +1,311 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
Button,
|
||||
CurrencyInput,
|
||||
DatePicker,
|
||||
FocusModal,
|
||||
Heading,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { currencies } from "../../../../../lib/currencies"
|
||||
import { getDbAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type CreateGiftCardFormProps = {
|
||||
subscribe: (state: boolean) => void
|
||||
}
|
||||
|
||||
const CreateGiftCardSchema = zod.object({
|
||||
region_id: zod.string(),
|
||||
value: zod.string(),
|
||||
is_enabled: zod.boolean(),
|
||||
ends_at: zod.date().nullable(),
|
||||
email: zod.string().email(),
|
||||
personal_message: zod.string().optional(),
|
||||
})
|
||||
|
||||
export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => {
|
||||
const [showDateFields, setShowDateFields] = useState(false)
|
||||
|
||||
const { regions } = useAdminRegions({
|
||||
limit: 1000,
|
||||
fields: "id,name,currency_code",
|
||||
})
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const form = useForm<zod.infer<typeof CreateGiftCardSchema>>({
|
||||
defaultValues: {
|
||||
region_id: regions?.[0]?.id ?? "",
|
||||
value: "",
|
||||
is_enabled: true,
|
||||
ends_at: null,
|
||||
email: "",
|
||||
personal_message: "",
|
||||
},
|
||||
resolver: zodResolver(CreateGiftCardSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
setValue,
|
||||
setError,
|
||||
} = form
|
||||
|
||||
const regionId = useWatch({
|
||||
control: form.control,
|
||||
name: "region_id",
|
||||
})
|
||||
|
||||
const currencyCode = regions?.find((r) => r.id === regionId)?.currency_code
|
||||
const nativeSymbol = currencyCode
|
||||
? currencies[currencyCode.toUpperCase()].symbol_native
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty, subscribe])
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminCreateGiftCard()
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setValue("ends_at", null, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
|
||||
setShowDateFields(open)
|
||||
}
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
if (!currencyCode) {
|
||||
setError("region_id", {
|
||||
type: "manual",
|
||||
message: "Region not found",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
region_id: data.region_id,
|
||||
value: getDbAmount(parseFloat(data.value), currencyCode),
|
||||
is_disabled: !data.is_enabled,
|
||||
ends_at: data.ends_at ?? undefined,
|
||||
metadata: {
|
||||
email: data.email,
|
||||
personal_message: data.personal_message,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: ({ gift_card }) => {
|
||||
navigate(`../${gift_card.id}`, { replace: true })
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
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("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto py-16">
|
||||
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
|
||||
<div>
|
||||
<Heading>{t("giftCards.createGiftCard")}</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("giftCards.createGiftCardHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="region_id"
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.region")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Select {...field} onValueChange={onChange}>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{regions?.map((region) => (
|
||||
<Select.Item key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="value"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("giftCards.balance")}</Form.Label>
|
||||
<Form.Control>
|
||||
{!currencyCode || !nativeSymbol ? (
|
||||
<Tooltip content={t("giftCards.selectRegionFirst")}>
|
||||
<Input disabled />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<CurrencyInput
|
||||
code={currencyCode.toUpperCase()}
|
||||
symbol={nativeSymbol}
|
||||
min={0}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_enabled"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>{t("general.enabled")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("giftCards.enabledHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="ends_at"
|
||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Collapsible.Root open={showDateFields}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label optional>
|
||||
{t("fields.expiryDate")}
|
||||
</Form.Label>
|
||||
<Switch
|
||||
checked={showDateFields}
|
||||
onCheckedChange={handleOpenChange}
|
||||
/>
|
||||
</div>
|
||||
<Text
|
||||
size="small"
|
||||
className="text-ui-fg-subtle max-w-[85%] text-pretty"
|
||||
>
|
||||
{t("giftCards.expiryDateHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<div className="grid grid-cols-2 pt-4">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="grid grid-cols-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.email")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input type="email" autoComplete="off" {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="personal_message"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("giftCards.personalMessage")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FocusModal.Body>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-gift-card-form"
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FocusModal } from "@medusajs/ui"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
import { CreateGiftCardForm } from "./components/create-gift-card-form"
|
||||
|
||||
export const GiftCardCreate = () => {
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
return (
|
||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||
<FocusModal.Content>
|
||||
<CreateGiftCardForm subscribe={subscribe} />
|
||||
</FocusModal.Content>
|
||||
</FocusModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GiftCardCreate as Component } from "./gift-card-create"
|
||||
@@ -0,0 +1,194 @@
|
||||
import { ArrowUpRightOnBox, PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { GiftCard } from "@medusajs/medusa"
|
||||
import {
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import format from "date-fns/format"
|
||||
import { useAdminDeleteGiftCard } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useNavigate } from "react-router-dom"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { currencies } from "../../../../../lib/currencies"
|
||||
import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type GiftCardGeneralSectionProps = {
|
||||
giftCard: GiftCard
|
||||
}
|
||||
|
||||
export const GiftCardGeneralSection = ({
|
||||
giftCard,
|
||||
}: GiftCardGeneralSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteGiftCard(giftCard.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("giftCards.deleteGiftCardWarning", {
|
||||
code: giftCard.code,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("..")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let color: "green" | "red"
|
||||
|
||||
if (
|
||||
giftCard.is_disabled ||
|
||||
(giftCard.ends_at && new Date(giftCard.ends_at) < new Date())
|
||||
) {
|
||||
color = "red"
|
||||
} else {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
let text: string = t("general.enabled")
|
||||
|
||||
if (giftCard.is_disabled) {
|
||||
text = t("general.disabled")
|
||||
}
|
||||
|
||||
if (giftCard.ends_at && new Date(giftCard.ends_at) < new Date()) {
|
||||
text = t("general.expired")
|
||||
}
|
||||
|
||||
const recipient = giftCard.metadata?.email as string | undefined
|
||||
|
||||
const currencyCode = giftCard.region.currency_code.toUpperCase()
|
||||
const nativeSymbol = currencies[currencyCode].symbol_native
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Heading>{giftCard.code}</Heading>
|
||||
<Copy content={giftCard.code} className="text-ui-fg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StatusBadge color={color}>{text}</StatusBadge>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: "edit",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.delete"),
|
||||
icon: <Trash />,
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("giftCards.currentBalance")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{`${nativeSymbol} ${getPresentationalAmount(
|
||||
giftCard.balance,
|
||||
currencyCode
|
||||
)} ${currencyCode}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("giftCards.initialBalance")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{`${nativeSymbol} ${getPresentationalAmount(
|
||||
giftCard.value,
|
||||
currencyCode
|
||||
)} ${currencyCode}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("giftCards.recipient")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{recipient ?? "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.region")}
|
||||
</Text>
|
||||
<Link
|
||||
to={`/settings/regions/${giftCard.region.id}`}
|
||||
className="txt-compact-small text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg focus:shadow-borders-focus flex w-fit items-center gap-x-1.5 rounded-[4px] outline-none"
|
||||
>
|
||||
<span>{giftCard.region.name}</span>
|
||||
<ArrowUpRightOnBox />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.order")}
|
||||
</Text>
|
||||
|
||||
{giftCard.order ? (
|
||||
<Link
|
||||
to={`/orders/${giftCard.order.id}`}
|
||||
className="txt-compact-small text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg focus:shadow-borders-focus flex w-fit items-center gap-x-1.5 rounded-[4px] outline-none"
|
||||
>
|
||||
<span>{`#${giftCard.order.display_id}`}</span>
|
||||
<ArrowUpRightOnBox />
|
||||
</Link>
|
||||
) : (
|
||||
<Text size="small" leading="compact">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.issuedDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{format(new Date(giftCard.created_at), "dd MMM yyyy")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.expiryDate")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{giftCard.ends_at
|
||||
? format(new Date(giftCard.ends_at), "dd MMM yyyy")
|
||||
: "-"}
|
||||
</Text>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./gift-card-general-section"
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useAdminGiftCard } from "medusa-react"
|
||||
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||
import { GiftCardGeneralSection } from "./components/gift-card-general-section"
|
||||
import { giftCardLoader } from "./loader"
|
||||
|
||||
export const GiftCardDetail = () => {
|
||||
const { id } = useParams()
|
||||
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof giftCardLoader>
|
||||
>
|
||||
|
||||
const { gift_card, isLoading, isError, error } = useAdminGiftCard(id!, {
|
||||
initialData,
|
||||
})
|
||||
|
||||
if (isLoading || !gift_card) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<GiftCardGeneralSection giftCard={gift_card} />
|
||||
<JsonViewSection data={gift_card} />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { GiftCardDetail as Component } from "./gift-card-detail"
|
||||
export { giftCardLoader as loader } from "./loader"
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AdminGiftCardsRes } 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 giftCardDetailQuery = (id: string) => ({
|
||||
queryKey: adminProductKeys.detail(id),
|
||||
queryFn: async () => medusa.admin.giftCards.retrieve(id),
|
||||
})
|
||||
|
||||
export const giftCardLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const id = params.id
|
||||
const query = giftCardDetailQuery(id!)
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<Response<AdminGiftCardsRes>>(query.queryKey) ??
|
||||
(await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { GiftCard } from "@medusajs/medusa"
|
||||
import {
|
||||
Button,
|
||||
CurrencyInput,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
} from "@medusajs/ui"
|
||||
import * as Collapsible from "@radix-ui/react-collapsible"
|
||||
import { useAdminRegions, useAdminUpdateGiftCard } from "medusa-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { currencies } from "../../../../../lib/currencies"
|
||||
import { isAxiosError } from "../../../../../lib/is-axios-error"
|
||||
import {
|
||||
getDbAmount,
|
||||
getPresentationalAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
type EditGiftCardFormProps = {
|
||||
giftCard: GiftCard
|
||||
onSuccessfulSubmit: () => void
|
||||
subscribe: (state: boolean) => void
|
||||
}
|
||||
|
||||
const EditGiftCardSchema = zod.object({
|
||||
region_id: zod.string(),
|
||||
balance: zod.string(),
|
||||
is_enabled: zod.boolean(),
|
||||
ends_at: zod.date().nullable(),
|
||||
})
|
||||
|
||||
export const EditGiftCardForm = ({
|
||||
giftCard,
|
||||
onSuccessfulSubmit,
|
||||
subscribe,
|
||||
}: EditGiftCardFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [showDateFields, setShowDateFields] = useState(!!giftCard.ends_at)
|
||||
const form = useForm<zod.infer<typeof EditGiftCardSchema>>({
|
||||
defaultValues: {
|
||||
region_id: giftCard.region_id,
|
||||
balance: getPresentationalAmount(
|
||||
giftCard.balance,
|
||||
giftCard.region.currency_code
|
||||
).toString(),
|
||||
is_enabled: !giftCard.is_disabled,
|
||||
ends_at: giftCard.ends_at ? new Date(giftCard.ends_at) : null,
|
||||
},
|
||||
resolver: zodResolver(EditGiftCardSchema),
|
||||
})
|
||||
|
||||
const {
|
||||
formState: { isDirty },
|
||||
setValue,
|
||||
} = form
|
||||
|
||||
useEffect(() => {
|
||||
subscribe(isDirty)
|
||||
}, [isDirty, subscribe])
|
||||
|
||||
const { regions } = useAdminRegions({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useAdminUpdateGiftCard(giftCard.id)
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setValue("ends_at", null, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
|
||||
setShowDateFields(open)
|
||||
}
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const newBalance = getDbAmount(
|
||||
parseFloat(data.balance),
|
||||
giftCard.region.currency_code
|
||||
)
|
||||
|
||||
if (newBalance > giftCard.value) {
|
||||
form.setError("balance", {
|
||||
type: "manual",
|
||||
message: t("giftCards.balanceHigherThanValue"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (newBalance < 0) {
|
||||
form.setError("balance", {
|
||||
type: "manual",
|
||||
message: t("giftCards.balanceLowerThanZero"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
region_id: data.region_id,
|
||||
balance: getDbAmount(
|
||||
parseFloat(data.balance),
|
||||
giftCard.region.currency_code
|
||||
),
|
||||
is_disabled: !data.is_enabled,
|
||||
ends_at: data.ends_at,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onSuccessfulSubmit()
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isAxiosError(error)) {
|
||||
form.setError("balance", {
|
||||
type: "manual",
|
||||
message: error.response?.data.message,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<Drawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="balance"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.balance")}</Form.Label>
|
||||
<Form.Control>
|
||||
<CurrencyInput
|
||||
code={giftCard.region.currency_code.toUpperCase()}
|
||||
symbol={`/ ${getPresentationalAmount(
|
||||
giftCard.value,
|
||||
giftCard.region.currency_code
|
||||
)} ${
|
||||
currencies[giftCard.region.currency_code.toUpperCase()]
|
||||
.symbol_native
|
||||
}`}
|
||||
min={0}
|
||||
onValueChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="region_id"
|
||||
render={({ field: { onChange, ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.region")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Select {...field} onValueChange={onChange}>
|
||||
<Select.Trigger ref={ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{regions?.map((region) => (
|
||||
<Select.Item key={region.id} value={region.id}>
|
||||
{region.name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.Hint>{t("giftCards.regionHint")}</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="is_enabled"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>{t("general.enabled")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint className="!mt-1">
|
||||
{t("giftCards.enabledHint")}
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="ends_at"
|
||||
render={({ field: { value, onChange, ref: _ref, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Collapsible.Root open={showDateFields}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label optional>
|
||||
{t("fields.expiryDate")}
|
||||
</Form.Label>
|
||||
<Switch
|
||||
checked={showDateFields}
|
||||
onCheckedChange={handleOpenChange}
|
||||
/>
|
||||
</div>
|
||||
<Text
|
||||
size="small"
|
||||
className="text-ui-fg-subtle text-pretty"
|
||||
>
|
||||
{t("giftCards.expiryDateHint")}
|
||||
</Text>
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<div className="pt-4">
|
||||
<Form.Control>
|
||||
<DatePicker
|
||||
value={value ?? undefined}
|
||||
onChange={(v) => {
|
||||
onChange(v ?? null)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Drawer.Body>
|
||||
<Drawer.Footer>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-gift-card-form"
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Drawer, Heading } from "@medusajs/ui"
|
||||
import { useAdminGiftCard } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||
import { EditGiftCardForm } from "./components/edit-gift-card-form"
|
||||
|
||||
export const GiftCardEdit = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||
|
||||
const { gift_card, isLoading, isError, error } = useAdminGiftCard(id!)
|
||||
|
||||
const handleSuccessfulSubmit = () => {
|
||||
onOpenChange(false, true)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header>
|
||||
<Heading>{t("giftCards.editGiftCard")}</Heading>
|
||||
</Drawer.Header>
|
||||
{!isLoading && gift_card && (
|
||||
<EditGiftCardForm
|
||||
giftCard={gift_card}
|
||||
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||
subscribe={subscribe}
|
||||
/>
|
||||
)}
|
||||
</Drawer.Content>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GiftCardEdit as Component } from "./gift-card-edit"
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { useAdminGiftCards } from "medusa-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { useGiftCardTableColumns } from "./use-gift-card-table-columns"
|
||||
import { useGiftCardTableFilters } from "./use-gift-card-table-filters"
|
||||
import { useGiftCardTableQuery } from "./use-gift-card-table-query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const GiftCardListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const { searchParams, raw } = useGiftCardTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { gift_cards, count, isError, error, isLoading } = useAdminGiftCards(
|
||||
{
|
||||
...searchParams,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
|
||||
const filters = useGiftCardTableFilters()
|
||||
const columns = useGiftCardTableColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: gift_cards ?? [],
|
||||
columns,
|
||||
enablePagination: true,
|
||||
count,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading>{t("giftCards.domain")}</Heading>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button size="small" variant="secondary" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
table={table}
|
||||
pagination
|
||||
navigateTo={(row) => `/gift-cards/${row.original.id}`}
|
||||
filters={filters}
|
||||
count={count}
|
||||
search
|
||||
isLoading={isLoading}
|
||||
rowCount={PAGE_SIZE}
|
||||
orderBy={["created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./gift-card-list-table"
|
||||
@@ -0,0 +1,136 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { GiftCard } from "@medusajs/medusa"
|
||||
import { Badge, usePrompt } from "@medusajs/ui"
|
||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useAdminDeleteGiftCard } from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
|
||||
import { MoneyAmountCell } from "../../../../../components/table/table-cells/common/money-amount-cell"
|
||||
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
|
||||
import { DisplayIdCell } from "../../../../../components/table/table-cells/order/display-id-cell"
|
||||
|
||||
const columnHelper = createColumnHelper<GiftCard>()
|
||||
|
||||
export const useGiftCardTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("code", {
|
||||
header: t("fields.code"),
|
||||
cell: ({ getValue }) => {
|
||||
return (
|
||||
<Badge size="2xsmall" className="overflow-hidden truncate">
|
||||
{getValue()}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("order", {
|
||||
header: t("fields.order"),
|
||||
cell: ({ getValue }) => {
|
||||
const order = getValue()
|
||||
return <DisplayIdCell displayId={order?.display_id} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("region", {
|
||||
header: t("fields.region"),
|
||||
cell: ({ row }) => {
|
||||
return row.original.region.name
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("is_disabled", {
|
||||
header: t("fields.status"),
|
||||
cell: ({ getValue }) => {
|
||||
const isDisabled = getValue()
|
||||
|
||||
return (
|
||||
<StatusCell color={isDisabled ? "red" : "green"}>
|
||||
{isDisabled ? t("general.disabled") : t("general.enabled")}
|
||||
</StatusCell>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_at", {
|
||||
header: t("fields.dateIssued"),
|
||||
cell: ({ getValue }) => {
|
||||
return <DateCell date={getValue()} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("value", {
|
||||
header: t("giftCards.initialBalance"),
|
||||
cell: ({ getValue, row }) => {
|
||||
const currencyCode = row.original.region.currency_code
|
||||
const value = getValue()
|
||||
|
||||
return <MoneyAmountCell amount={value} currencyCode={currencyCode} />
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("balance", {
|
||||
header: t("giftCards.currentBalance"),
|
||||
cell: ({ getValue, row }) => {
|
||||
const currencyCode = row.original.region.currency_code
|
||||
const value = getValue()
|
||||
|
||||
return <MoneyAmountCell amount={value} currencyCode={currencyCode} />
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row }) => <GiftCardActions giftCard={row.original} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
) as ColumnDef<GiftCard>[]
|
||||
}
|
||||
|
||||
const GiftCardActions = ({ giftCard }: { giftCard: GiftCard }) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteGiftCard(giftCard.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("giftCards.deleteGiftCardWarning", {
|
||||
code: giftCard.code,
|
||||
}),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
await mutateAsync()
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
to: `/gift-cards/${giftCard.id}/edit`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Filter } from "../../../../../components/table/data-table"
|
||||
|
||||
export const useGiftCardTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
let filters: Filter[] = []
|
||||
|
||||
const dateFilters: Filter[] = [
|
||||
{ label: t("fields.createdAt"), key: "created_at" },
|
||||
{ label: t("fields.updatedAt"), key: "updated_at" },
|
||||
].map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: "date",
|
||||
}))
|
||||
|
||||
filters = [...filters, ...dateFilters]
|
||||
|
||||
return filters
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AdminGetGiftCardsParams } from "@medusajs/medusa"
|
||||
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||
|
||||
export const useGiftCardTableQuery = ({ pageSize }: { pageSize: number }) => {
|
||||
const queryObject = useQueryParams(["offset", "q"])
|
||||
|
||||
const { offset, q } = queryObject
|
||||
|
||||
const searchParams: AdminGetGiftCardsParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { GiftCardListTable } from "./components/gift-card-list-table"
|
||||
|
||||
export const GiftCardList = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<GiftCardListTable />
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GiftCardList as Component } from "./gift-card-list"
|
||||
@@ -1 +0,0 @@
|
||||
export { GiftCardList as Component } from "./list";
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const GiftCardList = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Heading>Gift Card List</Heading>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -65,11 +65,11 @@ export const CreateLocationForm = () => {
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<FocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</FocusModal.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("general.save")}
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</FocusModal.Header>
|
||||
|
||||
@@ -163,8 +163,8 @@ const SalesChannelActions = ({
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("locations.removeSalesChannelsWarning", { count: 1 }),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -184,12 +184,12 @@ const SalesChannelActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -206,10 +206,10 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<Drawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.cancel")}
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</Drawer.Close>
|
||||
<Button size="small">{t("general.save")}</Button>
|
||||
<Button size="small">{t("actions.save")}</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</form>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const LocationsListTable = () => {
|
||||
<div>
|
||||
<Link to="create">
|
||||
<Button size="small" variant="secondary">
|
||||
{t("general.create")}
|
||||
{t("actions.create")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -176,8 +176,8 @@ const LocationActions = ({
|
||||
}),
|
||||
verificationText: location.name,
|
||||
verificationInstruction: t("general.typeToConfirm"),
|
||||
confirmText: t("general.delete"),
|
||||
cancelText: t("general.cancel"),
|
||||
confirmText: t("actions.delete"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
@@ -194,12 +194,12 @@ const LocationActions = ({
|
||||
actions: [
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("general.edit"),
|
||||
label: t("actions.edit"),
|
||||
to: `/settings/locations/${location.id}/edit`,
|
||||
},
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("general.delete"),
|
||||
label: t("actions.delete"),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user