Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 1878371

Browse files
committed
feat(F-183): Implemented Selection and Filtering of Multiple Authors and Institutions in the Search Page, Topic Trends Page, World Map Page
1 parent 6d1c02a commit 1878371

4 files changed

Lines changed: 266 additions & 217 deletions

File tree

frontend/src/components/shared/SearchForm.jsx

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import React from "react";
2-
import DropdownTrigger from "./DropdownTrigger/DropdownTrigger";
32
import searchPublicationsIcon from "../../assets/icons/search-publications.svg";
43
import authorIcon from "../../assets/icons/author.svg";
54
import institutionIcon from "../../assets/icons/institution.svg";
65

76
const SearchForm = ({
87
searchKeyword,
98
setSearchKeyword,
10-
author,
11-
setAuthor,
12-
setAuthorObject,
13-
institution,
14-
setInstitution,
15-
setInstitutionObject,
9+
selectedAuthors = [],
10+
setSelectedAuthors,
11+
selectedInstitutions = [],
12+
setSelectedInstitutions,
1613
onSearch,
1714
onOpenAdvancedFilters,
18-
onAuthorClick,
19-
onInstitutionClick,
15+
onAuthorsClick,
16+
onInstitutionsClick,
2017
loading,
2118
darkMode = false,
2219
description = "Enter keywords and apply filters to find relevant research"
@@ -97,39 +94,120 @@ const SearchForm = ({
9794
<span style={{ display: 'flex', alignItems: 'center' }}>
9895
<img src={authorIcon} alt="Author" style={{ marginRight: 3, width: 18, height: 18 }} />
9996
</span>
100-
Author
97+
Authors
10198
</div>
10299
<div style={{ width: '100%' }}>
103-
<DropdownTrigger
104-
value={author}
105-
placeholder="Click to search authors..."
106-
onClick={onAuthorClick}
107-
onClear={author ? () => {
108-
setAuthor("");
109-
if (typeof setAuthorObject === 'function') setAuthorObject(null);
110-
} : undefined}
111-
darkMode={darkMode}
112-
/>
100+
<div
101+
style={{
102+
minHeight: 48,
103+
border: '1px solid #404040',
104+
borderRadius: 8,
105+
background: '#222',
106+
display: 'flex',
107+
alignItems: 'center',
108+
flexWrap: 'wrap',
109+
gap: 6,
110+
padding: '8px 12px',
111+
cursor: 'pointer',
112+
color: '#fff'
113+
}}
114+
onClick={onAuthorsClick}
115+
>
116+
{selectedAuthors.length === 0 && (
117+
<span style={{ color: '#888' }}>Click to search authors...</span>
118+
)}
119+
{selectedAuthors.map(author => (
120+
<span key={author.id} style={{
121+
background: '#4F6AF6',
122+
color: '#fff',
123+
borderRadius: 12,
124+
padding: '2px 10px',
125+
display: 'flex',
126+
alignItems: 'center',
127+
fontSize: 14,
128+
marginRight: 4
129+
}}>
130+
{author.display_name}
131+
<button
132+
style={{
133+
background: 'none',
134+
border: 'none',
135+
color: '#fff',
136+
marginLeft: 6,
137+
cursor: 'pointer',
138+
fontWeight: 'bold',
139+
fontSize: 16
140+
}}
141+
onClick={e => {
142+
e.stopPropagation();
143+
setSelectedAuthors(selectedAuthors.filter(a => a.id !== author.id));
144+
}}
145+
aria-label="Remove author"
146+
>×</button>
147+
</span>
148+
))}
149+
</div>
113150
</div>
114151
</div>
152+
{/* Institutions Multi-Select */}
115153
<div style={{ flex: 1 }}>
116154
<div style={subLabelStyle}>
117155
<span style={{ display: 'flex', alignItems: 'center' }}>
118156
<img src={institutionIcon} alt="Institution" style={{ marginRight: 3, width: 18, height: 18 }} />
119157
</span>
120-
Institution
158+
Institutions
121159
</div>
122160
<div style={{ width: '100%' }}>
123-
<DropdownTrigger
124-
value={institution}
125-
placeholder="Click to search institutions..."
126-
onClick={onInstitutionClick}
127-
onClear={institution ? () => {
128-
setInstitution("");
129-
if (typeof setInstitutionObject === 'function') setInstitutionObject(null);
130-
} : undefined}
131-
darkMode={darkMode}
132-
/>
161+
<div
162+
style={{
163+
minHeight: 48,
164+
border: '1px solid #404040',
165+
borderRadius: 8,
166+
background: '#222',
167+
display: 'flex',
168+
alignItems: 'center',
169+
flexWrap: 'wrap',
170+
gap: 6,
171+
padding: '8px 12px',
172+
cursor: 'pointer',
173+
color: '#fff'
174+
}}
175+
onClick={onInstitutionsClick}
176+
>
177+
{selectedInstitutions.length === 0 && (
178+
<span style={{ color: '#888' }}>Click to search institutions...</span>
179+
)}
180+
{selectedInstitutions.map(inst => (
181+
<span key={inst.id} style={{
182+
background: '#4F6AF6',
183+
color: '#fff',
184+
borderRadius: 12,
185+
padding: '2px 10px',
186+
display: 'flex',
187+
alignItems: 'center',
188+
fontSize: 14,
189+
marginRight: 4
190+
}}>
191+
{inst.display_name}
192+
<button
193+
style={{
194+
background: 'none',
195+
border: 'none',
196+
color: '#fff',
197+
marginLeft: 6,
198+
cursor: 'pointer',
199+
fontWeight: 'bold',
200+
fontSize: 16
201+
}}
202+
onClick={e => {
203+
e.stopPropagation();
204+
setSelectedInstitutions(selectedInstitutions.filter(i => i.id !== inst.id));
205+
}}
206+
aria-label="Remove institution"
207+
>×</button>
208+
</span>
209+
))}
210+
</div>
133211
</div>
134212
</div>
135213
</div>

frontend/src/pages/search.js

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import React, { useState, useEffect } from 'react';
1+
import { useState, useEffect } from 'react';
22
import { useLocation } from 'react-router-dom';
33
import TopBar from '../components/shared/TopBar';
44
import SearchHeader from '../components/shared/SearchHeader';
55
import SearchForm from '../components/shared/SearchForm';
66
import AdvancedFiltersDrawer from '../components/shared/AdvancedFiltersDrawer';
77
import SearchResultsList from '../components/shared/SearchResultsList';
8-
import ModalDropdown from '../components/shared/ModalDropdown';
98
import MultiSelectModalDropdown from '../components/shared/MultiSelectModalDropdown/MultiSelectModalDropdown';
109
import useDropdownSearch from '../hooks/useDropdownSearch';
1110
import ApiCallInfoBox from '../components/shared/ApiCallInfoBox';
@@ -17,10 +16,9 @@ const SearchPageLight = ({ darkMode = true }) => {
1716
const location = useLocation();
1817
// Main search/filter state
1918
const [searchKeyword, setSearchKeyword] = useState("");
20-
const [author, setAuthor] = useState("");
21-
const [authorObject, setAuthorObject] = useState(null);
22-
const [institution, setInstitution] = useState("");
23-
const [institutionObject, setInstitutionObject] = useState(null);
19+
// Multi-select for authors and institutions
20+
const [selectedAuthors, setSelectedAuthors] = useState([]);
21+
const [selectedInstitutions, setSelectedInstitutions] = useState([]);
2422
// Advanced filters
2523
const [publicationYear, setPublicationYear] = useState("");
2624
const [startYear, setStartYear] = useState("");
@@ -135,10 +133,8 @@ const SearchPageLight = ({ darkMode = true }) => {
135133
// Function to clear all search fields
136134
const clearAllFields = () => {
137135
setSearchKeyword("");
138-
setAuthor("");
139-
setAuthorObject(null);
140-
setInstitution("");
141-
setInstitutionObject(null);
136+
setSelectedAuthors([]);
137+
setSelectedInstitutions([]);
142138
setPublicationYear("");
143139
setStartYear("");
144140
setEndYear("");
@@ -210,17 +206,13 @@ const SearchPageLight = ({ darkMode = true }) => {
210206
}
211207
};
212208

213-
// Fetch institution details by ID
209+
// Fetch institution details by ID (for URL param support)
214210
const fetchInstitutionById = async (institutionId) => {
215211
try {
216212
const response = await fetch(`${OPENALEX_API_BASE}/institutions/I${institutionId}`);
217213
if (response.ok) {
218214
const institutionData = await response.json();
219-
setInstitution(institutionData.display_name);
220-
setInstitutionObject({
221-
id: institutionData.id,
222-
display_name: institutionData.display_name
223-
});
215+
setSelectedInstitutions([{ id: institutionData.id, display_name: institutionData.display_name }]);
224216
}
225217
} catch (error) {
226218
console.error('Failed to fetch institution details:', error);
@@ -239,8 +231,8 @@ const SearchPageLight = ({ darkMode = true }) => {
239231
// Track user inputs for disclaimer
240232
const inputs = [];
241233
if (searchKeyword.trim()) inputs.push({ category: 'Keywords', value: searchKeyword.trim() });
242-
if (authorObject && authorObject.display_name) inputs.push({ category: 'Author', value: authorObject.display_name });
243-
if (institutionObject && institutionObject.display_name) inputs.push({ category: 'Institution', value: institutionObject.display_name });
234+
if (selectedAuthors.length > 0) inputs.push({ category: 'Authors', value: selectedAuthors.map(a => a.display_name).join(', ') });
235+
if (selectedInstitutions.length > 0) inputs.push({ category: 'Institutions', value: selectedInstitutions.map(i => i.display_name).join(', ') });
244236
if (selectedPublicationTypes.length > 0) inputs.push({ category: 'Publication Types', value: selectedPublicationTypes.map(pt => pt.display_name).join(', ') });
245237
if (publicationYear.trim()) inputs.push({ category: 'Publication Year', value: publicationYear.trim() });
246238
if (startYear.trim() && endYear.trim()) inputs.push({ category: 'Year Range', value: `${startYear.trim()}-${endYear.trim()}` });
@@ -254,15 +246,17 @@ const SearchPageLight = ({ darkMode = true }) => {
254246
// Format: title_and_abstract.search:keyword (OpenAlex handles multi-word automatically)
255247
filters.push(`title_and_abstract.search:${keyword}`);
256248
}
257-
if (authorObject && authorObject.id) {
258-
// Use the author object if available (from dropdown)
259-
const authorId = authorObject.id.split('/').pop();
260-
filters.push(`authorships.author.id:A${authorId}`);
249+
if (selectedAuthors.length > 0) {
250+
selectedAuthors.forEach(a => {
251+
const authorId = 'A' + a.id.split('/').pop();
252+
filters.push(`authorships.author.id:${authorId}`);
253+
});
261254
}
262-
if (institutionObject && institutionObject.id) {
263-
// Use the institution object if available (from URL params or dropdown)
264-
const instId = institutionObject.id.split('/').pop();
265-
filters.push(`authorships.institutions.id:I${instId}`);
255+
if (selectedInstitutions.length > 0) {
256+
selectedInstitutions.forEach(i => {
257+
const institutionId = 'I' + i.id.split('/').pop();
258+
filters.push(`authorships.institutions.id:${institutionId}`);
259+
});
266260
}
267261
if (selectedPublicationTypes.length > 0) {
268262
// Handle multiple publication types
@@ -368,19 +362,17 @@ const SearchPageLight = ({ darkMode = true }) => {
368362
<SearchForm
369363
searchKeyword={searchKeyword}
370364
setSearchKeyword={setSearchKeyword}
371-
author={author}
372-
setAuthor={setAuthor}
373-
setAuthorObject={setAuthorObject}
374-
institution={institution}
375-
setInstitution={setInstitution}
376-
setInstitutionObject={setInstitutionObject}
365+
selectedAuthors={selectedAuthors}
366+
setSelectedAuthors={setSelectedAuthors}
367+
selectedInstitutions={selectedInstitutions}
368+
setSelectedInstitutions={setSelectedInstitutions}
377369
onSearch={handleSearch}
378370
onOpenAdvancedFilters={() => setShowAdvanced(true)}
379-
onAuthorClick={() => {
371+
onAuthorsClick={() => {
380372
setShowAuthorModal(true);
381373
clearAuthorSuggestions();
382374
}}
383-
onInstitutionClick={() => {
375+
onInstitutionsClick={() => {
384376
setShowInstitutionModal(true);
385377
clearInstitutionSuggestions();
386378
}}
@@ -515,33 +507,39 @@ const SearchPageLight = ({ darkMode = true }) => {
515507
</div>
516508
)}
517509

518-
{/* Author Modal Dropdown */}
519-
<ModalDropdown
510+
{/* Authors Multi-Select Modal */}
511+
<MultiSelectModalDropdown
520512
isOpen={showAuthorModal}
521513
onClose={() => setShowAuthorModal(false)}
522-
title="Select Author"
514+
title="Select Authors"
523515
placeholder="Type to search authors..."
524516
onSearchChange={searchAuthors}
525517
suggestions={authorSuggestions}
526-
onSelect={(author) => {
527-
setAuthorObject(author);
528-
setAuthor(author.display_name);
518+
selectedItems={selectedAuthors}
519+
onSelect={author => {
520+
setSelectedAuthors(prev => prev.some(a => a.id === author.id) ? prev : [...prev, author]);
521+
}}
522+
onDeselect={author => {
523+
setSelectedAuthors(prev => prev.filter(a => a.id !== author.id));
529524
}}
530525
darkMode={darkMode}
531526
loading={authorLoading}
532527
/>
533528

534-
{/* Institution Modal Dropdown */}
535-
<ModalDropdown
529+
{/* Institutions Multi-Select Modal */}
530+
<MultiSelectModalDropdown
536531
isOpen={showInstitutionModal}
537532
onClose={() => setShowInstitutionModal(false)}
538-
title="Select Institution"
533+
title="Select Institutions"
539534
placeholder="Type to search institutions..."
540535
onSearchChange={searchInstitutions}
541536
suggestions={institutionSuggestions}
542-
onSelect={(institution) => {
543-
setInstitutionObject(institution);
544-
setInstitution(institution.display_name);
537+
selectedItems={selectedInstitutions}
538+
onSelect={institution => {
539+
setSelectedInstitutions(prev => prev.some(i => i.id === institution.id) ? prev : [...prev, institution]);
540+
}}
541+
onDeselect={institution => {
542+
setSelectedInstitutions(prev => prev.filter(i => i.id !== institution.id));
545543
}}
546544
darkMode={darkMode}
547545
loading={institutionLoading}

0 commit comments

Comments
 (0)