Skip to content
Open
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
15 changes: 2 additions & 13 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,11 +439,8 @@
}
},
"src/app/components/elements/modals/QuestionSearchModal.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 2
},
"react-hooks/exhaustive-deps": {
"count": 3
"count": 2
}
},
"src/app/components/elements/modals/QuizSettingModal.tsx": {
Expand Down Expand Up @@ -869,7 +866,7 @@
"count": 2
},
"@typescript-eslint/no-floating-promises": {
"count": 10
"count": 9
},
"react-hooks/exhaustive-deps": {
"count": 2
Expand Down Expand Up @@ -983,14 +980,6 @@
"count": 1
}
},
"src/app/components/pages/SubjectLandingPage.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 1
},
"react-hooks/exhaustive-deps": {
"count": 1
}
},
"src/app/components/pages/SubjectOverviewPage.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 2
Expand Down
4 changes: 0 additions & 4 deletions src/IsaacAppTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,6 @@ export type Action =
| {type: ACTION_TYPE.QUESTION_UNLOCK; questionId: string}
| {type: ACTION_TYPE.QUESTION_SET_CURRENT_ATTEMPT; questionId: string; attempt: Immutable<ApiTypes.ChoiceDTO | ValidatedChoice<ApiTypes.ChoiceDTO>>}

| {type: ACTION_TYPE.QUESTION_SEARCH_REQUEST}
| {type: ACTION_TYPE.QUESTION_SEARCH_RESPONSE_SUCCESS; questionResults: ApiTypes.SearchResultsWrapper<ApiTypes.ContentSummaryDTO>, searchId?: string}
| {type: ACTION_TYPE.QUESTION_SEARCH_RESPONSE_FAILURE}

| {type: ACTION_TYPE.MY_QUESTION_ANSWERS_BY_DATE_REQUEST}
| {type: ACTION_TYPE.MY_QUESTION_ANSWERS_BY_DATE_RESPONSE_SUCCESS; myAnsweredQuestionsByDate: ApiTypes.AnsweredQuestionsByDate}
| {type: ACTION_TYPE.MY_QUESTION_ANSWERS_BY_DATE_RESPONSE_FAILURE}
Expand Down
135 changes: 67 additions & 68 deletions src/app/components/elements/modals/QuestionSearchModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, {lazy, Suspense, useCallback, useEffect, useMemo, useReducer, useState} from "react";
import React, {lazy, useEffect, useMemo, useReducer, useState} from "react";
import {
AppState,
clearQuestionSearch,
closeActiveModal,
searchQuestions,
useAppDispatch,
useAppSelector
useAppSelector,
useSearchQuestionsQuery
} from "../../../state";
import debounce from "lodash/debounce";
import isEqual from "lodash/isEqual";
Expand Down Expand Up @@ -35,8 +34,8 @@ import {
useDeviceSize,
EXAM_BOARD, QUESTIONS_PER_GAMEBOARD
} from "../../../services";
import {ContentSummary, GameboardBuilderQuestions, GameboardBuilderQuestionsStackProps} from "../../../../IsaacAppTypes";
import {AudienceContext, Difficulty, ExamBoard} from "../../../../IsaacApiTypes";
import {ContentSummary, GameboardBuilderQuestions, GameboardBuilderQuestionsStackProps, QuestionSearchQuery} from "../../../../IsaacAppTypes";
import {AudienceContext, ContentSummaryDTO, Difficulty, ExamBoard} from "../../../../IsaacApiTypes";
import {GroupBase} from "react-select/dist/declarations/src/types";
import {Loading} from "../../handlers/IsaacSpinner";
import {StyledSelect} from "../inputs/StyledSelect";
Expand All @@ -49,6 +48,8 @@ import { CollapsibleList } from "../CollapsibleList";
import { StyledCheckbox } from "../inputs/StyledCheckbox";
import { updateTopicChoices, initialiseListState, listStateReducer } from "../../../services";
import { HorizontalScroller } from "../inputs/HorizontalScroller";
import { skipToken } from "@reduxjs/toolkit/query";
import { ShowLoadingQuery } from "../../handlers/ShowLoadingQuery";

// Immediately load GameboardBuilderRow, but allow splitting
const importGameboardBuilderRow = import("../GameboardBuilderRow");
Expand All @@ -72,6 +73,9 @@ export const QuestionSearchModal = (
const deviceSize = useDeviceSize();
const sublistDelimiter = " >>> ";

const [searchParams, setSearchParams] = useState<QuestionSearchQuery | undefined>(undefined);
const searchQuestionsQuery = useSearchQuestionsQuery(searchParams ?? skipToken);

const [topicSelections, setTopicSelections] = useState<ChoiceTree[]>([]);
const [searchTopics, setSearchTopics] = useState<string[]>([]);
const [searchQuestionName, setSearchQuestionName] = useState("");
Expand All @@ -83,7 +87,6 @@ export const QuestionSearchModal = (
if (userContext.contexts.length === 1 && !EXAM_BOARD_NULL_OPTIONS.includes(userExamBoard)) setSearchExamBoards([userExamBoard]);
}, [userContext.contexts[0].examBoard]);

