import * as React from 'react' import * as types from 'notion-types' import throttle from 'lodash.throttle' import { getBlockTitle, getBlockParentPage } from 'notion-utils' import { SearchIcon } from '../icons/search-icon' import { ClearIcon } from '../icons/clear-icon' import { LoadingIcon } from '../icons/loading-icon' import { PageTitle } from './page-title' import { cs } from '../utils' import { NotionContextConsumer, NotionContextProvider } from '../context' // TODO: modal.default.setAppElement('.notion-viewport') export class SearchDialog extends React.Component<{ isOpen: boolean rootBlockId: string onClose: () => void searchNotion: (params: types.SearchParams) => Promise }> { constructor(props) { super(props) this._inputRef = React.createRef() } state = { isLoading: false, query: '', searchResult: null, searchError: null } _inputRef: any _search: any componentDidMount() { this._search = throttle(this._searchImpl.bind(this), 1000) this._warmupSearch() } render() { const { isOpen, onClose } = this.props const { isLoading, query, searchResult, searchError } = this.state const hasQuery = !!query.trim() return ( {(ctx) => { const { components, defaultPageIcon, mapPageUrl } = ctx return (
{isLoading ? ( ) : ( )}
{query && (
)}
{hasQuery && searchResult && ( <> {searchResult.results.length ? (
{searchResult.results.map((result) => ( {result.highlight?.html && (
)} ))}
{searchResult.total} {searchResult.total === 1 ? ' result' : ' results'}
) : (
No results
Try different search terms
)} )} {hasQuery && !searchResult && searchError && (
Search error
)}
) }} ) } _onAfterOpen = () => { if (this._inputRef.current) { this._inputRef.current.focus() } } _onChangeQuery = (e) => { const query = e.target.value this.setState({ query }) if (!query.trim()) { this.setState({ isLoading: false, searchResult: null, searchError: null }) return } else { this._search() } } _onClearQuery = () => { this._onChangeQuery({ target: { value: '' } }) } _warmupSearch = async () => { const { searchNotion, rootBlockId } = this.props // search is generally implemented as a serverless function wrapping the notion // private API, upon opening the search dialog, so we eagerly invoke an empty // search in order to warm up the serverless lambda await searchNotion({ query: '', ancestorId: rootBlockId }) } _searchImpl = async () => { const { searchNotion, rootBlockId } = this.props const { query } = this.state if (!query.trim()) { this.setState({ isLoading: false, searchResult: null, searchError: null }) return } this.setState({ isLoading: true }) const result: any = await searchNotion({ query, ancestorId: rootBlockId }) console.log('search', query, result) let searchResult: any = null // TODO let searchError: types.APIError = null if (result.error || result.errorId) { searchError = result } else { searchResult = { ...result } const results = searchResult.results .map((result: any) => { const block = searchResult.recordMap.block[result.id]?.value if (!block) return const title = getBlockTitle(block, searchResult.recordMap) if (!title) { return } result.title = title result.block = block result.recordMap = searchResult.recordMap result.page = getBlockParentPage(block, searchResult.recordMap, { inclusive: true }) || block if (!result.page.id) { return } if (result.highlight?.text) { result.highlight.html = result.highlight.text .replace(//gi, '') .replace(/<\/gzkNfoUU>/gi, '') } return result }) .filter(Boolean) // dedupe results by page id const searchResultsMap = results.reduce( (map, result) => ({ ...map, [result.page.id]: result }), {} ) searchResult.results = Object.values(searchResultsMap) } if (this.state.query === query) { this.setState({ isLoading: false, searchResult, searchError }) } } }