feat(dashboard): manage groups from customer details (#7346)

This commit is contained in:
Frane Polić authored and GitHub committed 2024-05-19 11:10:13 +02:00
1 parent 05e42bc6fc
commit ce75755ac6
11 files changed
+450 -20

No files matched your search

@@ -18,7 +18,9 @@ import { EditCustomerGroupSchema } from "../../v2-routes/customer-groups/custome
import { customersQueryKeys } from "./customers"
const CUSTOMER_GROUPS_QUERY_KEY = "customer_groups" as const
const customerGroupsQueryKeys = queryKeysFactory(CUSTOMER_GROUPS_QUERY_KEY)
export const customerGroupsQueryKeys = queryKeysFactory(
CUSTOMER_GROUPS_QUERY_KEY
)
export const useCustomerGroup = (
id: string,
@@ -162,7 +164,7 @@ export const useRemoveCustomersFromGroup = (
options?: UseMutationOptions<
AdminCustomerGroupResponse,
Error,
{ customer_ids: { id: string }[] }
{ customer_ids: string[] }
>
) => {
return useMutation({
@@ -26,7 +26,7 @@ export const useCustomerTableFilters = (
if (customer_groups && !isGroupsExcluded) {
const customerGroupFilter: Filter = {
key: "groups",
label: t("customers.groups"),
label: t("customers.groups.label"),
type: "select",
multiple: true,
options: customer_groups.map((s) => ({
@@ -414,6 +414,12 @@
"hint": "Create a new customer to manage their details.",
"successToast": "Customer {{email}} was successfully created."
},
"groups": {
"label": "Customer groups",
"remove": "Are you sure you want to remove the customer from \"{{name}}\" customer group?",
"removeMany": "Are you sure you want to customer from following customer groups: {{groups}}?",
"alreadyAddedTooltip": "The customer is already in this customer group."
},
"edit": {
"header": "Edit Customer",
"emailDisabledTooltip": "The email address cannot be changed for registered customers.",
@@ -428,7 +434,9 @@
"guest": "Guest",
"registered": "Registered",
"groups": "Groups"
}
},
"registered": "Registered",
"guest": "Guest"
},
"customerGroups": {
"domain": "Customer Groups",
@@ -62,7 +62,7 @@ async function batchAddCustomers(
async function batchRemoveCustomers(
id: string,
payload: { customer_ids: { id: string }[] }
payload: { customer_ids: string[] }
) {
return postRequest<AdminCustomerGroupResponse>(
`/admin/customer-groups/${id}/customers`,
@@ -367,6 +367,13 @@ export const RouteMap: RouteObject[] = [
lazy: () =>
import("../../v2-routes/customers/customer-edit"),
},
{
path: "add-customer-groups",
lazy: () =>
import(
"../../v2-routes/customers/customers-add-customer-group"
),
},
],
},
],
@@ -2,21 +2,35 @@ import {
AdminCustomerGroupResponse,
AdminCustomerResponse,
} from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import {
Button,
Checkbox,
Container,
Heading,
toast,
usePrompt,
} from "@medusajs/ui"
import { createColumnHelper, RowSelectionState } from "@tanstack/react-table"
import { t } from "i18next"
import { useMemo } from "react"
import { useMemo, useState } from "react"
import { PencilSquare } from "@medusajs/icons"
import { PencilSquare, Trash } from "@medusajs/icons"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import {
customerGroupsQueryKeys,
useCustomerGroups,
useRemoveCustomersFromGroup,
} from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { Link } from "react-router-dom"
import { client } from "../../../../../lib/client"
import { queryClient } from "../../../../../lib/medusa.ts"
type CustomerGroupSectionProps = {
customer: AdminCustomerResponse["customer"]
@@ -27,9 +41,13 @@ const PAGE_SIZE = 10
export const CustomerGroupSection = ({
customer,
}: CustomerGroupSectionProps) => {
const prompt = usePrompt()
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { raw, searchParams } = useCustomerGroupTableQuery({
pageSize: PAGE_SIZE,
})
const { customer_groups, count, isLoading, isError, error } =
useCustomerGroups(
{
@@ -43,7 +61,7 @@ export const CustomerGroupSection = ({
)
const filters = useCustomerGroupTableFilters()
const columns = useColumns()
const columns = useColumns(customer.id)
const { table } = useDataTable({
data: customer_groups ?? [],
@@ -53,10 +71,52 @@ export const CustomerGroupSection = ({
enablePagination: true,
enableRowSelection: true,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
})
if (isError) {
throw error
const handleRemove = async () => {
const customerGroupIds = Object.keys(rowSelection)
const res = await prompt({
title: t("general.areYouSure"),
description: t("customers.groups.removeMany", {
groups: customer_groups
.filter((g) => customerGroupIds.includes(g.id))
.map((g) => g.name)
.join(","),
}),
confirmText: t("actions.remove"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
try {
/**
* TODO: use this for now until add customer groups to customers batch is implemented
*/
const promises = customerGroupIds.map((id) =>
client.customerGroups.removeCustomers(id, {
customer_ids: [customer.id],
})
)
await Promise.all(promises)
await queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("general.close"),
})
}
}
if (isError) {
@@ -67,6 +127,11 @@ export const CustomerGroupSection = ({
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("customerGroups.domain")}</Heading>
<Link to={`/customers/${customer.id}/add-customer-groups`}>
<Button variant="secondary" size="small">
{t("general.add")}
</Button>
</Link>
</div>
<DataTable
table={table}
@@ -79,20 +144,55 @@ export const CustomerGroupSection = ({
search
pagination
orderBy={["name", "created_at", "updated_at"]}
commands={[
{
action: handleRemove,
label: t("actions.remove"),
shortcut: "r",
},
]}
queryObject={raw}
/>
</Container>
)
}
// TODO: Add remove association when /customer-groups/:id/batch has been created.
const CustomerGroupRowActions = ({
group,
customerId,
}: {
group: AdminCustomerGroupResponse["customer_group"]
customerId: string
}) => {
const prompt = usePrompt()
const { t } = useTranslation()
const { mutateAsync } = useRemoveCustomersFromGroup(group.id)
const onRemove = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("customers.groups.remove", {
name: group.name,
}),
confirmText: t("actions.remove"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
try {
await mutateAsync({ customer_ids: [customerId] })
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("general.close"),
})
}
}
return (
<ActionMenu
groups={[
@@ -103,6 +203,11 @@ const CustomerGroupRowActions = ({
icon: <PencilSquare />,
to: `/customer-groups/${group.id}/edit`,
},
{
label: t("actions.remove"),
onClick: onRemove,
icon: <Trash />,
},
],
},
]}
@@ -113,15 +218,48 @@ const CustomerGroupRowActions = ({
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
const useColumns = () => {
const useColumns = (customerId: string) => {
const columns = useCustomerGroupTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...columns,
columnHelper.display({
id: "actions",
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
cell: ({ row }) => (
<CustomerGroupRowActions
group={row.original}
customerId={customerId}
/>
),
}),
],
[columns]
@@ -0,0 +1,254 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
import {
OnChangeFn,
RowSelectionState,
createColumnHelper,
} from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { AdminCustomerGroupResponse } from "@medusajs/types"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/route-modal"
import { DataTable } from "../../../../../components/table/data-table"
import {
useCustomerGroups,
customerGroupsQueryKeys,
} from "../../../../../hooks/api/customer-groups"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { client } from "../../../../../lib/client"
import { queryClient } from "../../../../../lib/medusa"
type AddCustomerGroupsFormProps = {
customerId: string
}
export const AddCustomerGroupsSchema = zod.object({
customer_group_ids: zod.array(zod.string()).min(1),
})
const PAGE_SIZE = 10
export const AddCustomerGroupsForm = ({
customerId,
}: AddCustomerGroupsFormProps) => {
const { t } = useTranslation()
const { handleSuccess } = useRouteModal()
const [isPending, setIsPending] = useState(false)
const form = useForm<zod.infer<typeof AddCustomerGroupsSchema>>({
defaultValues: {
customer_group_ids: [],
},
resolver: zodResolver(AddCustomerGroupsSchema),
})
const { setValue } = form
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
setValue(
"customer_group_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
)
}, [rowSelection, setValue])
const { searchParams, raw } = useCustomerGroupTableQuery({
pageSize: PAGE_SIZE,
})
const filters = useCustomerGroupTableFilters()
const {
customer_groups,
count,
isPending: isLoading,
isError,
error,
} = useCustomerGroups({
fields: "*customers",
...searchParams,
})
const updater: OnChangeFn<RowSelectionState> = (fn) => {
const state = typeof fn === "function" ? fn(rowSelection) : fn
const ids = Object.keys(state)
setValue("customer_group_ids", ids, {
shouldDirty: true,
shouldTouch: true,
})
setRowSelection(state)
}
const columns = useColumns()
const { table } = useDataTable({
data: customer_groups ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: (row) => {
return !row.original.customers?.map((c) => c.id).includes(customerId)
},
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater,
},
})
const handleSubmit = form.handleSubmit(async (data) => {
setIsPending(true)
try {
/**
* TODO: use this for now until add customer groups to customers batch is implemented
*/
const promises = data.customer_group_ids.map((id) =>
client.customerGroups.addCustomers(id, { customer_ids: [customerId] })
)
await Promise.all(promises)
await queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
handleSuccess(`/customers/${customerId}`)
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
} finally {
setIsPending(false)
}
})
if (isError) {
throw error
}
return (
<RouteFocusModal.Form form={form}>
<form
className="flex h-full flex-col overflow-hidden"
onSubmit={handleSubmit}
>
<RouteFocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
{form.formState.errors.customer_group_ids && (
<Hint variant="error">
{form.formState.errors.customer_group_ids.message}
</Hint>
)}
<RouteFocusModal.Close asChild>
<Button variant="secondary" size="small">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button
type="submit"
variant="primary"
size="small"
isLoading={isPending}
>
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
filters={filters}
orderBy={["name", "created_at", "updated_at"]}
isLoading={isLoading}
layout="fill"
search
queryObject={raw}
/>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}
const columnHelper =
createColumnHelper<AdminCustomerGroupResponse["customer_group"]>()
const useColumns = () => {
const { t } = useTranslation()
const base = useCustomerGroupTableColumns()
const columns = useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
const isPreSelected = !row.getCanSelect()
const isSelected = row.getIsSelected() || isPreSelected
const Component = (
<Checkbox
checked={isSelected}
disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
if (isPreSelected) {
return (
<Tooltip
content={t("customers.groups.alreadyAddedTooltip")}
side="right"
>
{Component}
</Tooltip>
)
}
return Component
},
}),
...base,
],
[t, base]
)
return columns
}
@@ -0,0 +1 @@
export * from "./add-customer-groups-form"
@@ -0,0 +1,13 @@
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { AddCustomerGroupsForm } from "./components/add-customers-form"
export const CustomerAddCustomerGroups = () => {
const { id } = useParams()
return (
<RouteFocusModal>
<AddCustomerGroupsForm customerId={id!} />
</RouteFocusModal>
)
}
@@ -0,0 +1 @@
export { CustomerAddCustomerGroups as Component } from "./customer-add-customer-groups"
@@ -11,19 +11,25 @@ const usePrompt = () => {
return new Promise((resolve) => {
let open = true
const mountRoot = createRoot(document.createElement("div"))
const onCancel = () => {
open = false
render()
mountRoot.unmount()
resolve(false)
// TEMP FIX for Radix issue with dropdowns persisting pointer-events: none on body after closing
document.body.style.pointerEvents = "auto"
}
const onConfirm = () => {
open = false
resolve(true)
render()
}
mountRoot.unmount()
const mountRoot = createRoot(document.createElement("div"))
// TEMP FIX for Radix issue with dropdowns persisting pointer-events: none on body after closing
document.body.style.pointerEvents = "auto"
}
const render = () => {
mountRoot.render(