const [isSearching, setIsSearching] = useState(false);
const [searchBook, setSearchBook] = useState<string[]>([]);
const isBookSearch = searchBook.length > 0;

Expand All @@ -101,30 +104,22 @@ export const QuestionSearchModal = (

const modalQuestions : GameboardBuilderQuestions = {selectedQuestions, questionOrder, setSelectedQuestions, setQuestionOrder};

const {results: questions} = useAppSelector((state: AppState) => state && state.questionSearchResult) || {};
const user = useAppSelector((state: AppState) => state && state.user);

useEffect(() => {
setIsSearching(false);
}, [questions]);

const searchDebounce = useCallback(
const searchDebounce = useMemo(() =>
debounce((searchString: string, topics: string[], examBoards: string[], book: string[], stages: string[], difficulties: string[], fasttrack: boolean, startIndex: number) => {
// Clear front-end sorting so as not to override ElasticSearch's match ranking
setQuestionsSort({});

const isBookSearch = book.length > 0; // Tasty.
if ([searchString, topics, book, stages, difficulties, examBoards].every(v => v.length === 0) && !fasttrack) {
// Nothing to search for
dispatch(clearQuestionSearch);
return;
}

const tags = (isBookSearch ? book : [...([topics].map((tags) => tags.join(" ")))].filter((query) => query != "")).join(",");

setIsSearching(true);

dispatch(searchQuestions({
setSearchParams({
querySource: "gameboardBuilder",
searchString: searchString || undefined,
tags: tags || undefined,
Expand All @@ -134,12 +129,11 @@ export const QuestionSearchModal = (
fasttrack,
startIndex,
limit: 300
}));
});

logEvent(eventLog,"SEARCH_QUESTIONS", {searchString, topics, examBoards, book, stages, difficulties, fasttrack, startIndex});
}, 250),
[]
);
}, 250, { leading: true }),
[eventLog]);

const sortableTableHeaderUpdateState = (sortState: Record<string, SortOrder>, setSortState: React.Dispatch<React.SetStateAction<Record<string, SortOrder>>>, key: string) => (order: SortOrder) => {
const newSortState = {...sortState};
Expand All @@ -155,19 +149,17 @@ export const QuestionSearchModal = (
searchDebounce(searchQuestionName, searchTopics, searchExamBoards, searchBook, searchStages, searchDifficulties, searchFastTrack, 0);
},[searchDebounce, searchQuestionName, searchTopics, searchExamBoards, searchBook, searchFastTrack, searchStages, searchDifficulties]);

const sortedQuestions = useMemo(() => {
return questions && sortQuestions(isBookSearch ? {title: SortOrder.ASC} : questionsSort, creationContext)(
questions.filter(question => {
const qIsPublic = searchResultIsPublic(question, user);
if (isBookSearch) return qIsPublic;
const qTopicsMatch =
searchTopics.length === 0 ||
(question.tags && question.tags.filter((tag) => searchTopics.includes(tag)).length > 0);
const sortAndFilterBySearch = (questions: ContentSummaryDTO[]) => questions && sortQuestions(isBookSearch ? {title: SortOrder.ASC} : questionsSort, creationContext)(
questions.filter(question => {
const qIsPublic = searchResultIsPublic(question, user);
if (isBookSearch) return qIsPublic;
const qTopicsMatch =
searchTopics.length === 0 ||
(question.tags && question.tags.filter((tag) => searchTopics.includes(tag)).length > 0);

return qIsPublic && qTopicsMatch;
})
);
}, [questions, user, searchTopics, isBookSearch, questionsSort, creationContext]);
return qIsPublic && qTopicsMatch;
})
);

