Skip to content

♻️ Tweaks in frontend #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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