feat(dashboard): Refactor Users table to use the new DataTable block (#11058)

**What**
- Updates the Users table to use the new DataTable
- Fixes an issue where initial parsing of URL filters weren't formatted correctly.
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2025-01-22 13:19:23 +00:00
1 parent 0bef3202f3
commit 6346836c2d
12 files changed
+668 -1875

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
feat(dashboard): Refactor Users table to use the new DataTable block
@@ -152,6 +152,7 @@ export const DataTable = <TData,>({
const [filtering, setFiltering] = useState<DataTableFilteringState>( const [filtering, setFiltering] = useState<DataTableFilteringState>(
parseFilterState(filterIds, filterParams) parseFilterState(filterIds, filterParams)
) )
const handleFilteringChange = (value: DataTableFilteringState) => { const handleFilteringChange = (value: DataTableFilteringState) => {
setFiltering(value) setFiltering(value)
@@ -341,10 +342,7 @@ function parseFilterState(
const filterValue = value[id] const filterValue = value[id]
if (filterValue) { if (filterValue) {
filters[id] = { filters[id] = JSON.parse(filterValue)
id,
value: JSON.parse(filterValue),
}
} }
} }
@@ -0,0 +1,95 @@
import { createDataTableFilterHelper } from "@medusajs/ui"
import { subDays, subMonths } from "date-fns"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useDate } from "../../../../hooks/use-date"
const filterHelper = createDataTableFilterHelper<any>()
const useDateFilterOptions = () => {
const { t } = useTranslation()
const today = useMemo(() => {
const date = new Date()
date.setHours(0, 0, 0, 0)
return date
}, [])
return useMemo(() => {
return [
{
label: t("filters.date.today"),
value: {
$gte: today.toISOString(),
},
},
{
label: t("filters.date.lastSevenDays"),
value: {
$gte: subDays(today, 7).toISOString(), // 7 days ago
},
},
{
label: t("filters.date.lastThirtyDays"),
value: {
$gte: subDays(today, 30).toISOString(), // 30 days ago
},
},
{
label: t("filters.date.lastNinetyDays"),
value: {
$gte: subDays(today, 90).toISOString(), // 90 days ago
},
},
{
label: t("filters.date.lastTwelveMonths"),
value: {
$gte: subMonths(today, 12).toISOString(), // 12 months ago
},
},
]
}, [today, t])
}
export const useDataTableDateFilters = (disableRangeOption?: boolean) => {
const { t } = useTranslation()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
const rangeOptions = useMemo(() => {
if (disableRangeOption) {
return {
disableRangeOption: true,
}
}
return {
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
options: dateFilterOptions,
}
}, [disableRangeOption, t, dateFilterOptions])
return useMemo(() => {
return [
filterHelper.accessor("created_at", {
type: "date",
label: t("fields.createdAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
...rangeOptions,
}),
filterHelper.accessor("updated_at", {
type: "date",
label: t("fields.updatedAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
...rangeOptions,
}),
]
}, [t, dateFilterOptions, getFullDate, rangeOptions])
}
@@ -1,55 +0,0 @@
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
export const useDateFilterOptions = () => {
const { t } = useTranslation()
const today = useMemo(() => {
const date = new Date()
date.setHours(0, 0, 0, 0)
return date
}, [])
return useMemo(() => {
return [
{
label: t("filters.date.today"),
value: {
$gte: today.toISOString(),
},
},
{
label: t("filters.date.lastSevenDays"),
value: {
$gte: new Date(
today.getTime() - 7 * 24 * 60 * 60 * 1000
).toISOString(), // 7 days ago
},
},
{
label: t("filters.date.lastThirtyDays"),
value: {
$gte: new Date(
today.getTime() - 30 * 24 * 60 * 60 * 1000
).toISOString(), // 30 days ago
},
},
{
label: t("filters.date.lastNinetyDays"),
value: {
$gte: new Date(
today.getTime() - 90 * 24 * 60 * 60 * 1000
).toISOString(), // 90 days ago
},
},
{
label: t("filters.date.lastTwelveMonths"),
value: {
$gte: new Date(
today.getTime() - 365 * 24 * 60 * 60 * 1000
).toISOString(), // 365 days ago
},
},
]
}, [today, t])
}
File diff suppressed because it is too large. Load diff
@@ -2279,6 +2279,16 @@
"developer": "Developer", "developer": "Developer",
"member": "Member" "member": "Member"
}, },
"list": {
"empty": {
"heading": "No users found",
"description": "Once a user has been invited, they will appear here."
},
"filtered": {
"heading": "No results",
"description": "No users match the current filter criteria."
}
},
"deleteUserWarning": "You are about to delete the user {{name}}. This action cannot be undone.", "deleteUserWarning": "You are about to delete the user {{name}}. This action cannot be undone.",
"deleteUserSuccess": "User {{name}} deleted successfully", "deleteUserSuccess": "User {{name}} deleted successfully",
"invite": "Invite" "invite": "Invite"
@@ -3,7 +3,6 @@ import { HttpTypes } from "@medusajs/types"
import { import {
Container, Container,
createDataTableColumnHelper, createDataTableColumnHelper,
createDataTableFilterHelper,
toast, toast,
usePrompt, usePrompt,
} from "@medusajs/ui" } from "@medusajs/ui"
@@ -13,13 +12,13 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../../components/data-table" import { DataTable } from "../../../../../components/data-table"
import { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters"
import { SingleColumnPage } from "../../../../../components/layout/pages" import { SingleColumnPage } from "../../../../../components/layout/pages"
import { useDashboardExtension } from "../../../../../extensions" import { useDashboardExtension } from "../../../../../extensions"
import { import {
useCustomerGroups, useCustomerGroups,
useDeleteCustomerGroupLazy, useDeleteCustomerGroupLazy,
} from "../../../../../hooks/api" } from "../../../../../hooks/api"
import { useDateFilterOptions } from "../../../../../hooks/filters/use-date-filter-options"
import { useDate } from "../../../../../hooks/use-date" import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -215,35 +214,10 @@ const useColumns = () => {
}, [t, navigate, getFullDate, handleDeleteCustomerGroup]) }, [t, navigate, getFullDate, handleDeleteCustomerGroup])
} }
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminCustomerGroup>()
const useFilters = () => { const useFilters = () => {
const { t } = useTranslation() const dateFilters = useDataTableDateFilters()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
return useMemo(() => { return useMemo(() => {
return [ return dateFilters
filterHelper.accessor("created_at", { }, [dateFilters])
type: "date",
label: t("fields.createdAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
options: dateFilterOptions,
}),
filterHelper.accessor("updated_at", {
type: "date",
label: t("fields.updatedAt"),
format: "date",
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
}),
]
}, [t, dateFilterOptions, getFullDate])
} }
@@ -18,12 +18,11 @@ import { useTranslation } from "react-i18next"
import { CellContext } from "@tanstack/react-table" import { CellContext } from "@tanstack/react-table"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../../components/data-table" import { DataTable } from "../../../../../components/data-table"
import { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters"
import { import {
useDeleteVariantLazy, useDeleteVariantLazy,
useProductVariants, useProductVariants,
} from "../../../../../hooks/api/products" } from "../../../../../hooks/api/products"
import { useDateFilterOptions } from "../../../../../hooks/filters/use-date-filter-options"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants" import { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants"
@@ -357,8 +356,7 @@ const filterHelper =
const useFilters = () => { const useFilters = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { getFullDate } = useDate() const dateFilters = useDataTableDateFilters()
const dateFilterOptions = useDateFilterOptions()
return useMemo(() => { return useMemo(() => {
return [ return [
@@ -378,28 +376,9 @@ const useFilters = () => {
{ label: t("filters.radio.no"), value: "false" }, { label: t("filters.radio.no"), value: "false" },
], ],
}), }),
filterHelper.accessor("created_at", { ...dateFilters,
type: "date",
label: t("fields.createdAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
options: dateFilterOptions,
}),
filterHelper.accessor("updated_at", {
type: "date",
label: t("fields.updatedAt"),
format: "date",
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
}),
] ]
}, [t, dateFilterOptions, getFullDate]) }, [t, dateFilters])
} }
const commandHelper = createDataTableCommandHelper() const commandHelper = createDataTableCommandHelper()
@@ -1,42 +0,0 @@
import { UserDTO } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { UserRowActions } from "./user-row-actions"
const columnHelper = createColumnHelper<UserDTO>()
export const useUserTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
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 <span className="text-ui-fg-muted">-</span>
}
return name
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <UserRowActions user={row.original} />,
}),
],
[t]
)
}
@@ -1,24 +0,0 @@
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useUserTableQuery = ({
pageSize = 20,
prefix,
}: {
pageSize?: number
prefix?: string
}) => {
const raw = useQueryParams(["q", "order", "offset"], prefix)
const { offset, ...params } = raw
const searchParams = {
limit: pageSize,
offset: offset ? parseInt(offset) : 0,
...params,
}
return {
searchParams,
raw,
}
}
@@ -1,39 +1,35 @@
import { Button, Container, Heading } from "@medusajs/ui" import { HttpTypes } from "@medusajs/types"
import { Container, createDataTableColumnHelper } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { _DataTable } from "../../../../../components/table/data-table"
import { PencilSquare } from "@medusajs/icons"
import { DataTable } from "../../../../../components/data-table"
import { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters"
import { useUsers } from "../../../../../hooks/api/users" import { useUsers } from "../../../../../hooks/api/users"
import { useDataTable } from "../../../../../hooks/use-data-table" import { useDate } from "../../../../../hooks/use-date"
import { useUserTableColumns } from "./use-user-table-columns" import { useQueryParams } from "../../../../../hooks/use-query-params"
import { useUserTableQuery } from "./use-user-table-query"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
export const UserListTable = () => { export const UserListTable = () => {
const { raw, searchParams } = useUserTableQuery({ const { q, order, offset } = useQueryParams(["q", "order", "offset"])
pageSize: PAGE_SIZE, const { users, count, isPending, isError, error } = useUsers(
}) {
const { q,
users, order,
count, offset: offset ? parseInt(offset) : 0,
isPending: isLoading, limit: PAGE_SIZE,
isError, },
error, {
} = useUsers(searchParams, { placeholderData: keepPreviousData,
placeholderData: keepPreviousData, }
}) )
const columns = useUserTableColumns() const columns = useColumns()
const filters = useFilters()
const { table } = useDataTable({
data: users ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
const { t } = useTranslation() const { t } = useTranslation()
@@ -43,28 +39,109 @@ export const UserListTable = () => {
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <DataTable
<Heading>{t("users.domain")}</Heading> data={users}
<Button size="small" variant="secondary" asChild>
<Link to="invite">{t("users.invite")}</Link>
</Button>
</div>
<_DataTable
table={table}
columns={columns} columns={columns}
count={count} filters={filters}
getRowId={(row) => row.id}
rowCount={count}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
isLoading={isLoading} heading={t("users.domain")}
orderBy={[ rowHref={(row) => `${row.id}`}
{ key: "email", label: t("fields.email") }, isLoading={isPending}
{ key: "first_name", label: t("fields.firstName") }, action={{
{ key: "last_name", label: t("fields.lastName") }, label: t("users.invite"),
]} to: "invite",
navigateTo={(row) => `${row.id}`} }}
search emptyState={{
pagination empty: {
queryObject={raw} heading: t("users.list.empty.heading"),
description: t("users.list.empty.description"),
},
filtered: {
heading: t("users.list.filtered.heading"),
description: t("users.list.filtered.description"),
},
}}
/> />
</Container> </Container>
) )
} }
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminUser>()
const useColumns = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { getFullDate } = useDate()
return useMemo(
() => [
columnHelper.accessor("email", {
header: t("fields.email"),
cell: ({ row }) => {
return row.original.email
},
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("first_name", {
header: t("fields.firstName"),
cell: ({ row }) => {
return row.original.first_name || "-"
},
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("last_name", {
header: t("fields.lastName"),
cell: ({ row }) => {
return row.original.last_name || "-"
},
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("created_at", {
header: t("fields.createdAt"),
cell: ({ row }) => {
return getFullDate({ date: row.original.created_at })
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.accessor("updated_at", {
header: t("fields.updatedAt"),
cell: ({ row }) => {
return getFullDate({ date: row.original.updated_at })
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.action({
actions: [
{
label: t("actions.edit"),
icon: <PencilSquare />,
onClick: (ctx) => {
navigate(`${ctx.row.original.id}/edit`)
},
},
],
}),
],
[t, getFullDate, navigate]
)
}
const useFilters = () => {
const dateFilters = useDataTableDateFilters()
return useMemo(() => {
return dateFilters
}, [dateFilters])
}
@@ -1,24 +0,0 @@
import { PencilSquare } from "@medusajs/icons"
import { UserDTO } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
export const UserRowActions = ({ user }: { user: UserDTO }) => {
const { t } = useTranslation()
return (
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `${user.id}/edit`,
},
],
},
]}
/>
)
}