feat(dashboard): Customer groups domain (#6098)
**What** - Adds list, detail, edit, create, add customers views to the CG domain. **Note** - Missing metadata in forms (coming in separate PR for entire admin) - Missing table filters (separate PR) CLOSES CORE-1648
This commit is contained in:
1 parent
19bbae61f8
commit
80feb972cb
33 files changed
+1540
-48
No files matched your search
@@ -35,6 +35,7 @@
|
|||||||
"areYouSure": "Are you sure?",
|
"areYouSure": "Are you sure?",
|
||||||
"noRecordsFound": "No records found",
|
"noRecordsFound": "No records found",
|
||||||
"typeToConfirm": "Please type {val} to confirm:",
|
"typeToConfirm": "Please type {val} to confirm:",
|
||||||
|
"noResultsTitle": "No results",
|
||||||
"noResultsMessage": "Try changing the filters or search query",
|
"noResultsMessage": "Try changing the filters or search query",
|
||||||
"noRecordsTitle": "No records",
|
"noRecordsTitle": "No records",
|
||||||
"noRecordsMessage": "There are no records to show",
|
"noRecordsMessage": "There are no records to show",
|
||||||
@@ -80,7 +81,15 @@
|
|||||||
"firstSeen": "First seen"
|
"firstSeen": "First seen"
|
||||||
},
|
},
|
||||||
"customerGroups": {
|
"customerGroups": {
|
||||||
"domain": "Customer Groups"
|
"domain": "Customer Groups",
|
||||||
|
"createGroup": "Create group",
|
||||||
|
"createCustomerGroup": "Create Customer Group",
|
||||||
|
"createCustomerGroupHint": "Create a new customer group to segment your customers.",
|
||||||
|
"customerAlreadyAdded": "The customer has already been added to the group.",
|
||||||
|
"editCustomerGroup": "Edit Customer Group",
|
||||||
|
"removeCustomersWarning_one": "You are about to remove {{count}} customer from the customer group. This action cannot be undone.",
|
||||||
|
"removeCustomersWarning_other": "You are about to remove {{count}} customers from the customer group. This action cannot be undone.",
|
||||||
|
"deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone."
|
||||||
},
|
},
|
||||||
"orders": {
|
"orders": {
|
||||||
"domain": "Orders"
|
"domain": "Orders"
|
||||||
|
|||||||
+5
-3
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
type NoResultsProps = {
|
type NoResultsProps = {
|
||||||
title: string
|
title?: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const NoResults = ({ title, message }: NoResultsProps) => {
|
|||||||
<div className="flex flex-col items-center gap-y-2">
|
<div className="flex flex-col items-center gap-y-2">
|
||||||
<MagnifyingGlass />
|
<MagnifyingGlass />
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{title}
|
{title ?? t("general.noResultsTitle")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
{message ?? t("general.noResultsMessage")}
|
{message ?? t("general.noResultsMessage")}
|
||||||
@@ -51,7 +51,9 @@ export const NoRecords = ({ title, message, action }: NoRecordsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{action && (
|
{action && (
|
||||||
<Link to={action.to}>
|
<Link to={action.to}>
|
||||||
<Button variant="secondary">{action.label}</Button>
|
<Button variant="secondary" size="small">
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./table-row-actions"
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
import { EllipsisHorizontal } from "@medusajs/icons"
|
||||||
|
import { DropdownMenu, IconButton } from "@medusajs/ui"
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
type TableRowAction = {
|
||||||
|
icon: ReactNode
|
||||||
|
label: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
to: string
|
||||||
|
onClick?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
onClick: () => void
|
||||||
|
to?: never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type TableRowActionGroup = {
|
||||||
|
actions: TableRowAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableRowActionsProps = {
|
||||||
|
groups: TableRowActionGroup[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableRowActions = ({ groups }: TableRowActionsProps) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<IconButton size="small" variant="transparent">
|
||||||
|
<EllipsisHorizontal />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{groups.map((group, index) => {
|
||||||
|
if (!group.actions.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLast = index === groups.length - 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
{group.actions.map((action, index) => {
|
||||||
|
if (action.onClick) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={index}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
action.onClick()
|
||||||
|
}}
|
||||||
|
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
<span>{action.label}</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={action.to} key={index}>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
<span>{action.label}</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{!isLast && <DropdownMenu.Separator />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AdminCustomerGroupsRes,
|
||||||
AdminCustomersRes,
|
AdminCustomersRes,
|
||||||
AdminProductsRes,
|
AdminProductsRes,
|
||||||
AdminRegionsRes,
|
AdminRegionsRes,
|
||||||
@@ -183,12 +184,43 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () => import("../../routes/customer-groups/list"),
|
lazy: () =>
|
||||||
|
import("../../routes/customer-groups/customer-group-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/customer-groups/customer-group-create"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () => import("../../routes/customer-groups/details"),
|
lazy: () =>
|
||||||
|
import("../../routes/customer-groups/customer-group-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminCustomerGroupsRes) =>
|
||||||
|
data.customer_group.name,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "add-customers",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/customer-groups/customer-group-add-customers"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "edit",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/customer-groups/customer-group-edit"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+347
@@ -0,0 +1,347 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Customer } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FocusModal,
|
||||||
|
Hint,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
adminCustomerKeys,
|
||||||
|
useAdminAddCustomersToCustomerGroup,
|
||||||
|
useAdminCustomers,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import {
|
||||||
|
NoRecords,
|
||||||
|
NoResults,
|
||||||
|
} from "../../../../../components/common/empty-table-content"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
import { Query } from "../../../../../components/filtering/query"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
import { queryClient } from "../../../../../lib/medusa"
|
||||||
|
|
||||||
|
type AddCustomersFormProps = {
|
||||||
|
customerGroupId: string
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddCustomersSchema = zod.object({
|
||||||
|
customer_ids: zod.array(zod.string()).min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const AddCustomersForm = ({
|
||||||
|
customerGroupId,
|
||||||
|
subscribe,
|
||||||
|
}: AddCustomersFormProps) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof AddCustomersSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
customer_ids: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AddCustomersSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue(
|
||||||
|
"customer_ids",
|
||||||
|
Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
)
|
||||||
|
}, [rowSelection])
|
||||||
|
|
||||||
|
const params = useQueryParams(["q"])
|
||||||
|
const { customers, count, isLoading, isError, error } = useAdminCustomers({
|
||||||
|
expand: "groups",
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: pageIndex * PAGE_SIZE,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: customers ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
meta: {
|
||||||
|
customerGroupId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading: isMutating } =
|
||||||
|
useAdminAddCustomersToCustomerGroup(customerGroupId)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
customer_ids: data.customer_ids.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(adminCustomerKeys.lists())
|
||||||
|
navigate(`/customer-groups/${customerGroupId}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const noRecords =
|
||||||
|
!isLoading &&
|
||||||
|
!customers?.length &&
|
||||||
|
!Object.values(params).filter(Boolean).length
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
{form.formState.errors.customer_ids && (
|
||||||
|
<Hint variant="error">
|
||||||
|
{form.formState.errors.customer_ids.message}
|
||||||
|
</Hint>
|
||||||
|
)}
|
||||||
|
<FocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
isLoading={isMutating}
|
||||||
|
>
|
||||||
|
{t("general.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||||
|
{noRecords ? (
|
||||||
|
<div className="w-full flex-1 flex items-center justify-center">
|
||||||
|
<NoRecords />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y w-full flex-1 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between w-full 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-1 flex items-center justify-center min-h-full">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</FocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Customer>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
||||||
|
const Component = (
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
disabled={isAdded}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isAdded) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={t("customerGroups.customerAlreadyAdded")}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
{Component}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: t("fields.email"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "name",
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const name = [row.original.first_name, row.original.last_name]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
|
||||||
|
return name || "-"
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-customers-form"
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { AddCustomersForm } from "./components/add-customers-form"
|
||||||
|
|
||||||
|
export const CustomerGroupAddCustomers = () => {
|
||||||
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content>
|
||||||
|
<AddCustomersForm customerGroupId={id!} subscribe={subscribe} />
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CustomerGroupAddCustomers as Component } from "./customer-group-add-customers"
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui"
|
||||||
|
import { useAdminCreateCustomerGroup } from "medusa-react"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
|
||||||
|
type CreateCustomerGroupFormProps = {
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateCustomerGroupSchema = zod.object({
|
||||||
|
name: zod.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateCustomerGroupForm = ({
|
||||||
|
subscribe,
|
||||||
|
}: CreateCustomerGroupFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof CreateCustomerGroupSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(CreateCustomerGroupSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminCreateCustomerGroup()
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
name: data.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ customer_group }) => {
|
||||||
|
navigate(`/customer-groups/${customer_group.id}`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="flex items-center gap-x-2 justify-end">
|
||||||
|
<FocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{t("general.create")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
|
||||||
|
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
|
||||||
|
<div>
|
||||||
|
<Heading>{t("customerGroups.createCustomerGroup")}</Heading>
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
{t("customerGroups.createCustomerGroupHint")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-group-form"
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { CreateCustomerGroupForm } from "./components/create-customer-group-form"
|
||||||
|
|
||||||
|
export const CustomerGroupCreate = () => {
|
||||||
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content>
|
||||||
|
<CreateCustomerGroupForm subscribe={subscribe} />
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CustomerGroupCreate as Component } from "./customer-group-create"
|
||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { Customer, CustomerGroup } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CommandBar,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
clx,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminCustomerGroupCustomers,
|
||||||
|
useAdminRemoveCustomersFromCustomerGroup,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
import {
|
||||||
|
NoRecords,
|
||||||
|
NoResults,
|
||||||
|
} from "../../../../../components/common/empty-table-content"
|
||||||
|
import { TableRowActions } from "../../../../../components/common/table-row-actions"
|
||||||
|
import { Query } from "../../../../../components/filtering/query"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
type CustomerGroupCustomerSectionProps = {
|
||||||
|
group: CustomerGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const CustomerGroupCustomerSection = ({
|
||||||
|
group,
|
||||||
|
}: CustomerGroupCustomerSectionProps) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const params = useQueryParams(["q"])
|
||||||
|
const { customers, count, isLoading, isError, error } =
|
||||||
|
useAdminCustomerGroupCustomers(
|
||||||
|
group.id,
|
||||||
|
{
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: pageIndex * PAGE_SIZE,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: customers ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
meta: {
|
||||||
|
customerGroupId: group.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id)
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleRemoveCustomers = async () => {
|
||||||
|
const selected = Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("customerGroups.removeCustomersWarning", {
|
||||||
|
count: selected.length,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.continue"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
customer_ids: selected.map((s) => ({ id: s })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setRowSelection({})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noRecords =
|
||||||
|
!isLoading &&
|
||||||
|
!customers?.length &&
|
||||||
|
!Object.values(params).filter(Boolean).length
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0 divide-y">
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("customers.domain")}</Heading>
|
||||||
|
<Link to={`/customer-groups/${group.id}/add-customers`}>
|
||||||
|
<Button variant="secondary" size="small">
|
||||||
|
{t("general.add")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{noRecords ? (
|
||||||
|
<NoRecords />
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Query />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(customers?.length || 0) > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<Table.Header className="border-t-0">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_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 cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap [&_td:first-of-type]:w-[1%] [&_td:first-of-type]:whitespace-nowrap",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/customers/${row.original.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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="border-b">
|
||||||
|
<NoResults />
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||||
|
<CommandBar.Bar>
|
||||||
|
<CommandBar.Value>
|
||||||
|
{t("general.countSelected", {
|
||||||
|
count: Object.keys(rowSelection).length,
|
||||||
|
})}
|
||||||
|
</CommandBar.Value>
|
||||||
|
<CommandBar.Seperator />
|
||||||
|
<CommandBar.Command
|
||||||
|
action={handleRemoveCustomers}
|
||||||
|
shortcut="r"
|
||||||
|
label={t("general.remove")}
|
||||||
|
/>
|
||||||
|
</CommandBar.Bar>
|
||||||
|
</CommandBar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomerActions = ({
|
||||||
|
customer,
|
||||||
|
customerGroupId,
|
||||||
|
}: {
|
||||||
|
customer: Customer
|
||||||
|
customerGroupId: string
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { mutateAsync } =
|
||||||
|
useAdminRemoveCustomersFromCustomerGroup(customerGroupId)
|
||||||
|
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("customerGroups.removeCustomersWarning", {
|
||||||
|
count: 1,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.continue"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
customer_ids: [{ id: customer.id }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowActions
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("general.edit"),
|
||||||
|
to: `/customers/${customer.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("general.remove"),
|
||||||
|
onClick: handleRemove,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Customer>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: t("fields.email"),
|
||||||
|
cell: ({ getValue }) => <span>{getValue()}</span>,
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "name",
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const name = [row.original.first_name, row.original.last_name]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
|
||||||
|
return name || "-"
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("has_account", {
|
||||||
|
header: t("fields.account"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const hasAccount = getValue()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={hasAccount ? "green" : "blue"}>
|
||||||
|
{hasAccount ? t("customers.registered") : t("customers.guest")}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const { customerGroupId } = table.options.meta as {
|
||||||
|
customerGroupId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomerActions
|
||||||
|
customer={row.original}
|
||||||
|
customerGroupId={customerGroupId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-group-customer-section"
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import { EllipsisHorizontal, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import type { CustomerGroup } from "@medusajs/medusa"
|
||||||
|
import { Container, DropdownMenu, Heading, IconButton } from "@medusajs/ui"
|
||||||
|
import { useAdminDeleteCustomerGroup } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
|
type CustomerGroupGeneralSectionProps = {
|
||||||
|
group: CustomerGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomerGroupGeneralSection = ({
|
||||||
|
group,
|
||||||
|
}: CustomerGroupGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate("/customer-groups", { replace: true })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<Heading>{group.name}</Heading>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<IconButton size="small" variant="transparent">
|
||||||
|
<EllipsisHorizontal />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<Link to={`/customer-groups/${group.id}/edit`}>
|
||||||
|
<DropdownMenu.Item className="flex items-center gap-x-2">
|
||||||
|
<PencilSquare className="text-ui-fg-subtle" />
|
||||||
|
<span>{t("general.edit")}</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Link>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
className="flex items-center gap-x-2"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash className="text-ui-fg-subtle" />
|
||||||
|
<span>{t("general.delete")}</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-group-general-section"
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
export const CustomerGroupDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof customerGroupLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
|
||||||
|
id!,
|
||||||
|
undefined,
|
||||||
|
{ initialData }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !customer_group) {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw json("An unknown error occurred", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<CustomerGroupGeneralSection group={customer_group} />
|
||||||
|
<CustomerGroupCustomerSection group={customer_group} />
|
||||||
|
<JsonViewSection data={customer_group} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export { CustomerGroupDetail as Component } from "./customer-group-detail"
|
||||||
|
export { customerGroupLoader as loader } from "./loader"
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { AdminCustomerGroupsRes } from "@medusajs/medusa"
|
||||||
|
import { Response } from "@medusajs/medusa-js"
|
||||||
|
import { adminProductKeys } from "medusa-react"
|
||||||
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
|
||||||
|
const customerGroupDetailQuery = (id: string) => ({
|
||||||
|
queryKey: adminProductKeys.detail(id),
|
||||||
|
queryFn: async () => medusa.admin.customerGroups.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const customerGroupLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = customerGroupDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminCustomerGroupsRes>>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { CustomerGroup } from "@medusajs/medusa"
|
||||||
|
import { Button, Drawer, Input } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdateCustomerGroup } from "medusa-react"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as z from "zod"
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
|
||||||
|
type EditCustomerGroupFormProps = {
|
||||||
|
group: CustomerGroup
|
||||||
|
onSuccessfulSubmit: () => void
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditCustomerGroupSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditCustomerGroupForm = ({
|
||||||
|
group,
|
||||||
|
onSuccessfulSubmit,
|
||||||
|
subscribe,
|
||||||
|
}: EditCustomerGroupFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof EditCustomerGroupSchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
name: group.name || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditCustomerGroupSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdateCustomerGroup(group.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onSuccessfulSubmit()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col overflow-hidden flex-1"
|
||||||
|
>
|
||||||
|
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto flex-1 max-w-full">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.name")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} size="small" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Drawer.Body>
|
||||||
|
<Drawer.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
<Drawer.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Drawer.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isLoading}>
|
||||||
|
{t("general.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Drawer.Footer>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-customer-group-form"
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
import { Drawer, Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminCustomerGroup } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { EditCustomerGroupForm } from "./components/edit-customer-group-form"
|
||||||
|
|
||||||
|
export const CustomerGroupEdit = () => {
|
||||||
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { customer_group, isLoading, isError, error } = useAdminCustomerGroup(
|
||||||
|
id!
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleSuccessfulSubmit = () => {
|
||||||
|
onOpenChange(false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||||
|
<Drawer.Content>
|
||||||
|
<Drawer.Header>
|
||||||
|
<Heading>{t("customerGroups.editCustomerGroup")}</Heading>
|
||||||
|
</Drawer.Header>
|
||||||
|
{!isLoading && customer_group && (
|
||||||
|
<EditCustomerGroupForm
|
||||||
|
subscribe={subscribe}
|
||||||
|
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||||
|
group={customer_group}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer.Content>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CustomerGroupEdit as Component } from "./customer-group-edit"
|
||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { CustomerGroup } from "@medusajs/medusa"
|
||||||
|
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminCustomerGroups,
|
||||||
|
useAdminDeleteCustomerGroup,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
import {
|
||||||
|
NoRecords,
|
||||||
|
NoResults,
|
||||||
|
} from "../../../../../components/common/empty-table-content"
|
||||||
|
import { TableRowActions } from "../../../../../components/common/table-row-actions"
|
||||||
|
import { OrderBy } from "../../../../../components/filtering/order-by"
|
||||||
|
import { Query } from "../../../../../components/filtering/query"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const CustomerGroupListTable = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const params = useQueryParams(["q", "order"])
|
||||||
|
const { customer_groups, count, isLoading, isError, error } =
|
||||||
|
useAdminCustomerGroups({
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: pageIndex * PAGE_SIZE,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: customer_groups ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const noRecords =
|
||||||
|
!isLoading &&
|
||||||
|
!customer_groups?.length &&
|
||||||
|
!Object.values(params).filter(Boolean).length
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0 divide-y">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Heading level="h2">{t("customerGroups.domain")}</Heading>
|
||||||
|
<Link to="/customer-groups/create">
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.create")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{noRecords ? (
|
||||||
|
<NoRecords
|
||||||
|
action={{
|
||||||
|
label: t("customerGroups.createGroup"),
|
||||||
|
to: "/customer-groups/create",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
<div className="flex items-center px-6 py-2 justify-between">
|
||||||
|
<div className="flex items-center gap-x-2"></div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Query />
|
||||||
|
<OrderBy keys={["name", "created_at", "updated_at"]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{(customer_groups?.length || 0) > 0 ? (
|
||||||
|
<Table>
|
||||||
|
<Table.Header className="border-t-0">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className=" [&_th:last-of-type]:w-[1%] [&_th:last-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 cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/customer-groups/${row.original.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{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="border-b">
|
||||||
|
<NoResults />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminDeleteCustomerGroup(group.id)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("customerGroups.deleteCustomerGroupWarning", {
|
||||||
|
name: group.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.delete"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowActions
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("general.edit"),
|
||||||
|
to: `/customer-groups/${group.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("general.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<CustomerGroup>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row }) => <CustomerGroupActions group={row.original} />,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-group-list-table"
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
import { CustomerGroupListTable } from "./components/customer-group-list-table"
|
||||||
|
|
||||||
|
export const CustomerGroupsList = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<CustomerGroupListTable />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CustomerGroupsList as Component } from "./customer-group-list"
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Container, Heading } from "@medusajs/ui";
|
|
||||||
|
|
||||||
export const CustomerGroupDetails = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Container>
|
|
||||||
<Heading>Customers</Heading>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { CustomerGroupDetails as Component } from "./details";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { CustomerGroupsList as Component } from "./list";
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Container, Heading } from "@medusajs/ui";
|
|
||||||
|
|
||||||
export const CustomerGroupsList = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Container>
|
|
||||||
<Heading>Customer Groups</Heading>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+8
-17
@@ -182,30 +182,21 @@ const useColumns = () => {
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: t("fields.email"),
|
||||||
|
cell: ({ getValue }) => <span>{getValue()}</span>,
|
||||||
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "name",
|
id: "name",
|
||||||
header: t("fields.name"),
|
header: t("fields.name"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const firstName = row.original.first_name
|
const name = [row.original.first_name, row.original.last_name]
|
||||||
const lastName = row.original.last_name
|
.filter(Boolean)
|
||||||
|
.join(" ")
|
||||||
|
|
||||||
let value = "-"
|
return name || "-"
|
||||||
|
|
||||||
if (firstName && lastName) {
|
|
||||||
value = `${firstName} ${lastName}`
|
|
||||||
} else if (firstName) {
|
|
||||||
value = firstName
|
|
||||||
} else if (lastName) {
|
|
||||||
value = lastName
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span>{value}</span>
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
|
||||||
header: t("fields.email"),
|
|
||||||
cell: ({ getValue }) => <span>{getValue()}</span>,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("has_account", {
|
columnHelper.accessor("has_account", {
|
||||||
header: t("fields.account"),
|
header: t("fields.account"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
|
|||||||
Reference in new issue
Block a user