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
2025-01-22 14:19:23 +01:00
committed by GitHub
parent 0bef3202f3
commit 6346836c2d
12 changed files with 668 additions and 1875 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
feat(dashboard): Refactor Users table to use the new DataTable block

View File

@@ -152,6 +152,7 @@ export const DataTable = <TData,>({
const [filtering, setFiltering] = useState<DataTableFilteringState>(
parseFilterState(filterIds, filterParams)
)
const handleFilteringChange = (value: DataTableFilteringState) => {
setFiltering(value)
@@ -341,10 +342,7 @@ function parseFilterState(
const filterValue = value[id]
if (filterValue) {
filters[id] = {
id,
value: JSON.parse(filterValue),
}
filters[id] = JSON.parse(filterValue)
}
}

View File

@@ -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])
}

View File

@@ -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

View File

@@ -2279,6 +2279,16 @@
"developer": "Developer",
"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.",
"deleteUserSuccess": "User {{name}} deleted successfully",
"invite": "Invite"

View File

@@ -3,7 +3,6 @@ import { HttpTypes } from "@medusajs/types"
import {
Container,
createDataTableColumnHelper,
createDataTableFilterHelper,
toast,
usePrompt,
} from "@medusajs/ui"
@@ -13,13 +12,13 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
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 { useDashboardExtension } from "../../../../../extensions"
import {
useCustomerGroups,
useDeleteCustomerGroupLazy,
} from "../../../../../hooks/api"
import { useDateFilterOptions } from "../../../../../hooks/filters/use-date-filter-options"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -215,35 +214,10 @@ const useColumns = () => {
}, [t, navigate, getFullDate, handleDeleteCustomerGroup])
}
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminCustomerGroup>()
const useFilters = () => {
const { t } = useTranslation()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
const dateFilters = useDataTableDateFilters()
return useMemo(() => {
return [
filterHelper.accessor("created_at", {
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])
return dateFilters
}, [dateFilters])
}

View File

@@ -18,12 +18,11 @@ import { useTranslation } from "react-i18next"
import { CellContext } from "@tanstack/react-table"
import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../../components/data-table"
import { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters"
import {
useDeleteVariantLazy,
useProductVariants,
} 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 { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants"
@@ -357,8 +356,7 @@ const filterHelper =
const useFilters = () => {
const { t } = useTranslation()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
const dateFilters = useDataTableDateFilters()
return useMemo(() => {
return [
@@ -378,28 +376,9 @@ const useFilters = () => {
{ label: t("filters.radio.no"), value: "false" },
],
}),
filterHelper.accessor("created_at", {
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,
}),
...dateFilters,
]
}, [t, dateFilterOptions, getFullDate])
}, [t, dateFilters])
}
const commandHelper = createDataTableCommandHelper()

View File

@@ -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]
)
}

View File

@@ -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,
}
}

View File

@@ -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 { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { _DataTable } from "../../../../../components/table/data-table"
import { useNavigate } from "react-router-dom"
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 { useDataTable } from "../../../../../hooks/use-data-table"
import { useUserTableColumns } from "./use-user-table-columns"
import { useUserTableQuery } from "./use-user-table-query"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
const PAGE_SIZE = 20
export const UserListTable = () => {
const { raw, searchParams } = useUserTableQuery({
pageSize: PAGE_SIZE,
})
const {
users,
count,
isPending: isLoading,
isError,
error,
} = useUsers(searchParams, {
placeholderData: keepPreviousData,
})
const { q, order, offset } = useQueryParams(["q", "order", "offset"])
const { users, count, isPending, isError, error } = useUsers(
{
q,
order,
offset: offset ? parseInt(offset) : 0,
limit: PAGE_SIZE,
},
{
placeholderData: keepPreviousData,
}
)
const columns = useUserTableColumns()
const { table } = useDataTable({
data: users ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
const columns = useColumns()
const filters = useFilters()
const { t } = useTranslation()
@@ -43,28 +39,109 @@ export const UserListTable = () => {
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{t("users.domain")}</Heading>
<Button size="small" variant="secondary" asChild>
<Link to="invite">{t("users.invite")}</Link>
</Button>
</div>
<_DataTable
table={table}
<DataTable
data={users}
columns={columns}
count={count}
filters={filters}
getRowId={(row) => row.id}
rowCount={count}
pageSize={PAGE_SIZE}
isLoading={isLoading}
orderBy={[
{ key: "email", label: t("fields.email") },
{ key: "first_name", label: t("fields.firstName") },
{ key: "last_name", label: t("fields.lastName") },
]}
navigateTo={(row) => `${row.id}`}
search
pagination
queryObject={raw}
heading={t("users.domain")}
rowHref={(row) => `${row.id}`}
isLoading={isPending}
action={{
label: t("users.invite"),
to: "invite",
}}
emptyState={{
empty: {
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>
)
}
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])
}

View File

@@ -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`,
},
],
},
]}
/>
)
}