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

Improvement: Migrate Search Results from seperate page to home page #493

Merged
merged 1 commit into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docat/docat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def is_forbidden_project_name(name: str) -> bool:
a page on the docat website.
"""
name = name.lower().strip()
return name in ["upload", "claim", "delete", "search", "help"]
return name in ["upload", "claim", "delete", "help"]


def get_all_projects(upload_folder_path: Path, include_hidden: bool) -> Projects:
Expand Down
2 changes: 1 addition & 1 deletion docat/tests/test_rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_rename_rejects_forbidden_project_name(client_with_claimed_project):
assert create_response.status_code == 201

with patch("os.rename") as rename_mock:
for project_name in ["upload", "claim", "delete", "Search ", "help"]:
for project_name in ["upload", "claim", "Delete ", "help"]:
rename_response = client_with_claimed_project.put(f"/api/some-project/rename/{project_name}", headers={"Docat-Api-Key": "1234"})
assert rename_response.status_code == 400
assert rename_response.json() == {
Expand Down
2 changes: 1 addition & 1 deletion docat/tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_upload_rejects_forbidden_project_name(client_with_claimed_project):
"""

with patch("docat.app.remove_docs") as remove_mock:
for project_name in ["upload", "claim", "delete", "Search ", "help"]:
for project_name in ["upload", "claim", " Delete ", "help"]:
response = client_with_claimed_project.post(
f"/api/{project_name}/1.0.0", files={"file": ("index.html", io.BytesIO(b"<h1>Hello World</h1>"), "plain/text")}
)
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@types/react-dom": "^18.0.10",
"@typescript-eslint/parser": "^5.54.1",
"eslint": "^8.0.1",
"fuse.js": "^6.6.2",
"http-proxy-middleware": "^2.0.6",
"lodash": "^4.17.21",
"react": "^18.2.0",
Expand Down
12 changes: 5 additions & 7 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import Help from './pages/Help'
import Home from './pages/Home'
import NotFound from './pages/NotFound'
import Upload from './pages/Upload'
import Search from './pages/Search'
import EscapeSlashForDocsPath from './pages/EscapeSlashForDocsPath'
import { MessageBannerProvider } from './data-providers/MessageBannerProvider'
import { SearchProvider } from './data-providers/SearchProvider'

