Skip to content

Commit

Permalink
Cosmetic improvements (listed below) + community functionality (#46)
Browse files Browse the repository at this point in the history
* added community functionality

* fixed table layout and made cells flex

* improved record author css

* made titles hyperlinks + just display file name

* files are now hyperlinks
  • Loading branch information
mrzengel authored Jul 25, 2024
1 parent c447110 commit dc74cca
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 54 deletions.
9 changes: 7 additions & 2 deletions src/API/API_functions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ export async function testZenodoConnection() {
}
}

export async function searchRecords(search_field: string, page: number) {
export async function searchRecords(search_field: string, page: number, kwargs: Record<string, any> = {}) {
try {
const data = await requestAPI(`zenodo-jupyterlab/search-records?search_field=${encodeURIComponent(search_field)}&page=${encodeURIComponent(page)}`, {
let url = `zenodo-jupyterlab/search-records?search_field=${encodeURIComponent(search_field)}&page=${encodeURIComponent(page)}`;

for (const [key, value] of Object.entries(kwargs)) {
url += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}
const data = await requestAPI(url, {
method: 'GET'
});
return data;
Expand Down
202 changes: 184 additions & 18 deletions src/components/SearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { searchRecords, searchCommunities, recordInformation } from '../API/API_functions';
import { createUseStyles } from 'react-jss';
import clsx from 'clsx';
Expand Down Expand Up @@ -43,7 +43,7 @@ const useStyles = createUseStyles({
table: {
width: '100%',
borderCollapse: 'collapse',
marginTop: '20px',
tableLayout: 'fixed',
},
tableBody: {
borderLeft: '2px solid black',
Expand Down Expand Up @@ -78,6 +78,7 @@ const useStyles = createUseStyles({
cell: {
padding: '10px',
textAlign: 'left',
wordWrap: 'break-word',
},
checkboxContainer: {
display: 'flex',
Expand All @@ -101,7 +102,81 @@ const useStyles = createUseStyles({
},
spacer: {
flexGrow: '1',
}
},
communityTitle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f0f0f0', // Light gray background
border: '1px solid #ccc', // Gray border
padding: '10px', // Padding around the text
borderRadius: '4px', // Rounded corners
marginBottom: '0', // No margin below
textAlign: 'center', // Center align text
width: '100%', // Full width of container
boxSizing: 'border-box', // Include padding and border in element's total width and height
position: 'relative', // Ensure positioning context for the close button
},
closeButton: {
position: 'absolute', // Position relative to communityTitle
right: '10px', // Position close button on the right
cursor: 'pointer',
fontSize: '16px',
backgroundColor: 'transparent',
border: 'none',
fontWeight: 'bold',
},
authorList: {
color: '#666', // Light gray color for authors
margin: '10px 0',
'& li': {
listStyleType: 'none',
display: 'inline-block',
marginRight: '10px',
position: 'relative',
cursor: 'pointer',
},
'& li::after': {
content: "';'",
marginLeft: '5px',
},
'& li:last-child::after': {
content: "''",
}
},
tooltip: {
visibility: 'hidden',
backgroundColor: '#fff',
color: '#666',
textAlign: 'center',
borderRadius: '6px',
padding: '5px',
position: 'absolute',
zIndex: 1,
bottom: '100%', // Adjusted to prevent overlap with the name
left: '50%',
transform: 'translateX(-50%)',
whiteSpace: 'nowrap',
boxShadow: '0 5px 10px rgba(0,0,0,0.1)',
opacity: 0,
transition: 'opacity 0.3s, visibility 0.3s',
},
tooltipArrow: {
position: 'absolute',
top: '100%', // Adjusted to point downwards from the tooltip
left: '50%',
marginLeft: '-5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#fff transparent transparent transparent',
},
authorListItem: {
position: 'relative',
'&:hover $tooltip': {
visibility: 'visible',
opacity: 1,
}
},
});

const SearchWidget: React.FC = () => {
Expand All @@ -117,17 +192,27 @@ const SearchWidget: React.FC = () => {
const [recordLoading, setRecordLoading] = useState(false);
const [resultsPage, setResultsPage] = useState(1);
const [endPage, setEndPage] = useState(false);
const [selectedCommunityTitle, setSelectedCommunityTitle] = useState<string | null>(null);
const [selectedCommunityID, setSelectedCommunityID] = useState<string | null>(null);
const [triggerSearch, setTriggerSearch] = useState(false);

const handleSearch = async (page: number) => {
useEffect(() => {
if (triggerSearch) {
handleSearch(resultsPage, selectedType, {'communities': selectedCommunityID});
}
setTriggerSearch(false);
}, [triggerSearch, selectedCommunityID]);

const handleSearch = async (page: number, type: string, kwargs: Record<string, any> = {}) => {
setIsLoading(true);
setHasSearched(true);
try {
//const response = await searchRecords(searchTerm);
const response = selectedType === 'records'
? await searchRecords(searchTerm, page)
const response = type === 'records'
? await searchRecords(searchTerm, page, kwargs)
: await searchCommunities(searchTerm, page);
//const data: SearchResult[] = await response;
setResults(response[selectedType]);
setResults(response[type]);
setSelectedRecordID(null);
} catch (error) {
console.error('Error during search: ', error);
Expand All @@ -140,7 +225,7 @@ const SearchWidget: React.FC = () => {
setSelectedType(type);
};

const handleRowClick = async (recordID: number) => {
const handleRecordRowClick = async (recordID: number) => {
if (selectedRecordID === recordID) {
setSelectedRecordID(null);
setRecordInfo({});
Expand All @@ -160,10 +245,28 @@ const SearchWidget: React.FC = () => {
}
}

const handleCommunityRowClick = async (communityID: string, communityTitle: string) => {
/* if (selectedCommunityID === communityID) {
setSelectedCommunityID(null);
setSelectedCommunityTitle(null);
} */
setSelectedCommunityTitle(communityTitle);
setSelectedCommunityID(communityID);
setSelectedType('records');
setResultsPage(1);
setSearchTerm("");
//console.log(selectedCommunityID, resultsPage, selectedType);
//handleSearch(1, 'records', {'communities': communityID});
//console.log(selectedCommunityID, selectedCommunityTitle);
setTriggerSearch(true);

}

const handleNextPageClick = () => {
const nextPage = resultsPage + 1;
setResultsPage(nextPage);
handleSearch(nextPage);
//handleSearch(nextPage, selectedType);
setTriggerSearch(true);
if (results.length !> 0) {
setEndPage(true);
}
Expand All @@ -173,13 +276,48 @@ const SearchWidget: React.FC = () => {
setEndPage(false);
const prevPage = resultsPage - 1;
setResultsPage(prevPage);
handleSearch(prevPage);
//handleSearch(prevPage, selectedType);
setTriggerSearch(true);
}

const handleSearchClick = () => {
setEndPage(false);
setResultsPage(1);
handleSearch(1);
//setSelectedCommunityTitle(null);
setTriggerSearch(true);
}

const handleClearCommunity = () => {
setSelectedCommunityID(null);
setSelectedCommunityTitle(null);
setResultsPage(1);
setTriggerSearch(true);
}

function getFileNameFromUrl(url: string): string {
// Parse the URL using the URL constructor
const parsedUrl = new URL(url);

// Get the pathname from the URL
let pathname = parsedUrl.pathname;

// Remove the '/content' part if it exists
if (pathname.endsWith('/content')) {
pathname = pathname.slice(0, -'/content'.length);
}

// Extract the file name from the pathname
const pathSegments = pathname.split('/');
const fileName = pathSegments[pathSegments.length - 1];

return fileName;
}

function cleanUrl(url: string): string {
// Remove "/api" and "/content" from the URL
return url
.replace('/api', '') // Remove "/api"
.replace('/content', ''); // Remove "/content"
}

return (
Expand Down Expand Up @@ -219,6 +357,12 @@ const SearchWidget: React.FC = () => {
results.length > 0 ? (
lastSearchType === 'records' ? (
<div className={classes.tableContainer}>
{selectedCommunityTitle && (
<div className={classes.communityTitle}>
<p>Showing Results from "{selectedCommunityTitle}"</p>
<button className={classes.closeButton} onClick={handleClearCommunity}></button>
</div>
)}
<table className={classes.table}>
<thead>
<tr className={classes.headerRow}>
Expand All @@ -230,7 +374,7 @@ const SearchWidget: React.FC = () => {
<tbody className={classes.tableBody}>
{results.map((result, index) => (
<React.Fragment key={result.id}>
<tr className={clsx(classes.row, { [classes.alternateRow]: index % 2 !== 0 })} onClick={() => handleRowClick(result.id)}>
<tr className={clsx(classes.row, { [classes.alternateRow]: index % 2 !== 0 })} onClick={() => handleRecordRowClick(result.id)}>
<td className={classes.cell}>{result.title}</td>
<td className={classes.cell}>{result.resource_type}</td>
<td className={classes.cell}>{result.date}</td>
Expand All @@ -239,23 +383,31 @@ const SearchWidget: React.FC = () => {
<tr>
<td colSpan={3} className={classes.cell}>
<div>
<p><strong>Additional information for {result.title}:</strong></p>
<p><strong>Title: <a href={`https://zenodo.org/records/${result.id}`} target='_blank' rel='noopener noreferrer'>{result.title}</a></strong></p>
{recordInfo.authors && (
<div>
<p><strong>Authors:</strong></p>
<ul>
<ul className={classes.authorList}>
{recordInfo.authors.map((author: {'name': string, 'affiliation': string}, index: number) => (
<li key={index}>{author.name}, Affiliation: {author.affiliation}</li>
<li key={index} className={classes.authorListItem}>
{author.name}
{author.affiliation && (
<span className={classes.tooltip}>
{author.affiliation}
<span className={classes.tooltipArrow}></span>
</span>
)}
</li>
))}
</ul>
</div>
)}
{recordInfo.filelist && (
{recordInfo.filelist.length > 0 && (
<div>
<p><strong>Files:</strong></p>
<ul>
{recordInfo.filelist.map((file: string, index: number) => (
<li key={index}>{file}</li>
<li key={index}><a href={cleanUrl(file)} target='_blank' rel='noopener noreferrer'>{getFileNameFromUrl(file)}</a></li>
))}
</ul>
</div>
Expand Down Expand Up @@ -288,7 +440,7 @@ const SearchWidget: React.FC = () => {
</thead>
<tbody className={classes.tableBody}>
{results.map((result, index) => (
<tr key={result.id} className={classes.row} style={{ backgroundColor: index % 2 === 0 ? '#e6f7ff' : '#cceeff' }}>
<tr key={result.id} className={clsx(classes.row, { [classes.alternateRow]: index % 2 !== 0 })} onClick={() => handleCommunityRowClick(result.id, result.title)}>
<td className={classes.cell}>{result.title}</td>
<td className={classes.cell}>{result.date}</td>
</tr>
Expand All @@ -306,9 +458,23 @@ const SearchWidget: React.FC = () => {
)
) : (
!endPage ? (
<div>
{selectedCommunityTitle && (
<div className={classes.communityTitle}>
<p>Showing Results from "{selectedCommunityTitle}"</p>
<button className={classes.closeButton} onClick={handleClearCommunity}></button>
</div>
)}
<p>No results found.</p>
</div>
) : (
<div>
{selectedCommunityTitle && (
<div className={classes.communityTitle}>
<p>Showing Results from "{selectedCommunityTitle}"</p>
<button className={classes.closeButton} onClick={handleClearCommunity}></button>
</div>
)}
<p>No further results found. Please return to the previous page.</p>
<button className={classes.button} onClick={handleLastPageClick}>Last Page</button>
</div>
Expand Down
8 changes: 0 additions & 8 deletions src/pages/search.tsx

This file was deleted.

23 changes: 0 additions & 23 deletions src/run_search.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion zenodo_jupyterlab/server/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class SearchRecordHandler(APIHandler):
async def get(self):
search_field = self.get_query_argument('search_field', default="")
page = self.get_query_argument('page', default=1)
response = await searchRecords(search_field=search_field, page=page)
communities = self.get_query_argument('communities', default="")
response = await searchRecords(search_field=search_field, page=page, communities=communities)
self.finish({'records': response})

class SearchCommunityHandler(APIHandler):
Expand Down
4 changes: 2 additions & 2 deletions zenodo_jupyterlab/server/search.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from eossr.api.zenodo import search_records, search_communities, get_record

async def searchRecords(search_field, page):
async def searchRecords(search_field, page, **kwargs):
try:
records = search_records(search=search_field, size= 25, page = page)
records = search_records(search=search_field, size= 25, page = page, **kwargs)
response = []
for record in records:
response.append({'id': record.id, 'title': record.title, 'date': record.metadata['publication_date'], 'resource_type': record.metadata['resource_type']['title']})
Expand Down

0 comments on commit dc74cca

Please sign in to comment.