import { CurrencyDollar, EllipsisHorizontal, ExclamationCircle, PencilSquare, PhotoSolid, Spinner, Tag, Trash, } from "@medusajs/icons" import type { PriceList, Product } from "@medusajs/medusa" import { Checkbox, CommandBar, Container, DropdownMenu, Heading, IconButton, Input, Table, Text, clx, usePrompt, } from "@medusajs/ui" import { PaginationState, RowSelectionState, createColumnHelper, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable, type Row, } from "@tanstack/react-table" import { useAdminDeletePriceListProductPrices, useAdminDeletePriceListProductsPrices, useAdminPriceListProducts, } from "medusa-react" import * as React from "react" import { useTranslation } from "react-i18next" import { useNavigate, useSearchParams } from "react-router-dom" import { useDebouncedSearchParam } from "../../../../hooks/use-debounced-search-param" import useNotification from "../../../../hooks/use-notification" import { getErrorMessage } from "../../../../utils/error-messages" import { getDateComparisonOperatorFromSearchParams, getStringFromSearchParams, } from "../../../../utils/search-param-utils" import { ProductFilter, ProductFilterMenu } from "../../components" import { AddProductsModal } from "./add-products-modal" import { EditPricesModal } from "./edit-prices-modal" type PriceListPricesSectionProps = { priceList: PriceList } const PAGE_SIZE = 10 const TABLE_HEIGHT = (PAGE_SIZE + 1) * 48 const PriceListPricesSection = ({ priceList }: PriceListPricesSectionProps) => { const [searchParams] = useSearchParams() const navigate = useNavigate() const { t } = useTranslation() const [showAddProductsModal, setShowAddProductsModal] = React.useState(false) const [showEditPricesModal, setShowEditPricesModal] = React.useState(false) const [productIdsToEdit, setProductIdsToEdit] = React.useState< string[] | null >(null) /** * Table state. */ const [rowSelection, setRowSelection] = React.useState({}) const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: PAGE_SIZE, }) /** * Calculate the offset based on the pagination state. */ const offset = React.useMemo( () => pagination.pageIndex * pagination.pageSize, [pagination.pageIndex, pagination.pageSize] ) const { query, setQuery } = useDebouncedSearchParam() const onFiltersChange = (filters: ProductFilter) => { const current = new URLSearchParams(searchParams) if (filters.created_at) { current.set("created_at", JSON.stringify(filters.created_at)) } else { current.delete("created_at") } if (filters.updated_at) { current.set("updated_at", JSON.stringify(filters.updated_at)) } else { current.delete("updated_at") } navigate({ search: current.toString() }, { replace: true }) } const onClearFilters = () => { const current = new URLSearchParams(searchParams) current.delete("created_at") current.delete("updated_at") navigate({ search: current.toString() }, { replace: true }) } const prompt = usePrompt() const notification = useNotification() const { mutateAsync, isLoading: isDeletingProductPrices } = useAdminDeletePriceListProductsPrices(priceList.id) const handleDeleteProductPrices = async () => { const res = await prompt({ title: t("price-list-prices-section-prompt-title", "Are you sure?"), description: t( "price-list-prices-section-prompt-description", "This will permanently delete the product prices from the list" ), }) if (!res) { return } await mutateAsync( { product_ids: Object.keys(rowSelection), }, { onSuccess: () => { notification( t( "price-list-prices-secton-delete-success-title", "Prices deleted" ), t( "price-list-prices-section-delete-success-description", `Successfully deleted prices for {{count}} products`, { count: Object.keys(rowSelection).length, } ), "success" ) setRowSelection({}) }, onError: (err) => { notification( t( "price-list-prices-section-delete-error-title", "An error occurred" ), getErrorMessage(err), "error" ) }, } ) } const { products, count, isLoading, isError } = useAdminPriceListProducts( priceList.id, { limit: PAGE_SIZE, offset, expand: "variants,collection", created_at: getDateComparisonOperatorFromSearchParams( "created_at", searchParams ), updated_at: getDateComparisonOperatorFromSearchParams( "updated_at", searchParams ), q: getStringFromSearchParams("q", searchParams), }, { keepPreviousData: true, } ) const { products: allProducts } = useAdminPriceListProducts( priceList.id, { limit: count, fields: "id", }, { enabled: !!count, } ) const onEditPricesModalOpenChange = React.useCallback((open: boolean) => { switch (open) { case true: setShowEditPricesModal(true) break case false: setShowEditPricesModal(false) setProductIdsToEdit(null) setRowSelection({}) break } }, []) const onEditAllProductPrices = React.useCallback(() => { setProductIdsToEdit(allProducts?.map((p) => p.id) as string[]) setShowEditPricesModal(true) }, [allProducts]) const onEditSelectedProductPrices = React.useCallback(() => { setProductIdsToEdit(Object.keys(rowSelection)) setShowEditPricesModal(true) }, [rowSelection]) const onEditSingleProductPrices = (id: string) => { setProductIdsToEdit([id]) setShowEditPricesModal(true) } const pageCount = React.useMemo(() => { return count ? Math.ceil(count / PAGE_SIZE) : 0 }, [count]) const { columns } = usePriceListProudctColumns({ onEditProductPrices: onEditSingleProductPrices, }) const table = useReactTable({ columns, data: (products as Product[] | undefined) ?? [], getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getRowId: (row) => row.id, state: { rowSelection, pagination, }, meta: { priceListId: priceList.id, }, pageCount, enableRowSelection: true, onRowSelectionChange: setRowSelection, manualPagination: true, onPaginationChange: setPagination, }) return (
{t("price-list-prices-section-heading", "Prices")}
setQuery(e.target.value)} /> {t( "price-list-prices-section-prices-menu-edit", "Edit prices" )} setShowAddProductsModal(true)}> {t( "price-list-prices-section-prices-menu-add", "Add products" )}
{isLoading && (
)} {isError && (
{t( "price-list-prices-section-table-load-error", "An error occured while fetching the products. Try to reload the page, or if the issue persists, try again later." )}
)} {table.getHeaderGroups().map((headerGroup) => { return ( {headerGroup.headers.map((header) => { return ( {flexRender( header.column.columnDef.header, header.getContext() )} ) })} ) })} {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))}
p.id) as string[]) ?? []} priceList={priceList} open={showAddProductsModal} onOpenChange={setShowAddProductsModal} /> {productIdsToEdit && ( )} 0}> {t("price-list-prices-section-bar-count", "{{count}} selected", { count: Object.keys(rowSelection).length, })}
) } const columnHelper = createColumnHelper() type UsePriceListProudctColumnsProps = { onEditProductPrices: (id: string) => void } const usePriceListProudctColumns = ({ onEditProductPrices, }: UsePriceListProudctColumnsProps) => { const { t } = useTranslation() const columns = React.useMemo( () => [ columnHelper.display({ id: "select", header: ({ table }) => { return ( table.toggleAllPageRowsSelected(!!value) } aria-label={ t( "price-list-prices-section-select-all-checkbox-label", "Select all products on the current page" ) ?? undefined } /> ) }, cell: ({ row }) => { return ( row.toggleSelected(!!value)} aria-label={ t( "price-list-prices-section-select-checkbox-label", "Select row" ) ?? undefined } /> ) }, }), columnHelper.accessor("title", { header: () => t("price-list-prices-section-table-product", "Product"), cell: (info) => { const title = info.getValue() const thumbnail = info.row.original.thumbnail return (
{thumbnail ? ( { ) : ( )}
{title}
) }, }), columnHelper.accessor("collection", { header: () => t("price-list-prices-section-table-collection", "Collection"), cell: (info) => info.getValue()?.title ?? "-", }), columnHelper.accessor("variants", { header: () => t("price-list-prices-section-table-variants", "Variants"), cell: (info) => { const variants = info.getValue() return variants?.length ?? "-" }, }), columnHelper.display({ id: "actions", cell: ({ table, row }) => { const { priceListId } = table.options.meta as { priceListId: string | undefined } return ( ) }, }), ], [t, onEditProductPrices] ) return { columns } } type PriceListProductRowActionsProps = { row: Row priceListId?: string onEditProductPrices: (id: string) => void } const PriceListProductRowActions = ({ row, priceListId, onEditProductPrices, }: PriceListProductRowActionsProps) => { const { mutateAsync } = useAdminDeletePriceListProductPrices( priceListId!, row.original.id ) const prompt = usePrompt() const notification = useNotification() const onDelete = async () => { const response = await prompt({ title: "Are you sure?", description: "This will permanently delete the product prices from the list", }) if (!response) { return } return mutateAsync(undefined, { onSuccess: ({ deleted }) => { if (deleted) { notification( "Prices deleted", `Successfully deleted prices for ${row.original.title}`, "success" ) } if (!deleted) { notification( "Failed to delete prices", `No prices were deleted for ${row.original.title}`, "error" ) } }, onError: (err) => { notification("An error occurred", getErrorMessage(err), "error") }, }) } const onEdit = () => { onEditProductPrices(row.original.id) } return ( Edit prices Delete prices ) } export { PriceListPricesSection }