From 1100c21c63cd0427359fd25ec0d498b764915487 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:20:06 +0100 Subject: [PATCH] feat(dashboard): Users domain (#6212) --- .../public/locales/en/translation.json | 24 +- .../common/action-menu/action-menu.tsx | 16 +- .../empty-table-content.tsx | 27 +- .../router-provider/router-provider.tsx | 4 + .../profile-general-section.tsx | 8 + .../user-general-section.tsx | 34 +- .../src/routes/users/user-detail/index.ts | 1 + .../src/routes/users/user-detail/loader.ts | 21 + .../routes/users/user-detail/user-detail.tsx | 9 +- .../edit-user-form/edit-user-form.tsx | 108 +++++ .../components/edit-user-form/index.ts | 1 + .../src/routes/users/user-edit/user-edit.tsx | 33 +- .../invite-user-form/invite-user-form.tsx | 436 ++++++++++++++++++ .../routes/users/user-invite/user-invite.tsx | 7 +- .../user-list-table/user-list-table.tsx | 265 +++++++---- .../src/routes/users/user-list/user-list.tsx | 2 + 16 files changed, 871 insertions(+), 125 deletions(-) create mode 100644 packages/admin-next/dashboard/src/routes/users/user-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 8d0c34af31..7e8bee990e 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -4,9 +4,11 @@ "ascending": "Ascending", "descending": "Descending", "cancel": "Cancel", + "close": "Close", "save": "Save", "create": "Create", "delete": "Delete", + "invite": "Invite", "edit": "Edit", "confirm": "Confirm", "add": "Add", @@ -28,6 +30,7 @@ "enabled": "Enabled", "disabled": "Disabled", "active": "Active", + "revoke": "Revoke", "revoked": "Revoked", "remove": "Remove", "admin": "Admin", @@ -126,7 +129,22 @@ }, "users": { "domain": "Users", - "role": "Role", + "editUser": "Edit User", + "inviteUser": "Invite User", + "inviteUserHint": "Invite a new user to your store.", + "sendInvite": "Send invite", + "pendingInvites": "Pending Invites", + "revokeInviteWarning": "You are about to revoke the invite for {{email}}. This action cannot be undone.", + "resendInvite": "Resend invite", + "copyInviteLink": "Copy invite link", + "expiredOnDate": "Expired on {{date}}", + "validFromUntil": "Valid from <0>{{from}} - <1>{{until}}", + "acceptedOnDate": "Accepted on {{date}}", + "inviteStatus": { + "accepted": "Accepted", + "pending": "Pending", + "expired": "Expired" + }, "roles": { "admin": "Admin", "developer": "Developer", @@ -244,6 +262,8 @@ "account": "Account", "total": "Total", "created": "Created", - "key": "Key" + "key": "Key", + "role": "Role", + "sent": "Sent" } } diff --git a/packages/admin-next/dashboard/src/components/common/action-menu/action-menu.tsx b/packages/admin-next/dashboard/src/components/common/action-menu/action-menu.tsx index 57949865be..1a621ae89c 100644 --- a/packages/admin-next/dashboard/src/components/common/action-menu/action-menu.tsx +++ b/packages/admin-next/dashboard/src/components/common/action-menu/action-menu.tsx @@ -6,6 +6,7 @@ import { Link } from "react-router-dom" type Action = { icon: ReactNode label: string + disabled?: boolean } & ( | { to: string @@ -47,12 +48,13 @@ export const ActionMenu = ({ groups }: ActionMenuProps) => { if (action.onClick) { return ( { e.stopPropagation() action.onClick() }} - className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2" + className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2 disabled:opacity-50 disabled:cursor-not-allowed" > {action.icon} {action.label} @@ -62,12 +64,16 @@ export const ActionMenu = ({ groups }: ActionMenuProps) => { return (
- e.stopPropagation()}> - + + e.stopPropagation()}> {action.icon} {action.label} - - + +
) })} diff --git a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx index 858d344311..320df80867 100644 --- a/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx +++ b/packages/admin-next/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -1,18 +1,24 @@ import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons" -import { Button, Text } from "@medusajs/ui" +import { Button, Text, clx } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" type NoResultsProps = { title?: string message?: string + className?: string } -export const NoResults = ({ title, message }: NoResultsProps) => { +export const NoResults = ({ title, message, className }: NoResultsProps) => { const { t } = useTranslation() return ( -
+
@@ -33,13 +39,24 @@ type NoRecordsProps = { to: string label: string } + className?: string } -export const NoRecords = ({ title, message, action }: NoRecordsProps) => { +export const NoRecords = ({ + title, + message, + action, + className, +}: NoRecordsProps) => { const { t } = useTranslation() return ( -
+
diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index c1ccf72385..dbdb2d99dd 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -6,6 +6,7 @@ import type { AdminPublishableApiKeysRes, AdminRegionsRes, AdminSalesChannelsRes, + AdminUserRes, } from "@medusajs/medusa" import { Outlet, @@ -439,6 +440,9 @@ const router = createBrowserRouter([ { path: ":id", lazy: () => import("../../routes/users/user-detail"), + handle: { + crumb: (data: AdminUserRes) => data.user.email, + }, children: [ { path: "edit", diff --git a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx index 49ef382d96..b69b7aaf30 100644 --- a/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/profile/profile-detail/components/profile-general-section/profile-general-section.tsx @@ -41,6 +41,14 @@ export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => { {user.email}
+
+ + {t("fields.role")} + + + {t(`users.roles.${user.role}`)} + +
{t("profile.language")} diff --git a/packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/user-general-section.tsx b/packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/user-general-section.tsx index 90ab2cefb1..7d4234ec03 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/user-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-detail/components/user-general-section/user-general-section.tsx @@ -1,5 +1,5 @@ import { User } from "@medusajs/medusa" -import { Button, Container, Heading, Text } from "@medusajs/ui" +import { Button, Container, Heading, Text, clx } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" @@ -9,35 +9,39 @@ type UserGeneralSection = { export const UserGeneralSection = ({ user }: UserGeneralSection) => { const { t } = useTranslation() + + const name = [user.first_name, user.last_name].filter(Boolean).join(" ") + return ( - +
-
- {t("profile.domain")} - - {t("profile.manageYourProfileDetails")} - -
+ {user.email}
-
+
{t("fields.name")} - - {user.first_name} {user.last_name} + + {name ?? "-"}
-
+
- {t("fields.email")} + {t("fields.role")} - {user.email} + {t(`users.roles.${user.role}`)}
diff --git a/packages/admin-next/dashboard/src/routes/users/user-detail/index.ts b/packages/admin-next/dashboard/src/routes/users/user-detail/index.ts index 4f2ddfc432..d91450c4f0 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-detail/index.ts +++ b/packages/admin-next/dashboard/src/routes/users/user-detail/index.ts @@ -1 +1,2 @@ +export { userLoader as loader } from "./loader" export { UserDetail as Component } from "./user-detail" diff --git a/packages/admin-next/dashboard/src/routes/users/user-detail/loader.ts b/packages/admin-next/dashboard/src/routes/users/user-detail/loader.ts new file mode 100644 index 0000000000..1dbeb58ba8 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/users/user-detail/loader.ts @@ -0,0 +1,21 @@ +import { AdminUserRes } 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 userDetailQuery = (id: string) => ({ + queryKey: adminProductKeys.detail(id), + queryFn: async () => medusa.admin.users.retrieve(id), +}) + +export const userLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = userDetailQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/users/user-detail/user-detail.tsx b/packages/admin-next/dashboard/src/routes/users/user-detail/user-detail.tsx index eadcfc8eb7..460d6ba81f 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-detail/user-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-detail/user-detail.tsx @@ -1,11 +1,16 @@ import { useAdminUser } from "medusa-react" -import { Outlet, json, useParams } from "react-router-dom" +import { Outlet, json, useLoaderData, useParams } from "react-router-dom" import { JsonViewSection } from "../../../components/common/json-view-section" import { UserGeneralSection } from "./components/user-general-section" +import { userLoader } from "./loader" export const UserDetail = () => { + const initialData = useLoaderData() as Awaited> + const { id } = useParams() - const { user, isLoading, isError, error } = useAdminUser(id!) + const { user, isLoading, isError, error } = useAdminUser(id!, { + initialData, + }) if (isLoading) { return
Loading...
diff --git a/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx b/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx new file mode 100644 index 0000000000..66e2ef1347 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx @@ -0,0 +1,108 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { User } from "@medusajs/medusa" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateUser } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type EditUserFormProps = { + user: Omit + subscribe: (state: boolean) => void + onSuccessfulSubmit: () => void +} + +const EditUserFormSchema = zod.object({ + first_name: zod.string().optional(), + last_name: zod.string().optional(), +}) + +export const EditUserForm = ({ + user, + subscribe, + onSuccessfulSubmit, +}: EditUserFormProps) => { + const form = useForm>({ + defaultValues: { + first_name: user.first_name || "", + last_name: user.last_name || "", + }, + resolver: zodResolver(EditUserFormSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { t } = useTranslation() + + const { mutateAsync, isLoading } = useAdminUpdateUser(user.id) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync(values, { + onSuccess: () => { + onSuccessfulSubmit() + }, + }) + }) + + return ( +
+ + + { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + + +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/index.ts b/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/index.ts new file mode 100644 index 0000000000..1ea01b95e3 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/users/user-edit/components/edit-user-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-user-form" diff --git a/packages/admin-next/dashboard/src/routes/users/user-edit/user-edit.tsx b/packages/admin-next/dashboard/src/routes/users/user-edit/user-edit.tsx index aa9830cb94..1ff3bed6b8 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-edit/user-edit.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-edit/user-edit.tsx @@ -1,12 +1,39 @@ -import { Drawer } from "@medusajs/ui" +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminUser } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditUserForm } from "./components/edit-user-form" export const UserEdit = () => { - const [open, onOpenChange] = useRouteModalState() + const [open, onOpenChange, subscribe] = useRouteModalState() + + const { t } = useTranslation() + const { id } = useParams() + const { user, isLoading, isError, error } = useAdminUser(id!) + + const handleSuccessfulSubmit = () => { + onOpenChange(false, true) + } + + if (isError) { + throw error + } return ( - + + + {t("users.editUser")} + + {!isLoading && user && ( + + )} + ) } diff --git a/packages/admin-next/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx b/packages/admin-next/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx new file mode 100644 index 0000000000..7dd029d729 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx @@ -0,0 +1,436 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { ArrowPath, Link, XCircle } from "@medusajs/icons" +import { Invite } from "@medusajs/medusa" +import { + Button, + Container, + FocusModal, + Heading, + Input, + Select, + StatusBadge, + Table, + Text, + Tooltip, + clx, + usePrompt, +} from "@medusajs/ui" +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table" +import { format } from "date-fns" +import { + useAdminCreateInvite, + useAdminDeleteInvite, + useAdminInvites, + useAdminResendInvite, + useAdminStore, +} from "medusa-react" +import { useEffect, useMemo } from "react" +import { useForm } from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" +import * as zod from "zod" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { NoRecords } from "../../../../../components/common/empty-table-content" +import { Form } from "../../../../../components/common/form" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" + +type InviteUserFormProps = { + subscribe: (state: boolean) => void +} + +enum UserRole { + MEMBER = "member", + DEVELOPER = "developer", + ADMIN = "admin", +} + +const InviteUserSchema = zod.object({ + user: zod.string().email(), + role: zod.nativeEnum(UserRole), +}) + +const PAGE_SIZE = 10 + +export const InviteUserForm = ({ subscribe }: InviteUserFormProps) => { + const form = useForm>({ + defaultValues: { + user: "", + role: UserRole.MEMBER, + }, + resolver: zodResolver(InviteUserSchema), + }) + const { mutateAsync, isLoading: isMutating } = useAdminCreateInvite() + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { invites, isLoading, isError, error } = useAdminInvites() + const count = invites?.length ?? 0 + + const noRecords = !isLoading && count === 0 + + const columns = useColumns() + + const table = useReactTable({ + data: invites ?? [], + columns, + pageCount: Math.ceil(count / PAGE_SIZE), + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }) + + const { t } = useTranslation() + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + role: values.role, + user: values.user, + }, + { + onSuccess: () => { + form.reset() + }, + } + ) + }) + + if (isError) { + throw error + } + + return ( +
+ + +
+ + + +
+
+ +
+
+
+ {t("users.inviteUser")} + + {t("users.inviteUserHint")} + +
+
+
+ { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.role")} + + + + + + ) + }} + /> +
+
+ +
+
+
+ {t("users.pendingInvites")} + + {!noRecords ? ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ ) : ( + + )} +
+
+
+
+
+
+ + ) +} + +const InviteActions = ({ invite }: { invite: Invite }) => { + const { mutateAsync: revokeAsync } = useAdminDeleteInvite(invite.id) + const { mutateAsync: resendAsync } = useAdminResendInvite(invite.id) + const { store, isLoading, isError, error } = useAdminStore() + const prompt = usePrompt() + const { t } = useTranslation() + + const handleRevoke = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("users.revokeInviteWarning", { + email: invite.user_email, + }), + cancelText: t("general.cancel"), + confirmText: t("general.confirm"), + }) + + if (!res) { + return + } + + await revokeAsync() + } + + const handleResend = async () => { + await resendAsync() + } + + const handleCopyInviteLink = () => { + const template = store?.invite_link_template + + if (!template) { + return + } + + const link = template.replace("{invite_token}", invite.token) + navigator.clipboard.writeText(link) + } + + if (isError) { + throw error + } + + return ( + , + label: t("users.copyInviteLink"), + disabled: isLoading || !store?.invite_link_template, + onClick: handleCopyInviteLink, + }, + { + icon: , + label: t("users.resendInvite"), + onClick: handleResend, + }, + ], + }, + { + actions: [ + { + icon: , + label: t("general.revoke"), + onClick: handleRevoke, + }, + ], + }, + ]} + /> + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("user_email", { + header: t("fields.email"), + cell: ({ getValue }) => { + return getValue() + }, + }), + columnHelper.accessor("role", { + header: t("fields.role"), + cell: ({ getValue }) => { + return t(`users.roles.${getValue()}`) + }, + }), + columnHelper.accessor("accepted", { + header: t("fields.status"), + cell: ({ getValue, row }) => { + const accepted = getValue() + const expired = new Date(row.original.expires_at) < new Date() + + if (accepted) { + return ( + + + {t("users.inviteStatus.accepted")} + + + ) + } + + if (expired) { + return ( + + + {t("users.inviteStatus.expired")} + + + ) + } + + return ( + , + , + ]} + values={{ + from: format( + new Date(row.original.created_at), + "dd MMM, yyyy" + ), + until: format( + new Date(row.original.expires_at), + "dd MMM, yyyy" + ), + }} + /> + } + > + + {t("users.inviteStatus.pending")} + + + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/users/user-invite/user-invite.tsx b/packages/admin-next/dashboard/src/routes/users/user-invite/user-invite.tsx index cb4bd877a0..d8f0f2d485 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-invite/user-invite.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-invite/user-invite.tsx @@ -1,12 +1,15 @@ import { FocusModal } from "@medusajs/ui" import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { InviteUserForm } from "./components/invite-user-form/invite-user-form" export const UserInvite = () => { - const [open, onOpenChange] = useRouteModalState() + const [open, onOpenChange, subscribe] = useRouteModalState() return ( - + + + ) } diff --git a/packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/user-list-table.tsx b/packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/user-list-table.tsx index 19647c3823..8b4e191f7a 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/user-list-table.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-list/components/user-list-table/user-list-table.tsx @@ -1,7 +1,8 @@ +import { PencilSquare } from "@medusajs/icons" import { User } from "@medusajs/medusa" -import { Checkbox, Container, Heading, Table, clx } from "@medusajs/ui" +import { Button, Container, Heading, Table, clx } from "@medusajs/ui" import { - RowSelectionState, + PaginationState, createColumnHelper, flexRender, getCoreRowModel, @@ -10,28 +11,67 @@ import { import { useAdminUsers } from "medusa-react" import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" +import { Link, useNavigate } from "react-router-dom" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { + NoRecords, + NoResults, +} from "../../../../../components/common/empty-table-content" +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 UserListTable = () => { - const [rowSelection, setRowSelection] = useState({}) + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) - const { users, isLoading, isError, error } = useAdminUsers() + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const params = useQueryParams(["q", "order"]) + const { users, count, isLoading, isError, error } = useAdminUsers( + { + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + ...params, + }, + { + keepPreviousData: true, + } + ) const columns = useColumns() const table = useReactTable({ data: users ?? [], columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), state: { - rowSelection, + pagination, }, - onRowSelectionChange: setRowSelection, + onPaginationChange: setPagination, getCoreRowModel: getCoreRowModel(), + manualPagination: true, }) const { t } = useTranslation() const navigate = useNavigate() + const noRecords = + !isLoading && + !users?.length && + !Object.values(params).filter(Boolean).length + if (isError) { throw error } @@ -40,56 +80,121 @@ export const UserListTable = () => {
{t("users.domain")} +
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { + {!noRecords && ( +
+
+
+ + +
+
+ )} + {noRecords ? ( + + ) : ( +
+ {!isLoading && !users?.length ? ( +
+ +
+ ) : ( +
+ + {table.getHeaderGroups().map((headerGroup) => { return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + ) })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(row.original.id)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - -
-
+ + + {table.getRowModel().rows.map((row) => ( + navigate(row.original.id)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + + + )} + +
+ )} ) } +const UserActions = ({ user }: { user: Omit }) => { + const { t } = useTranslation() + + return ( + , + label: t("general.edit"), + to: `${user.id}/edit`, + }, + ], + }, + ]} + /> + ) +} + const columnHelper = createColumnHelper>() const useColumns = () => { @@ -97,59 +202,37 @@ const useColumns = () => { return useMemo( () => [ - columnHelper.display({ - id: "select", - header: ({ table }) => { - return ( - - table.toggleAllPageRowsSelected(!!value) - } - /> - ) - }, - cell: ({ row }) => { - return ( - row.toggleSelected(!!value)} - onClick={(e) => { - e.stopPropagation() - }} - /> - ) - }, - }), - columnHelper.display({ - id: "name", - header: t("fields.name"), - cell: ({ row }) => { - const { first_name, last_name } = row.original - - if (!first_name && !last_name) { - return - - } - - return `${first_name || ""} ${last_name || ""}`.trim() - }, - }), columnHelper.accessor("email", { header: t("fields.email"), cell: ({ row }) => { return row.original.email }, }), + columnHelper.display({ + id: "name", + header: t("fields.name"), + cell: ({ row }) => { + const name = [row.original.first_name, row.original.last_name] + .filter(Boolean) + .join(" ") + + if (!name) { + return - + } + + return name + }, + }), columnHelper.accessor("role", { - header: t("users.role"), + header: t("fields.role"), cell: ({ row }) => { return t(`users.roles.${row.original.role}`) }, }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), ], [t] ) diff --git a/packages/admin-next/dashboard/src/routes/users/user-list/user-list.tsx b/packages/admin-next/dashboard/src/routes/users/user-list/user-list.tsx index 412a8077ce..c90a5215fd 100644 --- a/packages/admin-next/dashboard/src/routes/users/user-list/user-list.tsx +++ b/packages/admin-next/dashboard/src/routes/users/user-list/user-list.tsx @@ -1,9 +1,11 @@ +import { Outlet } from "react-router-dom" import { UserListTable } from "./components/user-list-table" export const UserList = () => { return (
+
) }