function App(): JSX.Element {
function App (): JSX.Element {
const router = createHashRouter([
{
path: '/',
Expand All @@ -39,10 +39,6 @@ function App(): JSX.Element {
path: 'help',
element: <Help />
},
{
path: 'search',
element: <Search />
},
{
path: ':project',
children: [
Expand Down Expand Up @@ -83,7 +79,9 @@ function App(): JSX.Element {
<MessageBannerProvider>
<ConfigDataProvider>
<ProjectDataProvider>
<RouterProvider router={router} />
<SearchProvider>
<RouterProvider router={router} />
</SearchProvider>
</ProjectDataProvider>
</ConfigDataProvider>
</MessageBannerProvider>
Expand Down
60 changes: 34 additions & 26 deletions web/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
import _ from 'lodash'
import { TextField } from '@mui/material'
import { useNavigate } from 'react-router-dom'
import React from 'react'
import React, { useCallback } from 'react'
import styles from '../style/components/SearchBar.module.css'
import { Search } from '@mui/icons-material'
import { useSearch } from '../data-providers/SearchProvider'

export default function SearchBar(): JSX.Element {
const navigate = useNavigate()
const [searchQuery, setSearchQuery] = React.useState<string>('')
export default function SearchBar (): JSX.Element {
const { query, setQuery } = useSearch()
const [searchQuery, setSearchQuery] = React.useState<string>(query)

const updateSearchQueryInDataProvider = useCallback(
_.debounce(
(query: string): void => {
setQuery(query)
}, 500),
[])

const onSearch = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSearchQuery(e.target.value)
updateSearchQueryInDataProvider.cancel()
updateSearchQueryInDataProvider(e.target.value)
}

return (
<>
<Search
className={styles['search-icon']}
onClick={() => navigate('/search')}
/>
<div className={styles['search-bar']}>
<TextField
label="Search"
type="search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
navigate(`/search?query=${searchQuery}`)
}
}}
variant="standard"
></TextField>
</div>
</>
<div className={styles['search-bar']}>
<TextField
label="Search"
type="search"
value={searchQuery}
onChange={onSearch}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
e.preventDefault()
setQuery(searchQuery)
}
}}
variant="standard"
></TextField>
</div>
)
}
107 changes: 0 additions & 107 deletions web/src/components/SearchResults.tsx

This file was deleted.

18 changes: 6 additions & 12 deletions web/src/data-providers/ProjectDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import ProjectsResponse, { Project } from '../models/ProjectsResponse'
import { useMessageBanner } from './MessageBannerProvider'
import ProjectRepository from '../repositories/ProjectRepository'

interface ProjectState {
projects: Project[] | null
projectsWithHiddenVersions: Project[] | null
loadingFailed: boolean
reload: () => void
}

const Context = createContext<ProjectState>({
projects: null,
projectsWithHiddenVersions: null,
loadingFailed: false,
reload: (): void => {
console.warn('ProjectDataProvider not initialized')
Expand All @@ -32,7 +29,7 @@ const Context = createContext<ProjectState>({
*
* If reloading is required, call the reload function.
*/
export function ProjectDataProvider({ children }: any): JSX.Element {
export function ProjectDataProvider ({ children }: any): JSX.Element {
const { showMessage } = useMessageBanner()

const loadData = (): void => {
Expand All @@ -47,9 +44,8 @@ export function ProjectDataProvider({ children }: any): JSX.Element {
}

const data: ProjectsResponse = await response.json()
setProjects({
projects: ProjectRepository.filterHiddenVersions(data.projects),
projectsWithHiddenVersions: data.projects,
setState({
projects: data.projects,
loadingFailed: false,
reload: loadData
})
Expand All @@ -61,19 +57,17 @@ export function ProjectDataProvider({ children }: any): JSX.Element {
type: 'error'
})

setProjects({
setState({
projects: null,
projectsWithHiddenVersions: null,
loadingFailed: true,
reload: loadData
})
}
})()
}

const [projects, setProjects] = useState<ProjectState>({
const [state, setState] = useState<ProjectState>({
projects: null,
projectsWithHiddenVersions: null,
loadingFailed: false,
reload: loadData
})
Expand All @@ -82,7 +76,7 @@ export function ProjectDataProvider({ children }: any): JSX.Element {
loadData()
}, [])

return <Context.Provider value={projects}>{children}</Context.Provider>
return <Context.Provider value={state}>{children}</Context.Provider>
}

export const useProjects = (): ProjectState => useContext(Context)
75 changes: 75 additions & 0 deletions web/src/data-providers/SearchProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
/*
We need any, because we don't know the type of the children,
and we need the return those children again which is an "unsafe return"
*/

import React, { createContext, useContext, useEffect, useState } from 'react'
import { Project } from '../models/ProjectsResponse'
import { useProjects } from './ProjectDataProvider'
import Fuse from 'fuse.js'

interface SearchState {
filteredProjects: Project[] | null
query: string
setQuery: (query: string) => void
}

const Context = createContext<SearchState>({
filteredProjects: null,
query: '',
setQuery: (): void => {
console.warn('SearchDataProvider not initialized')
}
})

export function SearchProvider ({ children }: any): JSX.Element {
const { projects } = useProjects()

const filterProjects = (query: string): Project[] | null => {
if (projects == null) {
return null
}

if (query.trim() === '') {
return projects
}

const fuse = new Fuse(projects, {
keys: ['name'],
includeScore: true
})

// sort by match score
return fuse
.search(query)
.sort((x, y) => (x.score ?? 0) - (y.score ?? 0))
.map((result) => result.item)
}

const setQuery = (query: string): void => {
setState({
query,
filteredProjects: filterProjects(query),
setQuery
})
}

const [state, setState] = useState<SearchState>({
filteredProjects: null,
query: '',
setQuery
})

useEffect(() => {
setState({
query: '',
filteredProjects: filterProjects(''),
setQuery
})
}, [projects])

return <Context.Provider value={state}>{children}</Context.Provider>
}

export const useSearch = (): SearchState => useContext(Context)
Loading