Skip to content
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

Implement Search Pagination #297

Merged
merged 29 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5b111bc
search pagination with fixed page number
aahei May 12, 2023
891b859
pagination real max page number, but max = 5
aahei May 12, 2023
4c9ea6d
unlimited search result;
aahei May 12, 2023
8292c33
fix page number reset bug
aahei May 12, 2023
6fcca26
hide pagination when page num <= 1
aahei May 12, 2023
092bac1
add "no results found"
aahei May 12, 2023
8a4b81a
add index type assert in pagination
aahei May 12, 2023
326c455
remove assert; rewrite index condition
aahei May 12, 2023
3f87ac1
clean comment
aahei May 12, 2023
c4b90be
improve pagination performance by load only first 5 pages initially
aahei Jun 3, 2023
e72092d
format
aahei Jun 3, 2023
5e8e9aa
Merge branch 'master' of https://github.com/icssc/peterportal-client …
aahei Jun 3, 2023
012e10f
Added some comments
aahei Oct 21, 2023
c755c62
Merge branch 'search-pagination'
aahei Nov 30, 2023
9c3caaf
Merge branch 'master' into search-pagination
js0mmer Feb 1, 2024
b3cb733
Fix formatting + lint errors
js0mmer Feb 1, 2024
41974b1
Merge branch 'master' into search-pagination
js0mmer Feb 11, 2024
4e86ba4
Move some search states into slice to prevent infinite loop. Add deps…
js0mmer Feb 13, 2024
d2768a9
Squashed commit of the following:
js0mmer Feb 13, 2024
88afcc9
Decrease search timeout to 300ms
js0mmer Feb 13, 2024
78f5967
Add a comment
js0mmer Feb 13, 2024
2cd991a
Delete console logs for search
js0mmer Feb 13, 2024
2b6b4d6
Add another comment
js0mmer Feb 13, 2024
ab9aa28
Restore strict mode
js0mmer Feb 13, 2024
db6f4c5
Move search pagination css with hit container
js0mmer Feb 13, 2024
a0b86bc
Refactor no results logic
js0mmer Feb 13, 2024
ff4dc96
Restore useEffect for index switch
js0mmer Feb 13, 2024
775b8aa
Fix issues with current page persisting
js0mmer Feb 13, 2024
3b78b7f
Remove console log
js0mmer Feb 13, 2024
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: 5 additions & 0 deletions site/src/component/SearchHitContainer/SearchHitContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
padding-top: 2vh;
overflow-y: auto;
}

.no-results {
padding: 2vh 2vh;
text-align: center;
}
26 changes: 20 additions & 6 deletions site/src/component/SearchHitContainer/SearchHitContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './SearchHitContainer.scss';
import { useAppSelector } from '../../store/hooks';

import { SearchIndex, CourseGQLData, ProfessorGQLData } from '../../types/types';
import SearchPagination from '../SearchPagination/SearchPagination';

interface SearchHitContainerProps {
index: SearchIndex;
Expand All @@ -28,18 +29,31 @@ const SearchHitContainer: FC<SearchHitContainerProps> = ({ index, CourseHitItem,
<div ref={containerDivRef} className="search-hit-container">
{index == 'courses' && (
<>
{courseResults.map((course, i) => {
return <CourseHitItem key={`course-hit-item-${i}`} index={i} {...(course as CourseGQLData)} />;
})}
{courseResults.length === 0 ? (
<div className="no-results">No results found</div>
) : (
courseResults.map((course, i) => {
return <CourseHitItem key={`course-hit-item-${i}`} index={i} {...(course as CourseGQLData)} />;
})
)}
</>
)}
{index == 'professors' && ProfessorHitItem && (
<>
{professorResults.map((professor, i) => {
return <ProfessorHitItem key={`professor-hit-item-${i}`} index={i} {...(professor as ProfessorGQLData)} />;
})}
{professorResults.length === 0 ? (
<div className="no-results">No results found</div>
) : (
professorResults.map((professor, i) => {
return (
<ProfessorHitItem key={`professor-hit-item-${i}`} index={i} {...(professor as ProfessorGQLData)} />
);
})
)}
</>
)}
<div className="search-pagination">
<SearchPagination index={index} />
</div>
</div>
);
};
Expand Down
6 changes: 6 additions & 0 deletions site/src/component/SearchModule/SearchModule.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@
border-color: #80bdff;
}
}

// center the search pagination
.search-pagination {
display: flex;
justify-content: center;
}
46 changes: 30 additions & 16 deletions site/src/component/SearchModule/SearchModule.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { FC, useEffect } from 'react';
import { Search } from 'react-bootstrap-icons';
import { useState, useEffect, FC } from 'react';
import './SearchModule.scss';
import wfs from 'websoc-fuzzy-search';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import wfs from 'websoc-fuzzy-search';
import './SearchModule.scss';
import { Search } from 'react-bootstrap-icons';

import { searchAPIResults } from '../../helpers/util';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { setNames, setResults } from '../../store/slices/searchSlice';
import { setNames, setPageNumber, setResults } from '../../store/slices/searchSlice';
import { searchAPIResults } from '../../helpers/util';
import { SearchIndex } from '../../types/types';
import { NUM_RESULTS_PER_PAGE } from '../../helpers/constants';

const PAGE_SIZE = 10;
const SEARCH_TIMEOUT_MS = 500;
const FULL_RESULT_THRESHOLD = 3;
const INITIAL_MAX_PAGE = 5;

