feat: Admin V2 customer group (#7000)

This commit is contained in:
Oli Juhl authored and GitHub committed 2024-04-09 12:32:27 +02:00
1 parent dbddfc12ed
commit 3882cac622
43 files changed
+575 -301

No files matched your search

@@ -1,7 +1,7 @@
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk" 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 { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -13,7 +13,7 @@ const adminHeaders = {
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
env, env,
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
describe("POST /admin/customer-groups/:id/customers/batch", () => { describe("POST /admin/customer-groups/:id/customers/batch/add", () => {
let appContainer let appContainer
let customerModuleService: ICustomerModuleService let customerModuleService: ICustomerModuleService
@@ -48,7 +48,7 @@ medusaIntegrationTestRunner({
]) ])
const response = await api.post( 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 })), customer_ids: customers.map((c) => ({ id: c.id })),
}, },
@@ -1,7 +1,7 @@
import { ICustomerModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk" 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 { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -13,7 +13,7 @@ const adminHeaders = {
medusaIntegrationTestRunner({ medusaIntegrationTestRunner({
env, env,
testSuite: ({ dbConnection, getContainer, api }) => { testSuite: ({ dbConnection, getContainer, api }) => {
describe("DELETE /admin/customer-groups/:id/customers/remove", () => { describe("POST /admin/customer-groups/:id/customers/batch/remove", () => {
let appContainer let appContainer
let customerModuleService: ICustomerModuleService let customerModuleService: ICustomerModuleService
@@ -55,7 +55,7 @@ medusaIntegrationTestRunner({
) )
const response = await api.post( 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 })), customer_ids: customers.map((c) => ({ id: c.id })),
}, },
@@ -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 { import {
AdminCustomerGroupListResponse, AdminCustomerGroupListResponse,
AdminCustomerGroupResponse, AdminCustomerGroupResponse,
} from "@medusajs/types" } from "@medusajs/types"
import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query" import { z } from "zod"
import { client } from "../../lib/client" import { CreateCustomerGroupSchema } from "../../v2-routes/customer-groups/customer-group-create/components/create-customer-group-form"
import { queryKeysFactory } from "../../lib/query-key-factory" 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 CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const
const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY) const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY)
@@ -51,3 +62,124 @@ export const useCustomerGroups = (
return { ...data, ...rest } 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,
})
}
@@ -15,7 +15,7 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
const CUSTOMERS_QUERY_KEY = "customers" as const const CUSTOMERS_QUERY_KEY = "customers" as const
const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY) export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY)
export const useCustomer = ( export const useCustomer = (
id: string, id: string,
@@ -2,7 +2,10 @@ import {
AdminCustomerGroupListResponse, AdminCustomerGroupListResponse,
AdminCustomerGroupResponse, AdminCustomerGroupResponse,
} from "@medusajs/types" } 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>) { async function retrieveCustomerGroup(id: string, query?: Record<string, any>) {
return getRequest<AdminCustomerGroupResponse>( 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 = { export const customerGroups = {
retrieve: retrieveCustomerGroup, retrieve: retrieveCustomerGroup,
list: listCustomerGroups, list: listCustomerGroups,
create: createCustomerGroup,
update: updateCustomerGroup,
delete: deleteCustomerGroup,
addCustomers: batchAddCustomers,
removeCustomers: batchRemoveCustomers,
} }
@@ -11,6 +11,7 @@ import {
AdminProductCategoryResponse, AdminProductCategoryResponse,
SalesChannelDTO, SalesChannelDTO,
UserDTO, UserDTO,
AdminCustomerGroupResponse,
} from "@medusajs/types" } from "@medusajs/types"
import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom" import { Navigate, Outlet, RouteObject, useLocation } from "react-router-dom"
import { ErrorBoundary } from "../../components/error/error-boundary" 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"
),
},
],
},
],
},
], ],
}, },
], ],
@@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { Customer } from "@medusajs/medusa" import { Customer } from "@medusajs/medusa"
import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui" import { Button, Checkbox, Hint, Table, Tooltip, clx } from "@medusajs/ui"
import { import {
OnChangeFn,
PaginationState, PaginationState,
RowSelectionState, RowSelectionState,
createColumnHelper, createColumnHelper,
@@ -31,12 +32,19 @@ import {
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { queryClient } from "../../../../../lib/medusa" 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 = { type AddCustomersFormProps = {
customerGroupId: string customerGroupId: string
} }
const AddCustomersSchema = zod.object({ export const AddCustomersSchema = zod.object({
customer_ids: zod.array(zod.string()).min(1), customer_ids: zod.array(zod.string()).min(1),
}) })
@@ -83,36 +91,47 @@ export const AddCustomersForm = ({
) )
}, [rowSelection, setValue]) }, [rowSelection, setValue])
const params = useQueryParams(["q"]) const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
const { customers, count, isLoading, isError, error } = useAdminCustomers({ const filters = useCustomerTableFilters()
expand: "groups",
limit: PAGE_SIZE, const { customers, count, isLoading, isError, error } = useCustomers({
offset: pageIndex * PAGE_SIZE, fields: "id,email,first_name,last_name,*groups",
...params, ...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 columns = useColumns()
const table = useReactTable({ const { table } = useDataTable({
data: customers ?? [], data: customers ?? [],
columns, columns,
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), count,
state: { enablePagination: true,
pagination, enableRowSelection: (row) => {
rowSelection, return !row.original.groups?.map((gc) => gc.id).includes(customerGroupId)
}, },
getRowId: (row) => row.id, getRowId: (row) => row.id,
onPaginationChange: setPagination, pageSize: PAGE_SIZE,
onRowSelectionChange: setRowSelection, rowSelection: {
getCoreRowModel: getCoreRowModel(), state: rowSelection,
manualPagination: true, updater,
meta: {
customerGroupId,
}, },
}) })
const { mutateAsync, isLoading: isMutating } = const { mutateAsync, isLoading: isMutating } =
useAdminAddCustomersToCustomerGroup(customerGroupId) useAddCustomersToGroup(customerGroupId)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync( await mutateAsync(
@@ -121,18 +140,12 @@ export const AddCustomersForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(adminCustomerKeys.lists())
handleSuccess(`/customer-groups/${customerGroupId}`) handleSuccess(`/customer-groups/${customerGroupId}`)
}, },
} }
) )
}) })
const noRecords =
!isLoading &&
!customers?.length &&
!Object.values(params).filter(Boolean).length
if (isError) { if (isError) {
throw error throw error
} }
@@ -165,98 +178,32 @@ export const AddCustomersForm = ({
</Button> </Button>
</div> </div>
</RouteFocusModal.Header> </RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto"> <RouteFocusModal.Body>
{noRecords ? ( <DataTable
<div className="flex w-full flex-1 items-center justify-center"> table={table}
<NoRecords /> columns={columns}
</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} pageSize={PAGE_SIZE}
count={count}
filters={filters}
orderBy={[
"email",
"first_name",
"last_name",
"has_account",
"created_at",
"updated_at",
]}
isLoading={isLoading}
search
queryObject={raw}
/> />
</div>
)}
</RouteFocusModal.Body> </RouteFocusModal.Body>
</form> </form>
</RouteFocusModal.Form> </RouteFocusModal.Form>
) )
} }
const columnHelper = createColumnHelper<Customer>() const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => { const useColumns = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -279,21 +226,14 @@ const useColumns = () => {
/> />
) )
}, },
cell: ({ row, table }) => { cell: ({ row }) => {
const { customerGroupId } = table.options.meta as { const isPreSelected = !row.getCanSelect()
customerGroupId: string const isSelected = row.getIsSelected() || isPreSelected
}
const isAdded = row.original.groups
?.map((gc) => gc.id)
.includes(customerGroupId)
const isSelected = row.getIsSelected() || isAdded
const Component = ( const Component = (
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={isAdded} disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)} onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@@ -301,7 +241,7 @@ const useColumns = () => {
/> />
) )
if (isAdded) { if (isPreSelected) {
return ( return (
<Tooltip <Tooltip
content={t("customerGroups.customerAlreadyAdded")} content={t("customerGroups.customerAlreadyAdded")}
@@ -1,5 +1,4 @@
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal" import { RouteFocusModal } from "../../../components/route-modal"
import { AddCustomersForm } from "./components/add-customers-form" import { AddCustomersForm } from "./components/add-customers-form"
@@ -10,8 +10,9 @@ import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } 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), name: zod.string().min(1),
}) })
@@ -26,7 +27,7 @@ export const CreateCustomerGroupForm = () => {
resolver: zodResolver(CreateCustomerGroupSchema), resolver: zodResolver(CreateCustomerGroupSchema),
}) })
const { mutateAsync, isLoading } = useAdminCreateCustomerGroup() const { mutateAsync, isLoading } = useCreateCustomerGroup()
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync( await mutateAsync(
@@ -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 { 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 { import {
adminCustomerGroupKeys,
useAdminCustomPost,
useAdminCustomerGroupCustomers, useAdminCustomerGroupCustomers,
useAdminRemoveCustomersFromCustomerGroup,
} from "medusa-react" } 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 { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table" 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 { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters" import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query" import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table" 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 = { type CustomerGroupCustomerSectionProps = {
group: CustomerGroup group: AdminCustomerGroupResponse["customer_group"]
} }
const PAGE_SIZE = 10 const PAGE_SIZE = 10
@@ -25,22 +32,18 @@ const PAGE_SIZE = 10
export const CustomerGroupCustomerSection = ({ export const CustomerGroupCustomerSection = ({
group, group,
}: CustomerGroupCustomerSectionProps) => { }: CustomerGroupCustomerSectionProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { t } = useTranslation() const { t } = useTranslation()
const prompt = usePrompt()
const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE }) const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE })
const { customers, count, isLoading, isError, error } = const { customers, count, isLoading, isError, error } = useCustomers({
useAdminCustomerGroupCustomers(
group.id,
{
...searchParams, ...searchParams,
}, groups: group.id,
{ })
keepPreviousData: true,
}
)
const filters = useCustomerTableFilters(["groups"])
const columns = useColumns() const columns = useColumns()
const filters = useCustomerTableFilters(["groups"])
const { table } = useDataTable({ const { table } = useDataTable({
data: customers ?? [], data: customers ?? [],
@@ -50,21 +53,28 @@ export const CustomerGroupCustomerSection = ({
enablePagination: true, enablePagination: true,
enableRowSelection: true, enableRowSelection: true,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
meta: { meta: {
customerGroupId: group.id, customerGroupId: group.id,
}, },
}) })
const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id) if (isError) {
const prompt = usePrompt() throw error
}
const handleRemoveCustomers = async (selection: Record<string, boolean>) => { const { mutateAsync } = useRemoveCustomersFromGroup(group.id)
const selected = Object.keys(selection).filter((k) => selection[k])
const handleRemove = async () => {
const keys = Object.keys(rowSelection)
const res = await prompt({ const res = await prompt({
title: t("general.areYouSure"), title: t("general.areYouSure"),
description: t("customerGroups.removeCustomersWarning", { description: t("customerGroups.removeCustomersWarning", {
count: selected.length, count: keys.length,
}), }),
confirmText: t("actions.continue"), confirmText: t("actions.continue"),
cancelText: t("actions.cancel"), cancelText: t("actions.cancel"),
@@ -74,13 +84,16 @@ export const CustomerGroupCustomerSection = ({
return return
} }
await mutateAsync({ await mutateAsync(
customer_ids: selected.map((s) => ({ id: s })), {
}) customer_ids: keys.map((k) => ({ id: k })),
},
{
onSuccess: () => {
setRowSelection({})
},
} }
)
if (isError) {
throw error
} }
return ( return (
@@ -111,14 +124,14 @@ export const CustomerGroupCustomerSection = ({
"created_at", "created_at",
"updated_at", "updated_at",
]} ]}
queryObject={raw}
commands={[ commands={[
{ {
action: handleRemoveCustomers, action: handleRemove,
label: t("actions.remove"), label: t("actions.remove"),
shortcut: "r", shortcut: "r",
}, },
]} ]}
queryObject={raw}
/> />
</Container> </Container>
) )
@@ -128,12 +141,11 @@ const CustomerActions = ({
customer, customer,
customerGroupId, customerGroupId,
}: { }: {
customer: Customer customer: AdminCustomerResponse["customer"]
customerGroupId: string customerGroupId: string
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { mutateAsync } = const { mutateAsync } = useRemoveCustomersFromGroup(customerGroupId)
useAdminRemoveCustomersFromCustomerGroup(customerGroupId)
const prompt = usePrompt() const prompt = usePrompt()
@@ -182,7 +194,7 @@ const CustomerActions = ({
) )
} }
const columnHelper = createColumnHelper<Customer>() const columnHelper = createColumnHelper<AdminCustomerResponse["customer"]>()
const useColumns = () => { const useColumns = () => {
const columns = useCustomerTableColumns() const columns = useCustomerTableColumns()
@@ -1,13 +1,13 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import type { CustomerGroup } from "@medusajs/medusa"
import { Container, Heading } from "@medusajs/ui" import { Container, Heading } from "@medusajs/ui"
import { useAdminDeleteCustomerGroup } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useDeleteCustomerGroup } from "../../../../../hooks/api/customer-groups"
type CustomerGroupGeneralSectionProps = { type CustomerGroupGeneralSectionProps = {
group: CustomerGroup group: AdminCustomerGroupResponse["customer_group"]
} }
export const CustomerGroupGeneralSection = ({ export const CustomerGroupGeneralSection = ({
@@ -16,7 +16,7 @@ export const CustomerGroupGeneralSection = ({
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => { const handleDelete = async () => {
await mutateAsync(undefined, { await mutateAsync(undefined, {
@@ -1,9 +1,9 @@
import { useAdminCustomerGroup } from "medusa-react"
import { Outlet, json, useLoaderData, useParams } from "react-router-dom" import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section" import { JsonViewSection } from "../../../components/common/json-view-section"
import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section" import { CustomerGroupCustomerSection } from "./components/customer-group-customer-section"
import { CustomerGroupGeneralSection } from "./components/customer-group-general-section" import { CustomerGroupGeneralSection } from "./components/customer-group-general-section"
import { customerGroupLoader } from "./loader" import { customerGroupLoader } from "./loader"
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
export const CustomerGroupDetail = () => { export const CustomerGroupDetail = () => {
const initialData = useLoaderData() as Awaited< const initialData = useLoaderData() as Awaited<
@@ -11,7 +11,7 @@ export const CustomerGroupDetail = () => {
> >
const { id } = useParams() const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup( const { customer_group, isLoading, isError, error } = useCustomerGroup(
id!, id!,
undefined, undefined,
{ initialData } { initialData }
@@ -1,8 +1,8 @@
import { AdminCustomerGroupsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js" import { Response } from "@medusajs/medusa-js"
import { adminProductKeys } from "medusa-react" import { adminProductKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom" import { LoaderFunctionArgs } from "react-router-dom"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { medusa, queryClient } from "../../../lib/medusa" import { medusa, queryClient } from "../../../lib/medusa"
const customerGroupDetailQuery = (id: string) => ({ const customerGroupDetailQuery = (id: string) => ({
@@ -15,7 +15,7 @@ export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
const query = customerGroupDetailQuery(id!) const query = customerGroupDetailQuery(id!)
return ( return (
queryClient.getQueryData<Response<AdminCustomerGroupsRes>>( queryClient.getQueryData<Response<AdminCustomerGroupResponse>>(
query.queryKey query.queryKey
) ?? (await queryClient.fetchQuery(query)) ) ?? (await queryClient.fetchQuery(query))
) )
@@ -1,5 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { CustomerGroup } from "@medusajs/medusa"
import { Button, Input } from "@medusajs/ui" import { Button, Input } from "@medusajs/ui"
import { useAdminUpdateCustomerGroup } from "medusa-react" import { useAdminUpdateCustomerGroup } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
@@ -10,12 +9,14 @@ import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import { useUpdateCustomerGroup } from "../../../../../hooks/api/customer-groups"
type EditCustomerGroupFormProps = { type EditCustomerGroupFormProps = {
group: CustomerGroup group: AdminCustomerGroupResponse["customer_group"]
} }
const EditCustomerGroupSchema = z.object({ export const EditCustomerGroupSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
}) })
@@ -32,7 +33,7 @@ export const EditCustomerGroupForm = ({
resolver: zodResolver(EditCustomerGroupSchema), resolver: zodResolver(EditCustomerGroupSchema),
}) })
const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id) const { mutateAsync, isLoading } = useUpdateCustomerGroup(group.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
await mutateAsync(data, { await mutateAsync(data, {
@@ -1,15 +1,13 @@
import { Heading } from "@medusajs/ui" import { Heading } from "@medusajs/ui"
import { useAdminCustomerGroup } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal" import { RouteDrawer } from "../../../components/route-modal"
import { EditCustomerGroupForm } from "./components/edit-customer-group-form" import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
import { useCustomerGroup } from "../../../hooks/api/customer-groups"
export const CustomerGroupEdit = () => { export const CustomerGroupEdit = () => {
const { id } = useParams() const { id } = useParams()
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup( const { customer_group, isLoading, isError, error } = useCustomerGroup(id!)
id!
)
const { t } = useTranslation() const { t } = useTranslation()
@@ -1,5 +1,4 @@
import { Button, Container, Heading } from "@medusajs/ui" import { Button, Container, Heading } from "@medusajs/ui"
import { useAdminCustomerGroups } from "medusa-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table" 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 { useCustomerGroupTableColumns } from "./use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "./use-customer-group-table-filters" import { useCustomerGroupTableFilters } from "./use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "./use-customer-group-table-query" import { useCustomerGroupTableQuery } from "./use-customer-group-table-query"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
@@ -17,16 +17,10 @@ export const CustomerGroupListTable = () => {
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { customer_groups, count, isLoading, isError, error } = const { customer_groups, count, isLoading, isError, error } =
useAdminCustomerGroups( useCustomerGroups({
{
...searchParams, ...searchParams,
expand: "customers", fields: "id,name,customers.id",
fields: "id,name,customers,customers.id", })
},
{
keepPreviousData: true,
}
)
const filters = useCustomerGroupTableFilters() const filters = useCustomerGroupTableFilters()
const columns = useCustomerGroupTableColumns() const columns = useCustomerGroupTableColumns()
@@ -1,13 +1,14 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { CustomerGroup } from "@medusajs/medusa"
import { usePrompt } from "@medusajs/ui" import { usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteCustomerGroup } from "medusa-react"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu" 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 = () => { export const useCustomerGroupTableColumns = () => {
const { t } = useTranslation() 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 { t } = useTranslation()
const prompt = usePrompt() const prompt = usePrompt()
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => { const handleDelete = async () => {
const res = await prompt({ const res = await prompt({
@@ -1,4 +1,3 @@
import { AdminGetCustomerGroupsParams } from "@medusajs/medusa"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
type UseCustomerGroupTableQueryProps = { type UseCustomerGroupTableQueryProps = {
@@ -17,7 +16,7 @@ export const useCustomerGroupTableQuery = ({
const { offset, created_at, updated_at, q, order } = queryObject const { offset, created_at, updated_at, q, order } = queryObject
const searchParams: AdminGetCustomerGroupsParams = { const searchParams = {
limit: pageSize, limit: pageSize,
offset: offset ? Number(offset) : 0, offset: offset ? Number(offset) : 0,
order, order,
@@ -138,16 +138,6 @@ export const CreateCustomerForm = () => {
}} }}
/> />
</div> </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> </div>
</RouteFocusModal.Body> </RouteFocusModal.Body>
</form> </form>
@@ -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" type CustomerGroupSectionProps = {
// import { Container, Heading } from "@medusajs/ui" customer: AdminCustomerResponse["customer"]
// import { createColumnHelper } from "@tanstack/react-table" }
// import { useAdminCustomerGroups } from "medusa-react"
// import { useMemo } from "react"
// import { useTranslation } from "react-i18next"
// // TODO: Continue working on this when there is a natural way to get customer groups related to a customer. const PAGE_SIZE = 10
// type CustomerGroupSectionProps = {
// customer: Customer
// }
// export const CustomerGroupSection = ({ export const CustomerGroupSection = ({
// customer, customer,
// }: CustomerGroupSectionProps) => { }: CustomerGroupSectionProps) => {
// const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups( const { customer_groups, count, isLoading, isError, error } =
// { useCustomerGroups({
// id: customer.groups.map((g) => g.id).join(","), customers: { id: customer.id },
// } })
// )
// if (isError) { const filters = useCustomerGroupTableFilters()
// throw error const columns = useColumns()
// }
// return ( const { table } = useDataTable({
// <Container className="p-0 divide-y"> data: customer_groups ?? [],
// <div className="px-6 py-4"> columns,
// <Heading level="h2">Groups</Heading> count,
// </div> getRowId: (row) => row.id,
// </Container> enablePagination: true,
// ) enableRowSelection: true,
// } pageSize: PAGE_SIZE,
})
// const columnHelper = createColumnHelper<CustomerGroup>() if (isError) {
throw error
}
// const useColumns = () => { if (isError) {
// const { t } = useTranslation() throw error
}
// return useMemo( return (
// () => [ <Container className="divide-y p-0">
// columnHelper.display({ <div className="flex items-center justify-between px-6 py-4">
// id: "select", <Heading level="h2">{t("customerGroups.domain")}</Heading>
// }), </div>
// columnHelper.accessor("name", { <DataTable
// header: t("fields.name"), table={table}
// cell: ({ getValue }) => getValue(), columns={columns}
// }), pageSize={PAGE_SIZE}
// ], isLoading={isLoading}
// [t] 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]
)
}
@@ -3,6 +3,7 @@ import { CustomerGeneralSection } from "./components/customer-general-section"
import { JsonViewSection } from "../../../components/common/json-view-section" import { JsonViewSection } from "../../../components/common/json-view-section"
import { customerLoader } from "./loader" import { customerLoader } from "./loader"
import { useCustomer } from "../../../hooks/api/customers" import { useCustomer } from "../../../hooks/api/customers"
import { CustomerGroupSection } from "./components/customer-group-section"
export const CustomerDetail = () => { export const CustomerDetail = () => {
const { id } = useParams() const { id } = useParams()
@@ -32,7 +33,7 @@ export const CustomerDetail = () => {
{/* <CustomerOrderSection customer={customer} /> {/* <CustomerOrderSection customer={customer} />
// TODO: re-add when order endpoints are added to api-v2 // TODO: re-add when order endpoints are added to api-v2
*/} */}
{/* <CustomerGroupSection customer={customer} /> */} <CustomerGroupSection customer={customer} />
<JsonViewSection data={customer} /> <JsonViewSection data={customer} />
<Outlet /> <Outlet />
</div> </div>
@@ -1,15 +1,19 @@
import { createCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
} from "../../../../../../types/routing" } from "../../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../../validators"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../validators"
import { createCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
export const POST = async ( export const POST = async (
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>, req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>,
res: MedusaResponse res: MedusaResponse<AdminCustomerGroupResponse>
) => { ) => {
const { id } = req.params const { id } = req.params
const { customer_ids } = req.validatedBody const { customer_ids } = req.validatedBody
@@ -30,5 +34,15 @@ export const POST = async (
throw errors[0].error 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 })
} }
@@ -1,15 +1,19 @@
import { deleteCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
} from "../../../../../../types/routing" } from "../../../../../../../types/routing"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../../validators"
import { AdminPostCustomerGroupsGroupCustomersBatchReq } from "../../../validators"
import { deleteCustomerGroupCustomersWorkflow } from "@medusajs/core-flows"
export const POST = async ( export const POST = async (
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>, req: AuthenticatedMedusaRequest<AdminPostCustomerGroupsGroupCustomersBatchReq>,
res: MedusaResponse res: MedusaResponse<AdminCustomerGroupResponse>
) => { ) => {
const { id } = req.params const { id } = req.params
const { customer_ids } = req.validatedBody const { customer_ids } = req.validatedBody
@@ -30,8 +34,15 @@ export const POST = async (
throw errors[0].error throw errors[0].error
} }
res.status(200).json({ const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
object: "customer_group_customers",
deleted: true, const queryObject = remoteQueryObjectFromString({
entryPoint: "customer_group",
variables: { id },
fields: req.remoteQueryConfig.fields,
}) })
const [customer_group] = await remoteQuery(queryObject)
res.status(200).json({ customer_group })
} }
@@ -1,5 +1,6 @@
import * as QueryConfig from "./query-config" import * as QueryConfig from "./query-config"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { import {
AdminDeleteCustomerGroupsGroupCustomersBatchReq, AdminDeleteCustomerGroupsGroupCustomersBatchReq,
AdminGetCustomerGroupsGroupCustomersParams, AdminGetCustomerGroupsGroupCustomersParams,
@@ -9,7 +10,6 @@ import {
AdminPostCustomerGroupsGroupReq, AdminPostCustomerGroupsGroupReq,
AdminPostCustomerGroupsReq, AdminPostCustomerGroupsReq,
} from "./validators" } from "./validators"
import { transformBody, transformQuery } from "../../../api/middlewares"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware" import { authenticate } from "../../../utils/authenticate-middleware"
@@ -63,14 +63,24 @@ export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [
}, },
{ {
method: ["POST"], method: ["POST"],
matcher: "/admin/customer-groups/:id/customers/batch", matcher: "/admin/customer-groups/:id/customers/batch/add",
middlewares: [transformBody(AdminPostCustomerGroupsGroupCustomersBatchReq)], middlewares: [
transformBody(AdminPostCustomerGroupsGroupCustomersBatchReq),
transformQuery(
AdminGetCustomerGroupsGroupParams,
QueryConfig.retrieveTransformQueryConfig
),
],
}, },
{ {
method: ["POST"], method: ["POST"],
matcher: "/admin/customer-groups/:id/customers/remove", matcher: "/admin/customer-groups/:id/customers/batch/remove",
middlewares: [ middlewares: [
transformBody(AdminDeleteCustomerGroupsGroupCustomersBatchReq), transformBody(AdminDeleteCustomerGroupsGroupCustomersBatchReq),
transformQuery(
AdminGetCustomerGroupsGroupParams,
QueryConfig.retrieveTransformQueryConfig
),
], ],
}, },
] ]
@@ -1,14 +1,15 @@
import { OperatorMap } from "@medusajs/types" import { FindParams, extendedFindParamsMixin } from "../../../types/common"
import { Transform, Type } from "class-transformer"
import { import {
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { FindParams, extendedFindParamsMixin } from "../../../types/common" import { Transform, Type } from "class-transformer"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
import { IsType } from "../../../utils" import { IsType } from "../../../utils"
import { OperatorMap } from "@medusajs/types"
import { OperatorMapValidator } from "../../../types/validators/operator-map"
export class AdminGetCustomerGroupsGroupParams extends FindParams {} export class AdminGetCustomerGroupsGroupParams extends FindParams {}
@@ -61,6 +62,10 @@ export class AdminGetCustomerGroupsParams extends extendedFindParamsMixin({
limit: 100, limit: 100,
offset: 0, offset: 0,
}) { }) {
@IsOptional()
@IsString()
q?: string
@IsOptional() @IsOptional()
@IsString({ each: true }) @IsString({ each: true })
id?: string | string[] id?: string | string[]
@@ -120,6 +125,10 @@ export class AdminGetCustomerGroupsGroupCustomersParams extends extendedFindPara
offset: 0, offset: 0,
} }
) { ) {
@IsOptional()
@IsString()
q?: string
@IsOptional() @IsOptional()
@IsString({ each: true }) @IsString({ each: true })
id?: string | string[] id?: string | string[]
@@ -2,12 +2,11 @@ import {
deleteCustomersWorkflow, deleteCustomersWorkflow,
updateCustomersWorkflow, updateCustomersWorkflow,
} from "@medusajs/core-flows" } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AdminCustomerResponse, CustomerUpdatableFields } from "@medusajs/types"
import { import {
AdminCustomerResponse, ContainerRegistrationKeys,
CustomerUpdatableFields, remoteQueryObjectFromString,
ICustomerModuleService, } from "@medusajs/utils"
} from "@medusajs/types"
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse, MedusaResponse,
@@ -17,24 +16,26 @@ export const GET = async (
req: AuthenticatedMedusaRequest, req: AuthenticatedMedusaRequest,
res: MedusaResponse<AdminCustomerResponse> res: MedusaResponse<AdminCustomerResponse>
) => { ) => {
const customerModuleService = req.scope.resolve<ICustomerModuleService>( const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
ModuleRegistrationName.CUSTOMER
)
const customer = await customerModuleService.retrieve(req.params.id, { const variables = { id: req.params.id }
select: req.retrieveConfig.select,
relations: req.retrieveConfig.relations, 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 ( export const POST = async (
req: AuthenticatedMedusaRequest<CustomerUpdatableFields>, req: AuthenticatedMedusaRequest<CustomerUpdatableFields>,
res: MedusaResponse<AdminCustomerResponse> res: MedusaResponse<AdminCustomerResponse>
) => { ) => {
const updateCustomers = updateCustomersWorkflow(req.scope) const { errors } = await updateCustomersWorkflow(req.scope).run({
const { result, errors } = await updateCustomers.run({
input: { input: {
selector: { id: req.params.id }, selector: { id: req.params.id },
update: req.validatedBody, update: req.validatedBody,
@@ -46,7 +47,19 @@ export const POST = async (
throw errors[0].error 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 ( export const DELETE = async (
@@ -6,6 +6,7 @@ export const defaultAdminCustomerFields = [
"first_name", "first_name",
"last_name", "last_name",
"email", "email",
"phone",
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at", "deleted_at",
@@ -24,11 +24,13 @@ export const AdminCustomersParams = createFindParams({
q: z.string().optional(), q: z.string().optional(),
id: z.union([z.string(), z.array(z.string())]).optional(), id: z.union([z.string(), z.array(z.string())]).optional(),
email: z.union([z.string(), z.array(z.string())]).optional(), email: z.union([z.string(), z.array(z.string())]).optional(),
groups: z.union([ groups: z
.union([
AdminCustomerGroupInCustomerParams, AdminCustomerGroupInCustomerParams,
z.string(), z.string(),
z.array(z.string()), z.array(z.string()),
]).optional(), ])
.optional(),
company_name: z.union([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(), first_name: z.union([z.string(), z.array(z.string())]).optional(),
last_name: z.union([z.string(), z.array(z.string())]).optional(), last_name: z.union([z.string(), z.array(z.string())]).optional(),
@@ -6,6 +6,7 @@ import { PaginatedResponse } from "../../../common"
export interface CustomerGroupResponse { export interface CustomerGroupResponse {
id: string id: string
name: string | null name: string | null
customers: CustomerResponse[]
} }
/** /**