const addSelectionsRow = <div className="d-sm-flex flex-xl-column align-items-center mt-2">
<div className="flex-grow-1 mb-1">
Expand Down Expand Up @@ -289,41 +281,48 @@ export const QuestionSearchModal = (
{addSelectionsRow}
</Col>
<Col className="col-12 col-xl-9">
<Suspense fallback={<Loading/>}>
<HorizontalScroller enabled={sortedQuestions && sortedQuestions.length > 6} className="my-4">
<Table bordered className="my-0">
<thead>
<tr className="search-modal-table-header">
<th className="w-5"> </th>
<SortItemHeader<SortOrder>
className={siteSpecific("w-40", "w-30")}
setOrder={sortableTableHeaderUpdateState(questionsSort, setQuestionsSort, "title")}
defaultOrder={SortOrder.ASC}
reverseOrder={SortOrder.DESC}
currentOrder={questionsSort['title']}
alignment="start"
>Question title</SortItemHeader>
<th className={siteSpecific("w-25", "w-20")}>Topic</th>
<th className="w-15">Stage</th>
<th className="w-15">Difficulty</th>
{isAda && <th className="w-15">Exam boards</th>}
</tr>
</thead>
<tbody>
{isSearching ? <tr><td colSpan={isAda ? 6 : 5}><Loading/></td></tr> : sortedQuestions?.map(question =>
<GameboardBuilderRow
key={`question-search-modal-row-${question.id}`}
question={question}
currentQuestions={modalQuestions}
undoStack={undoStack}
redoStack={redoStack}
creationContext={creationContext}
/>
)}
</tbody>
</Table>
</HorizontalScroller>
</Suspense>
<HorizontalScroller enabled className="my-4">
<Table bordered className="my-0">
<thead>
<tr className="search-modal-table-header">
<th className="w-5"> </th>
<SortItemHeader<SortOrder>
className={siteSpecific("w-40", "w-30")}
setOrder={sortableTableHeaderUpdateState(questionsSort, setQuestionsSort, "title")}
defaultOrder={SortOrder.ASC}
reverseOrder={SortOrder.DESC}
currentOrder={questionsSort['title']}
alignment="start"
>Question title</SortItemHeader>
<th className={siteSpecific("w-25", "w-20")}>Topic</th>
<th className="w-15">Stage</th>
<th className="w-15">Difficulty</th>
{isAda && <th className="w-15">Exam boards</th>}
</tr>
</thead>
<tbody>
<ShowLoadingQuery
query={searchQuestionsQuery}
placeholder={<tr><td colSpan={isAda ? 6 : 5}><Loading/></td></tr>}
defaultErrorTitle="Failed to load questions."
thenRender={({results: questions}) => {
if (!questions) return <></>;
const sortedQuestions = sortAndFilterBySearch(questions);
return sortedQuestions?.map(question =>
<GameboardBuilderRow
key={`question-search-modal-row-${question.id}`}
question={question}
currentQuestions={modalQuestions}
undoStack={undoStack}
redoStack={redoStack}
creationContext={creationContext}
/>
);
}}
/>
</tbody>
</Table>
</HorizontalScroller>
</Col>
</Row>;
};
8 changes: 7 additions & 1 deletion src/app/components/handlers/ShowLoadingQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ShowLoadingQueryInfo<T> {
data?: T | NOT_FOUND_TYPE;
isLoading: boolean;
isFetching: boolean;
isUninitialized: boolean;
isError: boolean;
error?: FetchBaseQueryError | SerializedError;
}
Expand All @@ -38,6 +39,7 @@ export function combineQueries<T, R, S>(firstQuery: ShowLoadingQueryInfo<T>, sec
data: isFound<T>(firstQuery.data) && isFound<R>(secondQuery.data) ? combineResult(firstQuery.data, secondQuery.data) : undefined,
isLoading: firstQuery.isLoading || secondQuery.isLoading,
isFetching: firstQuery.isFetching || secondQuery.isFetching,
isUninitialized: firstQuery.isUninitialized || secondQuery.isUninitialized,
isError: firstQuery.isError || secondQuery.isError,
error: firstQuery.error ?? secondQuery.error,
};
Expand Down Expand Up @@ -75,7 +77,7 @@ type ShowLoadingQueryProps<T> = ShowLoadingQueryErrorProps<T> & ({
// - `maintainOnRefetch` (boolean indicating whether to keep showing the current data while refetching. use second parameter of `thenRender` to modify render tree accordingly)
// - `query` (the object returned by a RTKQ useQuery hook)
export function ShowLoadingQuery<T>({query, thenRender, children, placeholder, ifError, ifNotFound, defaultErrorTitle, maintainOnRefetch}: ShowLoadingQueryProps<T>) {
const {data, isLoading, isFetching, isError, error} = query;
const {data, isLoading, isFetching, isUninitialized, isError, error} = query;
const renderError = () => ifError ? <>{ifError(error)}</> : <DefaultQueryError error={error} title={defaultErrorTitle}/>;
if (isError && error) {
return "status" in error && typeof error.status === "number" && [NOT_FOUND, NO_CONTENT].includes(error.status) && ifNotFound ? <>{ifNotFound}</> : renderError();
Expand All @@ -84,6 +86,10 @@ export function ShowLoadingQuery<T>({query, thenRender, children, placeholder, i
const isStale = (isLoading || isFetching) && isFound<T>(data);
const showPlaceholder = (isLoading || isFetching) && (!maintainOnRefetch || !isDefined(data));

if (isUninitialized) {
return null;
}

if (showPlaceholder) {
return placeholder ? <>{placeholder}</> : <LoadingPlaceholder />;
}
Expand Down
Loading
Loading