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:
+1
@@ -0,0 +1 @@
|
||||
export { RefundReasonCreateForm } from "./refund-reason-create-form"
|
||||
+131
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonCreate as Component } from "./refund-reason-create"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { RefundReasonCreateForm } from "./components/refund-reason-create-form"
|
||||
|
||||
export const RefundReasonCreate = () => {
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
<RefundReasonCreateForm />
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { RefundReasonEditForm } from "./refund-reason-edit-form"
|
||||
+119
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { RefundReasonEdit as Component } from "./refund-reason-edit"
|
||||
+33
@@ -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>
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { RefundReasonListTable } from "./refund-reason-list-table"
|
||||
+130
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { refundReasonListLoader as loader } from "./loader"
|
||||
export { RefundReasonList as Component } from "./refund-reason-list"
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
+21
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user