feat(admin-shared,dashboard,js-sdk,types): refund reasons in dashboard (#13575)

CLOSES CORE-1209

This PR just adds the stuff necessary to support refund reasons in the dashboard. It adds the option in the settings tab and allows viewing, creating, editing and deleting refund reasons. I hate to open such a big PR but most of it is copy pasted from the return reasons. Major difference is only the fact that refund reasons don't have a `value` field
This commit is contained in:
William Bouchard
2025-09-23 11:51:40 -04:00
committed by GitHub
parent 543c9f7d0f
commit 5e827ec95d
39 changed files with 1190 additions and 131 deletions

View File

@@ -43,6 +43,10 @@ const useSettingRoutes = (): INavItem[] => {
label: t("returnReasons.domain"),
to: "/settings/return-reasons",
},
{
label: t("refundReasons.domain"),
to: "/settings/refund-reasons",
},
{
label: t("salesChannels.domain"),
to: "/settings/sales-channels",

View File

@@ -1780,6 +1780,42 @@ export function getRouteMap({
},
],
},
{
path: "refund-reasons",
element: <Outlet />,
handle: {
breadcrumb: () => t("refundReasons.domain"),
},
children: [
{
path: "",
lazy: () =>
import("../../routes/refund-reasons/refund-reason-list"),
children: [
{
path: "create",
lazy: () =>
import(
"../../routes/refund-reasons/refund-reason-create"
),
},
{
path: ":id",
children: [
{
path: "edit",
lazy: () =>
import(
"../../routes/refund-reasons/refund-reason-edit"
),
},
],
},
],
},
],
},
...(settingsRoutes?.[0]?.children || []),
],
},

View File

