Skip to content

Commit b905768

Browse files
authored
♻️ Tweaks in frontend (#1273)
1 parent 93e83c1 commit b905768

File tree

15 files changed

+184
-124
lines changed

15 files changed

+184
-124
lines changed

frontend/src/components/Admin/AddUser.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
2020
import { type UserCreate, UsersService } from "../../client"
2121
import type { ApiError } from "../../client/core/ApiError"
2222
import useCustomToast from "../../hooks/useCustomToast"
23-
import { emailPattern } from "../../utils"
23+
import { emailPattern, handleError } from "../../utils"
2424

2525
interface AddUserProps {
2626
isOpen: boolean
@@ -62,8 +62,7 @@ const AddUser = ({ isOpen, onClose }: AddUserProps) => {
6262
onClose()
6363
},
6464
onError: (err: ApiError) => {
65-
const errDetail = (err.body as any)?.detail
66-
showToast("Something went wrong.", `${errDetail}`, "error")
65+
handleError(err, showToast)
6766
},
6867
onSettled: () => {
6968
queryClient.invalidateQueries({ queryKey: ["users"] })

frontend/src/components/Admin/EditUser.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
UsersService,
2525
} from "../../client"
2626
import useCustomToast from "../../hooks/useCustomToast"
27-
import { emailPattern } from "../../utils"
27+
import { emailPattern, handleError } from "../../utils"
2828

2929
interface EditUserProps {
3030
user: UserPublic
@@ -60,8 +60,7 @@ const EditUser = ({ user, isOpen, onClose }: EditUserProps) => {
6060
onClose()
6161
},
6262
onError: (err: ApiError) => {
63-
const errDetail = (err.body as any)?.detail
64-
showToast("Something went wrong.", `${errDetail}`, "error")
63+
handleError(err, showToast)
6564
},
6665
onSettled: () => {
6766
queryClient.invalidateQueries({ queryKey: ["users"] })

frontend/src/components/Items/AddItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
1717

1818
import { type ApiError, type ItemCreate, ItemsService } from "../../client"
1919
import useCustomToast from "../../hooks/useCustomToast"
20+
import { handleError } from "../../utils"
2021

2122
interface AddItemProps {
2223
isOpen: boolean
@@ -49,8 +50,7 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => {
4950
onClose()
5051
},
5152
onError: (err: ApiError) => {
52-
const errDetail = (err.body as any)?.detail
53-
showToast("Something went wrong.", `${errDetail}`, "error")
53+
handleError(err, showToast)
5454
},
5555
onSettled: () => {
5656
queryClient.invalidateQueries({ queryKey: ["items"] })

frontend/src/components/Items/EditItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ItemsService,
2323
} from "../../client"
2424
import useCustomToast from "../../hooks/useCustomToast"
25+
import { handleError } from "../../utils"
2526

2627
interface EditItemProps {
2728
item: ItemPublic
@@ -51,8 +52,7 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => {
5152
onClose()
5253
},
5354
onError: (err: ApiError) => {
54-
const errDetail = (err.body as any)?.detail
55-
showToast("Something went wrong.", `${errDetail}`, "error")
55+
handleError(err, showToast)
5656
},
5757
onSettled: () => {
5858
queryClient.invalidateQueries({ queryKey: ["items"] })

frontend/src/components/UserSettings/ChangePassword.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
1414

1515
import { type ApiError, type UpdatePassword, UsersService } from "../../client"
1616
import useCustomToast from "../../hooks/useCustomToast"
17-
import { confirmPasswordRules, passwordRules } from "../../utils"
17+
import { confirmPasswordRules, handleError, passwordRules } from "../../utils"
1818

1919
interface UpdatePasswordForm extends UpdatePassword {
2020
confirm_password: string
@@ -42,8 +42,7 @@ const ChangePassword = () => {
4242
reset()
4343
},
4444
onError: (err: ApiError) => {
45-
const errDetail = (err.body as any)?.detail
46-
showToast("Something went wrong.", `${errDetail}`, "error")
45+
handleError(err, showToast)
4746
},
4847
})
4948

frontend/src/components/UserSettings/DeleteConfirmation.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useForm } from "react-hook-form"
1414
import { type ApiError, UsersService } from "../../client"
1515
import useAuth from "../../hooks/useAuth"
1616
import useCustomToast from "../../hooks/useCustomToast"
17+
import { handleError } from "../../utils"
1718

1819
interface DeleteProps {
1920
isOpen: boolean
@@ -42,8 +43,7 @@ const DeleteConfirmation = ({ isOpen, onClose }: DeleteProps) => {
4243
onClose()
4344
},
4445
onError: (err: ApiError) => {
45-
const errDetail = (err.body as any)?.detail
46-
showToast("Something went wrong.", `${errDetail}`, "error")
46+
handleError(err, showToast)
4747
},
4848
onSettled: () => {
4949
queryClient.invalidateQueries({ queryKey: ["currentUser"] })

frontend/src/components/UserSettings/UserInformation.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from "../../client"
2424
import useAuth from "../../hooks/useAuth"
2525
import useCustomToast from "../../hooks/useCustomToast"
26-
import { emailPattern } from "../../utils"
26+
import { emailPattern, handleError } from "../../utils"
2727

2828
const UserInformation = () => {
2929
const queryClient = useQueryClient()
@@ -57,13 +57,10 @@ const UserInformation = () => {
5757
showToast("Success!", "User updated successfully.", "success")
5858
},
5959
onError: (err: ApiError) => {
60-
const errDetail = (err.body as any)?.detail
61-
showToast("Something went wrong.", `${errDetail}`, "error")
60+
handleError(err, showToast)
6261
},
6362
onSettled: () => {
64-
// TODO: can we do just one call now?
65-
queryClient.invalidateQueries({ queryKey: ["users"] })
66-
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
63+
queryClient.invalidateQueries()
6764
},
6865
})
6966

@@ -104,6 +101,8 @@ const UserInformation = () => {
104101
size="md"
105102
py={2}
106103
color={!currentUser?.full_name ? "ui.dim" : "inherit"}
104+
isTruncated
105+
maxWidth="250px"
107106
>
108107
{currentUser?.full_name || "N/A"}
109108
</Text>
@@ -125,7 +124,7 @@ const UserInformation = () => {
125124
w="auto"
126125
/>
127126
) : (
128-
<Text size="md" py={2}>
127+
<Text size="md" py={2} isTruncated maxWidth="250px">
129128
{currentUser?.email}
130129
</Text>
131130
)}

frontend/src/hooks/useAuth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const useAuth = () => {
4747
errDetail = err.message
4848
}
4949

50-
showToast("Something went wrong.", `${errDetail}`, "error")
50+
showToast("Something went wrong.", errDetail, "error")
5151
},
5252
onSettled: () => {
5353
queryClient.invalidateQueries({ queryKey: ["users"] })
Lines changed: 122 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Badge,
33
Box,
4+
Button,
45
Container,
56
Flex,
67
Heading,
@@ -13,90 +14,65 @@ import {
1314
Thead,
1415
Tr,
1516
} from "@chakra-ui/react"
16-
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"
17-
import { createFileRoute } from "@tanstack/react-router"
17+
import { useQuery, useQueryClient } from "@tanstack/react-query"
18+
import { createFileRoute, useNavigate } from "@tanstack/react-router"
19+
import { useEffect } from "react"
20+
import { z } from "zod"
1821

19-
import { Suspense } from "react"
2022
import { type UserPublic, UsersService } from "../../client"
2123
import AddUser from "../../components/Admin/AddUser"
2224
import ActionsMenu from "../../components/Common/ActionsMenu"
2325
import Navbar from "../../components/Common/Navbar"
2426

27+
const usersSearchSchema = z.object({
28+
page: z.number().catch(1),
29+
})
30+
2531
export const Route = createFileRoute("/_layout/admin")({
2632
component: Admin,
33+
validateSearch: (search) => usersSearchSchema.parse(search),
2734
})
2835

29-
const MembersTableBody = () => {
36+
const PER_PAGE = 5
37+
38+
function getUsersQueryOptions({ page }: { page: number }) {
39+
return {
40+
queryFn: () =>
41+
UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
42+
queryKey: ["users", { page }],
43+
}
44+
}
45+
46+
function UsersTable() {
3047
const queryClient = useQueryClient()
3148
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
49+
const { page } = Route.useSearch()
50+
const navigate = useNavigate({ from: Route.fullPath })
51+
const setPage = (page: number) =>
52+
navigate({ search: (prev) => ({ ...prev, page }) })
3253

33-
const { data: users } = useSuspenseQuery({
34-
queryKey: ["users"],
35-
queryFn: () => UsersService.readUsers({}),
54+
const {
55+
data: users,
56+
isPending,
57+
isPlaceholderData,
58+
} = useQuery({
59+
...getUsersQueryOptions({ page }),
60+
placeholderData: (prevData) => prevData,
3661
})
3762

38-
return (
39-
<Tbody>
40-
{users.data.map((user) => (
41-
<Tr key={user.id}>
42-
<Td color={!user.full_name ? "ui.dim" : "inherit"}>
43-
{user.full_name || "N/A"}
44-
{currentUser?.id === user.id && (
45-
<Badge ml="1" colorScheme="teal">
46-
You
47-
</Badge>
48-
)}
49-
</Td>
50-
<Td>{user.email}</Td>
51-
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
52-
<Td>
53-
<Flex gap={2}>
54-
<Box
55-
w="2"
56-
h="2"
57-
borderRadius="50%"
58-
bg={user.is_active ? "ui.success" : "ui.danger"}
59-
alignSelf="center"
60-
/>
61-
{user.is_active ? "Active" : "Inactive"}
62-
</Flex>
63-
</Td>
64-
<Td>
65-
<ActionsMenu
66-
type="User"
67-
value={user}
68-
disabled={currentUser?.id === user.id ? true : false}
69-
/>
70-
</Td>
71-
</Tr>
72-
))}
73-
</Tbody>
74-
)
75-
}
63+
const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE
64+
const hasPreviousPage = page > 1
7665

77-
const MembersBodySkeleton = () => {
78-
return (
79-
<Tbody>
80-
<Tr>
81-
{new Array(5).fill(null).map((_, index) => (
82-
<Td key={index}>
83-
<SkeletonText noOfLines={1} paddingBlock="16px" />
84-
</Td>
85-
))}
86-
</Tr>
87-
</Tbody>
88-
)
89-
}
66+
useEffect(() => {
67+
if (hasNextPage) {
68+
queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 }))
69+
}
70+
}, [page, queryClient, hasNextPage])
9071

91-
function Admin() {
9272
return (
93-
<Container maxW="full">
94-
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
95-
User Management
96-
</Heading>
97-
<Navbar type={"User"} addModalAs={AddUser} />
73+
<>
9874
<TableContainer>
99-
<Table fontSize="md" size={{ base: "sm", md: "md" }}>
75+
<Table size={{ base: "sm", md: "md" }}>
10076
<Thead>
10177
<Tr>
10278
<Th width="20%">Full name</Th>
@@ -106,11 +82,89 @@ function Admin() {
10682
<Th width="10%">Actions</Th>
10783
</Tr>
10884
</Thead>
109-
<Suspense fallback={<MembersBodySkeleton />}>
110-
<MembersTableBody />
111-
</Suspense>
85+
{isPending ? (
86+
<Tbody>
87+
<Tr>
88+
{new Array(4).fill(null).map((_, index) => (
89+
<Td key={index}>
90+
<SkeletonText noOfLines={1} paddingBlock="16px" />
91+
</Td>
92+
))}
93+
</Tr>
94+
</Tbody>
95+
) : (
96+
<Tbody>
97+
{users?.data.map((user) => (
98+
<Tr key={user.id}>
99+
<Td
100+
color={!user.full_name ? "ui.dim" : "inherit"}
101+
isTruncated
102+
maxWidth="150px"
103+
>
104+
{user.full_name || "N/A"}
105+
{currentUser?.id === user.id && (
106+
<Badge ml="1" colorScheme="teal">
107+
You
108+
</Badge>
109+
)}
110+
</Td>
111+
<Td isTruncated maxWidth="150px">
112+
{user.email}
113+
</Td>
114+
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
115+
<Td>
116+
<Flex gap={2}>
117+
<Box
118+
w="2"
119+
h="2"
120+
borderRadius="50%"
121+
bg={user.is_active ? "ui.success" : "ui.danger"}
122+
alignSelf="center"
123+
/>
124+
{user.is_active ? "Active" : "Inactive"}
125+
</Flex>
126+
</Td>
127+
<Td>
128+
<ActionsMenu
129+
type="User"
130+
value={user}
131+
disabled={currentUser?.id === user.id ? true : false}
132+
/>
133+
</Td>
134+
</Tr>
135+
))}
136+
</Tbody>
137+
)}
112138
</Table>
113139
</TableContainer>
140+
<Flex
141+
gap={4}
142+
alignItems="center"
143+
mt={4}
144+
direction="row"
145+
justifyContent="flex-end"
146+
>
147+
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
148+
Previous
149+
</Button>
150+
<span>Page {page}</span>
151+
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
152+
Next
153+
</Button>
154+
</Flex>
155+
</>
156+
)
157+
}
158+
159+
function Admin() {
160+
return (
161+
<Container maxW="full">
162+
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
163+
Users Management
164+
</Heading>
165+
166+
<Navbar type={"User"} addModalAs={AddUser} />
167+
<UsersTable />
114168
</Container>
115169
)
116170
}

0 commit comments

Comments
 (0)