♻️ Tweaks in frontend (#1273)

This commit is contained in:
Alejandra
2024-08-01 13:01:03 -05:00
committed by GitHub
parent 93e83c1ad4
commit b905768656
15 changed files with 186 additions and 126 deletions

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ import {
ItemsService,
} from "../../client"
import useCustomToast from "../../hooks/useCustomToast"
import { handleError } from "../../utils"
interface EditItemProps {
item: ItemPublic
@@ -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"] })

View File

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

View File

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

View File

@@ -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()
@@ -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()
},
})
@@ -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>
@@ -125,7 +124,7 @@ const UserInformation = () => {
w="auto"
/>
) : (
<Text size="md" py={2}>
<Text size="md" py={2} isTruncated maxWidth="250px">
{currentUser?.email}
</Text>
)}

View File

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

View File

@@ -1,6 +1,7 @@
import {
Badge,
Box,
Button,
Container,
Flex,
Heading,
@@ -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"
export const Route = createFileRoute("/_layout/admin")({
component: Admin,
const usersSearchSchema = z.object({
page: z.number().catch(1),
})
const MembersTableBody = () => {
export const Route = createFileRoute("/_layout/admin")({
component: Admin,
validateSearch: (search) => usersSearchSchema.parse(search),
})
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>
@@ -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>
)
}

View File

@@ -3,7 +3,7 @@ import {
Container,
Flex,
Heading,
Skeleton,
SkeletonText,
Table,
TableContainer,
Tbody,
@@ -64,7 +64,7 @@ function ItemsTable() {
if (hasNextPage) {
queryClient.prefetchQuery(getItemsQueryOptions({ page: page + 1 }))
}
}, [page, queryClient])
}, [page, queryClient, hasNextPage])
return (
<>
@@ -80,25 +80,27 @@ function ItemsTable() {
</Thead>
{isPending ? (
<Tbody>
{new Array(5).fill(null).map((_, index) => (
<Tr key={index}>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<Flex>
<Skeleton height="20px" width="20px" />
</Flex>
</Td>
))}
</Tr>
))}
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{items?.data.map((item) => (
<Tr key={item.id} opacity={isPlaceholderData ? 0.5 : 1}>
<Td>{item.id}</Td>
<Td>{item.title}</Td>
<Td color={!item.description ? "ui.dim" : "inherit"}>
<Td isTruncated maxWidth="150px">
{item.title}
</Td>
<Td
color={!item.description ? "ui.dim" : "inherit"}
isTruncated
maxWidth="150px"
>
{item.description || "N/A"}
</Td>
<Td>

View File

@@ -14,7 +14,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
import { type ApiError, LoginService } from "../client"
import { isLoggedIn } from "../hooks/useAuth"
import useCustomToast from "../hooks/useCustomToast"
import { emailPattern } from "../utils"
import { emailPattern, handleError } from "../utils"
interface FormData {
email: string
@@ -57,8 +57,7 @@ function RecoverPassword() {
reset()
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
})

View File

@@ -15,7 +15,7 @@ import { type SubmitHandler, useForm } from "react-hook-form"
import { type ApiError, LoginService, type NewPassword } from "../client"
import { isLoggedIn } from "../hooks/useAuth"
import useCustomToast from "../hooks/useCustomToast"
import { confirmPasswordRules, passwordRules } from "../utils"
import { confirmPasswordRules, handleError, passwordRules } from "../utils"
interface NewPasswordForm extends NewPassword {
confirm_password: string
@@ -65,8 +65,7 @@ function ResetPassword() {
navigate({ to: "/login" })
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
handleError(err, showToast)
},
})

View File

@@ -1,3 +1,5 @@
import type { ApiError } from "./client"
export const emailPattern = {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
@@ -40,3 +42,12 @@ export const confirmPasswordRules = (
return rules
}
export const handleError = (err: ApiError, showToast: any) => {
const errDetail = (err.body as any)?.detail
let errorMessage = errDetail || "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
}