diff --git a/aiida-registry-app/src/App.css b/aiida-registry-app/src/App.css index 498823b6..eeeecd43 100644 --- a/aiida-registry-app/src/App.css +++ b/aiida-registry-app/src/App.css @@ -235,7 +235,6 @@ td { .submenu-entry { width:90%; - min-height:140px; padding:10px; margin: 20px; padding-left: 40px; @@ -440,12 +439,29 @@ padding-right: 5px; min-height: 56px; } - input[type='text'] { + .input-container { padding: 8px; border: 1px solid #ccc; border-radius: 4px; - font-size: large; flex: 1; + display: flex; + } + + .input-container input[type="text"] { + flex: 1; + border: none; + outline: none; + font-size: large; + } + + .enter-symbol { + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + font-size: 20px; + color: lightgray; + opacity: 0; } /* Styles for the suggestions list */ @@ -462,6 +478,16 @@ padding-right: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 99; } + .dropdown-search { + display: none; + font-size: 20px; + margin: auto; + width: inherit; + background-color: white; + border: 1px solid #ccc; + cursor: pointer; + min-height: 50px; + } .suggestion-item { padding: 8px; diff --git a/aiida-registry-app/src/Components/Details.jsx b/aiida-registry-app/src/Components/Details.jsx index 18fd62d8..09485c16 100644 --- a/aiida-registry-app/src/Components/Details.jsx +++ b/aiida-registry-app/src/Components/Details.jsx @@ -1,7 +1,7 @@ import { Link } from 'react-router-dom'; import jsonData from '../plugins_metadata.json' import base64Icon from '../base64Icon'; - +import { useEffect } from 'react'; import Markdown from 'markdown-to-jsx'; const entrypointtypes = jsonData["entrypointtypes"] @@ -12,8 +12,26 @@ const currentPath = import.meta.env.VITE_PR_PREVIEW_PATH || "/aiida-registry/"; function Details({pluginKey}) { const value = plugins[pluginKey]; - window.scrollTo(0, 0); - document.documentElement.style.scrollBehavior = 'smooth'; + useEffect(() => { + window.scrollTo(0, 0); + document.documentElement.style.scrollBehavior = 'smooth'; + const scrollToAnchor = () => { + const hash = window.location.hash; + + if (hash) { + //Add a space to the url and remove it again, this trick allows to scroll to the specified section. + let cur = window.location.href + window.location.href = cur+' '; + window.location.href = cur; + const targetSection = document.getElementById(hash); + if (targetSection) { + targetSection.scrollIntoView(); + } + } + }; + + scrollToAnchor(); + }, []); return ( <> diff --git a/aiida-registry-app/src/Components/MainIndex.jsx b/aiida-registry-app/src/Components/MainIndex.jsx index 4b4b1ffa..bb8fe469 100644 --- a/aiida-registry-app/src/Components/MainIndex.jsx +++ b/aiida-registry-app/src/Components/MainIndex.jsx @@ -9,7 +9,10 @@ import FormControl from '@mui/material/FormControl'; import Select from '@mui/material/Select'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import SearchIcon from '@mui/icons-material/Search'; +import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn'; import Fuse from 'fuse.js' +import { extractSentenceAroundKeyword } from './utils' + const globalsummary = jsonData["globalsummary"] const plugins = jsonData["plugins"] const status_dict = jsonData["status_dict"] @@ -17,16 +20,17 @@ const length = Object.keys(plugins).length; const currentPath = import.meta.env.VITE_PR_PREVIEW_PATH || "/aiida-registry/"; -//The search context enables accessing the search query and the plugins data among different components. +//The search context enables accessing the search query, search status, and search results among different components. const SearchContext = createContext(); const useSearchContext = () => useContext(SearchContext); export const SearchContextProvider = ({ children }) => { const [searchQuery, setSearchQuery] = useState(''); - const [sortedData, setSortedData] = useState(plugins); + const [searchResults, setSearchResults] = useState(); + const [isSearchSubmitted, setIsSearchSubmitted] = useState(false); return ( - + {children} ); @@ -61,14 +65,22 @@ function preparePluginsForSearch(plugins) { const pluginsListForSearch = preparePluginsForSearch(plugins); function Search() { - const { searchQuery, setSearchQuery, sortedData, setSortedData } = useSearchContext(); + const { searchQuery, setSearchQuery, setSearchResults, isSearchSubmitted, setIsSearchSubmitted } = useSearchContext(); // Update searchQuery when input changes const handleSearch = (searchQuery) => { setSearchQuery(searchQuery); - if (searchQuery == "") { - setSortedData(plugins) - } document.querySelector(".suggestions-list").style.display = "block"; + document.querySelector(".dropdown-search").style.display = "block"; + if (searchQuery == "" || isSearchSubmitted == true) { + setIsSearchSubmitted(false); + document.querySelector(".dropdown-search").style.display = "none"; + } + // Hide the Enter symbol when the input is empty + const enterSymbol = document.querySelector('.enter-symbol'); + if (enterSymbol) { + enterSymbol.style.opacity = searchQuery ? '1' : '0'; + } + } //Create a fuce instance for searching the provided keys. const fuse = new Fuse(pluginsListForSearch, { @@ -78,22 +90,16 @@ function Search() { threshold: 0.1, includeMatches: true, }) - let searchRes = fuse.search(searchQuery) - console.log(searchRes) - const suggestions = searchRes.map((item) => item.item.name); //get the list searched plugins - const resultObject = {}; - - //Convert the search results array to object - searchRes.forEach(item => { - resultObject[item.item.name] = plugins[item.item.name]; - }); + let searchResults = fuse.search(searchQuery) - //Update the sortedData state with the search results + //Update the searchResults state with the search results const handleSubmit = (e) => { e.preventDefault(); if (searchQuery) { - setSortedData(resultObject); + setSearchResults(searchResults); + setIsSearchSubmitted(true); document.querySelector(".suggestions-list").style.display = "none"; + document.querySelector(".dropdown-search").style.display = "none"; } }; @@ -103,15 +109,32 @@ function Search() {
+
handleSearch(e.target.value)} /> + +
{/* Display the list of suggestions */}
@@ -120,8 +143,9 @@ function Search() { export function MainIndex() { - const { searchQuery, setSearchQuery, sortedData, setSortedData } = useSearchContext(); + const { searchQuery, setSearchQuery, searchResults, isSearchSubmitted, setIsSearchSubmitted } = useSearchContext(); const [sortOption, setSortOption] = useState('commits'); //Available options: 'commits', 'release', and 'alpha'. + const [sortedData, setSortedData] = useState(plugins); useEffect(() => { document.documentElement.style.scrollBehavior = 'auto'; @@ -174,6 +198,8 @@ export function MainIndex() { const handleSort = (option) => { setSortOption(option); + setSearchQuery(''); + setIsSearchSubmitted(false); let sortedPlugins = {}; @@ -235,6 +261,39 @@ export function MainIndex() { + {isSearchSubmitted === true ? ( + <> +

Showing {searchResults.length} pages matching the search query.

+ {searchResults.length === 0 && ( +
+

Can't find what you're looking for?
+ Join the AiiDA community on Discourse and request a plugin here.

+
+ )} + {searchResults.map((suggestion) => ( + <> +
+

+ {suggestion.item.name} +

+ +
+ + ))} + + + ):( + <> {Object.entries(sortedData).map(([key, value]) => (

{key}

@@ -303,7 +362,23 @@ export function MainIndex() {
))} + + )} ); } + +function SearchResultSnippet({match_value}) { + const {searchQuery} = useSearchContext(); + const [before, matchedText, after] = extractSentenceAroundKeyword(match_value, searchQuery); + return ( + <> + {before != null && ( +

{before} + {matchedText} + {after}...

+ )} + + ) +} diff --git a/aiida-registry-app/src/Components/utils.js b/aiida-registry-app/src/Components/utils.js new file mode 100644 index 00000000..c9359409 --- /dev/null +++ b/aiida-registry-app/src/Components/utils.js @@ -0,0 +1,59 @@ +/** +* Find the sentence where the search query mentioned. +* @param {string} matchedEntryPoint Entry point data dumped as string. +* @param {string} keyword search query. +* @returns {Array} List of three strings, before matched word, matched word, and after matched word. +*/ +export function extractSentenceAroundKeyword(matchedEntryPoint, keyword) { + const jsonObject = JSON.parse(matchedEntryPoint); + let matchingSentences = []; + + //Recursively loop through the object until finding a string value. + function processKeyValuePairs(key, value) { + if (typeof value === "string" && value.toLowerCase().includes(keyword.toLowerCase())) { + return value.trim(); + + } else if (typeof value === "object") { + for (const innerKey in value) { + const processedValue = processKeyValuePairs(innerKey, value[innerKey]); + //If the string is part of an array (innerKey is a number), + //get the previous or next line to add more context. + if (innerKey > 0 && typeof processedValue == 'string') { + matchingSentences.push(key + ': ' + value[(innerKey - '1').toString()] + ' ' + processedValue); + } else if (innerKey == 0 && value.length > 1 && typeof processedValue == 'string') { + matchingSentences.push(key + ': ' + processedValue + ' ' + value['1']); + } else if (typeof processedValue == 'string') { + matchingSentences.push(innerKey + ': ' + processedValue); + } + } + } + } + + for (const key in jsonObject) { + processKeyValuePairs(key, jsonObject[key]); + } + + //The matchingSentences array now contains a list of matching sentences. + //We display only the first matching sentence in the list. + const matchingSentence = matchingSentences[0]; + let contextArray = []; + try { + const lowercaseMatchingSentence = matchingSentence.toLowerCase(); + const lowercaseKeyword = keyword.toLowerCase(); + + const indexOfKeyword = lowercaseMatchingSentence.indexOf(lowercaseKeyword); + + //This splitting helps in highlighting the keyword. + if (indexOfKeyword !== -1) { + const beforeKeyword = matchingSentence.substring(0, indexOfKeyword); + const keyword = matchingSentence.substring(indexOfKeyword, indexOfKeyword + lowercaseKeyword.length); + const afterKeyword = matchingSentence.substring(indexOfKeyword + lowercaseKeyword.length); + + contextArray = [beforeKeyword, keyword, afterKeyword]; + } + } catch (error) { + contextArray = [null, null, null]; + } + + return contextArray +}