From dc74cca0c810a8db19ac43b82c5158e88185e5fb Mon Sep 17 00:00:00 2001 From: mrzengel <99219497+mrzengel@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:05:08 +0200 Subject: [PATCH] Cosmetic improvements (listed below) + community functionality (#46) * 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 --- src/API/API_functions.tsx | 9 +- src/components/SearchPanel.tsx | 202 ++++++++++++++++++++++++--- src/pages/search.tsx | 8 -- src/run_search.tsx | 23 --- zenodo_jupyterlab/server/handlers.py | 3 +- zenodo_jupyterlab/server/search.py | 4 +- 6 files changed, 195 insertions(+), 54 deletions(-) delete mode 100644 src/pages/search.tsx delete mode 100644 src/run_search.tsx diff --git a/src/API/API_functions.tsx b/src/API/API_functions.tsx index 6993483..484bc08 100644 --- a/src/API/API_functions.tsx +++ b/src/API/API_functions.tsx @@ -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 = {}) { 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; diff --git a/src/components/SearchPanel.tsx b/src/components/SearchPanel.tsx index 0cb712c..f1be2f2 100644 --- a/src/components/SearchPanel.tsx +++ b/src/components/SearchPanel.tsx @@ -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'; @@ -43,7 +43,7 @@ const useStyles = createUseStyles({ table: { width: '100%', borderCollapse: 'collapse', - marginTop: '20px', + tableLayout: 'fixed', }, tableBody: { borderLeft: '2px solid black', @@ -78,6 +78,7 @@ const useStyles = createUseStyles({ cell: { padding: '10px', textAlign: 'left', + wordWrap: 'break-word', }, checkboxContainer: { display: 'flex', @@ -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 = () => { @@ -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(null); + const [selectedCommunityID, setSelectedCommunityID] = useState(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 = {}) => { 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); @@ -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({}); @@ -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); } @@ -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 ( @@ -219,6 +357,12 @@ const SearchWidget: React.FC = () => { results.length > 0 ? ( lastSearchType === 'records' ? (
+ {selectedCommunityTitle && ( +
+

Showing Results from "{selectedCommunityTitle}"

+ +
+ )} @@ -230,7 +374,7 @@ const SearchWidget: React.FC = () => { {results.map((result, index) => ( - handleRowClick(result.id)}> + handleRecordRowClick(result.id)}> @@ -239,23 +383,31 @@ const SearchWidget: React.FC = () => { {results.map((result, index) => ( - + handleCommunityRowClick(result.id, result.title)}> @@ -306,9 +458,23 @@ const SearchWidget: React.FC = () => { ) ) : ( !endPage ? ( +
+ {selectedCommunityTitle && ( +
+

Showing Results from "{selectedCommunityTitle}"

+ +
+ )}

No results found.

+
) : (
+ {selectedCommunityTitle && ( +
+

Showing Results from "{selectedCommunityTitle}"

+ +
+ )}

No further results found. Please return to the previous page.

diff --git a/src/pages/search.tsx b/src/pages/search.tsx deleted file mode 100644 index 6b4a1a5..0000000 --- a/src/pages/search.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; - - -export const Search: React.FC = ({ }) => { - return
-

This is the search page!!

-
-} \ No newline at end of file diff --git a/src/run_search.tsx b/src/run_search.tsx deleted file mode 100644 index ac2f603..0000000 --- a/src/run_search.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { exec, ExecException } from 'child_process'; - -function runSearchScript(query: string, callback: (data: any) => void) { - console.log("HHHHHHHH") - exec(`python example_search.py ${query}`, (error: ExecException | null, stdout: string, stderr: string) => { - if (error) { - console.error(`exec error: ${error}`); - return; - } - if (stderr) { - console.error(`stderr: ${stderr}`); - return; - } - try { - const result = JSON.parse(stdout); - callback(result); - } catch (e) { - console.error(`Error parsing JSON: ${e}`); - } - }); -} - -export default runSearchScript; \ No newline at end of file diff --git a/zenodo_jupyterlab/server/handlers.py b/zenodo_jupyterlab/server/handlers.py index 7ce592c..c7ac9d1 100644 --- a/zenodo_jupyterlab/server/handlers.py +++ b/zenodo_jupyterlab/server/handlers.py @@ -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): diff --git a/zenodo_jupyterlab/server/search.py b/zenodo_jupyterlab/server/search.py index 9dffc0e..fe3c89e 100644 --- a/zenodo_jupyterlab/server/search.py +++ b/zenodo_jupyterlab/server/search.py @@ -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']})
{result.title} {result.resource_type} {result.date}
-

Additional information for {result.title}:

+

Title: {result.title}

{recordInfo.authors && (

Authors:

-
    +
      {recordInfo.authors.map((author: {'name': string, 'affiliation': string}, index: number) => ( -
    • {author.name}, Affiliation: {author.affiliation}
    • +
    • + {author.name} + {author.affiliation && ( + + {author.affiliation} + + + )} +
    • ))}
)} - {recordInfo.filelist && ( + {recordInfo.filelist.length > 0 && (

Files:

@@ -288,7 +440,7 @@ const SearchWidget: React.FC = () => {
{result.title} {result.date}