diff --git a/frontend/src/components/AdminsComponents/AdminList.tsx b/frontend/src/components/AdminsComponents/AdminList.tsx index 3c98cf078..a4b0a7e96 100644 --- a/frontend/src/components/AdminsComponents/AdminList.tsx +++ b/frontend/src/components/AdminsComponents/AdminList.tsx @@ -2,9 +2,7 @@ import { User } from "../../utils/api/users/users"; import { AdminsTable } from "./styles"; import React from "react"; import { AdminListItem } from "./index"; -import LoadSpinner from "../Common/LoadSpinner"; import { ListDiv } from "../Common/Users/styles"; -import { SpinnerContainer } from "../Common/LoadSpinner/styles"; import { RemoveTh } from "../Common/Tables/styles"; /** @@ -21,20 +19,6 @@ export default function AdminList(props: { gotData: boolean; removeAdmin: (user: User) => void; }) { - if (props.loading) { - return ( - - - - ); - } else if (props.admins.length === 0) { - if (props.gotData) { - return
No admins
; - } else { - return null; - } - } - return ( diff --git a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx index 1e93b9b74..3011ea184 100644 --- a/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx +++ b/frontend/src/components/ProjectsComponents/CreateProjectComponents/InputFields/Coach/Coach.tsx @@ -23,7 +23,7 @@ export default function Coach({ useEffect(() => { async function callCoaches() { - setAvailableCoaches((await getCoaches(editionId, coach, 0)).users); + setAvailableCoaches((await getCoaches(editionId, coach, 0))!.users); } callCoaches(); }, [coach, editionId]); @@ -49,7 +49,7 @@ export default function Coach({ Add - + ); diff --git a/frontend/src/components/ProjectsComponents/ProjectTable.tsx b/frontend/src/components/ProjectsComponents/ProjectTable.tsx index 2823b81c6..d963538ec 100644 --- a/frontend/src/components/ProjectsComponents/ProjectTable.tsx +++ b/frontend/src/components/ProjectsComponents/ProjectTable.tsx @@ -9,7 +9,6 @@ import LoadSpinner from "../Common/LoadSpinner"; * A table of [[ProjectCard]]s. * @param props.projects A list of projects which needs to be shown. * @param props.loading Data is not available yet. - * @param props.gotData All data is received. * @param props.getMoreProjects A function to load more projects. * @param props.moreProjectsAvailable More unfetched projects available. * @param props.removeProject A function which will be called when a project is removed. @@ -17,12 +16,14 @@ import LoadSpinner from "../Common/LoadSpinner"; export default function ProjectTable(props: { projects: Project[]; loading: boolean; - gotData: boolean; - getMoreProjects: () => void; + getMoreProjects: (page: number, reset: boolean) => void; moreProjectsAvailable: boolean; removeProject: (project: Project) => void; }) { - if (props.gotData && props.projects.length === 0) { + if (props.projects.length === 0) { + if (props.loading) { + return ; + } return (
No projects found.
@@ -32,7 +33,7 @@ export default function ProjectTable(props: { return ( props.getMoreProjects(page, false)} hasMore={props.moreProjectsAvailable} loader={} initialLoad={true} diff --git a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx index 13b45ae44..2d5e97d54 100644 --- a/frontend/src/components/UsersComponents/Coaches/Coaches.tsx +++ b/frontend/src/components/UsersComponents/Coaches/Coaches.tsx @@ -4,15 +4,17 @@ import { User } from "../../../utils/api/users/users"; import { CoachList, AddCoach } from "./CoachesComponents"; import { SearchBar } from "../../Common/Forms"; import { SearchFieldDiv, TableDiv } from "../../Common/Users/styles"; +import LoadSpinner from "../../Common/LoadSpinner"; /** * List of coaches of the given edition. * This includes a searchfield and the option to remove and add coaches. * @param props.edition The edition of which coaches are shown. * @param props.coaches The list of coaches which need to be shown. + * @param props.loading Data is being loaded * @param props.getMoreCoaches A function to load more coaches. * @param props.searchCoaches A function to set the filter for coaches' username. - * @param props.gotData All data is received. + * @param props.setPage Set the next page to fetch * @param props.moreCoachesAvailable More unfetched coaches available. * @param props.searchTerm Current filter for coaches' names. * @param props.refreshCoaches A function which will be called when a coach is added. @@ -21,17 +23,22 @@ import { SearchFieldDiv, TableDiv } from "../../Common/Users/styles"; export default function Coaches(props: { edition: string; coaches: User[]; - getMoreCoaches: () => void; + loading: boolean; + getMoreCoaches: (page: number) => void; searchCoaches: (word: string) => void; - gotData: boolean; + setPage: (page: number) => void; moreCoachesAvailable: boolean; searchTerm: string; refreshCoaches: () => void; removeCoach: (user: User) => void; }) { let table; - if (props.gotData && props.coaches.length === 0) { - table =
No coaches found
; + if (props.coaches.length === 0) { + if (props.loading) { + table = ; + } else { + table =
No coaches found
; + } } else { table = ( Coaches props.searchCoaches(e.target.value)} + onChange={e => { + props.searchCoaches(e.target.value); + props.setPage(0); + }} value={props.searchTerm} placeholder="Search name..." /> diff --git a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx index e306197fd..f6c34d92f 100644 --- a/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx +++ b/frontend/src/components/UsersComponents/Coaches/CoachesComponents/CoachList.tsx @@ -19,13 +19,12 @@ export default function CoachList(props: { coaches: User[]; edition: string; removeCoach: (coach: User) => void; - getMoreCoaches: () => void; + getMoreCoaches: (page: number) => void; moreCoachesAvailable: boolean; }) { return ( } diff --git a/frontend/src/components/UsersComponents/Requests/Requests.tsx b/frontend/src/components/UsersComponents/Requests/Requests.tsx index ab598de4d..55262ef96 100644 --- a/frontend/src/components/UsersComponents/Requests/Requests.tsx +++ b/frontend/src/components/UsersComponents/Requests/Requests.tsx @@ -6,6 +6,7 @@ import { RequestList, RequestsHeader } from "./RequestsComponents"; import SearchBar from "../../Common/Forms/SearchBar"; import { SearchFieldDiv } from "../../Common/Users/styles"; import { toast } from "react-toastify"; +import { LoadSpinner } from "../../Common"; /** * A collapsible component which contains all coach requests for a given edition. @@ -18,7 +19,7 @@ export default function Requests(props: { edition: string; refreshCoaches: () => const [requests, setRequests] = useState([]); // All requests after filter const [loading, setLoading] = useState(false); // Waiting for data const [searchTerm, setSearchTerm] = useState(""); // The word set in the filter - const [gotData, setGotData] = useState(false); // Received data + const [requestedEdition, setRequestedEdition] = useState(props.edition); const [open, setOpen] = useState(false); // Collapsible is open const [moreRequestsAvailable, setMoreRequestsAvailable] = useState(true); // Endpoint has more requests available const [allRequestsFetched, setAllRequestsFetched] = useState(false); @@ -52,13 +53,15 @@ export default function Requests(props: { edition: string; refreshCoaches: () => * Request the next page from the list of requests. * The set searchterm will be used. */ - async function getData() { - if (loading) { + async function getData(requested: number, reset: boolean) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { return; } - if (allRequestsFetched) { - setGotData(true); + if (allRequestsFetched && !reset) { setRequests( allRequests.filter(request => request.user.name.toUpperCase().includes(searchTerm.toUpperCase()) @@ -77,67 +80,61 @@ export default function Requests(props: { edition: string; refreshCoaches: () => setController(newController); const response = await toast.promise( - getRequests(props.edition, searchTerm, page, newController), + getRequests(props.edition, searchTerm, requestedPage, newController), { error: "Failed to retrieve requests", } ); - if (response.requests.length === 0) { - setMoreRequestsAvailable(false); - } - if (page === 0) { - setRequests(response.requests); - } else { - setRequests(requests.concat(response.requests)); - } - - if (searchTerm === "") { - if (response.requests.length === 0) { - setAllRequestsFetched(true); + if (response !== null) { + if (response.requests.length === 0 && !filterChanged) { + setMoreRequestsAvailable(false); } - if (page === 0) { - setAllRequests(response.requests); + if (requestedPage === 0 || filterChanged) { + setRequests(response.requests); } else { - setAllRequests(allRequests.concat(response.requests)); + setRequests(requests.concat(response.requests)); } - } - setPage(page + 1); + if (searchTerm === "") { + if (response.requests.length === 0 && !filterChanged) { + setAllRequestsFetched(true); + } + if (requestedPage === 0) { + setAllRequests(response.requests); + } else { + setAllRequests(allRequests.concat(response.requests)); + } + } - setGotData(true); + setPage(requestedPage + 1); + } else { + setMoreRequestsAvailable(false); + } setLoading(false); } - /** - * update the requests when the edition changes - */ useEffect(() => { - refreshRequests(); - }, [props.edition]); - - /** - * Reset the list of requests and get the first page. - * Used when the edition is changed. - */ - function refreshRequests() { - setRequests([]); - setPage(0); - setAllRequestsFetched(false); - setGotData(false); - setMoreRequestsAvailable(true); - } - - function filter(searchTerm: string) { - setPage(0); - setGotData(false); - setMoreRequestsAvailable(true); - setSearchTerm(searchTerm); - setRequests([]); - } + if (props.edition !== requestedEdition) { + setRequests([]); + setPage(0); + setAllRequestsFetched(false); + setMoreRequestsAvailable(true); + getData(-1, true); + setRequestedEdition(props.edition); + } else { + setPage(0); + setMoreRequestsAvailable(true); + getData(-1, false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, props.edition]); let list; - if (gotData && requests.length === 0) { + if (requests.length === 0) { + if (loading) { + list = ; + } list =
No requests found
; } else { list = ( @@ -160,7 +157,8 @@ export default function Requests(props: { edition: string; refreshCoaches: () => { - filter(e.target.value); + setPage(0); + setSearchTerm(e.target.value); }} value={searchTerm} placeholder="Search name..." diff --git a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx index 16c2a2e6e..beb03a4f6 100644 --- a/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx +++ b/frontend/src/components/UsersComponents/Requests/RequestsComponents/RequestList.tsx @@ -17,13 +17,12 @@ export default function RequestList(props: { requests: Request[]; removeRequest: (coachAdded: boolean, request: Request) => void; moreRequestsAvailable: boolean; - getMoreRequests: () => void; + getMoreRequests: (page: number, reset: boolean) => void; }) { return ( props.getMoreRequests(page, false)} hasMore={props.moreRequestsAvailable} loader={} useWindow={false} diff --git a/frontend/src/utils/api/mail_overview.ts b/frontend/src/utils/api/mail_overview.ts index b4e7f3700..111d7d47b 100644 --- a/frontend/src/utils/api/mail_overview.ts +++ b/frontend/src/utils/api/mail_overview.ts @@ -1,6 +1,7 @@ import { Email, Student } from "../../data/interfaces"; import { EmailType } from "../../data/enums"; import { axiosInstance } from "./api"; +import axios from "axios"; /** * A student together with its email history @@ -26,17 +27,25 @@ export async function getMailOverview( name: string, filters: EmailType[], controller: AbortController -): Promise { - const FormatFilters: string[] = filters.map(filter => { - return `&email_status=${Object.values(EmailType).indexOf(filter)}`; - }); - const concatted: string = FormatFilters.join(""); +): Promise { + try { + const FormatFilters: string[] = filters.map(filter => { + return `&email_status=${Object.values(EmailType).indexOf(filter)}`; + }); + const concatted: string = FormatFilters.join(""); - const response = await axiosInstance.get( - `/editions/${edition}/students/emails?page=${page}&name=${name}${concatted}`, - { signal: controller.signal } - ); - return response.data as StudentEmails; + const response = await axiosInstance.get( + `/editions/${edition}/students/emails?page=${page}&name=${name}${concatted}`, + { signal: controller.signal } + ); + return response.data as StudentEmails; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } } /** diff --git a/frontend/src/utils/api/projects.ts b/frontend/src/utils/api/projects.ts index 7d1fd0755..d60b7f3f8 100644 --- a/frontend/src/utils/api/projects.ts +++ b/frontend/src/utils/api/projects.ts @@ -14,7 +14,6 @@ import { axiosInstance } from "./api"; * @param ownProjects To filter on your own projects. * @param page The requested page. * @param controller An optional AbortController to cancel the request - * @returns */ export async function getProjects( edition: string, @@ -22,20 +21,27 @@ export async function getProjects( ownProjects: boolean, page: number, controller: AbortController -): Promise { - // eslint-disable-next-line promise/param-names - const response = await axiosInstance.get( - "/editions/" + - edition + - "/projects?name=" + - name + - "&coach=" + - ownProjects.toString() + - "&page=" + - page.toString(), - { signal: controller.signal } - ); - return response.data as Projects; +): Promise { + try { + const response = await axiosInstance.get( + "/editions/" + + edition + + "/projects?name=" + + name + + "&coach=" + + ownProjects.toString() + + "&page=" + + page.toString(), + { signal: controller.signal } + ); + return response.data as Projects; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } } /** diff --git a/frontend/src/utils/api/users/coaches.ts b/frontend/src/utils/api/users/coaches.ts index e9539e021..155f3b7d5 100644 --- a/frontend/src/utils/api/users/coaches.ts +++ b/frontend/src/utils/api/users/coaches.ts @@ -1,5 +1,6 @@ import { UsersList } from "./users"; import { axiosInstance } from "../api"; +import axios from "axios"; /** * Get a page from all coaches from the given edition. @@ -13,18 +14,26 @@ export async function getCoaches( name: string, page: number, controller: AbortController | null = null -): Promise { +): Promise { if (controller === null) { const response = await axiosInstance.get( `/users?edition=${edition}&page=${page}&name=${name}` ); return response.data as UsersList; } else { - const response = await axiosInstance.get( - `/users?edition=${edition}&page=${page}&name=${name}`, - { signal: controller.signal } - ); - return response.data as UsersList; + try { + const response = await axiosInstance.get( + `/users?edition=${edition}&page=${page}&name=${name}`, + { signal: controller.signal } + ); + return response.data as UsersList; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } } } diff --git a/frontend/src/utils/api/users/requests.ts b/frontend/src/utils/api/users/requests.ts index 2741706ce..9950caa6d 100644 --- a/frontend/src/utils/api/users/requests.ts +++ b/frontend/src/utils/api/users/requests.ts @@ -1,5 +1,6 @@ import { User } from "./users"; import { axiosInstance } from "../api"; +import axios from "axios"; /** * Interface of a request @@ -28,12 +29,20 @@ export async function getRequests( name: string, page: number, controller: AbortController -): Promise { - const response = await axiosInstance.get( - `/users/requests?edition=${edition}&page=${page}&user=${name}`, - { signal: controller.signal } - ); - return response.data as GetRequestsResponse; +): Promise { + try { + const response = await axiosInstance.get( + `/users/requests?edition=${edition}&page=${page}&user=${name}`, + { signal: controller.signal } + ); + return response.data as GetRequestsResponse; + } catch (error) { + if (axios.isAxiosError(error) && error.code === "ERR_CANCELED") { + return null; + } else { + throw error; + } + } } /** diff --git a/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx b/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx index 19488cdb2..3f3f607a9 100644 --- a/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx +++ b/frontend/src/views/MailOverviewPage/MailOverviewPage.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { getMailOverview, setStateRequest, StudentEmail } from "../../utils/api/mail_overview"; import Dropdown from "react-bootstrap/Dropdown"; import InfiniteScroll from "react-infinite-scroller"; @@ -12,6 +12,7 @@ import { MessageDiv, MailOverviewDiv, SearchAndChangeDiv, + ClearDiv, } from "./styles"; import { EmailType } from "../../data/enums"; import { useParams } from "react-router-dom"; @@ -32,9 +33,11 @@ interface EmailRow { * Page that shows the email status of all students, with the possibility to change the status */ export default function MailOverviewPage() { + const { editionId } = useParams(); + const [emailRows, setEmailRows] = useState([]); - const [gotEmails, setGotEmails] = useState(false); const [loading, setLoading] = useState(false); + const [requestedEdition, setRequestedEdition] = useState(editionId); const [moreEmailsAvailable, setMoreEmailsAvailable] = useState(true); // Endpoint has more emailRows available const [page, setPage] = useState(0); const [allSelected, setAllSelected] = useState(false); @@ -44,14 +47,16 @@ export default function MailOverviewPage() { // Keep track of the set filters const [searchTerm, setSearchTerm] = useState(""); const [filters, setFilters] = useState([]); - - const { editionId } = useParams(); + const [filtersChanged, setFiltersChanged] = useState(0); /** * update the table with new values */ - async function updateMailOverview() { - if (loading) { + async function updateMailOverview(requested: number) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { return; } @@ -64,56 +69,56 @@ export default function MailOverviewPage() { setController(newController); const response = await toast.promise( - getMailOverview(editionId, page, searchTerm, filters, newController), + getMailOverview(editionId, requestedPage, searchTerm, filters, newController), { error: "Failed to retrieve states" } ); - if (response.studentEmails.length === 0) { - setMoreEmailsAvailable(false); - } - if (page === 0) { - setEmailRows( - response.studentEmails.map(email => { - return { - email: email, - checked: false, - }; - }) - ); - } else { - setEmailRows( - emailRows.concat( + + if (response !== null) { + if (response.studentEmails.length === 0 && !filterChanged) { + setMoreEmailsAvailable(false); + } + if (requestedPage === 0) { + setEmailRows( response.studentEmails.map(email => { return { email: email, checked: false, }; }) - ) - ); + ); + } else { + setEmailRows( + emailRows.concat( + response.studentEmails.map(email => { + return { + email: email, + checked: false, + }; + }) + ) + ); + } + setPage(requestedPage + 1); + } else { + setMoreEmailsAvailable(false); } - setPage(page + 1); - - setGotEmails(true); setLoading(false); } - function refresh() { - setPage(0); - setGotEmails(false); - setMoreEmailsAvailable(true); - setEmailRows([]); - setAllSelected(false); - } - - function searchName(newSearchTerm: string) { - setSearchTerm(newSearchTerm); - refresh(); - } - - function changeFilter(newFilter: EmailType[]) { - setFilters(newFilter); - refresh(); - } + useEffect(() => { + if (editionId !== requestedEdition) { + setEmailRows([]); + setPage(0); + setMoreEmailsAvailable(true); + updateMailOverview(-1); + setRequestedEdition(editionId); + } else { + setPage(0); + setMoreEmailsAvailable(true); + updateMailOverview(-1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, filtersChanged, editionId]); /** * Keeps the selectedRows list up-to-date when a student is selected/unselected in the table @@ -156,74 +161,70 @@ export default function MailOverviewPage() { }) ); setAllSelected(false); - refresh(); } let table; - if (gotEmails && emailRows.length === 0) { - table = ( - - No students found. - - ); + if (emailRows.length === 0) { + if (loading) { + table = ; + } else { + table = ( + + No students found. + + ); + } } else { table = ( - - } - initialLoad={true} - useWindow={false} - getScrollParent={() => document.getElementById("root")} - > - - - - + } + initialLoad={true} + useWindow={false} + getScrollParent={() => document.getElementById("root")} + > + + + + + selectAll(e.target.checked)} + checked={allSelected} + /> + + First Name + Last Name + Current State + Date + + + + {emailRows.map(row => ( + + selectAll(e.target.checked)} - checked={allSelected} + onChange={e => + selectNewStudent(row.email.student, e.target.checked) + } + checked={row.checked} /> - - First Name - Last Name - Current State - Date + + {row.email.student.firstName} + {row.email.student.lastName} + {Object.values(EmailType)[row.email.emails[0].decision]} + + {new Date(String(row.email.emails[0].date)).toLocaleString( + "nl-be" + )} + - - - {emailRows.map(row => ( - - - - selectNewStudent( - row.email.student, - e.target.checked - ) - } - checked={row.checked} - /> - - {row.email.student.firstName} - {row.email.student.lastName} - - {Object.values(EmailType)[row.email.emails[0].decision]} - - - {new Date(String(row.email.emails[0].date)).toLocaleString( - "nl-be" - )} - - - ))} - - - - + ))} + + + ); } @@ -232,7 +233,8 @@ export default function MailOverviewPage() { { - searchName(e.target.value); + setPage(0); + setSearchTerm(e.target.value); }} value={searchTerm} placeholder="Search a student" @@ -245,8 +247,16 @@ export default function MailOverviewPage() { placeholder=" Filter on State" showArrow={true} isObject={false} - onRemove={changeFilter} - onSelect={changeFilter} + onRemove={e => { + setPage(0); + setFilters(e); + setFiltersChanged(filtersChanged + 1); + }} + onSelect={e => { + setPage(0); + setFilters(e); + setFiltersChanged(filtersChanged + 1); + }} options={Object.values(EmailType)} /> @@ -267,7 +277,8 @@ export default function MailOverviewPage() { - {table} + + {table} ); } diff --git a/frontend/src/views/MailOverviewPage/styles.ts b/frontend/src/views/MailOverviewPage/styles.ts index 7fbf66782..6684ef0ca 100644 --- a/frontend/src/views/MailOverviewPage/styles.ts +++ b/frontend/src/views/MailOverviewPage/styles.ts @@ -51,3 +51,7 @@ export const MailOverviewDiv = styled.div` width: fit-content; margin: auto; `; + +export const ClearDiv = styled.div` + clear: both; +`; diff --git a/frontend/src/views/UsersPage/UsersPage.tsx b/frontend/src/views/UsersPage/UsersPage.tsx index 51dda0130..6b9cd1f61 100644 --- a/frontend/src/views/UsersPage/UsersPage.tsx +++ b/frontend/src/views/UsersPage/UsersPage.tsx @@ -11,27 +11,31 @@ import { toast } from "react-toastify"; * Page for admins to manage coach and admin settings. */ function UsersPage() { + const params = useParams(); + // Note: The coaches are not in the coaches component because accepting a request needs to refresh the coaches list. const [allCoaches, setAllCoaches] = useState([]); const [coaches, setCoaches] = useState([]); // All coaches from the selected edition const [loading, setLoading] = useState(false); // Waiting for data (used for spinner) - const [gotData, setGotData] = useState(false); // Received data const [moreCoachesAvailable, setMoreCoachesAvailable] = useState(true); // Endpoint has more coaches available + const [requestedEdition, setRequestedEdition] = useState(params.editionId); const [allCoachesFetched, setAllCoachesFetched] = useState(false); const [searchTerm, setSearchTerm] = useState(""); // The word set in filter for coachlist const [page, setPage] = useState(0); // The next page to request const [controller, setController] = useState(undefined); - const params = useParams(); const navigate = useNavigate(); /** * Request the next page from the list of coaches. * The set searchterm will be used. */ - async function getCoachesData() { - if (loading) { + async function getCoachesData(requested: number) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { return; } @@ -54,54 +58,50 @@ function UsersPage() { setController(newController); const response = await toast.promise( - getCoaches(params.editionId as string, searchTerm, page, newController), + getCoaches(params.editionId as string, searchTerm, requestedPage, newController), { error: "Failed to retrieve coaches" } ); - if (response.users.length === 0) { - setMoreCoachesAvailable(false); - } - if (page === 0) { - setCoaches(response.users); - } else { - setCoaches(coaches.concat(response.users)); - } - if (searchTerm === "") { - if (response.users.length === 0) { - setAllCoachesFetched(true); + if (response !== null) { + if (response.users.length === 0 && !filterChanged) { + setMoreCoachesAvailable(false); } - if (page === 0) { - setAllCoaches(response.users); + if (requestedPage === 0 || filterChanged) { + setCoaches(response.users); } else { - setAllCoaches(allCoaches.concat(response.users)); + setCoaches(coaches.concat(response.users)); } - } - setPage(page + 1); + if (searchTerm === "") { + if (response.users.length === 0) { + setAllCoachesFetched(true); + } + if (requestedPage === 0) { + setAllCoaches(response.users); + } else { + setAllCoaches(allCoaches.concat(response.users)); + } + } + + setPage(page + 1); + } else { + setMoreCoachesAvailable(false); + } - setGotData(true); setLoading(false); } - /** - * update the coaches when the edition changes - */ useEffect(() => { - refreshCoaches(); - }, [params.editionId]); - - /** - * Set the searchTerm and request the first page with this filter. - * The current list of coaches will be resetted. - * @param searchTerm The string to filter coaches with by username. - */ - function filterCoachesData(searchTerm: string) { - setPage(0); - setGotData(false); - setMoreCoachesAvailable(true); - setSearchTerm(searchTerm); - setCoaches([]); - } + if (params.editionId !== requestedEdition) { + setRequestedEdition(params.editionId); + refreshCoaches(); + } else { + setPage(0); + setMoreCoachesAvailable(true); + getCoachesData(-1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, params.editionId]); /** * Reset the list of coaches and get the first page. @@ -111,8 +111,8 @@ function UsersPage() { setCoaches([]); setPage(0); setAllCoachesFetched(false); - setGotData(false); setMoreCoachesAvailable(true); + getCoachesData(-1); } /** @@ -143,9 +143,10 @@ function UsersPage() { ([]); const [projects, setProjects] = useState([]); - const [gotProjects, setGotProjects] = useState(false); const [loading, setLoading] = useState(false); + const [requestedEdition, setRequestedEdition] = useState(params.editionId); const [moreProjectsAvailable, setMoreProjectsAvailable] = useState(true); // Endpoint has more coaches available const [allProjectsFetched, setAllProjectsFetched] = useState(false); @@ -33,24 +35,34 @@ export default function ProjectPage() { const navigate = useNavigate(); const [page, setPage] = useState(0); - const params = useParams(); const editionId = params.editionId!; - const { role, editions } = useAuth(); + const { role, editions, userId } = useAuth(); /** * Used to fetch the projects */ - async function loadProjects() { - if (loading) { + async function loadProjects(requested: number, reset: boolean) { + const filterChanged = requested === -1; + const requestedPage = requested === -1 ? 0 : page; + + if (loading && !filterChanged) { return; } - if (allProjectsFetched) { + if (allProjectsFetched && !reset) { + const newUserId: number = userId === null ? -1 : userId; + setProjects( - allProjects.filter(project => - project.name.toUpperCase().includes(searchString.toUpperCase()) - ) + allProjects + .filter(project => + project.name.toUpperCase().includes(searchString.toUpperCase()) + ) + .filter( + project => + !ownProjects || + project.coaches.map(coach => coach.userId).includes(newUserId) + ) ); setMoreProjectsAvailable(false); return; @@ -65,52 +77,53 @@ export default function ProjectPage() { setController(newController); const response = await toast.promise( - getProjects(editionId, searchString, ownProjects, page, newController), + getProjects(editionId, searchString, ownProjects, requestedPage, newController), { error: "Failed to retrieve projects" } ); - if (response.projects.length === 0) { - setMoreProjectsAvailable(false); - } - if (page === 0) { - setProjects(response.projects); - } else { - setProjects(projects.concat(response.projects)); - } - if (searchString === "") { - if (response.projects.length === 0) { - setAllProjectsFetched(true); + if (response !== null) { + if (response.projects.length === 0 && !filterChanged) { + setMoreProjectsAvailable(false); } - if (page === 0) { - setAllProjects(response.projects); + if (requestedPage === 0 || filterChanged) { + setProjects(response.projects); } else { - setAllProjects(allProjects.concat(response.projects)); + setProjects(projects.concat(response.projects)); } - } - setPage(page + 1); + if (searchString === "" && !ownProjects) { + if (response.projects.length === 0) { + setAllProjectsFetched(true); + } + if (requestedPage === 0) { + setAllProjects(response.projects); + } else { + setAllProjects(allProjects.concat(response.projects)); + } + } - setGotProjects(true); + setPage(requestedPage + 1); + } else { + setMoreProjectsAvailable(false); + } setLoading(false); } - /** - * update the projects when the edition changes - */ useEffect(() => { - refreshProjects(); - }, [editionId]); - - /** - * Reset fetched projects - */ - function refreshProjects() { - setProjects([]); - setPage(0); - setMoreProjectsAvailable(true); - setAllProjectsFetched(false); - setGotProjects(false); - } + if (params.editionId !== requestedEdition) { + setProjects([]); + setPage(0); + setAllProjectsFetched(false); + setMoreProjectsAvailable(true); + loadProjects(-1, true); + setRequestedEdition(params.editionId); + } else { + setPage(0); + setMoreProjectsAvailable(true); + loadProjects(-1, false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchString, ownProjects, params.editionId]); /** * Remove a project in local list. @@ -124,25 +137,16 @@ export default function ProjectPage() { ); } - /** - * Filter the projects by name - * @param searchTerm - */ - function filter(searchTerm: string) { - setPage(0); - setGotProjects(false); - setMoreProjectsAvailable(true); - setSearchString(searchTerm); - setProjects([]); - } - return (
filter(e.target.value)} + onChange={e => { + setPage(0); + setSearchString(e.target.value); + }} value={searchString} placeholder="Search project..." /> @@ -164,14 +168,13 @@ export default function ProjectPage() { label="Only own projects" checked={ownProjects} onChange={() => { + setPage(0); setOwnProjects(!ownProjects); - refreshProjects(); }} />