interface SearchModuleProps {
index: SearchIndex;
Expand All @@ -21,12 +23,14 @@
const dispatch = useAppDispatch();
const courseSearch = useAppSelector((state) => state.search.courses);
const professorSearch = useAppSelector((state) => state.search.professors);
const [hasFullResults, setHasFullResults] = useState(false);
const [lastQuery, setLastQuery] = useState('');
let pendingRequest: NodeJS.Timeout | null = null;

// Search empty string to load some results
useEffect(() => {
searchNames('');
}, [index]);

Check warning on line 33 in site/src/component/SearchModule/SearchModule.tsx

View workflow job for this annotation

GitHub Actions / Lint and check formatting

React Hook useEffect has a missing dependency: 'searchNames'. Either include it or remove the dependency array

// Refresh search results when names and page number changes
useEffect(() => {
Expand All @@ -38,17 +42,17 @@

const searchNames = (query: string) => {
try {
/*
TODO: Search optimization
- Currently sending a query request for every input change
- Goal is to have only one query request pending
- Use setTimeout/clearTimeout to keep track of pending query request
*/
// Get all results only when query changes or user reaches the fourth page or after
const currentPage = index === 'courses' ? courseSearch.pageNumber : professorSearch.pageNumber;
const nameResults = wfs({
query: query,
numResults: PAGE_SIZE * 5,
resultType: index === 'courses' ? 'COURSE' : 'INSTRUCTOR',
filterOptions: {},
// Load INITIAL_MAX_PAGE pages first
// when user reaches the 4th page or after, load all results
numResults:
lastQuery !== query || currentPage < FULL_RESULT_THRESHOLD
? NUM_RESULTS_PER_PAGE * INITIAL_MAX_PAGE
: undefined,
});
let names: string[] = [];
if (index === 'courses') {
Expand All @@ -65,14 +69,24 @@
}
console.log('From frontend search', names);
dispatch(setNames({ index, names }));
// reset page number and hasFullResults flag if query changes
if (query !== lastQuery) {
dispatch(setPageNumber({ index, pageNumber: 0 }));
setHasFullResults(false);
setLastQuery(query);
}
} catch (e) {
console.log(e);
}
};

const searchResults = async (index: SearchIndex, pageNumber: number, names: string[]) => {
if (!hasFullResults && pageNumber >= FULL_RESULT_THRESHOLD) {
setHasFullResults(true);
searchNames(lastQuery);
}
// Get the subset of names based on the page
const pageNames = names.slice(PAGE_SIZE * pageNumber, PAGE_SIZE * (pageNumber + 1));
const pageNames = names.slice(NUM_RESULTS_PER_PAGE * pageNumber, NUM_RESULTS_PER_PAGE * (pageNumber + 1));
const results = await searchAPIResults(index, pageNames);
dispatch(setResults({ index, results: Object.values(results) }));
};
Expand Down
61 changes: 61 additions & 0 deletions site/src/component/SearchPagination/SearchPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FC } from 'react';
import { Pagination } from 'react-bootstrap';
import { NUM_RESULTS_PER_PAGE } from '../../helpers/constants';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { setPageNumber } from '../../store/slices/searchSlice';
import { SearchIndex } from '../../types/types';

interface SearchPaginationProps {
index: SearchIndex;
}

/* SearchPagination is the page buttons at the bottom of the search results */
const SearchPagination: FC<SearchPaginationProps> = ({ index }) => {
aahei marked this conversation as resolved.
Show resolved Hide resolved
const dispatch = useAppDispatch();
const coursePageNumber = useAppSelector((state) => state.search.courses.pageNumber);
const professorPageNumber = useAppSelector((state) => state.search.professors.pageNumber);

const courseData = useAppSelector((state) => state.search.courses);
const professorData = useAppSelector((state) => state.search.professors);

const clickPageNumber = (pageNumber: number) => {
dispatch(setPageNumber({ index, pageNumber }));
};

let numPages = 0;
let active = 0;
if (index === 'courses') {
numPages = Math.ceil(courseData.names.length / NUM_RESULTS_PER_PAGE);
active = coursePageNumber;
} else if (index === 'professors') {
numPages = Math.ceil(professorData.names.length / NUM_RESULTS_PER_PAGE);
active = professorPageNumber;
}

// only show 5 page numbers at a time
const items = [];
let startPageNumber = Math.max(0, active - 2);
const endPageNumber = Math.min(numPages, startPageNumber + 5); // exclusive
startPageNumber = Math.max(0, endPageNumber - 5);
for (let i = startPageNumber; i < endPageNumber; i++) {
items.push(
<Pagination.Item key={i} active={i === active} onClick={() => clickPageNumber(i)}>
{i + 1}
</Pagination.Item>,
);
}

return (
// hide if there is no page or only one page
numPages <= 1 ? null : (
<Pagination>
<Pagination.First onClick={() => clickPageNumber(0)} disabled={active === 0} />
<Pagination.Prev onClick={() => clickPageNumber(active - 1)} disabled={active === 0} />
{items}
<Pagination.Next onClick={() => clickPageNumber(active + 1)} disabled={active === numPages - 1} />
</Pagination>
)
);
};

export default SearchPagination;
4 changes: 4 additions & 0 deletions site/src/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// SearchPage
// Defined in the constants file because it is used in both
// SearchModule (the search bar) and SearchPagination (the pagination buttons)
export const NUM_RESULTS_PER_PAGE = 10;
Loading