Skip to content

Commit

Permalink
Merge pull request #100 from ls1intum/fix/pagination-issue-in-applica…
Browse files Browse the repository at this point in the history
…tion-management

Fix pagination issue in application management
  • Loading branch information
Stephan Krusche authored Jul 2, 2024
2 parents 29417a7 + e49a059 commit e83f306
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 148 deletions.
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
db_backups

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea

# AWS User-specific
.idea/**/aws.xml
Expand Down
163 changes: 52 additions & 111 deletions client/src/management/components/ThesisApplicationsDatatable.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,71 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
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 { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { Query } from '../../state/query'
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
female: boolean
status: string[]
}

export const ThesisApplicationsDatatable = (): JSX.Element => {
const { applicationId } = useParams()
const navigate = useNavigate()
const [bodyRef] = useAutoAnimate<HTMLTableSectionElement>()

const [page, setPage] = useState(1)
const [limit, setLimit] = useState(20)

const [selectedApplications, setSelectedApplications] = useState<ThesisApplication[]>([])
const [openedApplication, setOpenedApplication] = useState<ThesisApplication>()

const [searchQuery, setSearchQuery] = useState('')
const [tablePage, setTablePage] = useState(1)
const [tablePageSize, setTablePageSize] = useState(20)
const [tableRecords, setTableRecords] = useState<ThesisApplication[]>([])
const [selectedTableRecords, setSelectedTableRecords] = useState<ThesisApplication[]>([])
const [selectedApplicationToView, setSelectedApplicationToView] = useState<
ThesisApplication | undefined
>(undefined)
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<ThesisApplication>>({
const [sort, setSort] = useState<DataTableSortStatus<ThesisApplication>>({
columnAccessor: 'createdAt',
direction: 'desc',
})
const [filters, setFilters] = useState<Filters>({
male: false,
female: false,
status: [],
})
const [filteredStates, setFilteredStates] = useState<string[] | undefined>(['NOT_ASSESSED'])

const { data: fetchedThesisApplications, isLoading } = useQuery<Pageable<ThesisApplication>>({
const { data: applications, isLoading } = useQuery<Pageable<ThesisApplication>>({
queryKey: [
Query.THESIS_APPLICATION,
tablePage,
tablePageSize,
page,
limit,
searchQuery,
sortStatus.columnAccessor,
sortStatus.direction,
filteredStates?.join(','),
sort.columnAccessor,
sort.direction,
],
queryFn: () =>
getThesisApplications(
tablePage - 1,
tablePageSize,
page - 1,
limit,
filteredStates,
searchQuery,
sortStatus.columnAccessor,
sortStatus.direction,
sort.columnAccessor,
sort.direction,
),
})

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

useEffect(() => {
const filteredSortedData = fetchedThesisApplications?.content
.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(filteredSortedData ?? [])

if (selectedApplicationToView) {
setSelectedApplicationToView(
fetchedThesisApplications?.content
.filter((ca) => ca.id === selectedApplicationToView.id)
.at(0),
)
}
}, [
fetchedThesisApplications,
tablePageSize,
tablePage,
searchQuery,
filters,
sortStatus,
selectedApplicationToView,
])

return (
<Stack>
{selectedApplicationToView && (
{openedApplication && (
<Modal
centered
size='90%'
opened={!!selectedApplicationToView}
opened={!!openedApplication}
onClose={() => {
navigate('/management/thesis-applications')
setSelectedApplicationToView(undefined)
setOpenedApplication(undefined)
}}
>
<ThesisApplicationForm
application={selectedApplicationToView}
application={openedApplication}
accessMode={ApplicationFormAccessMode.INSTRUCTOR}
/>
</Modal>
Expand All @@ -146,25 +88,25 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
verticalSpacing='md'
striped
highlightOnHover
totalRecords={fetchedThesisApplications?.totalElements ?? 0}
recordsPerPage={tablePageSize}
page={tablePage}
onPageChange={(page) => {
setTablePage(page)
totalRecords={applications?.totalElements ?? 0}
recordsPerPage={limit}
page={page}
onPageChange={(x) => {
setPage(x)
}}
recordsPerPageOptions={[5, 10, 15, 20, 25, 30, 35, 40, 50, 100, 200, 300]}
onRecordsPerPageChange={(pageSize) => {
setTablePageSize(pageSize)
setLimit(pageSize)
}}
sortStatus={sortStatus}
sortStatus={sort}
onSortStatusChange={(status) => {
setTablePage(1)
setSortStatus(status)
setPage(1)
setSort(status)
}}
bodyRef={bodyRef}
records={tableRecords}
selectedRecords={selectedTableRecords}
onSelectedRecordsChange={setSelectedTableRecords}
records={applications?.content}
selectedRecords={selectedApplications}
onSelectedRecordsChange={setSelectedApplications}
columns={[
{
accessor: 'application_status',
Expand All @@ -181,20 +123,17 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
value: key,
}
})}
value={filters.status}
value={filteredStates}
placeholder='Search status...'
onChange={(value) => {
setFilters({
...filters,
status: value,
})
setFilteredStates(value.length > 0 ? value : undefined)
}}
leftSection={<IconSearch size={16} />}
clearable
searchable
/>
),
filtering: filters.status.length > 0,
filtering: (filteredStates?.length ?? 0) > 0,
render: (application) => {
let color: string = 'gray'
switch (application.applicationStatus) {
Expand Down Expand Up @@ -251,24 +190,28 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
<ActionIcon
variant='transparent'
color='blue'
onClick={(e: React.MouseEvent) => {
onClick={(e) => {
e.stopPropagation()
navigate(`/management/thesis-applications/${application.id}`)

setOpenedApplication(application)
}}
>
<IconEyeEdit size={16} />
</ActionIcon>
<Link
to={`/management/thesis-applications/${application.id}`}
to='/management/thesis-applications'
onClick={(e) => {
e.stopPropagation()

setOpenedApplication(application)
}}
target='_blank'
rel='noopener noreferrer'
>
<ActionIcon
variant='transparent'
color='blue'
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
}}
onClick={(e) => e.stopPropagation()}
>
<IconExternalLink size={16} />
</ActionIcon>
Expand All @@ -277,9 +220,7 @@ export const ThesisApplicationsDatatable = (): JSX.Element => {
),
},
]}
onRowClick={({ record: application }) => {
navigate(`/management/thesis-applications/${application.id}`)
}}
onRowClick={({ record: application }) => setOpenedApplication(application)}
/>
</Stack>
)
Expand Down
2 changes: 2 additions & 0 deletions client/src/network/thesisApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Pageable } from '../interface/pageable'
export const getThesisApplications = async (
page: number,
limit: number,
states?: string[],
searchQuery?: string,
sortBy?: string,
sortOrder?: 'asc' | 'desc',
Expand All @@ -18,6 +19,7 @@ export const getThesisApplications = async (
params: {
page,
limit,
states: states?.join(',') ?? Object.keys(ApplicationStatus).join(','),
searchQuery,
sortBy,
sortOrder,
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:
- SPRING_DATASOURCE_USERNAME=thesis-track-postgres
- SPRING_DATASOURCE_PASSWORD=thesis-track-postgres
- SPRING_JPA_HIBERNATE_DDL_AUTO=update
- KEYCLOAK_ISSUER_URI=http://keycloak:8080/realms/thesis-track
- KEYCLOAK_JWK_SET_URI=http://keycloak:8080/realms/thesis-track/protocol/openid-connect/certs
ports:
- "8080:8080"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

Expand All @@ -52,16 +53,14 @@ public ThesisApplicationController(final ThesisApplicationService thesisApplicat
@PreAuthorize("hasRole('chair-member') || hasRole('thesis-track-admin')")
public ResponseEntity<Page<ThesisApplication>> getAll(@RequestParam Integer page,
@RequestParam final Integer limit,
@RequestParam(required = false) final String states,
@RequestParam(required = false) final String searchQuery,
@RequestParam(defaultValue = "createdAt", required = false) final String sortBy,
@RequestParam(defaultValue = "desc", required = false) final String sortOrder) {
return ResponseEntity.ok(thesisApplicationService.getAll(page, limit, searchQuery, sortBy, sortOrder));
}
List<String> filteredStates = Arrays.asList(states.split(","));
filteredStates.removeIf(String::isEmpty);

@GetMapping("/not-assessed")
@PreAuthorize("hasRole('chair-member') || hasRole('thesis-track-admin')")
public ResponseEntity<List<ThesisApplication>> getAllNotAssessed() {
return ResponseEntity.ok(thesisApplicationService.getAllNotAssessed());
return ResponseEntity.ok(thesisApplicationService.getAll(page, limit, filteredStates, searchQuery, sortBy, sortOrder));
}

@GetMapping("/{thesisApplicationId}/examination-report")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@
@Data
@Entity
@Table
@org.hibernate.annotations.NamedQueries({
@org.hibernate.annotations.NamedQuery(
name = "ThesisApplication.findAllNotAssessed",
query = "SELECT ta FROM ThesisApplication ta " +
"WHERE ta.applicationStatus = thesistrack.ls1.model.enums.ApplicationStatus.NOT_ASSESSED"
)
})
public class ThesisApplication implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import thesistrack.ls1.model.ThesisApplication;

import java.util.List;
import java.util.Set;
import java.util.UUID;

@Repository
public interface ThesisApplicationRepository extends JpaRepository<ThesisApplication, UUID> {
@Transactional
List<ThesisApplication> findAllNotAssessed();

@Query("SELECT ta FROM ThesisApplication ta WHERE " +
"LOWER(ta.student.firstName) LIKE %?1% OR " +
"LOWER(ta.student.lastName) LIKE %?1% OR " +
"LOWER(ta.student.email) LIKE %?1% OR " +
"LOWER(ta.student.matriculationNumber) LIKE %?1% OR " +
"LOWER(ta.student.tumId) LIKE %?1%")
Page<ThesisApplication> searchThesisApplications(final String searchQuery, final Pageable page);
"(ta.applicationStatus IN(:states)) AND " +
"(LOWER(ta.student.firstName) LIKE %:searchQuery% OR " +
"LOWER(ta.student.lastName) LIKE %:searchQuery% OR " +
"LOWER(ta.student.email) LIKE %:searchQuery% OR " +
"LOWER(ta.student.matriculationNumber) LIKE %:searchQuery% OR " +
"LOWER(ta.student.tumId) LIKE %:searchQuery%)")
Page<ThesisApplication> searchThesisApplications(@Param("states") Set<String> states, @Param("searchQuery") String searchQuery, final Pageable page);
}
Loading

0 comments on commit e83f306

Please sign in to comment.