Added search and other

This commit is contained in:
sianida26 2024-06-01 21:43:09 +07:00
parent c6f9f930e7
commit a542ba1ffa
11 changed files with 586 additions and 303 deletions

View File

@ -7,6 +7,10 @@ import compressImage from "../../utils/compressImage";
import checkPermission from "../../middlewares/checkPermission";
import { createId } from "@paralleldrive/cuid2";
import { writeFileSync } from "fs";
import db from "../../drizzle";
import { users } from "../../drizzle/schema/users";
import { isNull, sql } from "drizzle-orm";
import { unionAll } from "drizzle-orm/pg-core";
const fileSchema = z.object({
file: z.instanceof(File),
@ -15,26 +19,49 @@ const fileSchema = z.object({
const devRoutes = new Hono<HonoEnv>()
.use(authInfo)
.use(checkPermission("dev-routes"))
.get("/middleware", async (c) => {
return c.json({
message: "Middleware works!",
});
.get(
"/test",
checkPermission("users.readAll"),
requestValidator(
"query",
z.object({
includeTrashed: z.string().default("false"),
withMetadata: z.string().default("false"),
page: z.coerce.number().int().min(1).optional(),
limit: z.coerce.number().int().min(1).max(1000).optional(),
})
.post("/file", requestValidator("form", fileSchema), async (c) => {
const { file } = c.req.valid("form");
),
async (c) => {
const userQuery = db
.select({
id: users.id,
name: users.name,
email: users.email,
username: users.username,
isEnabled: users.isEnabled,
createdAt: users.createdAt,
updatedAt: users.updatedAt,
fullCount: sql<number>`null`,
})
.from(users);
const buffer = await compressImage({
inputFile: file,
targetSize: 500 * 1024,
});
const totalCount = db
.select({
id: sql<string>`null`,
name: sql<string>`null`,
email: sql<string>`null`,
username: sql<string>`null`,
isEnabled: sql<boolean>`null`,
createdAt: sql<Date>`null`,
updatedAt: sql<Date>`null`,
fullCount: sql<number>`count(*)`,
})
.from(users);
const filename = `${createId()}.jpg`;
const result = await unionAll(userQuery, totalCount);
writeFileSync(`images/${filename}`, buffer);
return c.json({
message: `File saved! name: ${filename}`,
});
});
return c.json(result);
}
);
export default devRoutes;

View File

@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, count, eq, ilike, isNull, or, sql } from "drizzle-orm";
import { Hono } from "hono";
import { z } from "zod";
@ -12,6 +12,7 @@ import HonoEnv from "../../types/HonoEnv";
import requestValidator from "../../utils/requestValidator";
import authInfo from "../../middlewares/authInfo";
import checkPermission from "../../middlewares/checkPermission";
import { unionAll } from "drizzle-orm/mysql-core";
export const userFormSchema = z.object({
name: z.string().min(1).max(255),
@ -44,20 +45,40 @@ export const userUpdateSchema = userFormSchema.extend({
const usersRoute = new Hono<HonoEnv>()
.use(authInfo)
/**
* Get All Users (With Metadata)
*
* Query params:
* - includeTrashed: boolean (default: false)\
* - withMetadata: boolean
*/
.get(
"/",
checkPermission("users.readAll"),
requestValidator(
"query",
z.object({
includeTrashed: z.string().default("false"),
includeTrashed: z
.string()
.optional()
.transform((v) => v?.toLowerCase() === "true"),
withMetadata: z
.string()
.optional()
.transform((v) => v?.toLowerCase() === "true"),
page: z.coerce.number().int().min(0).default(0),
limit: z.coerce.number().int().min(1).max(1000).default(1),
q: z.string().default(""),
})
),
async (c) => {
const includeTrashed =
c.req.query("includeTrashed")?.toLowerCase() === "true";
const { includeTrashed, page, limit, q } = c.req.valid("query");
let usersData = await db
const totalCountQuery = includeTrashed
? sql<number>`(SELECT count(*) FROM ${users})`
: sql<number>`(SELECT count(*) FROM ${users} WHERE ${users.deletedAt} IS NULL)`;
const result = await db
.select({
id: users.id,
name: users.name,
@ -67,13 +88,34 @@ const usersRoute = new Hono<HonoEnv>()
createdAt: users.createdAt,
updatedAt: users.updatedAt,
...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
// password: users.password,
fullCount: totalCountQuery,
})
.from(users)
.where(!includeTrashed ? isNull(users.deletedAt) : undefined);
.where(
and(
includeTrashed ? undefined : isNull(users.deletedAt),
q
? or(
ilike(users.name, q),
ilike(users.username, q),
ilike(users.email, q),
eq(users.id, q)
)
: undefined
)
)
.offset(page * limit)
.limit(limit);
return c.json(usersData);
return c.json({
data: result.map((d) => ({ ...d, fullCount: undefined })),
_metadata: {
currentPage: page,
totalPages: Math.ceil(result[0]?.fullCount ?? 0 / limit),
totalItems: Number(result[0]?.fullCount) ?? 0,
perPage: limit,
},
});
}
)
//get user by id
@ -292,5 +334,4 @@ const usersRoute = new Hono<HonoEnv>()
message: "User restored successfully",
});
});
export default usersRoute;

View File

@ -0,0 +1,229 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import {
Button,
Card,
Flex,
Pagination,
Select,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { Link } from "@tanstack/react-router";
import React, { ReactNode, useState } from "react";
import { TbPlus, TbSearch } from "react-icons/tb";
import DashboardTable from "./DashboardTable";
import {
ColumnDef,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
QueryKey,
UseQueryOptions,
keepPreviousData,
useQuery,
} from "@tanstack/react-query";
import { useDebouncedCallback } from "@mantine/hooks";
type PaginatedResponse<T extends Record<string, unknown>> = {
data: Array<T>;
_metadata: {
currentPage: number;
totalPages: number;
perPage: number;
totalItems: number;
};
};
//ref: https://x.com/TkDodo/status/1491451513264574501
type Props<
TQueryKey extends QueryKey,
TQueryFnData extends Record<string, unknown>,
TError,
TData extends Record<string, unknown> = TQueryFnData,
> = {
title: string;
createButton?: string | true | React.ReactNode;
modals?: React.ReactNode[];
queryOptions: (
page: number,
limit: number,
q?: string
) => UseQueryOptions<
PaginatedResponse<TQueryFnData>,
TError,
PaginatedResponse<TData>,
TQueryKey
>;
columnDefs: ColumnDef<any>[];
};
/**
* Creates a "Create New" button or returns the provided React node.
*
* @param property - The property that determines the type of button to create. It can be a boolean, string, or React node.
* @returns The create button element.
*/
const createCreateButton = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
property: Props<any, any, any>["createButton"] = true
) => {
if (property === true) {
return (
<Button
leftSection={<TbPlus />}
component={Link}
search={{ create: true }}
>
Create New
</Button>
);
} else if (typeof property === "string") {
return (
<Button
leftSection={<TbPlus />}
component={Link}
search={{ create: true }}
>
{property}
</Button>
);
} else {
return property;
}
};
/**
* PageTemplate component for displaying a paginated table with search and filter functionality.
* @param props - The properties object.
* @returns The rendered PageTemplate component.
*/
export default function PageTemplate<
TQueryKey extends QueryKey,
TQueryFnData extends Record<string, unknown>,
TError,
TData extends Record<string, unknown> = TQueryFnData,
>(props: Props<TQueryKey, TQueryFnData, TError, TData>) {
const [filterOptions, setFilterOptions] = useState({
page: 0,
limit: 10,
q: "",
});
// const [deboucedSearchQuery] = useDebouncedValue(filterOptions.q, 500);
const query = useQuery({
...(typeof props.queryOptions === "function"
? props.queryOptions(
filterOptions.page,
filterOptions.limit,
filterOptions.q
)
: props.queryOptions),
placeholderData: keepPreviousData,
});
const table = useReactTable({
data: query.data?.data ?? [],
columns: props.columnDefs,
getCoreRowModel: getCoreRowModel(),
defaultColumn: {
cell: (props) => <Text>{props.getValue() as ReactNode}</Text>,
},
});
/**
* Handles the change in search query input with debounce.
*
* @param value - The new search query value.
*/
const handleSearchQueryChange = useDebouncedCallback((value: string) => {
setFilterOptions((prev) => ({
page: 0,
limit: prev.limit,
q: value,
}));
}, 500);
/**
* Handles the change in page number.
*
* @param page - The new page number.
*/
const handlePageChange = (page: number) => {
setFilterOptions((prev) => ({
page: page - 1,
limit: prev.limit,
q: prev.q,
}));
};
return (
<Stack>
<Title order={1}>{props.title}</Title>
<Card>
{/* Top Section */}
<Flex justify="flex-end">
{createCreateButton(props.createButton)}
</Flex>
{/* Table Functionality */}
<div className="flex flex-col">
{/* Search */}
<div className="flex pb-4">
<TextInput
leftSection={<TbSearch />}
value={filterOptions.q}
onChange={(e) =>
handleSearchQueryChange(e.target.value)
}
placeholder="Search..."
/>
</div>
{/* Table */}
<DashboardTable table={table} />
{/* Pagination */}
{query.data && (
<div className="pt-4 flex-wrap flex items-center gap-4">
<Select
label="Per Page"
data={["5", "10", "50", "100", "500", "1000"]}
allowDeselect={false}
defaultValue="10"
searchValue={filterOptions.limit.toString()}
onChange={(value) =>
setFilterOptions((prev) => ({
page: prev.page,
limit: parseInt(value ?? "10"),
q: prev.q,
}))
}
checkIconPosition="right"
className="w-20"
/>
<Pagination
value={filterOptions.page + 1}
total={query.data._metadata.totalPages}
onChange={handlePageChange}
/>
<Text c="dimmed" size="sm">
Showing {query.data.data.length} of{" "}
{query.data._metadata.totalItems}
</Text>
</div>
)}
</div>
{/* The Modals */}
{props.modals?.map((modal, index) => (
<React.Fragment key={index}>{modal}</React.Fragment>
))}
</Card>
</Stack>
);
}

View File

@ -5,71 +5,87 @@ import {
Center,
Flex,
Modal,
MultiSelect,
PasswordInput,
ScrollArea,
Stack,
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getRouteApi, useSearch } from "@tanstack/react-router";
import { getRouteApi } from "@tanstack/react-router";
import { createUser, updateUser } from "../queries/userQueries";
import { TbDeviceFloppy } from "react-icons/tb";
import client from "../../../honoClient";
import { getUserByIdQueryOptions } from "../queries/userQueries";
import { useEffect } from "react";
import { notifications } from "@mantine/notifications";
import FormResponseError from "@/errors/FormResponseError";
import createInputComponents from "@/utils/createInputComponents";
/**
* Change this
*/
const routeApi = getRouteApi("/_dashboardLayout/users/");
export default function UserFormModal() {
const searchParams = useSearch({ from: "/_dashboardLayout/users/" }) as {
detail: string;
edit: string;
create: boolean;
};
/**
* DON'T CHANGE FOLLOWING:
*/
const queryClient = useQueryClient();
const userId = searchParams.detail || searchParams.edit;
const navigate = routeApi.useNavigate();
const rolesQuery = useQuery({
queryKey: ["roles"],
queryFn: async () => {
const res = await client.roles.$get();
const searchParams = routeApi.useSearch();
if (res.ok) {
return await res.json();
const dataId = searchParams.detail || searchParams.edit;
const isModalOpen = Boolean(dataId || searchParams.create);
const detailId = searchParams.detail;
const editId = searchParams.edit;
const formType = detailId ? "detail" : editId ? "edit" : "create";
/**
* CHANGE FOLLOWING:
*/
const userQuery = useQuery(getUserByIdQueryOptions(dataId));
const modalTitle =
formType.charAt(0).toUpperCase() + formType.slice(1) + " User";
const form = useForm({
initialValues: {
id: "",
email: "",
name: "",
username: "",
photoProfileUrl: "",
password: "",
roles: [] as string[],
},
});
useEffect(() => {
const data = userQuery.data;
if (!data) {
form.reset();
return;
}
throw new Error(await res.text());
},
form.setValues({
id: data.id,
email: data.email ?? "",
name: data.name,
photoProfileUrl: "",
username: data.username,
password: "",
roles: data.roles.map((v) => v.id), //only extract the id
});
const userQuery = useQuery({
queryKey: ["users", userId],
enabled: Boolean(userId),
queryFn: async () => {
if (!userId) return null;
const res = await client.users[":id"].$get({
param: {
id: userId,
},
query: {},
});
if (res.ok) {
console.log("ok");
return await res.json();
}
console.log("not ok");
throw new Error(await res.text());
},
});
const isModalOpen =
Boolean(userId && userQuery.data) || searchParams.create;
form.setErrors({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userQuery.data]);
const mutation = useMutation({
mutationKey: ["usersMutation"],
@ -100,51 +116,6 @@ export default function UserFormModal() {
},
});
const navigate = routeApi.useNavigate();
const detailId = searchParams.detail;
const editId = searchParams.edit;
const formType = detailId ? "detail" : editId ? "edit" : "create";
const modalTitle =
formType.charAt(0).toUpperCase() + formType.slice(1) + " User";
const form = useForm({
initialValues: {
id: "",
email: "",
name: "",
username: "",
photoProfileUrl: "",
password: "",
roles: [] as string[],
},
// validate: zodResolver(userFormDataSchema),
// validateInputOnChange: false,
});
useEffect(() => {
const data = userQuery.data;
if (!data) {
form.reset();
return;
}
form.setValues({
id: data.id,
email: data.email ?? "",
name: data.name,
photoProfileUrl: "",
username: data.username,
password: "",
roles: data.roles.map((v) => v.id), //only extract the id
});
form.setErrors({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userQuery.data]);
const handleSubmit = async (values: typeof form.values) => {
if (formType === "detail") return;
@ -183,6 +154,22 @@ export default function UserFormModal() {
navigate({ search: {} });
};
/**
* YOU MIGHT NOT NEED FOLLOWING:
*/
const rolesQuery = useQuery({
queryKey: ["roles"],
queryFn: async () => {
const res = await client.roles.$get();
if (res.ok) {
return await res.json();
}
throw new Error(await res.text());
},
});
return (
<Modal
opened={isModalOpen}
@ -205,59 +192,51 @@ export default function UserFormModal() {
</Center>
</Stack>
{form.values.id && (
<TextInput
label="ID"
readOnly
variant="filled"
disabled={mutation.isPending}
{...form.getInputProps("id")}
/>
)}
<TextInput
data-autofocus
label="Name"
readOnly={formType === "detail"}
disabled={mutation.isPending}
{...form.getInputProps("name")}
/>
<TextInput
label="Username"
readOnly={formType === "detail"}
disabled={mutation.isPending}
{...form.getInputProps("username")}
/>
<TextInput
label="Email"
readOnly={formType === "detail"}
disabled={mutation.isPending}
{...form.getInputProps("email")}
/>
{formType === "create" && (
<PasswordInput
label="Password"
disabled={mutation.isPending}
{...form.getInputProps("password")}
/>
)}
{/* Role */}
<MultiSelect
label="Roles"
readOnly={formType === "detail"}
disabled={mutation.isPending}
value={form.values.roles}
onChange={(values) => form.setFieldValue("roles", values)}
data={rolesQuery.data?.map((role) => ({
{createInputComponents({
disableAll: mutation.isPending,
readonlyAll: formType === "detail",
inputs: [
{
type: "text",
readOnly: true,
variant: "filled",
...form.getInputProps("id"),
hidden: !form.values.id,
},
{
type: "text",
label: "Name",
...form.getInputProps("name"),
},
{
type: "text",
label: "Username",
...form.getInputProps("username"),
},
{
type: "text",
label: "Email",
...form.getInputProps("email"),
},
{
type: "password",
label: "Password",
hidden: formType !== "create",
},
{
type: "multi-select",
label: "Roles",
value: form.values.roles,
onChange: (values) =>
form.setFieldValue("roles", values),
data: rolesQuery.data?.map((role) => ({
value: role.id,
label: role.name,
}))}
error={form.errors.roles}
/>
})),
error: form.errors.roles,
},
],
})}
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">

View File

@ -3,18 +3,35 @@ import fetchRPC from "@/utils/fetchRPC";
import { queryOptions } from "@tanstack/react-query";
import { InferRequestType } from "hono";
export const userQueryOptions = queryOptions({
queryKey: ["users"],
queryFn: () => fetchUsers(),
export const userQueryOptions = (page: number, limit: number, q?: string) =>
queryOptions({
queryKey: ["users", { page, limit, q }],
queryFn: () =>
fetchRPC(
client.users.$get({
query: {
limit: String(limit),
page: String(page),
q,
},
})
),
});
export const fetchUsers = async () => {
return await fetchRPC(
client.users.$get({
export const getUserByIdQueryOptions = (userId: string | undefined) =>
queryOptions({
queryKey: ["user", userId],
queryFn: () =>
fetchRPC(
client.users[":id"].$get({
param: {
id: userId!,
},
query: {},
})
);
};
),
enabled: Boolean(userId),
});
export const createUser = async (
form: InferRequestType<typeof client.users.$post>["form"]

View File

@ -1,74 +0,0 @@
import { Button, Flex, Text } from "@mantine/core";
import { Link, getRouteApi } from "@tanstack/react-router";
import React from "react";
import { TbPlus } from "react-icons/tb";
import DashboardTable from "../../../components/DashboardTable";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import createColumns from "./columns";
import { useSuspenseQuery } from "@tanstack/react-query";
import { userQueryOptions } from "../queries/userQueries";
import UserFormModal from "../modals/UserFormModal";
import UserDeleteModal from "../modals/UserDeleteModal";
const routeApi = getRouteApi("/_dashboardLayout/users/");
export default function UsersTable() {
const navigate = routeApi.useNavigate();
const usersQuery = useSuspenseQuery(userQueryOptions);
const table = useReactTable({
data: usersQuery.data,
columns: createColumns({
permissions: {
create: true,
read: true,
delete: true,
update: true,
},
actions: {
detail: (id: string) =>
navigate({
search: {
detail: id,
},
}),
edit: (id: string) =>
navigate({
search: {
edit: id,
},
}),
delete: (id: string) =>
navigate({
search: {
delete: id,
},
}),
},
}),
getCoreRowModel: getCoreRowModel(),
defaultColumn: {
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
},
});
return (
<>
<Flex justify="flex-end">
<Button
leftSection={<TbPlus />}
component={Link}
search={{ create: true }}
>
New User
</Button>
</Flex>
<DashboardTable table={table} />
<UserFormModal />
<UserDeleteModal />
</>
);
}

View File

@ -10,17 +10,12 @@ import { Link } from "@tanstack/react-router";
interface ColumnOptions {
permissions: Partial<CrudPermission>;
actions: {
detail: (id: string) => void;
edit: (id: string) => void;
delete: (id: string, name: string) => void;
};
}
const createColumns = (options: ColumnOptions) => {
const columnHelper =
createColumnHelper<
InferResponseType<typeof client.users.$get>[number]
InferResponseType<typeof client.users.$get>["data"][number]
>();
const columns = [
@ -70,7 +65,11 @@ const createColumns = (options: ColumnOptions) => {
columnHelper.display({
id: "status",
header: "Status",
cell: () => <Badge color="green">Active</Badge>,
cell: (props) => (
<Badge color={props.row.original.isEnabled ? "green" : "gray"}>
{props.row.original.isEnabled ? "Active" : "Inactive"}
</Badge>
),
}),
columnHelper.display({
@ -86,27 +85,21 @@ const createColumns = (options: ColumnOptions) => {
{
label: "Detail",
permission: options.permissions.read,
action: () =>
options.actions.detail(props.row.original.id),
action: `?detail=${props.row.original.id}`,
color: "green",
icon: <TbEye />,
},
{
label: "Edit",
permission: options.permissions.update,
action: () =>
options.actions.edit(props.row.original.id),
action: `?edit=${props.row.original.id}`,
color: "yellow",
icon: <TbPencil />,
},
{
label: "Delete",
permission: options.permissions.delete,
action: () =>
options.actions.delete(
props.row.original.id,
props.row.original.name ?? ""
),
action: `?delete=${props.row.original.id}`,
color: "red",
icon: <TbTrash />,
},

View File

@ -14,6 +14,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { Route as rootRoute } from './routes/__root'
import { Route as DashboardLayoutImport } from './routes/_dashboardLayout'
import { Route as DashboardLayoutUsersIndexImport } from './routes/_dashboardLayout/users/index'
import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboardLayout/dashboard/index'
// Create Virtual Routes
@ -21,9 +22,6 @@ import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboar
const IndexLazyImport = createFileRoute('/')()
const LogoutIndexLazyImport = createFileRoute('/logout/')()
const LoginIndexLazyImport = createFileRoute('/login/')()
const DashboardLayoutUsersIndexLazyImport = createFileRoute(
'/_dashboardLayout/users/',
)()
// Create/Update Routes
@ -47,8 +45,7 @@ const LoginIndexLazyRoute = LoginIndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login/index.lazy').then((d) => d.Route))
const DashboardLayoutUsersIndexLazyRoute =
DashboardLayoutUsersIndexLazyImport.update({
const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({
path: '/users/',
getParentRoute: () => DashboardLayoutRoute,
} as any).lazy(() =>
@ -104,7 +101,7 @@ declare module '@tanstack/react-router' {
id: '/_dashboardLayout/users/'
path: '/users'
fullPath: '/users'
preLoaderRoute: typeof DashboardLayoutUsersIndexLazyImport
preLoaderRoute: typeof DashboardLayoutUsersIndexImport
parentRoute: typeof DashboardLayoutImport
}
}
@ -116,10 +113,50 @@ export const routeTree = rootRoute.addChildren({
IndexLazyRoute,
DashboardLayoutRoute: DashboardLayoutRoute.addChildren({
DashboardLayoutDashboardIndexRoute,
DashboardLayoutUsersIndexLazyRoute,
DashboardLayoutUsersIndexRoute,
}),
LoginIndexLazyRoute,
LogoutIndexLazyRoute,
})
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/_dashboardLayout",
"/login/",
"/logout/"
]
},
"/": {
"filePath": "index.lazy.tsx"
},
"/_dashboardLayout": {
"filePath": "_dashboardLayout.tsx",
"children": [
"/_dashboardLayout/dashboard/",
"/_dashboardLayout/users/"
]
},
"/login/": {
"filePath": "login/index.lazy.tsx"
},
"/logout/": {
"filePath": "logout/index.lazy.tsx"
},
"/_dashboardLayout/dashboard/": {
"filePath": "_dashboardLayout/dashboard/index.tsx",
"parent": "/_dashboardLayout"
},
"/_dashboardLayout/users/": {
"filePath": "_dashboardLayout/users/index.tsx",
"parent": "/_dashboardLayout"
}
}
}
ROUTE_MANIFEST_END */

View File

@ -1,33 +1,40 @@
import { Card, Stack, Title } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router";
import UsersTable from "../../../modules/usersManagement/tables/UsersTable";
import { z } from "zod";
import { userQueryOptions } from "@/modules/usersManagement/queries/userQueries";
import PageTemplate from "@/components/PageTemplate";
import { createLazyFileRoute } from "@tanstack/react-router";
import UserFormModal from "@/modules/usersManagement/modals/UserFormModal";
import { createColumnHelper } from "@tanstack/react-table";
import ExtractQueryDataType from "@/types/ExtractQueryDataType";
const searchParamSchema = z.object({
create: z.boolean().default(false).optional(),
edit: z.string().default("").optional(),
delete: z.string().default("").optional(),
detail: z.string().default("").optional(),
});
export const Route = createFileRoute("/_dashboardLayout/users/")({
export const Route = createLazyFileRoute("/_dashboardLayout/users/")({
component: UsersPage,
validateSearch: searchParamSchema,
loader: ({ context: { queryClient } }) => {
queryClient.ensureQueryData(userQueryOptions);
},
});
type DataType = ExtractQueryDataType<typeof userQueryOptions>;
const columnHelper = createColumnHelper<DataType>();
export default function UsersPage() {
return (
<Stack>
<Title order={1}>Users</Title>
<Card>
<UsersTable />
</Card>
</Stack>
<PageTemplate
title="Users"
queryOptions={userQueryOptions}
modals={[<UserFormModal />]}
columnDefs={[
columnHelper.display({
id: "sequence",
header: "#",
cell: (props) => props.row.index + 1,
size: 1,
}),
columnHelper.accessor("email", {
header: "Email",
}),
columnHelper.accessor("email", {
header: "Email",
}),
]}
/>
);
}

View File

@ -0,0 +1,18 @@
import { userQueryOptions } from "@/modules/usersManagement/queries/userQueries";
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
const searchParamSchema = z.object({
create: z.boolean().default(false).optional(),
edit: z.string().default("").optional(),
delete: z.string().default("").optional(),
detail: z.string().default("").optional(),
});
export const Route = createFileRoute("/_dashboardLayout/users/")({
validateSearch: searchParamSchema,
loader: ({ context: { queryClient } }) => {
queryClient.ensureQueryData(userQueryOptions(0, 10));
},
});

View File

@ -0,0 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { UseQueryOptions } from "@tanstack/react-query";
type ExtractDataType<T> =
T extends UseQueryOptions<infer U, any, any, any> ? U : never;
type ExtractQueryDataType<T> = ExtractDataType<ReturnType<T>>["data"][number];
export default ExtractQueryDataType;