feat(medusa-react,medusa,types,dashboard): added empty state + table for promotions list page (#6827)
what: - adds empty state for promotions list page - lists all promotions with pagination <img width="1663" alt="Screenshot 2024-03-26 at 14 19 27" src="https://github.com/medusajs/medusa/assets/5105988/ed0d5c65-d003-40f5-b899-540970d892f5"> <img width="1664" alt="Screenshot 2024-03-27 at 20 46 17" src="https://github.com/medusajs/medusa/assets/5105988/4aa40f09-fe3f-4f34-af7a-f5c183254c76">
This commit is contained in:
1 parent
6113af0a66
commit
0c0b425de7
33 files changed
+542
-11
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"medusa-react": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(medusa-react,medusa,types,dashboard): added empty state for promotions list page
|
||||||
@@ -46,6 +46,11 @@ export const injectionZones = [
|
|||||||
"discount.details.after",
|
"discount.details.after",
|
||||||
"discount.list.before",
|
"discount.list.before",
|
||||||
"discount.list.after",
|
"discount.list.after",
|
||||||
|
// Promotion injection zones
|
||||||
|
"promotion.details.before",
|
||||||
|
"promotion.details.after",
|
||||||
|
"promotion.list.before",
|
||||||
|
"promotion.list.after",
|
||||||
// Gift card injection zones
|
// Gift card injection zones
|
||||||
"gift_card.details.before",
|
"gift_card.details.before",
|
||||||
"gift_card.details.after",
|
"gift_card.details.after",
|
||||||
|
|||||||
@@ -131,6 +131,15 @@
|
|||||||
},
|
},
|
||||||
"required": ["domain"]
|
"required": ["domain"]
|
||||||
},
|
},
|
||||||
|
"promotions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["domain"]
|
||||||
|
},
|
||||||
"giftCards": {
|
"giftCards": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -176,6 +185,23 @@
|
|||||||
},
|
},
|
||||||
"required": ["domain", "role", "roles"]
|
"required": ["domain", "role", "roles"]
|
||||||
},
|
},
|
||||||
|
"statuses": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"scheduled": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expired": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -502,6 +502,16 @@
|
|||||||
"disabled": "Disabled"
|
"disabled": "Disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"promotions": {
|
||||||
|
"domain": "Promotions",
|
||||||
|
"fields": {
|
||||||
|
"method": "Method",
|
||||||
|
"campaign": "Campaign"
|
||||||
|
},
|
||||||
|
"deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.",
|
||||||
|
"createPromotionTitle": "Create Promotion",
|
||||||
|
"type": "Promotion type"
|
||||||
|
},
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"domain": "Pricing",
|
"domain": "Pricing",
|
||||||
"deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
|
"deletePriceListWarning": "You are about to delete the price list {{name}}. This action cannot be undone.",
|
||||||
@@ -801,6 +811,12 @@
|
|||||||
"serverError": "Server error - Try again later.",
|
"serverError": "Server error - Try again later.",
|
||||||
"invalidCredentials": "Wrong email or password"
|
"invalidCredentials": "Wrong email or password"
|
||||||
},
|
},
|
||||||
|
"statuses": {
|
||||||
|
"scheduled": "Scheduled",
|
||||||
|
"expired": "Expired",
|
||||||
|
"active": "Active",
|
||||||
|
"disabled": "Disabled"
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ReceiptPercent />,
|
icon: <ReceiptPercent />,
|
||||||
label: t("discounts.domain"),
|
label: t("promotions.domain"),
|
||||||
to: "/discounts",
|
to: "/promotions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CurrencyDollar />,
|
icon: <CurrencyDollar />,
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
type CellProps = {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeCell = ({ code }: CellProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||||
|
{/* // TODO: border color inversion*/}
|
||||||
|
<span className="bg-ui-tag-neutral-bg truncate rounded-md border border-neutral-200 p-1 text-xs">
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeHeader = ({ text }: HeaderProps) => {
|
||||||
|
return (
|
||||||
|
<div className=" flex h-full w-full items-center ">
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./code-cell"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./text-cell"
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
type CellProps = {
|
||||||
|
text?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextCell = ({ text }: CellProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
|
||||||
|
<span className="truncate">{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextHeader = ({ text }: HeaderProps) => {
|
||||||
|
return (
|
||||||
|
<div className=" flex h-full w-full items-center">
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./status-cell"
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import {
|
||||||
|
getPromotionStatus,
|
||||||
|
PromotionStatus,
|
||||||
|
} from "../../../../../lib/promotions"
|
||||||
|
import { StatusCell as StatusCell_ } from "../../common/status-cell"
|
||||||
|
|
||||||
|
type PromotionCellProps = {
|
||||||
|
promotion: PromotionDTO
|
||||||
|
}
|
||||||
|
type StatusColors = "grey" | "orange" | "green" | "red"
|
||||||
|
type StatusMap = Record<string, [StatusColors, string]>
|
||||||
|
|
||||||
|
export const StatusCell = ({ promotion }: PromotionCellProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const statusMap: StatusMap = {
|
||||||
|
[PromotionStatus.DISABLED]: ["grey", t("statuses.disabled")],
|
||||||
|
[PromotionStatus.ACTIVE]: ["green", t("statuses.active")],
|
||||||
|
[PromotionStatus.SCHEDULED]: ["orange", t("statuses.scheduled")],
|
||||||
|
[PromotionStatus.EXPIRED]: ["red", t("statuses.expired")],
|
||||||
|
}
|
||||||
|
|
||||||
|
const [color, text] = statusMap[getPromotionStatus(promotion)]
|
||||||
|
|
||||||
|
return <StatusCell_ color={color}>{text}</StatusCell_>
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CodeCell,
|
||||||
|
CodeHeader,
|
||||||
|
} from "../../../components/table/table-cells/common/code-cell"
|
||||||
|
import {
|
||||||
|
TextCell,
|
||||||
|
TextHeader,
|
||||||
|
} from "../../../components/table/table-cells/common/text-cell"
|
||||||
|
import { StatusCell } from "../../../components/table/table-cells/promotion/status-cell"
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PromotionDTO>()
|
||||||
|
|
||||||
|
export const usePromotionTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "code",
|
||||||
|
header: () => <CodeHeader text={t("fields.code")} />,
|
||||||
|
cell: ({ row }) => <CodeCell code={row.original.code!} />,
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.display({
|
||||||
|
id: "campaign",
|
||||||
|
header: () => <TextHeader text={t("promotions.fields.campaign")} />,
|
||||||
|
cell: ({ row }) => <TextCell text={row.original.campaign?.name} />,
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.display({
|
||||||
|
id: "method",
|
||||||
|
header: () => <TextHeader text={t("promotions.fields.method")} />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const text = row.original.is_automatic
|
||||||
|
? "Automatic"
|
||||||
|
: "Promotion Code"
|
||||||
|
|
||||||
|
return <TextCell text={text} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.display({
|
||||||
|
id: "status",
|
||||||
|
header: () => <TextHeader text={t("fields.status")} />,
|
||||||
|
cell: ({ row }) => <StatusCell promotion={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
) as ColumnDef<PromotionDTO>[]
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Filter } from "../../../components/table/data-table"
|
||||||
|
|
||||||
|
export const usePromotionTableFilters = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
let filters: Filter[] = [
|
||||||
|
{ label: t("fields.createdAt"), key: "created_at", type: "date" },
|
||||||
|
{ label: t("fields.updatedAt"), key: "updated_at", type: "date" },
|
||||||
|
]
|
||||||
|
|
||||||
|
return filters
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { AdminGetPromotionsParams } from "@medusajs/medusa"
|
||||||
|
import { useQueryParams } from "../../use-query-params"
|
||||||
|
|
||||||
|
type UsePromotionTableQueryProps = {
|
||||||
|
prefix?: string
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePromotionTableQuery = ({
|
||||||
|
prefix,
|
||||||
|
pageSize = 20,
|
||||||
|
}: UsePromotionTableQueryProps) => {
|
||||||
|
const queryObject = useQueryParams(
|
||||||
|
["offset", "q", "created_at", "updated_at"],
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
const { offset, q, created_at, updated_at } = queryObject
|
||||||
|
|
||||||
|
const searchParams: AdminGetPromotionsParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
created_at: created_at ? JSON.parse(created_at) : undefined,
|
||||||
|
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
|
||||||
|
offset: offset ? Number(offset) : 0,
|
||||||
|
q,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchParams,
|
||||||
|
raw: queryObject,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./auth"
|
export * from "./auth"
|
||||||
|
export * from "./promotion"
|
||||||
export * from "./store"
|
export * from "./store"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
AdminGetPromotionsParams,
|
||||||
|
AdminPromotionsListRes,
|
||||||
|
} from "@medusajs/medusa"
|
||||||
|
import { queryKeysFactory, useAdminCustomQuery } from "medusa-react"
|
||||||
|
|
||||||
|
const QUERY_KEY = "admin_promotions"
|
||||||
|
export const adminPromotionKeys = queryKeysFactory<
|
||||||
|
typeof QUERY_KEY,
|
||||||
|
AdminGetPromotionsParams
|
||||||
|
>(QUERY_KEY)
|
||||||
|
|
||||||
|
export const useV2Promotions = (
|
||||||
|
query?: AdminGetPromotionsParams,
|
||||||
|
options?: object
|
||||||
|
) => {
|
||||||
|
const { data, ...rest } = useAdminCustomQuery<
|
||||||
|
AdminGetPromotionsParams,
|
||||||
|
AdminPromotionsListRes
|
||||||
|
>("/admin/promotions", adminPromotionKeys.list(query), query, options)
|
||||||
|
|
||||||
|
return { ...data, ...rest }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
export enum PromotionStatus {
|
||||||
|
SCHEDULED = "SCHEDULED",
|
||||||
|
EXPIRED = "EXPIRED",
|
||||||
|
ACTIVE = "ACTIVE",
|
||||||
|
DISABLED = "DISABLED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPromotionStatus = (promotion: PromotionDTO) => {
|
||||||
|
const date = new Date()
|
||||||
|
const campaign = promotion.campaign
|
||||||
|
|
||||||
|
if (!campaign) {
|
||||||
|
return PromotionStatus.ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(campaign.starts_at!) > date) {
|
||||||
|
return PromotionStatus.SCHEDULED
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudget = campaign.budget
|
||||||
|
const overBudget =
|
||||||
|
campaignBudget && campaignBudget.used! > campaignBudget.limit!
|
||||||
|
|
||||||
|
if ((campaign.ends_at && new Date(campaign.ends_at) < date) || overBudget) {
|
||||||
|
return PromotionStatus.EXPIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
return PromotionStatus.ACTIVE
|
||||||
|
}
|
||||||
@@ -77,6 +77,18 @@ export const v2Routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/promotions",
|
||||||
|
handle: {
|
||||||
|
crumb: () => "Promotions",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
lazy: () => import("../../v2-routes/promotions/promotion-list"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./promotion-list-table"
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { PromotionDTO } from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
|
import { useAdminDeleteDiscount } from "medusa-react"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, Outlet, useLoaderData } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
|
import { usePromotionTableColumns } from "../../../../../hooks/table/columns-v2/use-promotion-table-columns"
|
||||||
|
import { usePromotionTableFilters } from "../../../../../hooks/table/filters-v2/use-promotion-table-filters"
|
||||||
|
import { usePromotionTableQuery } from "../../../../../hooks/table/query-v2/use-promotion-table-query"
|
||||||
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useV2Promotions } from "../../../../../lib/api-v2"
|
||||||
|
import { promotionsLoader } from "../../loader"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
export const PromotionListTable = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<ReturnType<typeof promotionsLoader>>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { searchParams, raw } = usePromotionTableQuery({ pageSize: PAGE_SIZE })
|
||||||
|
const { promotions, count, isLoading, isError, error } = useV2Promotions(
|
||||||
|
{ ...searchParams },
|
||||||
|
{
|
||||||
|
initialData,
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const filters = usePromotionTableFilters()
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const { table } = useDataTable({
|
||||||
|
data: (promotions ?? []) as PromotionDTO[],
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
enablePagination: true,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="divide-y p-0">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("promotions.domain")}</Heading>
|
||||||
|
|
||||||
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
<Link to="create">{t("actions.create")}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
table={table}
|
||||||
|
columns={columns}
|
||||||
|
count={count}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
filters={filters}
|
||||||
|
search
|
||||||
|
pagination
|
||||||
|
isLoading={isLoading}
|
||||||
|
queryObject={raw}
|
||||||
|
navigateTo={(row) => `${row.original.id}`}
|
||||||
|
orderBy={["created_at", "updated_at"]}
|
||||||
|
/>
|
||||||
|
<Outlet />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PromotionActions = ({ promotion }: { promotion: PromotionDTO }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
// TODO: change to promotions delete endpoint
|
||||||
|
const { mutateAsync } = useAdminDeleteDiscount(promotion.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("promotions.deleteWarning", {
|
||||||
|
code: promotion.code,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle error scenario here
|
||||||
|
await mutateAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `/promotions/${promotion.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<PromotionDTO>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const base = usePromotionTableColumns()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
...base,
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <PromotionActions promotion={row.original} />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[base]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { promotionsLoader } from "./loader"
|
||||||
|
export { PromotionsList as Component } from "./promotions-list.tsx"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { AdminPromotionsListRes } from "@medusajs/medusa"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { QueryClient } from "@tanstack/react-query"
|
||||||
|
import { adminPromotionKeys, useV2Promotions } from "../../../lib/api-v2"
|
||||||
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const promotionsListQuery = () => ({
|
||||||
|
queryKey: adminPromotionKeys.list(),
|
||||||
|
queryFn: async () => useV2Promotions({ limit: 20, offset: 0 }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const promotionsLoader = (client: QueryClient) => {
|
||||||
|
return async () => {
|
||||||
|
const query = promotionsListQuery()
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminPromotionsListRes>>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await client.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import after from "medusa-admin:widgets/promotion/list/after"
|
||||||
|
import before from "medusa-admin:widgets/promotion/list/before"
|
||||||
|
|
||||||
|
import { PromotionListTable } from "./components/promotion-list-table"
|
||||||
|
|
||||||
|
export const PromotionsList = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{before.widgets.map((w, i) => (
|
||||||
|
<w.Component key={i} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PromotionListTable />
|
||||||
|
|
||||||
|
{after.widgets.map((w, i) => (
|
||||||
|
<w.Component key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./store/"
|
export * from "./admin"
|
||||||
export * from "./admin/"
|
export * from "./store"
|
||||||
|
export * from "./utils"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./contexts"
|
export * from "./contexts"
|
||||||
export * from "./hooks/"
|
|
||||||
export * from "./helpers"
|
export * from "./helpers"
|
||||||
|
export * from "./hooks"
|
||||||
|
export * from "./types"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./promotions"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./validators"
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
|
import { createPromotionsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { CreatePromotionDTO, IPromotionModuleService } from "@medusajs/types"
|
import { CreatePromotionDTO, IPromotionModuleService } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../types/routing"
|
} from "../../../types/routing"
|
||||||
|
|
||||||
import { createPromotionsWorkflow } from "@medusajs/core-flows"
|
|
||||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
req: AuthenticatedMedusaRequest,
|
req: AuthenticatedMedusaRequest,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PaginatedResponse, PromotionDTO } from "@medusajs/types"
|
||||||
|
|
||||||
|
export type AdminPromotionsListRes = PaginatedResponse<{
|
||||||
|
promotions: PromotionDTO[]
|
||||||
|
}>
|
||||||
@@ -20,7 +20,11 @@ import {
|
|||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
|
import {
|
||||||
|
DateComparisonOperator,
|
||||||
|
FindParams,
|
||||||
|
extendedFindParamsMixin,
|
||||||
|
} from "../../../types/common"
|
||||||
import { XorConstraint } from "../../../types/validators/xor"
|
import { XorConstraint } from "../../../types/validators/xor"
|
||||||
import { AdminPostCampaignsReq } from "../campaigns/validators"
|
import { AdminPostCampaignsReq } from "../campaigns/validators"
|
||||||
|
|
||||||
@@ -33,6 +37,29 @@ export class AdminGetPromotionsParams extends extendedFindParamsMixin({
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
code?: string
|
code?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search terms to search promotions' code fields.
|
||||||
|
*/
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
q?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date filters to apply on the promotions' `created_at` date.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
created_at?: DateComparisonOperator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date filters to apply on the promotions' `updated_at` date.
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => DateComparisonOperator)
|
||||||
|
updated_at?: DateComparisonOperator
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminPostPromotionsReq {
|
export class AdminPostPromotionsReq {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./api"
|
export * from "./api"
|
||||||
|
export * from "./api-v2"
|
||||||
export * from "./api/middlewares"
|
export * from "./api/middlewares"
|
||||||
export * from "./interfaces"
|
export * from "./interfaces"
|
||||||
export * from "./joiner-config"
|
export * from "./joiner-config"
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export type RequestQueryFields = {
|
|||||||
*
|
*
|
||||||
* Fields included in the response if it's paginated.
|
* Fields included in the response if it's paginated.
|
||||||
*/
|
*/
|
||||||
export type PaginatedResponse = {
|
export type PaginatedResponse<T = unknown> = {
|
||||||
/**
|
/**
|
||||||
* The limit applied on the retrieved items.
|
* The limit applied on the retrieved items.
|
||||||
*/
|
*/
|
||||||
@@ -245,7 +245,7 @@ export type PaginatedResponse = {
|
|||||||
* The total count of items.
|
* The total count of items.
|
||||||
*/
|
*/
|
||||||
count: number
|
count: number
|
||||||
}
|
} & T
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fields returned in the response of a DELETE request.
|
* The fields returned in the response of a DELETE request.
|
||||||
|
|||||||
Reference in new issue
Block a user