fix(dashboard,medusa): Fixes to Customer and Customer Groups domains (#7081)

**What**
- Cleanup of domains
- Adds toasts
- Adds delete customer hook
- Fixes validation of create and update customer endpoints.
This commit is contained in:
Kasper Fabricius Kristensen
2024-04-17 10:32:21 +02:00
committed by GitHub
parent 122b3ea76b
commit 7e66dd0dd0
29 changed files with 636 additions and 390 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): Fix validation of V2 POST /customers and POST /customers/:id

View File

@@ -43,7 +43,9 @@
"unsavedChangesTitle": "Are you sure you want to leave this page?",
"unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page.",
"includesTaxTooltip": "Enter the total amount including tax. The net amount excluding tax will be automatically calculated and saved.",
"timeline": "Timeline"
"timeline": "Timeline",
"success": "Success",
"error": "Error"
},
"validation": {
"mustBeInt": "The value must be a whole number.",
@@ -353,29 +355,56 @@
},
"customers": {
"domain": "Customers",
"editCustomer": "Edit Customer",
"createCustomer": "Create Customer",
"createCustomerHint": "Create a new customer to manage their details.",
"passwordHint": "Create a password for the customer to use when logging in to the storefront. Make sure that you communicate the password to the customer.",
"changePassword": "Change password",
"changePasswordPromptTitle": "Change password",
"changePasswordPromptDescription": "You are about to change the password for {{email}}. Make sure that you have communicated the new password to the customer before proceeding.",
"guest": "Guest",
"registered": "Registered",
"firstSeen": "First seen",
"viewOrder": "View order",
"groups": "Groups"
"create": {
"header": "Create Customer",
"hint": "Create a new customer to manage their details.",
"successToast": "Customer {{email}} was successfully created."
},
"edit": {
"header": "Edit Customer",
"emailDisabledTooltip": "The email address cannot be changed for registered customers.",
"successToast": "Customer {{email}} was successfully updated."
},
"delete": {
"title": "Delete Customer",
"description": "You are about to delete the customer {{email}}. This action cannot be undone.",
"successToast": "Customer {{email}} was successfully deleted."
},
"fields": {
"guest": "Guest",
"registered": "Registered",
"groups": "Groups"
}
},
"customerGroups": {
"domain": "Customer Groups",
"createGroup": "Create group",
"createCustomerGroup": "Create Customer Group",
"createCustomerGroupHint": "Create a new customer group to segment your customers.",
"customerAlreadyAdded": "The customer has already been added to the group.",
"editCustomerGroup": "Edit Customer Group",
"removeCustomersWarning_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.",
"removeCustomersWarning_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone.",
"deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone."
"create": {
"header": "Create Customer Group",
"hint": "Create a new customer group to segment your customers.",
"successToast": "Customer group {{name}} was successfully created."
},
"edit": {
"header": "Edit Customer Group",
"successToast": "Customer group {{name}} was successfully updated."
},
"delete": {
"title": "Delete Customer Group",
"description": "You are about to delete the customer group {{name}}. This action cannot be undone.",
"successToast": "Customer group {{name}} was successfully deleted."
},
"customers": {
"alreadyAddedTooltip": "The customer has already been added to the group.",
"add": {
"successToast_one": "Customer was successfully added to the group.",
"successToast_other": "Customers were successfully added to the group."
},
"remove": {
"title_one": "Remove customer",
"title_other": "Remove customers",
"description_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.",
"description_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone."
}
}
},
"orders": {
"domain": "Orders",
@@ -575,7 +604,7 @@
"addZone": "Add shipping zone",
"enablePickup": "Enable pickup",
"enableDelivery": "Enable delivery",
"noRecords" : {
"noRecords": {
"action": "Add Location",
"title": "No inventory locations",
"message": "Please create an invnetory location first."

View File

@@ -1,3 +1,5 @@
import { PlaceholderCell } from "../placeholder-cell"
type CellProps = {
text?: string | number
}
@@ -7,6 +9,10 @@ type HeaderProps = {
}
export const TextCell = ({ text }: CellProps) => {
if (!text) {
return <PlaceholderCell />
}
return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden">
<span className="truncate">{text}</span>
@@ -16,8 +22,8 @@ export const TextCell = ({ text }: CellProps) => {
export const TextHeader = ({ text }: HeaderProps) => {
return (
<div className=" flex h-full w-full items-center">
<span>{text}</span>
<div className="flex h-full w-full items-center">
<span className="truncate">{text}</span>
</div>
)
}

View File

@@ -9,7 +9,9 @@ export const AccountCell = ({ hasAccount }: AccountCellProps) => {
const { t } = useTranslation()
const color = hasAccount ? "green" : ("orange" as const)
const text = hasAccount ? t("customers.registered") : t("customers.guest")
const text = hasAccount
? t("customers.fields.registered")
: t("customers.fields.guest")
return <StatusCell color={color}>{text}</StatusCell>
}

View File

@@ -1,11 +1,16 @@
import { useTranslation } from "react-i18next"
import { DateCell } from "../../common/date-cell"
import { PlaceholderCell } from "../../common/placeholder-cell"
type FirstSeenCellProps = {
createdAt: Date
createdAt?: Date | string | null
}
export const FirstSeenCell = ({ createdAt }: FirstSeenCellProps) => {
if (!createdAt) {
return <PlaceholderCell />
}
return <DateCell date={createdAt} />
}
@@ -14,7 +19,7 @@ export const FirstSeenHeader = () => {
return (
<div className="flex h-full w-full items-center">
<span className="truncate">{t("customers.firstSeen")}</span>
<span className="truncate">{t("fields.createdAt")}</span>
</div>
)
}

View File

@@ -1,3 +1,8 @@
import {
AdminCustomerListResponse,
AdminCustomerResponse,
DeleteResponse,
} from "@medusajs/types"
import {
QueryKey,
UseMutationOptions,
@@ -9,10 +14,6 @@ import { client } from "../../lib/client"
import { queryClient } from "../../lib/medusa"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
import {
AdminCustomerResponse,
AdminCustomerListResponse,
} from "@medusajs/types"
const CUSTOMERS_QUERY_KEY = "customers" as const
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
@@ -88,3 +89,21 @@ export const useUpdateCustomer = (
...options,
})
}
export const useDeleteCustomer = (
id: string,
options?: UseMutationOptions<DeleteResponse, Error, void>
) => {
return useMutation({
mutationFn: () => client.customers.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: customersQueryKeys.detail(id),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -1,35 +1,34 @@
import { Text } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import {
CreatedAtCell,
CreatedAtHeader,
} from "../../../components/table/table-cells/common/created-at-cell"
import { NameHeader } from "../../../components/table/table-cells/common/name-cell"
TextCell,
TextHeader,
} from "../../../components/table/table-cells/common/text-cell"
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
export const useCustomerGroupTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.display({
id: "name",
header: () => <NameHeader />,
cell: ({
row: {
original: { name },
},
}) => <Text size="small">{name}</Text>,
columnHelper.accessor("name", {
header: () => <TextHeader text={t("fields.name")} />,
cell: ({ getValue }) => <TextCell text={getValue() || "-"} />,
}),
columnHelper.accessor("customers", {
header: () => <TextHeader text={t("customers.domain")} />,
cell: ({ getValue }) => {
const count = getValue()?.length ?? 0
columnHelper.accessor("created_at", {
header: () => <CreatedAtHeader />,
cell: ({ getValue }) => <CreatedAtCell date={getValue()} />,
return <TextCell text={count} />
},
}),
],
[]
[t]
)
}

View File

@@ -1,7 +1,7 @@
import { Customer } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { AdminCustomerResponse } from "@medusajs/types"
import {
EmailCell,
EmailHeader,
@@ -19,7 +19,7 @@ import {
FirstSeenHeader,
} from "../../../components/table/table-cells/customer/first-seen-cell"
const columnHelper = createColumnHelper<Customer>()
const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
export const useCustomerTableColumns = () => {
return useMemo(

View File

@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../../../components/table/data-table"
import { Filter } from "../../../components/table/data-table"
export const useCustomerGroupTableFilters = () => {
const { t } = useTranslation()

View File

@@ -1,9 +1,10 @@
import {
AdminCustomerListResponse,
AdminCustomerResponse,
DeleteResponse,
} from "@medusajs/types"
import { CreateCustomerReq, UpdateCustomerReq } from "../../types/api-payloads"
import { getRequest, postRequest } from "./common"
import { deleteRequest, getRequest, postRequest } from "./common"
async function retrieveCustomer(id: string, query?: Record<string, any>) {
return getRequest<AdminCustomerResponse>(`/admin/customers/${id}`, query)
@@ -21,9 +22,14 @@ async function updateCustomer(id: string, payload: UpdateCustomerReq) {
return postRequest<AdminCustomerResponse>(`/admin/customers/${id}`, payload)
}
async function deleteCustomer(id: string) {
return deleteRequest<DeleteResponse>(`/admin/customers/${id}`)
}
export const customers = {
retrieve: retrieveCustomer,
list: listCustomers,
create: createCustomer,
update: updateCustomer,
delete: deleteCustomer,
}

View File

@@ -49,7 +49,7 @@ export type UpdateApiKeyReq = UpdateApiKeyDTO
// Customers
export type CreateCustomerReq = CreateCustomerDTO
export type UpdateCustomerReq = UpdateCustomerDTO
export type UpdateCustomerReq = Omit<UpdateCustomerDTO, "id">
// Sales Channels
export type CreateSalesChannelReq = CreateSalesChannelDTO

View File

@@ -1,44 +1,27 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
import {
OnChangeFn,
PaginationState,
RowSelectionState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
adminCustomerKeys,
useAdminAddCustomersToCustomerGroup,
useAdminCustomers,
} from "medusa-react"
import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import {
NoRecords,
NoResults,
} from "../../../../../components/common/empty-table-content"
import { Query } from "../../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
import { AdminCustomerResponse } from "@medusajs/types"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa"
import { useCustomers } from "../../../../../hooks/api/customers"
import { DataTable } from "../../../../../components/table/data-table"
import { useAddCustomersToGroup } from "../../../../../hooks/api/customer-groups"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCustomers } from "../../../../../hooks/api/customers"
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 { DataTable } from "../../../../../components/table/data-table"
import { AdminCustomerResponse } from "@medusajs/types"
import { useDataTable } from "../../../../../hooks/use-data-table"
type AddCustomersFormProps = {
customerGroupId: string
@@ -65,19 +48,6 @@ export const AddCustomersForm = ({
const { setValue } = form
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
@@ -130,8 +100,7 @@ export const AddCustomersForm = ({
},
})
const { mutateAsync, isLoading: isMutating } =
useAddCustomersToGroup(customerGroupId)
const { mutateAsync, isPending } = useAddCustomersToGroup(customerGroupId)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
@@ -140,6 +109,13 @@ export const AddCustomersForm = ({
},
{
onSuccess: () => {
toast.success(t("general.success"), {
description: t("customerGroups.customers.add.successToast", {
count: data.customer_ids.length,
}),
dismissLabel: t("actions.close"),
})
handleSuccess(`/customer-groups/${customerGroupId}`)
},
}
@@ -172,13 +148,13 @@ export const AddCustomersForm = ({
type="submit"
variant="primary"
size="small"
isLoading={isMutating}
isLoading={isPending}
>
{t("general.add")}
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
table={table}
columns={columns}
@@ -194,6 +170,7 @@ export const AddCustomersForm = ({
"updated_at",
]}
isLoading={isLoading}
layout="fill"
search
queryObject={raw}
/>
@@ -207,6 +184,7 @@ const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => {
const { t } = useTranslation()
const base = useCustomerTableColumns()
const columns = useMemo(
() => [
@@ -244,7 +222,7 @@ const useColumns = () => {
if (isPreSelected) {
return (
<Tooltip
content={t("customerGroups.customerAlreadyAdded")}
content={t("customerGroups.customers.alreadyAddedTooltip")}
side="right"
>
{Component}
@@ -255,23 +233,9 @@ const useColumns = () => {
return Component
},
}),
columnHelper.accessor("email", {
header: t("fields.email"),
cell: ({ getValue }) => getValue(),
}),
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 || "-"
},
}),
...base,
],
[t]
[t, base]
)
return columns

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useAdminCreateCustomerGroup } from "medusa-react"
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -27,7 +26,7 @@ export const CreateCustomerGroupForm = () => {
resolver: zodResolver(CreateCustomerGroupSchema),
})
const { mutateAsync, isLoading } = useCreateCustomerGroup()
const { mutateAsync, isPending } = useCreateCustomerGroup()
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
@@ -36,8 +35,21 @@ export const CreateCustomerGroupForm = () => {
},
{
onSuccess: ({ customer_group }) => {
toast.success(t("general.success"), {
description: t("customerGroups.create.successToast", {
name: customer_group.name,
}),
dismissLabel: t("actions.close"),
})
handleSuccess(`/customer-groups/${customer_group.id}`)
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
}
)
})
@@ -56,7 +68,7 @@ export const CreateCustomerGroupForm = () => {
type="submit"
variant="primary"
size="small"
isLoading={isLoading}
isLoading={isPending}
>
{t("actions.create")}
</Button>
@@ -65,9 +77,9 @@ export const CreateCustomerGroupForm = () => {
<RouteFocusModal.Body className="flex flex-col items-center pt-[72px]">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("customerGroups.createCustomerGroup")}</Heading>
<Heading>{t("customerGroups.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("customerGroups.createCustomerGroupHint")}
{t("customerGroups.create.hint")}
</Text>
</div>
<div className="grid grid-cols-2 gap-4">

View File

@@ -1,27 +1,22 @@
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
import { Customer } from "@medusajs/medusa"
import { PencilSquare, Trash } from "@medusajs/icons"
import {
adminCustomerGroupKeys,
useAdminCustomPost,
useAdminCustomerGroupCustomers,
} from "medusa-react"
AdminCustomerGroupResponse,
AdminCustomerResponse,
} from "@medusajs/types"
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { Link } from "react-router-dom"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useRemoveCustomersFromGroup } from "../../../../../hooks/api/customer-groups"
import { useCustomers } from "../../../../../hooks/api/customers"
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"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { AdminCustomerGroupResponse, AdminCustomerResponse } from "@medusajs/types"
import { useCustomers } from "../../../../../hooks/api/customers"
import {
useRemoveCustomersFromGroup,
useUpdateCustomerGroup,
} from "../../../../../hooks/api/customer-groups"
type CustomerGroupCustomerSectionProps = {
group: AdminCustomerGroupResponse["customer_group"]
@@ -72,8 +67,10 @@ export const CustomerGroupCustomerSection = ({
const keys = Object.keys(rowSelection)
const res = await prompt({
title: t("general.areYouSure"),
description: t("customerGroups.removeCustomersWarning", {
title: t("customerGroups.customers.remove.title", {
count: keys.length,
}),
description: t("customerGroups.customers.remove.description", {
count: keys.length,
}),
confirmText: t("actions.continue"),
@@ -151,8 +148,10 @@ const CustomerActions = ({
const handleRemove = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("customerGroups.removeCustomersWarning", {
title: t("customerGroups.customers.remove.title", {
count: 1,
}),
description: t("customerGroups.customers.remove.description", {
count: 1,
}),
confirmText: t("actions.continue"),

View File

@@ -1,9 +1,9 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { Container, Heading, Text, toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
type CustomerGroupGeneralSectionProps = {
@@ -14,43 +14,80 @@ export const CustomerGroupGeneralSection = ({
group,
}: CustomerGroupGeneralSectionProps) => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => {
const res = await prompt({
title: t("customerGroups.delete.title"),
description: t("customerGroups.delete.description", {
name: group.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("customerGroups.delete.successToast", {
name: group.name,
}),
dismissLabel: t("actions.close"),
})
navigate("/customer-groups", { replace: true })
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
})
}
return (
<Container className="flex items-center justify-between px-6 py-4">
<Heading>{group.name}</Heading>
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/customer-groups/${group.id}/edit`,
},
],
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
},
]}
/>
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{group.name}</Heading>
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/customer-groups/${group.id}/edit`,
},
],
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
},
]}
/>
</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("customers.domain")}
</Text>
<Text size="small" leading="compact">
{group.customers?.length || "-"}
</Text>
</div>
</Container>
)
}

View File

@@ -1,9 +1,9 @@
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section"
import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
import { customerGroupLoader } from "./loader"
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
export const CustomerGroupDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -13,20 +13,18 @@ export const CustomerGroupDetail = () => {
const { id } = useParams()
const { customer_group, isLoading, isError, error } = useCustomerGroup(
id!,
undefined,
{
fields: "+customers.id",
},
{ initialData }
)
if (isLoading) {
if (isLoading || !customer_group) {
return <div>Loading...</div>
}
if (isError || !customer_group) {
if (error) {
throw error
}
throw json("An unknown error occurred", 500)
if (isError) {
throw error
}
return (

View File

@@ -7,7 +7,10 @@ import { medusa, queryClient } from "../../../lib/medusa"
const customerGroupDetailQuery = (id: string) => ({
queryKey: adminProductKeys.detail(id),
queryFn: async () => medusa.admin.customerGroups.retrieve(id),
queryFn: async () =>
medusa.admin.customerGroups.retrieve(id, {
fields: "+customers.id",
}),
})
export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomerGroup } from "medusa-react"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { Button, Input, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as z from "zod"
@@ -9,7 +9,6 @@ import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useUpdateCustomerGroup } from "../../../../../hooks/api/customer-groups"
type EditCustomerGroupFormProps = {
@@ -33,11 +32,18 @@ export const EditCustomerGroupForm = ({
resolver: zodResolver(EditCustomerGroupSchema),
})
const { mutateAsync, isLoading } = useUpdateCustomerGroup(group.id)
const { mutateAsync, isPending } = useUpdateCustomerGroup(group.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {
onSuccess: () => {
onSuccess: ({ customer_group }) => {
toast.success(t("general.success"), {
description: t("customerGroups.edit.successToast", {
name: customer_group.name,
}),
dismissLabel: t("actions.close"),
})
handleSuccess()
},
})
@@ -73,7 +79,7 @@ export const EditCustomerGroupForm = ({
{t("actions.cancel")}
</Button>
</RouteDrawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>

View File

@@ -2,8 +2,8 @@ import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal"
import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
export const CustomerGroupEdit = () => {
const { id } = useParams()
@@ -18,7 +18,7 @@ export const CustomerGroupEdit = () => {
return (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("customerGroups.editCustomerGroup")}</Heading>
<Heading>{t("customerGroups.edit.header")}</Heading>
</RouteDrawer.Header>
{!isLoading && customer_group && (
<EditCustomerGroupForm group={customer_group} />

View File

@@ -1,12 +1,21 @@
import { Button, Container, Heading } from "@medusajs/ui"
import { PencilSquare, Trash } from "@medusajs/icons"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import {
useCustomerGroups,
useDeleteCustomerGroup,
} from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
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"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
const PAGE_SIZE = 20
@@ -23,7 +32,7 @@ export const CustomerGroupListTable = () => {
})
const filters = useCustomerGroupTableFilters()
const columns = useCustomerGroupTableColumns()
const columns = useColumns()
const { table } = useDataTable({
data: customer_groups ?? [],
@@ -64,3 +73,89 @@ export const CustomerGroupListTable = () => {
</Container>
)
}
const CustomerGroupRowActions = ({
group,
}: {
group: AdminCustomerGroupResponse["customer_group"]
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => {
const res = await prompt({
title: t("customerGroups.delete.title"),
description: t("customerGroups.delete.description", {
name: group.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("customerGroups.delete.successToast", {
name: group.name,
}),
dismissLabel: t("actions.close"),
})
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
})
}
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 />,
},
],
},
]}
/>
)
}
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
const useColumns = () => {
const columns = useCustomerGroupTableColumns()
return useMemo(
() => [
...columns,
columnHelper.display({
id: "actions",
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
}),
],
[columns]
)
}

View File

@@ -1,90 +0,0 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
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: AdminCustomerGroupResponse["customer_group"]
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteCustomerGroup(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 />,
},
],
},
]}
/>
)
}

View File

@@ -1,32 +0,0 @@
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 = {
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

@@ -1,33 +1,37 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Button, Heading, Input, Text } from "@medusajs/ui"
import { Form } from "../../../../../components/common/form"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { Form } from "../../../../../components/common/form"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { zodResolver } from "@hookform/resolvers/zod"
import { useCreateCustomer } from "../../../../../hooks/api/customers"
const CreateCustomerSchema = zod.object({
email: zod.string().email(),
first_name: zod.string().min(1),
last_name: zod.string().min(1),
phone: zod.string().min(1).optional(),
first_name: zod.string().optional(),
last_name: zod.string().optional(),
company_name: zod.string().optional(),
phone: zod.string().optional(),
})
export const CreateCustomerForm = () => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const { mutateAsync, isLoading } = useCreateCustomer()
const { mutateAsync, isPending } = useCreateCustomer()
const form = useForm<zod.infer<typeof CreateCustomerSchema>>({
defaultValues: {
email: "",
first_name: "",
last_name: "",
phone: "",
company_name: "",
},
resolver: zodResolver(CreateCustomerSchema),
})
@@ -35,15 +39,28 @@ export const CreateCustomerForm = () => {
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
email: data.email,
first_name: data.first_name,
last_name: data.last_name,
phone: data.phone,
email: data.email || undefined,
first_name: data.first_name || undefined,
last_name: data.last_name || undefined,
company_name: data.company_name || undefined,
phone: data.phone || undefined,
},
{
onSuccess: ({ customer }) => {
toast.success(t("general.success"), {
description: t("customers.create.successToast", {
email: customer.email,
}),
dismissLabel: t("actions.close"),
})
handleSuccess(`/customers/${customer.id}`)
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
}
)
})
@@ -62,7 +79,7 @@ export const CreateCustomerForm = () => {
size="small"
variant="primary"
type="submit"
isLoading={isLoading}
isLoading={isPending}
>
{t("actions.create")}
</Button>
@@ -71,19 +88,19 @@ export const CreateCustomerForm = () => {
<RouteFocusModal.Body className="flex flex-col items-center py-16">
<div className="flex w-full max-w-[720px] flex-col gap-y-8">
<div>
<Heading>{t("customers.createCustomer")}</Heading>
<Heading>{t("customers.create.header")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("customers.createCustomerHint")}
{t("customers.create.hint")}
</Text>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name="first_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.firstName")}</Form.Label>
<Form.Label optional>{t("fields.firstName")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
@@ -98,7 +115,7 @@ export const CreateCustomerForm = () => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.lastName")}</Form.Label>
<Form.Label optional>{t("fields.lastName")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
@@ -122,6 +139,21 @@ export const CreateCustomerForm = () => {
)
}}
/>
<Form.Field
control={form.control}
name="company_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.company")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="phone"

View File

@@ -1,8 +1,17 @@
import { PencilSquare } from "@medusajs/icons"
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { PencilSquare, Trash } from "@medusajs/icons"
import { AdminCustomerResponse } from "@medusajs/types"
import {
Container,
Heading,
StatusBadge,
Text,
toast,
usePrompt,
} from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteCustomer } from "../../../../../hooks/api/customers"
type CustomerGeneralSectionProps = {
customer: AdminCustomerResponse["customer"]
@@ -12,6 +21,10 @@ export const CustomerGeneralSection = ({
customer,
}: CustomerGeneralSectionProps) => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const { mutateAsync } = useDeleteCustomer(customer.id)
const name = [customer.first_name, customer.last_name]
.filter(Boolean)
@@ -19,8 +32,44 @@ export const CustomerGeneralSection = ({
const statusColor = customer.has_account ? "green" : "orange"
const statusText = customer.has_account
? t("customers.registered")
: t("customers.guest")
? t("customers.fields.registered")
: t("customers.fields.guest")
const handleDelete = async () => {
const res = await prompt({
title: t("customers.delete.title"),
description: t("customers.delete.description", {
email: customer.email,
}),
verificationInstruction: t("general.typeToConfirm"),
verificationText: customer.email,
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("customers.delete.successToast", {
email: customer.email,
}),
dismissLabel: t("actions.close"),
})
navigate("/customers", { replace: true })
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
})
}
return (
<Container className="divide-y p-0">
@@ -39,6 +88,15 @@ export const CustomerGeneralSection = ({
},
],
},
{
actions: [
{
label: t("actions.delete"),
icon: <Trash />,
onClick: handleDelete,
},
],
},
]}
/>
</div>
@@ -48,7 +106,15 @@ export const CustomerGeneralSection = ({
{t("fields.name")}
</Text>
<Text size="small" leading="compact">
{name ?? "-"}
{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.company")}
</Text>
<Text size="small" leading="compact">
{customer.company_name || "-"}
</Text>
</div>
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
@@ -56,7 +122,7 @@ export const CustomerGeneralSection = ({
{t("fields.phone")}
</Text>
<Text size="small" leading="compact">
{customer.phone ?? "-"}
{customer.phone || "-"}
</Text>
</div>
</Container>

View File

@@ -1,17 +1,22 @@
import { Customer, CustomerGroup } from "@medusajs/medusa"
import { Container, Heading } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import {
AdminCustomerGroupResponse,
AdminCustomerResponse,
} from "@medusajs/types"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCustomerGroupTableFilters } from "../../../../customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters"
import { Container, Heading } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { t } from "i18next"
import { useMemo } from "react"
import { PencilSquare } from "@medusajs/icons"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type CustomerGroupSectionProps = {
customer: AdminCustomerResponse["customer"]
@@ -22,10 +27,20 @@ const PAGE_SIZE = 10
export const CustomerGroupSection = ({
customer,
}: CustomerGroupSectionProps) => {
const { raw, searchParams } = useCustomerGroupTableQuery({
pageSize: PAGE_SIZE,
})
const { customer_groups, count, isLoading, isError, error } =
useCustomerGroups({
customers: { id: customer.id },
})
useCustomerGroups(
{
...searchParams,
fields: "+customers.id",
customers: { id: customer.id },
},
{
placeholderData: keepPreviousData,
}
)
const filters = useCustomerGroupTableFilters()
const columns = useColumns()
@@ -64,27 +79,51 @@ export const CustomerGroupSection = ({
search
pagination
orderBy={["name", "created_at", "updated_at"]}
queryObject={raw}
/>
</Container>
)
}
// TODO: Add remove association when /customer-groups/:id/batch has been created.
const CustomerGroupRowActions = ({
group,
}: {
group: AdminCustomerGroupResponse["customer_group"]
}) => {
const { t } = useTranslation()
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
to: `/customer-groups/${group.id}/edit`,
},
],
},
]}
/>
)
}
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
const useColumns = () => {
const { t } = useTranslation()
const columns = useCustomerGroupTableColumns()
return useMemo(
() => [
...columns,
columnHelper.display({
id: "select",
}),
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => getValue(),
id: "actions",
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
}),
],
[t]
[columns]
)
}

View File

@@ -1,9 +1,9 @@
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { CustomerGeneralSection } from "./components/customer-general-section"
import { Outlet, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { customerLoader } from "./loader"
import { useCustomer } from "../../../hooks/api/customers"
import { CustomerGeneralSection } from "./components/customer-general-section"
import { CustomerGroupSection } from "./components/customer-group-section"
import { customerLoader } from "./loader"
export const CustomerDetail = () => {
const { id } = useParams()
@@ -15,16 +15,12 @@ export const CustomerDetail = () => {
initialData,
})
if (isLoading) {
if (isLoading || !customer) {
return <div>Loading...</div>
}
if (isError || !customer) {
if (error) {
throw error
}
throw json("An unknown error occurred", 500)
if (isError) {
throw error
}
return (

View File

@@ -1,14 +1,15 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminCustomerResponse } from "@medusajs/types"
import { Button, Input, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Button, Input } from "@medusajs/ui"
import { ConditionalTooltip } from "../../../../../components/common/conditional-tooltip"
import { Form } from "../../../../../components/common/form"
import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { Form } from "../../../../../components/common/form"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { zodResolver } from "@hookform/resolvers/zod"
import { AdminCustomerResponse } from "@medusajs/types"
import { useUpdateCustomer } from "../../../../../hooks/api/customers"
type EditCustomerFormProps = {
@@ -17,8 +18,9 @@ type EditCustomerFormProps = {
const EditCustomerSchema = zod.object({
email: zod.string().email(),
first_name: zod.string().min(1).optional(),
last_name: zod.string().min(1).optional(),
first_name: zod.string().optional(),
last_name: zod.string().optional(),
company_name: zod.string().optional(),
phone: zod.string().optional(),
})
@@ -31,25 +33,40 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
email: customer.email || "",
first_name: customer.first_name || "",
last_name: customer.last_name || "",
company_name: customer.company_name || "",
phone: customer.phone || "",
},
resolver: zodResolver(EditCustomerSchema),
})
const { mutateAsync, isLoading } = useUpdateCustomer(customer.id)
const { mutateAsync, isPending } = useUpdateCustomer(customer.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
{
email: customer.has_account ? undefined : data.email,
first_name: data.first_name,
last_name: data.last_name,
phone: data.phone,
first_name: data.first_name || null,
last_name: data.last_name || null,
phone: data.phone || null,
company_name: data.company_name || null,
},
{
onSuccess: () => {
onSuccess: ({ customer }) => {
toast.success(t("general.success"), {
description: t("customers.edit.successToast", {
email: customer.email,
}),
dismissLabel: t("actions.close"),
})
handleSuccess()
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
}
)
})
@@ -67,7 +84,12 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
<Form.Item>
<Form.Label>{t("fields.email")}</Form.Label>
<Form.Control>
<Input {...field} disabled={customer.has_account} />
<ConditionalTooltip
showTooltip={customer.has_account}
content={t("customers.edit.emailDisabledTooltip")}
>
<Input {...field} disabled={customer.has_account} />
</ConditionalTooltip>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
@@ -104,6 +126,21 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
)
}}
/>
<Form.Field
control={form.control}
name="company_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.company")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="phone"
@@ -129,7 +166,7 @@ export const EditCustomerForm = ({ customer }: EditCustomerFormProps) => {
</Button>
</RouteDrawer.Close>
<Button
isLoading={isLoading}
isLoading={isPending}
type="submit"
variant="primary"
size="small"

View File

@@ -1,17 +1,19 @@
import { PencilSquare } from "@medusajs/icons"
import { Button, Container, Heading } from "@medusajs/ui"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import { AdminCustomerResponse } from "@medusajs/types"
import { Button, Container, Heading } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useCustomers } from "../../../../../hooks/api/customers"
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"
import { useCustomers } from "../../../../../hooks/api/customers"
const PAGE_SIZE = 20
@@ -19,9 +21,14 @@ export const CustomerListTable = () => {
const { t } = useTranslation()
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
const { customers, count, isLoading, isError, error } = useCustomers({
...searchParams,
})
const { customers, count, isLoading, isError, error } = useCustomers(
{
...searchParams,
},
{
placeholderData: keepPreviousData,
}
)
const filters = useCustomerTableFilters()
const columns = useColumns()
@@ -110,5 +117,5 @@ const useColumns = () => {
}),
],
[columns]
) as ColumnDef<AdminCustomerResponse["customer"]>[]
)
}

View File

@@ -44,14 +44,20 @@ export const AdminCustomersParams = createFindParams({
)
export const AdminCreateCustomer = z.object({
email: z.string().email().optional(),
company_name: z.string().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
email: z.string().optional(),
phone: z.string().optional(),
})
export const AdminUpdateCustomer = AdminCreateCustomer
export const AdminUpdateCustomer = z.object({
email: z.string().email().nullable().optional(),
company_name: z.string().nullable().optional(),
first_name: z.string().nullable().optional(),
last_name: z.string().nullable().optional(),
phone: z.string().nullable().optional(),
})
export const AdminCreateCustomerAddress = z.object({
address_name: z.string().optional(),