Added search and other
This commit is contained in:
parent
c6f9f930e7
commit
a542ba1ffa
|
|
@ -7,6 +7,10 @@ import compressImage from "../../utils/compressImage";
|
||||||
import checkPermission from "../../middlewares/checkPermission";
|
import checkPermission from "../../middlewares/checkPermission";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { writeFileSync } from "fs";
|
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({
|
const fileSchema = z.object({
|
||||||
file: z.instanceof(File),
|
file: z.instanceof(File),
|
||||||
|
|
@ -15,26 +19,49 @@ const fileSchema = z.object({
|
||||||
const devRoutes = new Hono<HonoEnv>()
|
const devRoutes = new Hono<HonoEnv>()
|
||||||
.use(authInfo)
|
.use(authInfo)
|
||||||
.use(checkPermission("dev-routes"))
|
.use(checkPermission("dev-routes"))
|
||||||
.get("/middleware", async (c) => {
|
.get(
|
||||||
return c.json({
|
"/test",
|
||||||
message: "Middleware works!",
|
checkPermission("users.readAll"),
|
||||||
});
|
requestValidator(
|
||||||
})
|
"query",
|
||||||
.post("/file", requestValidator("form", fileSchema), async (c) => {
|
z.object({
|
||||||
const { file } = c.req.valid("form");
|
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(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
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({
|
const totalCount = db
|
||||||
inputFile: file,
|
.select({
|
||||||
targetSize: 500 * 1024,
|
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(result);
|
||||||
|
}
|
||||||
return c.json({
|
);
|
||||||
message: `File saved! name: ${filename}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export default devRoutes;
|
export default devRoutes;
|
||||||
|
|
|
||||||
|
|
@ -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 { Hono } from "hono";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
@ -12,6 +12,7 @@ import HonoEnv from "../../types/HonoEnv";
|
||||||
import requestValidator from "../../utils/requestValidator";
|
import requestValidator from "../../utils/requestValidator";
|
||||||
import authInfo from "../../middlewares/authInfo";
|
import authInfo from "../../middlewares/authInfo";
|
||||||
import checkPermission from "../../middlewares/checkPermission";
|
import checkPermission from "../../middlewares/checkPermission";
|
||||||
|
import { unionAll } from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
export const userFormSchema = z.object({
|
export const userFormSchema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
|
|
@ -44,20 +45,40 @@ export const userUpdateSchema = userFormSchema.extend({
|
||||||
|
|
||||||
const usersRoute = new Hono<HonoEnv>()
|
const usersRoute = new Hono<HonoEnv>()
|
||||||
.use(authInfo)
|
.use(authInfo)
|
||||||
|
/**
|
||||||
|
* Get All Users (With Metadata)
|
||||||
|
*
|
||||||
|
* Query params:
|
||||||
|
* - includeTrashed: boolean (default: false)\
|
||||||
|
* - withMetadata: boolean
|
||||||
|
*/
|
||||||
.get(
|
.get(
|
||||||
"/",
|
"/",
|
||||||
checkPermission("users.readAll"),
|
checkPermission("users.readAll"),
|
||||||
requestValidator(
|
requestValidator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
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) => {
|
async (c) => {
|
||||||
const includeTrashed =
|
const { includeTrashed, page, limit, q } = c.req.valid("query");
|
||||||
c.req.query("includeTrashed")?.toLowerCase() === "true";
|
|
||||||
|
|
||||||
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({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
name: users.name,
|
name: users.name,
|
||||||
|
|
@ -67,13 +88,34 @@ const usersRoute = new Hono<HonoEnv>()
|
||||||
createdAt: users.createdAt,
|
createdAt: users.createdAt,
|
||||||
updatedAt: users.updatedAt,
|
updatedAt: users.updatedAt,
|
||||||
...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
|
...(includeTrashed ? { deletedAt: users.deletedAt } : {}),
|
||||||
|
fullCount: totalCountQuery,
|
||||||
// password: users.password,
|
|
||||||
})
|
})
|
||||||
.from(users)
|
.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
|
//get user by id
|
||||||
|
|
@ -292,5 +334,4 @@ const usersRoute = new Hono<HonoEnv>()
|
||||||
message: "User restored successfully",
|
message: "User restored successfully",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default usersRoute;
|
export default usersRoute;
|
||||||
|
|
|
||||||
229
apps/frontend/src/components/PageTemplate.tsx
Normal file
229
apps/frontend/src/components/PageTemplate.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,71 +5,87 @@ import {
|
||||||
Center,
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Modal,
|
Modal,
|
||||||
MultiSelect,
|
|
||||||
PasswordInput,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
TextInput,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { createUser, updateUser } from "../queries/userQueries";
|
||||||
import { TbDeviceFloppy } from "react-icons/tb";
|
import { TbDeviceFloppy } from "react-icons/tb";
|
||||||
import client from "../../../honoClient";
|
import client from "../../../honoClient";
|
||||||
|
import { getUserByIdQueryOptions } from "../queries/userQueries";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import FormResponseError from "@/errors/FormResponseError";
|
import FormResponseError from "@/errors/FormResponseError";
|
||||||
|
import createInputComponents from "@/utils/createInputComponents";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change this
|
||||||
|
*/
|
||||||
const routeApi = getRouteApi("/_dashboardLayout/users/");
|
const routeApi = getRouteApi("/_dashboardLayout/users/");
|
||||||
|
|
||||||
export default function UserFormModal() {
|
export default function UserFormModal() {
|
||||||
const searchParams = useSearch({ from: "/_dashboardLayout/users/" }) as {
|
/**
|
||||||
detail: string;
|
* DON'T CHANGE FOLLOWING:
|
||||||
edit: string;
|
*/
|
||||||
create: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const userId = searchParams.detail || searchParams.edit;
|
const navigate = routeApi.useNavigate();
|
||||||
|
|
||||||
const rolesQuery = useQuery({
|
const searchParams = routeApi.useSearch();
|
||||||
queryKey: ["roles"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await client.roles.$get();
|
|
||||||
|
|
||||||
if (res.ok) {
|
const dataId = searchParams.detail || searchParams.edit;
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(await res.text());
|
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[],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const userQuery = useQuery({
|
useEffect(() => {
|
||||||
queryKey: ["users", userId],
|
const data = userQuery.data;
|
||||||
enabled: Boolean(userId),
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!userId) return null;
|
|
||||||
const res = await client.users[":id"].$get({
|
|
||||||
param: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
query: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (!data) {
|
||||||
console.log("ok");
|
form.reset();
|
||||||
return await res.json();
|
return;
|
||||||
}
|
}
|
||||||
console.log("not ok");
|
|
||||||
throw new Error(await res.text());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isModalOpen =
|
form.setValues({
|
||||||
Boolean(userId && userQuery.data) || searchParams.create;
|
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 mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ["usersMutation"],
|
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) => {
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
if (formType === "detail") return;
|
if (formType === "detail") return;
|
||||||
|
|
||||||
|
|
@ -183,6 +154,22 @@ export default function UserFormModal() {
|
||||||
navigate({ search: {} });
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
opened={isModalOpen}
|
opened={isModalOpen}
|
||||||
|
|
@ -205,59 +192,51 @@ export default function UserFormModal() {
|
||||||
</Center>
|
</Center>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{form.values.id && (
|
{createInputComponents({
|
||||||
<TextInput
|
disableAll: mutation.isPending,
|
||||||
label="ID"
|
readonlyAll: formType === "detail",
|
||||||
readOnly
|
inputs: [
|
||||||
variant="filled"
|
{
|
||||||
disabled={mutation.isPending}
|
type: "text",
|
||||||
{...form.getInputProps("id")}
|
readOnly: true,
|
||||||
/>
|
variant: "filled",
|
||||||
)}
|
...form.getInputProps("id"),
|
||||||
|
hidden: !form.values.id,
|
||||||
<TextInput
|
},
|
||||||
data-autofocus
|
{
|
||||||
label="Name"
|
type: "text",
|
||||||
readOnly={formType === "detail"}
|
label: "Name",
|
||||||
disabled={mutation.isPending}
|
...form.getInputProps("name"),
|
||||||
{...form.getInputProps("name")}
|
},
|
||||||
/>
|
{
|
||||||
|
type: "text",
|
||||||
<TextInput
|
label: "Username",
|
||||||
label="Username"
|
...form.getInputProps("username"),
|
||||||
readOnly={formType === "detail"}
|
},
|
||||||
disabled={mutation.isPending}
|
{
|
||||||
{...form.getInputProps("username")}
|
type: "text",
|
||||||
/>
|
label: "Email",
|
||||||
|
...form.getInputProps("email"),
|
||||||
<TextInput
|
},
|
||||||
label="Email"
|
{
|
||||||
readOnly={formType === "detail"}
|
type: "password",
|
||||||
disabled={mutation.isPending}
|
label: "Password",
|
||||||
{...form.getInputProps("email")}
|
hidden: formType !== "create",
|
||||||
/>
|
},
|
||||||
|
{
|
||||||
{formType === "create" && (
|
type: "multi-select",
|
||||||
<PasswordInput
|
label: "Roles",
|
||||||
label="Password"
|
value: form.values.roles,
|
||||||
disabled={mutation.isPending}
|
onChange: (values) =>
|
||||||
{...form.getInputProps("password")}
|
form.setFieldValue("roles", values),
|
||||||
/>
|
data: rolesQuery.data?.map((role) => ({
|
||||||
)}
|
value: role.id,
|
||||||
|
label: role.name,
|
||||||
{/* Role */}
|
})),
|
||||||
<MultiSelect
|
error: form.errors.roles,
|
||||||
label="Roles"
|
},
|
||||||
readOnly={formType === "detail"}
|
],
|
||||||
disabled={mutation.isPending}
|
})}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,35 @@ import fetchRPC from "@/utils/fetchRPC";
|
||||||
import { queryOptions } from "@tanstack/react-query";
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
import { InferRequestType } from "hono";
|
import { InferRequestType } from "hono";
|
||||||
|
|
||||||
export const userQueryOptions = queryOptions({
|
export const userQueryOptions = (page: number, limit: number, q?: string) =>
|
||||||
queryKey: ["users"],
|
queryOptions({
|
||||||
queryFn: () => fetchUsers(),
|
queryKey: ["users", { page, limit, q }],
|
||||||
});
|
queryFn: () =>
|
||||||
|
fetchRPC(
|
||||||
|
client.users.$get({
|
||||||
|
query: {
|
||||||
|
limit: String(limit),
|
||||||
|
page: String(page),
|
||||||
|
q,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchUsers = async () => {
|
export const getUserByIdQueryOptions = (userId: string | undefined) =>
|
||||||
return await fetchRPC(
|
queryOptions({
|
||||||
client.users.$get({
|
queryKey: ["user", userId],
|
||||||
query: {},
|
queryFn: () =>
|
||||||
})
|
fetchRPC(
|
||||||
);
|
client.users[":id"].$get({
|
||||||
};
|
param: {
|
||||||
|
id: userId!,
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
enabled: Boolean(userId),
|
||||||
|
});
|
||||||
|
|
||||||
export const createUser = async (
|
export const createUser = async (
|
||||||
form: InferRequestType<typeof client.users.$post>["form"]
|
form: InferRequestType<typeof client.users.$post>["form"]
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -10,17 +10,12 @@ import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
interface ColumnOptions {
|
interface ColumnOptions {
|
||||||
permissions: Partial<CrudPermission>;
|
permissions: Partial<CrudPermission>;
|
||||||
actions: {
|
|
||||||
detail: (id: string) => void;
|
|
||||||
edit: (id: string) => void;
|
|
||||||
delete: (id: string, name: string) => void;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createColumns = (options: ColumnOptions) => {
|
const createColumns = (options: ColumnOptions) => {
|
||||||
const columnHelper =
|
const columnHelper =
|
||||||
createColumnHelper<
|
createColumnHelper<
|
||||||
InferResponseType<typeof client.users.$get>[number]
|
InferResponseType<typeof client.users.$get>["data"][number]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
|
@ -70,7 +65,11 @@ const createColumns = (options: ColumnOptions) => {
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "status",
|
id: "status",
|
||||||
header: "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({
|
columnHelper.display({
|
||||||
|
|
@ -86,27 +85,21 @@ const createColumns = (options: ColumnOptions) => {
|
||||||
{
|
{
|
||||||
label: "Detail",
|
label: "Detail",
|
||||||
permission: options.permissions.read,
|
permission: options.permissions.read,
|
||||||
action: () =>
|
action: `?detail=${props.row.original.id}`,
|
||||||
options.actions.detail(props.row.original.id),
|
|
||||||
color: "green",
|
color: "green",
|
||||||
icon: <TbEye />,
|
icon: <TbEye />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
permission: options.permissions.update,
|
permission: options.permissions.update,
|
||||||
action: () =>
|
action: `?edit=${props.row.original.id}`,
|
||||||
options.actions.edit(props.row.original.id),
|
|
||||||
color: "yellow",
|
color: "yellow",
|
||||||
icon: <TbPencil />,
|
icon: <TbPencil />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Delete",
|
label: "Delete",
|
||||||
permission: options.permissions.delete,
|
permission: options.permissions.delete,
|
||||||
action: () =>
|
action: `?delete=${props.row.original.id}`,
|
||||||
options.actions.delete(
|
|
||||||
props.row.original.id,
|
|
||||||
props.row.original.name ?? ""
|
|
||||||
),
|
|
||||||
color: "red",
|
color: "red",
|
||||||
icon: <TbTrash />,
|
icon: <TbTrash />,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as DashboardLayoutImport } from './routes/_dashboardLayout'
|
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'
|
import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboardLayout/dashboard/index'
|
||||||
|
|
||||||
// Create Virtual Routes
|
// Create Virtual Routes
|
||||||
|
|
@ -21,9 +22,6 @@ import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboar
|
||||||
const IndexLazyImport = createFileRoute('/')()
|
const IndexLazyImport = createFileRoute('/')()
|
||||||
const LogoutIndexLazyImport = createFileRoute('/logout/')()
|
const LogoutIndexLazyImport = createFileRoute('/logout/')()
|
||||||
const LoginIndexLazyImport = createFileRoute('/login/')()
|
const LoginIndexLazyImport = createFileRoute('/login/')()
|
||||||
const DashboardLayoutUsersIndexLazyImport = createFileRoute(
|
|
||||||
'/_dashboardLayout/users/',
|
|
||||||
)()
|
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
|
@ -47,13 +45,12 @@ const LoginIndexLazyRoute = LoginIndexLazyImport.update({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/login/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/login/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const DashboardLayoutUsersIndexLazyRoute =
|
const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({
|
||||||
DashboardLayoutUsersIndexLazyImport.update({
|
path: '/users/',
|
||||||
path: '/users/',
|
getParentRoute: () => DashboardLayoutRoute,
|
||||||
getParentRoute: () => DashboardLayoutRoute,
|
} as any).lazy(() =>
|
||||||
} as any).lazy(() =>
|
import('./routes/_dashboardLayout/users/index.lazy').then((d) => d.Route),
|
||||||
import('./routes/_dashboardLayout/users/index.lazy').then((d) => d.Route),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
const DashboardLayoutDashboardIndexRoute =
|
const DashboardLayoutDashboardIndexRoute =
|
||||||
DashboardLayoutDashboardIndexImport.update({
|
DashboardLayoutDashboardIndexImport.update({
|
||||||
|
|
@ -104,7 +101,7 @@ declare module '@tanstack/react-router' {
|
||||||
id: '/_dashboardLayout/users/'
|
id: '/_dashboardLayout/users/'
|
||||||
path: '/users'
|
path: '/users'
|
||||||
fullPath: '/users'
|
fullPath: '/users'
|
||||||
preLoaderRoute: typeof DashboardLayoutUsersIndexLazyImport
|
preLoaderRoute: typeof DashboardLayoutUsersIndexImport
|
||||||
parentRoute: typeof DashboardLayoutImport
|
parentRoute: typeof DashboardLayoutImport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,10 +113,50 @@ export const routeTree = rootRoute.addChildren({
|
||||||
IndexLazyRoute,
|
IndexLazyRoute,
|
||||||
DashboardLayoutRoute: DashboardLayoutRoute.addChildren({
|
DashboardLayoutRoute: DashboardLayoutRoute.addChildren({
|
||||||
DashboardLayoutDashboardIndexRoute,
|
DashboardLayoutDashboardIndexRoute,
|
||||||
DashboardLayoutUsersIndexLazyRoute,
|
DashboardLayoutUsersIndexRoute,
|
||||||
}),
|
}),
|
||||||
LoginIndexLazyRoute,
|
LoginIndexLazyRoute,
|
||||||
LogoutIndexLazyRoute,
|
LogoutIndexLazyRoute,
|
||||||
})
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* 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 */
|
||||||
|
|
|
||||||
|
|
@ -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 { 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({
|
export const Route = createLazyFileRoute("/_dashboardLayout/users/")({
|
||||||
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/")({
|
|
||||||
component: UsersPage,
|
component: UsersPage,
|
||||||
|
|
||||||
validateSearch: searchParamSchema,
|
|
||||||
|
|
||||||
loader: ({ context: { queryClient } }) => {
|
|
||||||
queryClient.ensureQueryData(userQueryOptions);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type DataType = ExtractQueryDataType<typeof userQueryOptions>;
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<DataType>();
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<PageTemplate
|
||||||
<Title order={1}>Users</Title>
|
title="Users"
|
||||||
<Card>
|
queryOptions={userQueryOptions}
|
||||||
<UsersTable />
|
modals={[<UserFormModal />]}
|
||||||
</Card>
|
columnDefs={[
|
||||||
</Stack>
|
columnHelper.display({
|
||||||
|
id: "sequence",
|
||||||
|
header: "#",
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
size: 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: "Email",
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: "Email",
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
apps/frontend/src/routes/_dashboardLayout/users/index.tsx
Normal file
18
apps/frontend/src/routes/_dashboardLayout/users/index.tsx
Normal 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));
|
||||||
|
},
|
||||||
|
});
|
||||||
9
apps/frontend/src/types/ExtractQueryDataType.d.ts
vendored
Normal file
9
apps/frontend/src/types/ExtractQueryDataType.d.ts
vendored
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user