@@ -1,29 +1,124 @@
import { HttpTypes } from "@medusajs/types"
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
import { sdk } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { FetchError } from "@medusajs/js-sdk"
import {
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from "@tanstack/react-query"
const REFUND_REASON_QUERY_KEY = "refund-reason" as const
export const refundReasonQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
import { FetchError } from "@medusajs/js-sdk"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
const REFUND_REASONS_QUERY_KEY = "refund_reasons" as const
export const refundReasonsQueryKeys = queryKeysFactory(REFUND_REASONS_QUERY_KEY)
export const useRefundReasons = (
query?: HttpTypes.RefundReasonFilters,
query?: HttpTypes.AdminRefundReasonListParams,
options?: Omit<
UseQueryOptions<
HttpTypes.RefundReasonsResponse,
HttpTypes.AdminRefundReasonListResponse,
FetchError,
HttpTypes.RefundReasonsResponse,
QueryKey
HttpTypes.AdminRefundReasonListResponse
>,
"queryKey" | "queryFn"
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.refundReason.list(query),
queryKey: [],
queryKey: refundReasonsQueryKeys.list(query),
...options,
})
return { ...data, ...rest }
}
export const useRefundReason = (
id: string,
query?: HttpTypes.AdminRefundReasonParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminRefundReasonResponse,
FetchError,
HttpTypes.AdminRefundReasonResponse
>,
"queryFn" | "queryKey"
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.refundReason.retrieve(id, query),
queryKey: refundReasonsQueryKeys.detail(id),
...options,
})
return { ...data, ...rest }
}
export const useCreateRefundReason = (
query?: HttpTypes.AdminRefundReasonParams,
options?: UseMutationOptions<
HttpTypes.RefundReasonResponse,
FetchError,
HttpTypes.AdminCreateRefundReason
>
) => {
return useMutation({
mutationFn: async (data) => sdk.admin.refundReason.create(data, query),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: refundReasonsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateRefundReason = (
id: string,
options?: UseMutationOptions<
HttpTypes.AdminRefundReasonResponse,
FetchError,
HttpTypes.AdminUpdateRefundReason
>
) => {
return useMutation({
mutationFn: async (data) => sdk.admin.refundReason.update(id, data),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: refundReasonsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: refundReasonsQueryKeys.detail(data.refund_reason.id),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeleteRefundReasonLazy = (
options?: UseMutationOptions<
HttpTypes.AdminRefundReasonDeleteResponse,
FetchError,
string
>
) => {
return useMutation({
mutationFn: (id: string) => sdk.admin.refundReason.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: refundReasonsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: refundReasonsQueryKeys.details(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -6,6 +6,7 @@ export * from "./use-order-table-columns"
export * from "./use-product-table-columns"
export * from "./use-product-tag-table-columns"
export * from "./use-product-type-table-columns"
export * from "./use-refund-reason-table-columns"
export * from "./use-region-table-columns"
export * from "./use-return-reason-table-columns"
export * from "./use-tax-rates-table-columns"

View File

@@ -0,0 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { createDataTableColumnHelper } from "@medusajs/ui"
import { DescriptionCell } from "../../../components/table/table-cells/sales-channel/description-cell"
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminRefundReason>()
export const useRefundReasonTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("label", {
header: () => t("fields.label"),
enableSorting: true,
sortLabel: t("fields.label"),
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("description", {
header: () => t("fields.description"),
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
enableSorting: true,
sortLabel: t("fields.description"),
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
],
[t]
)
}

View File

@@ -6,6 +6,7 @@ export * from "./use-order-table-query"
export * from "./use-product-table-query"
export * from "./use-product-tag-table-query"
export * from "./use-product-type-table-query"
export * from "./use-refund-reason-table-query"
export * from "./use-region-table-query"
export * from "./use-return-reason-table-query"
export * from "./use-shipping-option-table-query"

View File

@@ -0,0 +1,32 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseRefundReasonTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useRefundReasonTableQuery = ({
prefix,
pageSize = 20,
}: UseRefundReasonTableQueryProps) => {
const queryObject = useQueryParams(
["offset", "q", "order", "created_at", "updated_at"],
prefix
)
const { offset, q, order, created_at, updated_at } = queryObject
const searchParams: HttpTypes.AdminRefundReasonListParams = {
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,
}
}

View File

@@ -161,6 +161,9 @@
"noRecordsMessage": {
"type": "string"
},
"noRecordsMessageFiltered": {
"type": "string"
},
"unsavedChangesTitle": {
"type": "string"
},
@@ -229,6 +232,7 @@
"noSearchResultsFor",
"noRecordsTitle",
"noRecordsMessage",
"noRecordsMessageFiltered",
"unsavedChangesTitle",
"unsavedChangesDescription",
"includesTaxTooltip",
@@ -10202,6 +10206,115 @@
],
"additionalProperties": false
},
"refundReasons": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"calloutHint": {
"type": "string"
},
"editReason": {
"type": "string"
},
"create": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"hint": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["header", "subtitle", "hint", "successToast"],
"additionalProperties": false
},
"edit": {
"type": "object",
"properties": {
"header": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["header", "subtitle", "successToast"],
"additionalProperties": false
},
"delete": {
"type": "object",
"properties": {
"confirmation": {
"type": "string"
},
"successToast": {
"type": "string"
}
},
"required": ["confirmation", "successToast"],
"additionalProperties": false
},
"fields": {
"type": "object",
"properties": {
"label": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
}
},
"required": ["label", "placeholder"],
"additionalProperties": false
},
"description": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"placeholder": {
"type": "string"
}
},
"required": ["label", "placeholder"],
"additionalProperties": false
}
},
"required": ["label", "description"],
"additionalProperties": false
}
},
"required": [
"domain",
"subtitle",
"calloutHint",
"editReason",
"create",
"edit",
"delete",
"fields"
],
"additionalProperties": false
},
"login": {
"type": "object",
"properties": {
@@ -11554,70 +11667,6 @@
],
"additionalProperties": false
},
"views": {
"type": "object",
"properties": {
"save": {
"type": "string"
},
"saveAsNew": {
"type": "string"
},
"updateDefaultForEveryone": {
"type": "string"
},
"updateViewName": {
"type": "string"
},
"prompts": {
"type": "object",
"properties": {
"updateDefault": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"confirmText": {
"type": "string"
},
"cancelText": {
"type": "string"
}
},
"required": ["title", "description", "confirmText", "cancelText"],
"additionalProperties": false
},
"updateView": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"confirmText": {
"type": "string"
},
"cancelText": {
"type": "string"
}
},
"required": ["title", "description", "confirmText", "cancelText"],
"additionalProperties": false
}
},
"required": ["updateDefault", "updateView"],
"additionalProperties": false
}
},
"required": ["save", "saveAsNew", "updateDefaultForEveryone", "updateViewName", "prompts"],
"additionalProperties": false
},
"dateTime": {
"type": "object",
"properties": {
@@ -11681,6 +11730,76 @@
"seconds_other"
],
"additionalProperties": false
},
"views": {
"type": "object",
"properties": {
"save": {
"type": "string"
},
"saveAsNew": {
"type": "string"
},
"updateDefaultForEveryone": {
"type": "string"
},
"updateViewName": {
"type": "string"
},
"prompts": {
"type": "object",
"properties": {
"updateDefault": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"confirmText": {
"type": "string"
},
"cancelText": {
"type": "string"
}
},
"required": ["title", "description", "confirmText", "cancelText"],
"additionalProperties": false
},
"updateView": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"confirmText": {
"type": "string"
},
"cancelText": {
"type": "string"
}
},
"required": ["title", "description", "confirmText", "cancelText"],
"additionalProperties": false
}
},
"required": ["updateDefault", "updateView"],
"additionalProperties": false
}
},
"required": [
"save",
"saveAsNew",
"updateDefaultForEveryone",
"updateViewName",
"prompts"
],
"additionalProperties": false
}
},
"required": [
@@ -11724,6 +11843,7 @@
"salesChannels",
"apiKeyManagement",
"returnReasons",
"refundReasons",
"login",
"invite",
"resetPassword",
@@ -11736,8 +11856,8 @@
"statuses",
"labels",
"fields",
"views",
"dateTime"
"dateTime",
"views"
],
"additionalProperties": false
}

