Skip to content

Commit

Permalink
Merge pull request #493 from docat-org/improvement/472-simplify-search
Browse files Browse the repository at this point in the history
Improvement: Migrate Search Results from seperate page to home page
  • Loading branch information
randombenj authored Apr 14, 2023
2 parents 1049824 + c3c7057 commit d552b89
Show file tree
Hide file tree
Showing 20 changed files with 177 additions and 525 deletions.
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 @@ -62,19 +58,17 @@ export function ProjectDataProvider({ children }: any): JSX.Element {
showMs: 6000
})

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 @@ -83,7 +77,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

0 comments on commit d552b89

Please sign in to comment.