feat: Admin V2 customer group (#7000)

This commit is contained in:
Oli Juhl
2024-04-09 12:32:27 +02:00
committed by GitHub
parent dbddfc12ed
commit 3882cac622
43 changed files with 583 additions and 309 deletions

View File

@@ -1,7 +1,7 @@
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { ICustomerModuleService } from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -13,7 +13,7 @@ const adminHeaders = {
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
describe("POST /admin/customer-groups/:id/customers/batch", () => {
describe("POST /admin/customer-groups/:id/customers/batch/add", () => {
let appContainer
let customerModuleService: ICustomerModuleService
@@ -48,7 +48,7 @@ medusaIntegrationTestRunner({
])
const response = await api.post(
`/admin/customer-groups/${group.id}/customers/batch`,
`/admin/customer-groups/${group.id}/customers/batch/add`,
{
customer_ids: customers.map((c) => ({ id: c.id })),
},

View File

@@ -1,7 +1,7 @@
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { ICustomerModuleService } from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -13,7 +13,7 @@ const adminHeaders = {
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
describe("DELETE /admin/customer-groups/:id/customers/remove", () => {
describe("POST /admin/customer-groups/:id/customers/batch/remove", () => {
let appContainer
let customerModuleService: ICustomerModuleService
@@ -55,7 +55,7 @@ medusaIntegrationTestRunner({
)
const response = await api.post(
`/admin/customer-groups/${group.id}/customers/remove`,
`/admin/customer-groups/${group.id}/customers/batch/remove`,
{
customer_ids: customers.map((c) => ({ id: c.id })),
},

View File

@@ -1,10 +1,21 @@
import {
QueryKey,
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
} from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import {
AdminCustomerGroupListResponse,
AdminCustomerGroupResponse,
} from "@medusajs/types"
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query"
import { client } from "../../lib/client"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { z } from "zod"
import { CreateCustomerGroupSchema } from "../../v2-routes/customer-groups/customer-group-create/components/create-customer-group-form"
import { queryClient } from "../../lib/medusa"
import { EditCustomerGroupSchema } from "../../v2-routes/customer-groups/customer-group-edit/components/edit-customer-group-form"
import { customersQueryKeys } from "./customers"
const CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const
const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY)
@@ -51,3 +62,124 @@ export const useCustomerGroups = (
return { ...data, ...rest }
}
export const useCreateCustomerGroup = (
options?: UseMutationOptions<
AdminCustomerGroupResponse,
Error,
z.infer<typeof CreateCustomerGroupSchema>
>
) => {
return useMutation({
mutationFn: (payload) => client.customerGroups.create(payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useUpdateCustomerGroup = (
id: string,
options?: UseMutationOptions<
AdminCustomerGroupResponse,
Error,
z.infer<typeof EditCustomerGroupSchema>
>
) => {
return useMutation({
mutationFn: (payload) => client.customerGroups.update(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.detail(id),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useDeleteCustomerGroup = (
id: string,
options?: UseMutationOptions<
{ id: string; object: "customer-group"; deleted: boolean },
Error,
void
>
) => {
return useMutation({
mutationFn: () => client.customerGroups.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.detail(id),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useAddCustomersToGroup = (
id: string,
options?: UseMutationOptions<
AdminCustomerGroupResponse,
Error,
{ customer_ids: { id: string }[] }
>
) => {
return useMutation({
mutationFn: (payload) => client.customerGroups.addCustomers(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.detail(id),
})
queryClient.invalidateQueries({
queryKey: customersQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useRemoveCustomersFromGroup = (
id: string,
options?: UseMutationOptions<
AdminCustomerGroupResponse,
Error,
{ customer_ids: { id: string }[] }
>
) => {
return useMutation({
mutationFn: (payload) => client.customerGroups.removeCustomers(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.detail(id),
})
queryClient.invalidateQueries({
queryKey: customersQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -15,7 +15,7 @@ import {
} from "@medusajs/types"
const CUSTOMERS_QUERY_KEY = "customers" as const
const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
export const useCustomer = (
id: string,

View File

@@ -2,7 +2,10 @@ import {
AdminCustomerGroupListResponse,
AdminCustomerGroupResponse,
} from "@medusajs/types"
import { getRequest } from "./common"
import { z } from "zod"
import { CreateCustomerGroupSchema } from "../../v2-routes/customer-groups/customer-group-create/components/create-customer-group-form"
import { EditCustomerGroupSchema } from "../../v2-routes/customer-groups/customer-group-edit/components/edit-customer-group-form"
import { deleteRequest, getRequest, postRequest } from "./common"
async function retrieveCustomerGroup(id: string, query?: Record<string, any>) {
return getRequest<AdminCustomerGroupResponse>(
@@ -18,7 +21,59 @@ async function listCustomerGroups(query?: Record<string, any>) {
)
}
async function createCustomerGroup(
payload: z.infer<typeof CreateCustomerGroupSchema>
) {
return postRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups`,
payload
)
}
async function updateCustomerGroup(
id: string,
payload: z.infer<typeof EditCustomerGroupSchema>
) {
return postRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups/${id}`,
payload
)
}
async function deleteCustomerGroup(id: string) {
return deleteRequest<{
id: string
deleted: boolean
object: "customer-group"
}>(`/admin/customer-groups/${id}`)
}
async function batchAddCustomers(
id: string,
payload: { customer_ids: { id: string }[] }
) {
return postRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups/${id}/customers/batch/add`,
payload
)
}
async function batchRemoveCustomers(
id: string,
payload: { customer_ids: { id: string }[] }
) {
return postRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups/${id}/customers/batch/remove`,
payload
)
}
export const customerGroups = {
retrieve: retrieveCustomerGroup,
list: listCustomerGroups,
create: createCustomerGroup,
update: updateCustomerGroup,
delete: deleteCustomerGroup,
addCustomers: batchAddCustomers,
removeCustomers: batchRemoveCustomers,
}

View File

@@ -11,6 +11,7 @@ import {
AdminProductCategoryResponse,
SalesChannelDTO,
UserDTO,
AdminCustomerGroupResponse,
} from "@medusajs/types"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { ErrorBoundary } from "../../components/error/error-boundary"
@@ -343,6 +344,55 @@ export const v2Routes: RouteObject[] = [
},
],
},
{
path: "/customer-groups",
handle: {
crumb: () => "Customer Groups",
},
children: [
{
path: "",
lazy: () =>
import("../../v2-routes/customer-groups/customer-group-list"),
children: [
{
path: "create",
lazy: () =>
import(
"../../v2-routes/customer-groups/customer-group-create"
),
},
],
},
{
path: ":id",
lazy: () =>
import(
"../../v2-routes/customer-groups/customer-group-detail"
),
handle: {
crumb: (data: AdminCustomerGroupResponse) =>
data.customer_group.name,
},
children: [
{
path: "edit",
lazy: () =>
import(
"../../v2-routes/customer-groups/customer-group-edit"
),
},
{
path: "add-customers",
lazy: () =>
import(
"../../v2-routes/customer-groups/customer-group-add-customers"
),
},
],
},
],
},
],
},
],

View File

@@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import {
OnChangeFn,
PaginationState,
RowSelectionState,
createColumnHelper,
@@ -31,12 +32,19 @@ import {
} from "../../../../../components/route-modal"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa"
import { useCustomers } from "../../../../../hooks/api/customers"
import { useAddCustomersToGroup } from "../../../../../hooks/api/customer-groups"
import { useDataTable } from "../../../../../hooks/use-data-table"
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"
type AddCustomersFormProps = {
customerGroupId: string
}
const AddCustomersSchema = zod.object({
export const AddCustomersSchema = zod.object({
customer_ids: zod.array(zod.string()).min(1),
})
@@ -83,36 +91,47 @@ export const AddCustomersForm = ({
)
}, [rowSelection, setValue])
const params = useQueryParams(["q"])
const { customers, count, isLoading, isError, error } = useAdminCustomers({
expand: "groups",
limit: PAGE_SIZE,
offset: pageIndex * PAGE_SIZE,
...params,
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
const filters = useCustomerTableFilters()
const { customers, count, isLoading, isError, error } = useCustomers({
fields: "id,email,first_name,last_name,*groups",
...searchParams,
})
const updater: OnChangeFn<RowSelectionState> = (fn) => {
const state = typeof fn === "function" ? fn(rowSelection) : fn
const ids = Object.keys(state)
setValue("customer_ids", ids, {
shouldDirty: true,
shouldTouch: true,
})
setRowSelection(state)
}
const columns = useColumns()
const table = useReactTable({
const { table } = useDataTable({
data: customers ?? [],
columns,
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
state: {
pagination,
rowSelection,
count,
enablePagination: true,
enableRowSelection: (row) => {
return !row.original.groups?.map((gc) => gc.id).includes(customerGroupId)
},
getRowId: (row) => row.id,
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
meta: {
customerGroupId,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater,
},
})
const { mutateAsync, isLoading: isMutating } =
useAdminAddCustomersToCustomerGroup(customerGroupId)
useAddCustomersToGroup(customerGroupId)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(
@@ -121,18 +140,12 @@ export const AddCustomersForm = ({
},
{
onSuccess: () => {
queryClient.invalidateQueries(adminCustomerKeys.lists())
handleSuccess(`/customer-groups/${customerGroupId}`)
},
}
)
})
const noRecords =
!isLoading &&
!customers?.length &&
!Object.values(params).filter(Boolean).length
if (isError) {
throw error
}
@@ -165,98 +178,32 @@ export const AddCustomersForm = ({
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
{noRecords ? (
<div className="flex w-full flex-1 items-center justify-center">
<NoRecords />
</div>
) : (
<div className="flex w-full flex-1 flex-col divide-y">
<div className="flex w-full items-center justify-between px-6 py-4">
<div></div>
<div className="flex items-center gap-x-2">
<Query />
</div>
</div>
<div className="w-full flex-1 overflow-y-auto">
{(customers?.length || 0) > 0 ? (
<Table>
<Table.Header className="border-t-0">
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.HeaderCell>
)
})}
</Table.Row>
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
},
{
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
row.original.groups
?.map((cg) => cg.id)
.includes(customerGroupId),
}
)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
) : (
<div className="flex min-h-full flex-1 items-center justify-center">
<NoResults />
</div>
)}
</div>
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={PAGE_SIZE}
/>
</div>
)}
<RouteFocusModal.Body>
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
filters={filters}
orderBy={[
"email",
"first_name",
"last_name",
"has_account",
"created_at",
"updated_at",
]}
isLoading={isLoading}
search
queryObject={raw}
/>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}
const columnHelper = createColumnHelper<Customer>()
const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => {
const { t } = useTranslation()
@@ -279,21 +226,14 @@ const useColumns = () => {
/>
)
},
cell: ({ row, table }) => {
const { customerGroupId } = table.options.meta as {
customerGroupId: string
}
const isAdded = row.original.groups
?.map((gc) => gc.id)
.includes(customerGroupId)
const isSelected = row.getIsSelected() || isAdded
cell: ({ row }) => {
const isPreSelected = !row.getCanSelect()
const isSelected = row.getIsSelected() || isPreSelected
const Component = (
<Checkbox
checked={isSelected}
disabled={isAdded}
disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
@@ -301,7 +241,7 @@ const useColumns = () => {
/>
)
if (isAdded) {
if (isPreSelected) {
return (
<Tooltip
content={t("customerGroups.customerAlreadyAdded")}

View File

@@ -1,5 +1,4 @@
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCustomersForm } from "./components/add-customers-form"

View File

@@ -10,8 +10,9 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { useCreateCustomerGroup } from "../../../../../hooks/api/customer-groups"
const CreateCustomerGroupSchema = zod.object({
export const CreateCustomerGroupSchema = zod.object({
name: zod.string().min(1),
})
@@ -26,7 +27,7 @@ export const CreateCustomerGroupForm = () => {
resolver: zodResolver(CreateCustomerGroupSchema),
})
const { mutateAsync, isLoading } = useAdminCreateCustomerGroup()
const { mutateAsync, isLoading } = useCreateCustomerGroup()
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(

View File

@@ -1,23 +1,30 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { Customer, CustomerGroup } from "@medusajs/medusa"
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { Customer } from "@medusajs/medusa"
import { PencilSquare, Trash } from "@medusajs/icons"
import {
adminCustomerGroupKeys,
useAdminCustomPost,
useAdminCustomerGroupCustomers,
useAdminRemoveCustomersFromCustomerGroup,
} from "medusa-react"
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 { Link } from "react-router-dom"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
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: CustomerGroup
group: AdminCustomerGroupResponse["customer_group"]
}
const PAGE_SIZE = 10
@@ -25,22 +32,18 @@ const PAGE_SIZE = 10
export const CustomerGroupCustomerSection = ({
group,
}: CustomerGroupCustomerSectionProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { t } = useTranslation()
const prompt = usePrompt()
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
const { customers, count, isLoading, isError, error } =
useAdminCustomerGroupCustomers(
group.id,
{
...searchParams,
},
{
keepPreviousData: true,
}
)
const { customers, count, isLoading, isError, error } = useCustomers({
...searchParams,
groups: group.id,
})
const filters = useCustomerTableFilters(["groups"])
const columns = useColumns()
const filters = useCustomerTableFilters(["groups"])
const { table } = useDataTable({
data: customers ?? [],
@@ -50,21 +53,28 @@ export const CustomerGroupCustomerSection = ({
enablePagination: true,
enableRowSelection: true,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
meta: {
customerGroupId: group.id,
},
})
const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id)
const prompt = usePrompt()
if (isError) {
throw error
}
const handleRemoveCustomers = async (selection: Record<string, boolean>) => {
const selected = Object.keys(selection).filter((k) => selection[k])
const { mutateAsync } = useRemoveCustomersFromGroup(group.id)
const handleRemove = async () => {
const keys = Object.keys(rowSelection)
const res = await prompt({
title: t("general.areYouSure"),
description: t("customerGroups.removeCustomersWarning", {
count: selected.length,
count: keys.length,
}),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
@@ -74,13 +84,16 @@ export const CustomerGroupCustomerSection = ({
return
}
await mutateAsync({
customer_ids: selected.map((s) => ({ id: s })),
})
}
if (isError) {
throw error
await mutateAsync(
{
customer_ids: keys.map((k) => ({ id: k })),
},
{
onSuccess: () => {
setRowSelection({})
},
}
)
}
return (
@@ -111,14 +124,14 @@ export const CustomerGroupCustomerSection = ({
"created_at",
"updated_at",
]}
queryObject={raw}
commands={[
{
action: handleRemoveCustomers,
action: handleRemove,
label: t("actions.remove"),
shortcut: "r",
},
]}
queryObject={raw}
/>
</Container>
)
@@ -128,12 +141,11 @@ const CustomerActions = ({
customer,
customerGroupId,
}: {
customer: Customer
customer: AdminCustomerResponse["customer"]
customerGroupId: string
}) => {
const { t } = useTranslation()
const { mutateAsync } =
useAdminRemoveCustomersFromCustomerGroup(customerGroupId)
const { mutateAsync } = useRemoveCustomersFromGroup(customerGroupId)
const prompt = usePrompt()
@@ -182,7 +194,7 @@ const CustomerActions = ({
)
}
const columnHelper = createColumnHelper<Customer>()
const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => {
const columns = useCustomerTableColumns()

View File

@@ -1,13 +1,13 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import type { CustomerGroup } from "@medusajs/medusa"
import { Container, Heading } from "@medusajs/ui"
import { useAdminDeleteCustomerGroup } from "medusa-react"
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 = {
group: CustomerGroup
group: AdminCustomerGroupResponse["customer_group"]
}
export const CustomerGroupGeneralSection = ({
@@ -16,7 +16,7 @@ export const CustomerGroupGeneralSection = ({
const { t } = useTranslation()
const navigate = useNavigate()
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => {
await mutateAsync(undefined, {

View File

@@ -1,9 +1,9 @@
import { useAdminCustomerGroup } from "medusa-react"
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
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<
@@ -11,7 +11,7 @@ export const CustomerGroupDetail = () => {
>
const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
const { customer_group, isLoading, isError, error } = useCustomerGroup(
id!,
undefined,
{ initialData }

View File

@@ -1,8 +1,8 @@
import { AdminCustomerGroupsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { adminProductKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { medusa, queryClient } from "../../../lib/medusa"
const customerGroupDetailQuery = (id: string) => ({
@@ -15,7 +15,7 @@ export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
const query = customerGroupDetailQuery(id!)
return (
queryClient.getQueryData<Response<AdminCustomerGroupsRes>>(
queryClient.getQueryData<Response<AdminCustomerGroupResponse>>(
query.queryKey
) ?? (await queryClient.fetchQuery(query))
)

View File

@@ -1,5 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { CustomerGroup } from "@medusajs/medusa"
import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomerGroup } from "medusa-react"
import { useForm } from "react-hook-form"
@@ -10,12 +9,14 @@ import {
RouteDrawer,
useRouteModal,
} from "../../../../../components/route-modal"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useUpdateCustomerGroup } from "../../../../../hooks/api/customer-groups"
type EditCustomerGroupFormProps = {
group: CustomerGroup
group: AdminCustomerGroupResponse["customer_group"]
}
const EditCustomerGroupSchema = z.object({
export const EditCustomerGroupSchema = z.object({
name: z.string().min(1),
})
@@ -32,7 +33,7 @@ export const EditCustomerGroupForm = ({
resolver: zodResolver(EditCustomerGroupSchema),
})
const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id)
const { mutateAsync, isLoading } = useUpdateCustomerGroup(group.id)
const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, {

View File

@@ -1,15 +1,13 @@
import { Heading } from "@medusajs/ui"
import { useAdminCustomerGroup } from "medusa-react"
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"
export const CustomerGroupEdit = () => {
const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
id!
)
const { customer_group, isLoading, isError, error } = useCustomerGroup(id!)
const { t } = useTranslation()

View File

@@ -1,5 +1,4 @@
import { Button, Container, Heading } from "@medusajs/ui"
import { useAdminCustomerGroups } from "medusa-react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
@@ -7,6 +6,7 @@ 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
@@ -17,16 +17,10 @@ export const CustomerGroupListTable = () => {
pageSize: PAGE_SIZE,
})
const { customer_groups, count, isLoading, isError, error } =
useAdminCustomerGroups(
{
...searchParams,
expand: "customers",
fields: "id,name,customers,customers.id",
},
{
keepPreviousData: true,
}
)
useCustomerGroups({
...searchParams,
fields: "id,name,customers.id",
})
const filters = useCustomerGroupTableFilters()
const columns = useCustomerGroupTableColumns()

View File

@@ -1,13 +1,14 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { CustomerGroup } from "@medusajs/medusa"
import { usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteCustomerGroup } from "medusa-react"
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<CustomerGroup>()
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
export const useCustomerGroupTableColumns = () => {
const { t } = useTranslation()
@@ -35,11 +36,15 @@ export const useCustomerGroupTableColumns = () => {
)
}
const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
const CustomerGroupActions = ({
group,
}: {
group: AdminCustomerGroupResponse["customer_group"]
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => {
const res = await prompt({

View File

@@ -1,4 +1,3 @@
import { AdminGetCustomerGroupsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../../../../hooks/use-query-params"
type UseCustomerGroupTableQueryProps = {
@@ -17,7 +16,7 @@ export const useCustomerGroupTableQuery = ({
const { offset, created_at, updated_at, q, order } = queryObject
const searchParams: AdminGetCustomerGroupsParams = {
const searchParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
order,

View File

@@ -138,16 +138,6 @@ export const CreateCustomerForm = () => {
}}
/>
</div>
<div className="flex flex-col gap-y-4">
<div>
<Text size="small" leading="compact" weight="plus">
{t("fields.password")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("customers.passwordHint")}
</Text>
</div>
</div>
</div>
</RouteFocusModal.Body>
</form>

View File

@@ -1,54 +1,90 @@
// TODO: Should be added with Customer Groups UI
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 { t } from "i18next"
// import { Customer, CustomerGroup } from "@medusajs/medusa"
// import { Container, Heading } from "@medusajs/ui"
// import { createColumnHelper } from "@tanstack/react-table"
// import { useAdminCustomerGroups } from "medusa-react"
// import { useMemo } from "react"
// import { useTranslation } from "react-i18next"
type CustomerGroupSectionProps = {
customer: AdminCustomerResponse["customer"]
}
// // TODO: Continue working on this when there is a natural way to get customer groups related to a customer.
// type CustomerGroupSectionProps = {
// customer: Customer
// }
const PAGE_SIZE = 10
// export const CustomerGroupSection = ({
// customer,
// }: CustomerGroupSectionProps) => {
// const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups(
// {
// id: customer.groups.map((g) => g.id).join(","),
// }
// )
export const CustomerGroupSection = ({
customer,
}: CustomerGroupSectionProps) => {
const { customer_groups, count, isLoading, isError, error } =
useCustomerGroups({
customers: { id: customer.id },
})
// if (isError) {
// throw error
// }
const filters = useCustomerGroupTableFilters()
const columns = useColumns()
// return (
// <Container className="p-0 divide-y">
// <div className="px-6 py-4">
// <Heading level="h2">Groups</Heading>
// </div>
// </Container>
// )
// }
const { table } = useDataTable({
data: customer_groups ?? [],
columns,
count,
getRowId: (row) => row.id,
enablePagination: true,
enableRowSelection: true,
pageSize: PAGE_SIZE,
})
// const columnHelper = createColumnHelper<CustomerGroup>()
if (isError) {
throw error
}
// const useColumns = () => {
// const { t } = useTranslation()
if (isError) {
throw error
}
// return useMemo(
// () => [
// columnHelper.display({
// id: "select",
// }),
// columnHelper.accessor("name", {
// header: t("fields.name"),
// cell: ({ getValue }) => getValue(),
// }),
// ],
// [t]
// )
// }
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("customerGroups.domain")}</Heading>
</div>
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
isLoading={isLoading}
count={count}
navigateTo={(row) => `/customer-groups/${row.id}`}
filters={filters}
search
pagination
orderBy={["name", "created_at", "updated_at"]}
/>
</Container>
)
}
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
const useColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.display({
id: "select",
}),
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => getValue(),
}),
],
[t]
)
}

View File

@@ -3,6 +3,7 @@ import { CustomerGeneralSection } from "./components/customer-general-section"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { customerLoader } from "./loader"
import { useCustomer } from "../../../hooks/api/customers"
import { CustomerGroupSection } from "./components/customer-group-section"
export const CustomerDetail = () => {
const { id } = useParams()
@@ -32,7 +33,7 @@ export const CustomerDetail = () => {
{/* <CustomerOrderSection customer={customer} />
// TODO: re-add when order endpoints are added to api-v2
*/}
{/* <CustomerGroupSection customer={customer} /> */}
<CustomerGroupSection customer={customer} />
<JsonViewSection data={customer} />
<Outlet />
</div>

View File

@@ -1,15 +1,19 @@
import { createCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../validators"
import { createCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
} from "../../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../../validators"
export const POST = async (
// eslint-disable-next-line max-len
req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>,
res: MedusaResponse
res: MedusaResponse<AdminCustomerGroupResponse>
) => {
const { id } = req.params
const { customer_ids } = req.validatedBody
@@ -30,5 +34,15 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ customer_group_customers: result })
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "customer_group",
variables: { id },
fields: req.remoteQueryConfig.fields,
})
const [customer_group] = await remoteQuery(queryObject)
res.status(200).json({ customer_group })
}

View File

@@ -1,15 +1,19 @@
import { deleteCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../validators"
import { deleteCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
} from "../../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../../validators"
export const POST = async (
// eslint-disable-next-line max-len
req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>,
res: MedusaResponse
res: MedusaResponse<AdminCustomerGroupResponse>
) => {
const { id } = req.params
const { customer_ids } = req.validatedBody
@@ -30,8 +34,15 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({
object: "customer_group_customers",
deleted: true,
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "customer_group",
variables: { id },
fields: req.remoteQueryConfig.fields,
})
const [customer_group] = await remoteQuery(queryObject)
res.status(200).json({ customer_group })
}

View File

@@ -1,5 +1,6 @@
import * as QueryConfig from "./query-config"
import { transformBody, transformQuery } from "../../../api/middlewares"
import {
AdminDeleteCustomerGroupsGroupCustomersBatchReq,
AdminGetCustomerGroupsGroupCustomersParams,
@@ -9,7 +10,6 @@ import {
AdminPostCustomerGroupsGroupReq,
AdminPostCustomerGroupsReq,
} from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
@@ -63,14 +63,24 @@ export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [
},
{
method: ["POST"],
matcher: "/admin/customer-groups/:id/customers/batch",
middlewares: [transformBody(AdminPostCustomerGroupsGroupCustomersBatchReq)],
matcher: "/admin/customer-groups/:id/customers/batch/add",
middlewares: [
transformBody(AdminPostCustomerGroupsGroupCustomersBatchReq),
transformQuery(
AdminGetCustomerGroupsGroupParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/customer-groups/:id/customers/remove",
matcher: "/admin/customer-groups/:id/customers/batch/remove",
middlewares: [
transformBody(AdminDeleteCustomerGroupsGroupCustomersBatchReq),
transformQuery(
AdminGetCustomerGroupsGroupParams,
QueryConfig.retrieveTransformQueryConfig
),
],
},
]

View File

@@ -1,14 +1,15 @@
import { OperatorMap } from "@medusajs/types"
import { Transform, Type } from "class-transformer"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import {
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
import { Transform, Type } from "class-transformer"
import { IsType } from "../../../utils"
import { OperatorMap } from "@medusajs/types"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
export class AdminGetCustomerGroupsGroupParams extends FindParams {}
@@ -61,6 +62,10 @@ export class AdminGetCustomerGroupsParams extends extendedFindParamsMixin({
limit: 100,
offset: 0,
}) {
@IsOptional()
@IsString()
q?: string
@IsOptional()
@IsString({ each: true })
id?: string | string[]
@@ -120,6 +125,10 @@ export class AdminGetCustomerGroupsGroupCustomersParams extends extendedFindPara
offset: 0,
}
) {
@IsOptional()
@IsString()
q?: string
@IsOptional()
@IsString({ each: true })
id?: string | string[]

View File

@@ -2,12 +2,11 @@ import {
deleteCustomersWorkflow,
updateCustomersWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { AdminCustomerResponse, CustomerUpdatableFields } from "@medusajs/types"
import {
AdminCustomerResponse,
CustomerUpdatableFields,
ICustomerModuleService,
} from "@medusajs/types"
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
@@ -17,24 +16,26 @@ export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<AdminCustomerResponse>
) => {
const customerModuleService = req.scope.resolve<ICustomerModuleService>(
ModuleRegistrationName.CUSTOMER
)
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const customer = await customerModuleService.retrieve(req.params.id, {
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations,
const variables = { id: req.params.id }
const queryObject = remoteQueryObjectFromString({
entryPoint: "customer",
variables,
fields: req.remoteQueryConfig.fields,
})
res.status(200).json({ customer: customer as AdminCustomerResponse["customer"] })
const [customer] = await remoteQuery(queryObject)
res.status(200).json({ customer })
}
export const POST = async (
req: AuthenticatedMedusaRequest<CustomerUpdatableFields>,
res: MedusaResponse<AdminCustomerResponse>
) => {
const updateCustomers = updateCustomersWorkflow(req.scope)
const { result, errors } = await updateCustomers.run({
const { errors } = await updateCustomersWorkflow(req.scope).run({
input: {
selector: { id: req.params.id },
update: req.validatedBody,
@@ -46,7 +47,19 @@ export const POST = async (
throw errors[0].error
}
res.status(200).json({ customer: result[0] as AdminCustomerResponse["customer"] })
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const queryObject = remoteQueryObjectFromString({
entryPoint: "customer",
variables: {
filters: { id: req.params.id },
},
fields: req.remoteQueryConfig.fields,
})
const [customer] = await remoteQuery(queryObject)
res.status(200).json({ customer })
}
export const DELETE = async (

View File

@@ -6,6 +6,7 @@ export const defaultAdminCustomerFields = [
"first_name",
"last_name",
"email",
"phone",
"created_at",
"updated_at",
"deleted_at",

View File

@@ -24,11 +24,13 @@ export const AdminCustomersParams = createFindParams({
q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(),
email: z.union([z.string(), z.array(z.string())]).optional(),
groups: z.union([
AdminCustomerGroupInCustomerParams,
z.string(),
z.array(z.string()),
]).optional(),
groups: z
.union([
AdminCustomerGroupInCustomerParams,
z.string(),
z.array(z.string()),
])
.optional(),
company_name: z.union([z.string(), z.array(z.string())]).optional(),
first_name: z.union([z.string(), z.array(z.string())]).optional(),
last_name: z.union([z.string(), z.array(z.string())]).optional(),

View File

@@ -6,6 +6,7 @@ import { PaginatedResponse } from "../../../common"
export interface CustomerGroupResponse {
id: string
name: string | null
customers: CustomerResponse[]
}
/**