Skip to content

Commit

Permalink
♻️ Tweaks in frontend (#1273)
Browse files Browse the repository at this point in the history
  • Loading branch information
alejsdev authored Aug 1, 2024
1 parent 93e83c1 commit b905768
Show file tree
Hide file tree
Showing 15 changed files with 184 additions and 124 deletions.
5 changes: 2 additions & 3 deletions frontend/src/components/Admin/AddUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
import { type UserCreate, UsersService } from "../../client"
import type { ApiError } from "../../client/core/ApiError"
import useCustomToast from "../../hooks/useCustomToast"
import { emailPattern } from "../../utils"
import { emailPattern, handleError } from "../../utils"

interface AddUserProps {
isOpen: boolean
Expand Down Expand Up @@ -62,8 +62,7 @@ const AddUser = ({ isOpen, onClose }: AddUserProps) => {
onClose()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/Admin/EditUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
UsersService,
} from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { emailPattern } from "../../utils"
import { emailPattern, handleError } from "../../utils"

interface EditUserProps {
user: UserPublic
Expand Down Expand Up @@ -60,8 +60,7 @@ const EditUser = ({ user, isOpen, onClose }: EditUserProps) => {
onClose()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Items/AddItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"

import { type ApiError, type ItemCreate, ItemsService } from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { handleError } from "../../utils"

interface AddItemProps {
isOpen: boolean
Expand Down Expand Up @@ -49,8 +50,7 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => {
onClose()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Items/EditItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ItemsService,
} from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { handleError } from "../../utils"

interface EditItemProps {
item: ItemPublic
Expand Down Expand Up @@ -51,8 +52,7 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => {
onClose()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/UserSettings/ChangePassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"

import { type ApiError, type UpdatePassword, UsersService } from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { confirmPasswordRules, passwordRules } from "../../utils"
import { confirmPasswordRules, handleError, passwordRules } from "../../utils"

interface UpdatePasswordForm extends UpdatePassword {
confirm_password: string
Expand Down Expand Up @@ -42,8 +42,7 @@ const ChangePassword = () => {
reset()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/UserSettings/DeleteConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useForm } from "react-hook-form"
import { type ApiError, UsersService } from "../../client"
import useAuth from "../../hooks/useAuth"
import useCustomToast from "../../hooks/useCustomToast"
import { handleError } from "../../utils"

interface DeleteProps {
isOpen: boolean
Expand Down Expand Up @@ -42,8 +43,7 @@ const DeleteConfirmation = ({ isOpen, onClose }: DeleteProps) => {
onClose()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/UserSettings/UserInformation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "../../client"
import useAuth from "../../hooks/useAuth"
import useCustomToast from "../../hooks/useCustomToast"
import { emailPattern } from "../../utils"
import { emailPattern, handleError } from "../../utils"

const UserInformation = () => {
const queryClient = useQueryClient()
Expand Down Expand Up @@ -57,13 +57,10 @@ const UserInformation = () => {
showToast("Success!", "User updated successfully.", "success")
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
onSettled: () => {
// TODO: can we do just one call now?
queryClient.invalidateQueries({ queryKey: ["users"] })
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
queryClient.invalidateQueries()
},
})

Expand Down Expand Up @@ -104,6 +101,8 @@ const UserInformation = () => {
size="md"
py={2}
color={!currentUser?.full_name ? "ui.dim" : "inherit"}
isTruncated
maxWidth="250px"
>
{currentUser?.full_name || "N/A"}
</Text>
Expand All @@ -125,7 +124,7 @@ const UserInformation = () => {
w="auto"
/>
) : (
<Text size="md" py={2}>
<Text size="md" py={2} isTruncated maxWidth="250px">
{currentUser?.email}
</Text>
)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const useAuth = () => {
errDetail = err.message
}

showToast("Something went wrong.", `${errDetail}`, "error")
showToast("Something went wrong.", errDetail, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
190 changes: 122 additions & 68 deletions frontend/src/routes/_layout/admin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Badge,
Box,
Button,
Container,
Flex,
Heading,
Expand All @@ -13,90 +14,65 @@ import {
Thead,
Tr,
} from "@chakra-ui/react"
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"

import { Suspense } from "react"
import { type UserPublic, UsersService } from "../../client"
import AddUser from "../../components/Admin/AddUser"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"

const usersSearchSchema = z.object({
page: z.number().catch(1),
})

export const Route = createFileRoute("/_layout/admin")({
component: Admin,
validateSearch: (search) => usersSearchSchema.parse(search),
})

const MembersTableBody = () => {
const PER_PAGE = 5

function getUsersQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["users", { page }],
}
}

function UsersTable() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })

const { data: users } = useSuspenseQuery({
queryKey: ["users"],
queryFn: () => UsersService.readUsers({}),
const {
data: users,
isPending,
isPlaceholderData,
} = useQuery({
...getUsersQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})

return (
<Tbody>
{users.data.map((user) => (
<Tr key={user.id}>
<Td color={!user.full_name ? "ui.dim" : "inherit"}>
{user.full_name || "N/A"}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td>{user.email}</Td>
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? "ui.success" : "ui.danger"}
alignSelf="center"
/>
{user.is_active ? "Active" : "Inactive"}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
)
}
const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE
const hasPreviousPage = page > 1

const MembersBodySkeleton = () => {
return (
<Tbody>
<Tr>
{new Array(5).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
)
}
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])

function Admin() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
User Management
</Heading>
<Navbar type={"User"} addModalAs={AddUser} />
<>
<TableContainer>
<Table fontSize="md" size={{ base: "sm", md: "md" }}>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th width="20%">Full name</Th>
Expand All @@ -106,11 +82,89 @@ function Admin() {
<Th width="10%">Actions</Th>
</Tr>
</Thead>
<Suspense fallback={<MembersBodySkeleton />}>
<MembersTableBody />
</Suspense>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{users?.data.map((user) => (
<Tr key={user.id}>
<Td
color={!user.full_name ? "ui.dim" : "inherit"}
isTruncated
maxWidth="150px"
>
{user.full_name || "N/A"}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td isTruncated maxWidth="150px">
{user.email}
</Td>
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? "ui.success" : "ui.danger"}
alignSelf="center"
/>
{user.is_active ? "Active" : "Inactive"}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}

function Admin() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Users Management
</Heading>

<Navbar type={"User"} addModalAs={AddUser} />
<UsersTable />
</Container>
)
}
Loading

0 comments on commit b905768

Please sign in to comment.