View File

@@ -52,6 +52,7 @@
"noSearchResultsFor": "No search results for <0>'{{query}}'</0>",
"noRecordsTitle": "No records",
"noRecordsMessage": "There are no records to show",
"noRecordsMessageFiltered": "There are no records to show matching the filters",
"unsavedChangesTitle": "Are you sure you want to leave this form?",
"unsavedChangesDescription": "You have unsaved changes that will be lost if you exit this form.",
"includesTaxTooltip": "Prices in this column are tax inclusive.",
@@ -2740,6 +2741,37 @@
}
}
},
"refundReasons": {
"domain": "Refund Reasons",
"subtitle": "Manage reasons for issuing refunds.",
"calloutHint": "Manage the reasons to categorize refunds.",
"editReason": "Edit Refund Reason",
"create": {
"header": "Add Refund Reason",
"subtitle": "Specify the most common reasons for refunds.",
"hint": "Create a new refund reason to categorize refunds.",
"successToast": "Refund reason {{label}} was successfully created."
},
"edit": {
"header": "Edit Refund Reason",
"subtitle": "Edit the value of the refund reason.",
"successToast": "Refund reason {{label}} was successfully updated."
},
"delete": {
"confirmation": "You are about to delete the refund reason \"{{label}}\". This action cannot be undone.",
"successToast": "Refund reason was successfully deleted."
},
"fields": {
"label": {
"label": "Label",
"placeholder": "Gesture of goodwill"
},
"description": {
"label": "Description",
"placeholder": "Customer had a bad shopping experience"
}
}
},
"login": {
"forgotPassword": "Forgot password? - <0>Reset</0>",
"title": "Welcome to Medusa",

View File

@@ -0,0 +1 @@
export { RefundReasonCreateForm } from "./refund-reason-create-form"

View File

@@ -0,0 +1,131 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text, Textarea, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCreateRefundReason } from "../../../../../hooks/api"
const RefundReasonCreateSchema = z.object({
label: z.string().min(1),
description: z.string().optional(),
})
export const RefundReasonCreateForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof RefundReasonCreateSchema>>({
defaultValues: {
label: "",
description: "",
},
resolver: zodResolver(RefundReasonCreateSchema),
})
const { mutateAsync, isPending } = useCreateRefundReason()
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: ({ refund_reason }) => {
toast.success(
t("refundReasons.create.successToast", {
label: refund_reason.label,
})
)
handleSuccess(`../`)
},
onError: (error) => {
toast.error(error.message)
},
})
})
return (
<RouteFocusModal.Form form={form}>
<KeyboundForm
className="flex size-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex flex-1 justify-center overflow-auto px-6 py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div className="flex flex-col gap-y-1">
<RouteFocusModal.Title asChild>
<Heading>{t("refundReasons.create.header")}</Heading>
</RouteFocusModal.Title>
<RouteFocusModal.Description asChild>
<Text size="small" className="text-ui-fg-subtle">
{t("refundReasons.create.subtitle")}
</Text>
</RouteFocusModal.Description>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="label"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("refundReasons.fields.label.label")}
</Form.Label>
<Form.Control>
<Input
{...field}
placeholder={t(
"refundReasons.fields.label.placeholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<Form.Field
control={form.control}
name="description"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("refundReasons.fields.description.label")}
</Form.Label>
<Form.Control>
<Textarea
{...field}
placeholder={t(
"refundReasons.fields.description.placeholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
<div className="flex items-center justify-end gap-2">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary" type="button">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Footer>
</KeyboundForm>
</RouteFocusModal.Form>
)
}

View File

@@ -0,0 +1 @@
export { RefundReasonCreate as Component } from "./refund-reason-create"

View File

@@ -0,0 +1,10 @@
import { RouteFocusModal } from "../../../components/modals"
import { RefundReasonCreateForm } from "./components/refund-reason-create-form"
export const RefundReasonCreate = () => {
return (
<RouteFocusModal>
<RefundReasonCreateForm />
</RouteFocusModal>
)
}

View File

@@ -0,0 +1 @@
export { RefundReasonEditForm } from "./refund-reason-edit-form"

View File

@@ -0,0 +1,119 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types"
import { Button, Input, Textarea, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useUpdateRefundReason } from "../../../../../hooks/api/refund-reasons"
type RefundReasonEditFormProps = {
refundReason: HttpTypes.AdminRefundReason
}
const RefundReasonEditSchema = z.object({
label: z.string().min(1),
description: z.string().optional(),
})
export const RefundReasonEditForm = ({
refundReason,
}: RefundReasonEditFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const form = useForm<z.infer<typeof RefundReasonEditSchema>>({
defaultValues: {
label: refundReason.label,
description: refundReason.description ?? undefined,
},
resolver: zodResolver(RefundReasonEditSchema),
})
const { mutateAsync, isPending } = useUpdateRefundReason(refundReason.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: ({ refund_reason }) => {
toast.success(
t("refundReasons.edit.successToast", {
label: refund_reason.label,
})
)
handleSuccess()
},
onError: (error) => {
toast.error(error.message)
},
})
})
return (
<RouteDrawer.Form form={form}>
<KeyboundForm
className="flex size-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto">
<Form.Field
control={form.control}
name="label"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("refundReasons.fields.label.label")}
</Form.Label>
<Form.Control>
<Input
{...field}
placeholder={t("refundReasons.fields.label.placeholder")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="description"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("refundReasons.fields.description.label")}
</Form.Label>
<Form.Control>
<Textarea
{...field}
placeholder={t(
"refundReasons.fields.description.placeholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</RouteDrawer.Body>
<RouteDrawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<RouteDrawer.Close asChild>
<Button variant="secondary" size="small" type="button">
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>
</RouteDrawer.Footer>
</KeyboundForm>
</RouteDrawer.Form>
)
}

