Skip to content

Commit

Permalink
Add client side pagination and sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
airelawaleria committed May 21, 2024
1 parent a68f040 commit 7d6cec4
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 104 deletions.
28 changes: 28 additions & 0 deletions client/src/interface/pageable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface Pageable<T> {
content: T[]
empty: boolean
first: boolean
last: boolean
number: number
numberOfElements: number
pageable?: {
offset: number
pageNumber: number
pageSize: number
paged: boolean
sort: {
empty: boolean
sorted: boolean
unsorted: boolean
}
unpaged: boolean
}
size: number
sort?: {
empty: boolean
sorted: boolean
unsorted: boolean
}
totalElements: number
totalPages: number
}
130 changes: 55 additions & 75 deletions client/src/management/components/ThesisApplicationsDatatable.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useEffect, useState } from 'react'
import sortBy from 'lodash/sortBy'
import { ActionIcon, Badge, Group, Modal, MultiSelect, Stack, TextInput } from '@mantine/core'
import { DataTable, type DataTableSortStatus } from 'mantine-datatable'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import { IconExternalLink, IconEyeEdit, IconSearch } from '@tabler/icons-react'
import moment from 'moment'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useThesisApplicationStore } from '../../state/zustand/useThesisApplicationStore'
import { useQuery } from '@tanstack/react-query'
import { Query } from '../../state/query'
import { getThesisAdvisors, getThesisApplications } from '../../network/thesisApplication'
import { ThesisAdvisor, ThesisApplication } from '../../interface/thesisApplication'
import { getThesisApplications } from '../../network/thesisApplication'
import { ThesisApplication } from '../../interface/thesisApplication'
import { ApplicationStatus } from '../../interface/application'
import { Gender } from '../../interface/student'
import {
ApplicationFormAccessMode,
ThesisApplicationForm,
} from '../../student/form/ThesisApplicationForm'
import { Pageable } from '../../interface/pageable'