View File

@@ -0,0 +1 @@
export { RefundReasonEdit as Component } from "./refund-reason-edit"

View File

@@ -0,0 +1,33 @@
import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/modals"
import { useRefundReason } from "../../../hooks/api/refund-reasons"
import { RefundReasonEditForm } from "./components/refund-reason-edit-form"
export const RefundReasonEdit = () => {
const { id } = useParams()
const { t } = useTranslation()
const { refund_reason, isPending, isError, error } = useRefundReason(id!)
const ready = !isPending && !!refund_reason
if (isError) {
throw error
}
return (
<RouteDrawer>
<RouteDrawer.Header>
<RouteDrawer.Title asChild>
<Heading>{t("refundReasons.edit.header")}</Heading>
</RouteDrawer.Title>
<RouteDrawer.Description className="sr-only">
{t("refundReasons.edit.subtitle")}
</RouteDrawer.Description>
</RouteDrawer.Header>
{ready && <RefundReasonEditForm refundReason={refund_reason} />}
</RouteDrawer>
)
}

View File

@@ -0,0 +1 @@
export { RefundReasonListTable } from "./refund-reason-list-table"

View File

@@ -0,0 +1,130 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Container, createDataTableColumnHelper, toast, usePrompt, } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../../components/data-table"
import { useDeleteRefundReasonLazy, useRefundReasons, } from "../../../../../hooks/api"
import { useRefundReasonTableColumns } from "../../../../../hooks/table/columns"
import { useRefundReasonTableQuery } from "../../../../../hooks/table/query"
const PAGE_SIZE = 20
export const RefundReasonListTable = () => {
const { t } = useTranslation()
const { searchParams } = useRefundReasonTableQuery({
pageSize: PAGE_SIZE,
})
const { refund_reasons, count, isPending, isError, error } = useRefundReasons(
searchParams,
{
placeholderData: keepPreviousData,
}
)
const columns = useColumns()
if (isError) {
throw error
}
return (
<Container className="divide-y px-0 py-0">
<DataTable
data={refund_reasons}
columns={columns}
rowCount={count}
pageSize={PAGE_SIZE}
getRowId={(row) => row.id}
heading={t("refundReasons.domain")}
subHeading={t("refundReasons.subtitle")}
emptyState={{
empty: {
heading: t("general.noRecordsMessage"),
},
filtered: {
heading: t("general.noRecordsMessage"),
description: t("general.noRecordsMessageFiltered"),
},
}}
actions={[
{
label: t("actions.create"),
to: "create",
},
]}
isLoading={isPending}
enableSearch={true}
/>
</Container>
)
}
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminRefundReason>()
const useColumns = () => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const base = useRefundReasonTableColumns()
const { mutateAsync } = useDeleteRefundReasonLazy()
const handleDelete = useCallback(
async (refundReason: HttpTypes.AdminRefundReason) => {
const confirm = await prompt({
title: t("general.areYouSure"),
description: t("refundReasons.delete.confirmation", {
label: refundReason.label,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!confirm) {
return
}
await mutateAsync(refundReason.id, {
onSuccess: () => {
toast.success(t("refundReasons.delete.successToast"))
},
onError: (e) => {
toast.error(e.message)
},
})
},
[t, prompt, mutateAsync]
)
return useMemo(
() => [
...base,
columnHelper.action({
actions: (ctx) => [
[
{
icon: <PencilSquare />,
label: t("actions.edit"),
onClick: () =>
navigate(
`/settings/refund-reasons/${ctx.row.original.id}/edit`
),
},
],
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: () => handleDelete(ctx.row.original),
},
],
],
}),
],
[base, handleDelete, navigate, t]
)
}

View File

@@ -0,0 +1,2 @@
export { refundReasonListLoader as loader } from "./loader"
export { RefundReasonList as Component } from "./refund-reason-list"

View File

@@ -0,0 +1,16 @@
import { refundReasonsQueryKeys } from "../../../hooks/api"
import { sdk } from "../../../lib/client"
import { queryClient } from "../../../lib/query-client"
const refundReasonListQuery = () => ({
queryKey: refundReasonsQueryKeys.list(),
queryFn: async () => sdk.admin.refundReason.list(),
})
export const refundReasonListLoader = async () => {
const query = refundReasonListQuery()
return (
queryClient.getQueryData(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -0,0 +1,21 @@
import { SingleColumnPage } from "../../../components/layout/pages"
import { useExtension } from "../../../providers/extension-provider"
import { RefundReasonListTable } from "./components/refund-reason-list-table"
export const RefundReasonList = () => {
const { getWidgets } = useExtension()
return (
<SingleColumnPage
showMetadata={false}
showJSON={false}
hasOutlet
widgets={{
after: getWidgets("refund_reason.list.after"),
before: getWidgets("refund_reason.list.before"),
}}
>
<RefundReasonListTable />
</SingleColumnPage>
)
}