interface Filters {
male: boolean
Expand All @@ -27,12 +26,9 @@ interface Filters {
export const ThesisApplicationsDatatable = (): JSX.Element => {
const { applicationId } = useParams()
const navigate = useNavigate()
const { thesisApplications, setThesisApplications, setThesisAdvisors } =
useThesisApplicationStore()
const [bodyRef] = useAutoAnimate<HTMLTableSectionElement>()
const [searchQuery, setSearchQuery] = useState('')
const [tablePage, setTablePage] = useState(1)
const [totalDisplayedRecords, setTotalDisplayedRecords] = useState(thesisApplications.length)
const [tablePageSize, setTablePageSize] = useState(20)
const [tableRecords, setTableRecords] = useState<ThesisApplication[]>([])
const [selectedTableRecords, setSelectedTableRecords] = useState<ThesisApplication[]>([])
Expand All @@ -49,87 +45,68 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
status: [],
})

const { data: fetchedThesisApplications, isLoading } = useQuery<ThesisApplication[]>({
queryKey: [Query.THESIS_APPLICATION],
queryFn: () => getThesisApplications(),
})

useEffect(() => {
if (fetchedThesisApplications) {
setThesisApplications(fetchedThesisApplications)
}
}, [fetchedThesisApplications, setThesisApplications])

const { data: fetchedThesisAdvisors } = useQuery<ThesisAdvisor[]>({
queryKey: [Query.THESIS_ADVISOR],
queryFn: () => getThesisAdvisors(),
const { data: fetchedThesisApplications, isLoading } = useQuery<Pageable<ThesisApplication>>({
queryKey: [
Query.THESIS_APPLICATION,
tablePage,
tablePageSize,
sortStatus.columnAccessor,
sortStatus.direction,
],
queryFn: () =>
getThesisApplications(
tablePage - 1,
tablePageSize,
sortStatus.columnAccessor,
sortStatus.direction,
),
})

useEffect(() => {
if (fetchedThesisAdvisors) {
setThesisAdvisors(fetchedThesisAdvisors)
}
}, [fetchedThesisAdvisors, setThesisAdvisors])

useEffect(() => {
if (applicationId) {
setSelectedApplicationToView(thesisApplications.find((a) => a.id === applicationId))
setSelectedApplicationToView(
fetchedThesisApplications?.content.find((a) => a.id === applicationId),
)
} else {
setSelectedApplicationToView(undefined)
}
}, [thesisApplications, applicationId])
}, [fetchedThesisApplications, applicationId])

useEffect(() => {
const from = (tablePage - 1) * tablePageSize
const to = from + tablePageSize

const filteredSortedData = sortBy(
thesisApplications
.filter(({ student }) => {
return `${student.firstName ?? ''} ${student.lastName ?? ''} ${student.tumId ?? ''} ${
student.matriculationNumber ?? ''
}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
})
.filter(
(application) =>
filters.status.length === 0 || filters.status.includes(application.applicationStatus),
)
.filter((application) =>
filters.female && application.student.gender
? Gender[application.student.gender] === Gender.FEMALE
: true,
)
.filter((application) =>
filters.male && application.student.gender
? Gender[application.student.gender] === Gender.MALE
: true,
),
sortStatus.columnAccessor === 'fullName'
? ['student.firstName', 'student.lastName']
: sortStatus.columnAccessor,
)

setTotalDisplayedRecords(filteredSortedData.length)
const filteredSortedData = fetchedThesisApplications?.content
.filter(({ student }) => {
return `${student.firstName ?? ''} ${student.lastName ?? ''} ${student.tumId ?? ''} ${
student.matriculationNumber ?? ''
}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
})
.filter(
(application) =>
filters.status.length === 0 || filters.status.includes(application.applicationStatus),
)
.filter((application) =>
filters.female && application.student.gender
? Gender[application.student.gender] === Gender.FEMALE
: true,
)
.filter((application) =>
filters.male && application.student.gender
? Gender[application.student.gender] === Gender.MALE
: true,
)

setTableRecords(
(sortStatus.direction === 'desc' ? filteredSortedData.reverse() : filteredSortedData).slice(
from,
to,
),
)
if (from > filteredSortedData.length) {
setTablePage(1)
}
setTableRecords(filteredSortedData ?? [])

if (selectedApplicationToView) {
setSelectedApplicationToView(
thesisApplications.filter((ca) => ca.id === selectedApplicationToView.id).at(0),
fetchedThesisApplications?.content
.filter((ca) => ca.id === selectedApplicationToView.id)
.at(0),
)
}
}, [
thesisApplications,
fetchedThesisApplications,
tablePageSize,
tablePage,
searchQuery,
Expand Down Expand Up @@ -174,7 +151,7 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
verticalSpacing='md'
striped
highlightOnHover
totalRecords={totalDisplayedRecords}
totalRecords={fetchedThesisApplications?.totalElements ?? 0}
recordsPerPage={tablePageSize}
page={tablePage}
onPageChange={(page) => {
Expand All @@ -185,7 +162,10 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
setTablePageSize(pageSize)
}}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
onSortStatusChange={(status) => {
setTablePage(1)
setSortStatus(status)
}}
bodyRef={bodyRef}
records={tableRecords}
selectedRecords={selectedTableRecords}
Expand Down Expand Up @@ -254,7 +234,7 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
sortable: true,
},
{
accessor: 'fullName',
accessor: 'student.firstName',
title: 'Full name',
sortable: true,
render: (application) =>
Expand Down
31 changes: 28 additions & 3 deletions client/src/network/thesisApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,43 @@ import { axiosInstance, notAuthenticatedAxiosInstance } from './configService'
import { ThesisAdvisor, ThesisApplication } from '../interface/thesisApplication'
import { AxiosError } from 'axios'
import { ApplicationStatus } from '../interface/application'
import { Pageable } from '../interface/pageable'

export const getThesisApplications = async (): Promise<ThesisApplication[]> => {
export const getThesisApplications = async (
page: number,
limit: number,
sortBy?: string,
sortOrder?: 'asc' | 'desc',
): Promise<Pageable<ThesisApplication>> => {
try {
return (await axiosInstance.get(`/api/thesis-applications`)).data
return (
await axiosInstance.get(`/api/thesis-applications`, {
params: {
page,
limit,
sortBy,
sortOrder,
},
})
).data
} catch (err) {
notifications.show({
color: 'red',
autoClose: 10000,
title: 'Error',
message: `Could not fetch thesis applications.`,
})
return []
return {
content: [],
totalPages: 0,
totalElements: 0,
number: 0,
size: 0,
empty: true,
first: true,
last: true,
numberOfElements: 0,
}
}
}

Expand Down
21 changes: 0 additions & 21 deletions client/src/state/zustand/useThesisApplicationStore.ts

This file was deleted.

19 changes: 14 additions & 5 deletions client/src/student/form/ThesisApplicationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ import { ApplicationSuccessfulSubmission } from '../ApplicationSubmission/Applic
import { useEffect, useState } from 'react'
import { FormTextField } from './components/FormTextField'
import { FormSelectField } from './components/FormSelectField'
import { useThesisApplicationStore } from '../../state/zustand/useThesisApplicationStore'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
getThesisAdvisors,
getThesisApplicationBachelorReportFile,
getThesisApplicationCvFile,
getThesisApplicationExaminationFile,
Expand All @@ -47,7 +47,12 @@ import {
postThesisApplicationRejection,
postThesisApplicationThesisAdvisorAssignment,
} from '../../network/thesisApplication'
import { FocusTopic, ResearchArea, ThesisApplication } from '../../interface/thesisApplication'
import {
FocusTopic,
ResearchArea,
ThesisAdvisor,
ThesisApplication,
} from '../../interface/thesisApplication'
import { Query } from '../../state/query'

countries.registerLocale(enLocale)
Expand Down Expand Up @@ -76,7 +81,6 @@ export const ThesisApplicationForm = ({
}: ThesisApplicationFormProps): JSX.Element => {
const theme = useMantineTheme()
const queryClient = useQueryClient()
const { thesisAdvisors } = useThesisApplicationStore()
const [loadingOverlayVisible, loadingOverlayHandlers] = useDisclosure(false)
const [applicationSuccessfullySubmitted, setApplicationSuccessfullySubmitted] = useState(false)
const [notifyStudent, setNotifyStudent] = useState(true)
Expand Down Expand Up @@ -221,6 +225,11 @@ export const ThesisApplicationForm = ({
application?.thesisAdvisor?.id ?? null,
)

const { data: fetchedThesisAdvisors } = useQuery<ThesisAdvisor[]>({
queryKey: [Query.THESIS_ADVISOR],
queryFn: () => getThesisAdvisors(),
})

const assessThesisApplication = useMutation({
mutationFn: () =>
postThesisApplicationAssessment(application?.id ?? '', {
Expand Down Expand Up @@ -853,7 +862,7 @@ export const ThesisApplicationForm = ({
<>
<Select
label='Thesis Advisor'
data={thesisAdvisors.map((ta) => {
data={fetchedThesisAdvisors?.map((ta) => {
return {
value: ta.id ?? '',
label: `${ta.firstName} ${ta.lastName} (${ta.tumId})`,
Expand Down

0 comments on commit 7d6cec4

Please sign in to comment.