Convert Search page to functional component (#6268)
This commit is contained in:
@@ -15,7 +15,6 @@ import { getDataTransferFiles } from "@shared/utils/files";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import { isInternalUrl } from "@shared/utils/urls";
|
||||
import { AttachmentValidation } from "@shared/validations";
|
||||
import Document from "~/models/Document";
|
||||
import ClickablePadding from "~/components/ClickablePadding";
|
||||
import ErrorBoundary from "~/components/ErrorBoundary";
|
||||
import type { Props as EditorProps, Editor as SharedEditor } from "~/editor";
|
||||
@@ -104,7 +103,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
|
||||
const results = await documents.searchTitles(term);
|
||||
|
||||
return sortBy(
|
||||
results.map((document: Document) => ({
|
||||
results.map(({ document }) => ({
|
||||
title: document.title,
|
||||
subtitle: <DocumentBreadcrumb document={document} onlyText />,
|
||||
url: document.url,
|
||||
|
||||
@@ -116,7 +116,7 @@ const Heading = styled.h4<{ rtl?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
height: 22px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.25em;
|
||||
overflow: hidden;
|
||||
@@ -138,7 +138,7 @@ const ResultContext = styled(Highlight)`
|
||||
color: ${s("textTertiary")};
|
||||
font-size: 14px;
|
||||
margin-top: -0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
margin-bottom: 0;
|
||||
${ellipsis()}
|
||||
|
||||
${Mark} {
|
||||
|
||||
@@ -31,9 +31,11 @@ function SearchPopover({ shareId }: Props) {
|
||||
});
|
||||
|
||||
const [query, setQuery] = React.useState("");
|
||||
const searchResults = documents.searchResults(query);
|
||||
const { show, hide } = popover;
|
||||
|
||||
const [searchResults, setSearchResults] = React.useState<
|
||||
PaginatedItem[] | undefined
|
||||
>();
|
||||
const [cachedQuery, setCachedQuery] = React.useState(query);
|
||||
const [cachedSearchResults, setCachedSearchResults] = React.useState<
|
||||
PaginatedItem[] | undefined
|
||||
@@ -50,7 +52,16 @@ function SearchPopover({ shareId }: Props) {
|
||||
const performSearch = React.useCallback(
|
||||
async ({ query, ...options }) => {
|
||||
if (query?.length > 0) {
|
||||
return await documents.search(query, { shareId, ...options });
|
||||
const response: PaginatedItem[] = await documents.search(query, {
|
||||
shareId,
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.length) {
|
||||
setSearchResults(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ import Tooltip from "./Tooltip";
|
||||
|
||||
export type SearchResult = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
url: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ type RequestResponse<T> = {
|
||||
next: () => void;
|
||||
/** Page number */
|
||||
page: number;
|
||||
/** Offset */
|
||||
offset: number;
|
||||
/** Marks the end of pagination */
|
||||
end: boolean;
|
||||
};
|
||||
@@ -64,9 +66,7 @@ export default function usePaginatedRequest<T = unknown>(
|
||||
uniqBy((prev ?? []).concat(response.slice(0, displayLimit)), "id")
|
||||
);
|
||||
setPage((prev) => prev + 1);
|
||||
if (response.length <= displayLimit) {
|
||||
setEnd(true);
|
||||
}
|
||||
setEnd(response.length <= displayLimit);
|
||||
}
|
||||
}, [response, displayLimit, loading]);
|
||||
|
||||
@@ -87,5 +87,12 @@ export default function usePaginatedRequest<T = unknown>(
|
||||
setOffset((prev) => prev + displayLimit);
|
||||
}, [displayLimit]);
|
||||
|
||||
return { data, next, loading, error, page, end };
|
||||
React.useEffect(() => {
|
||||
setEnd(false);
|
||||
setData(undefined);
|
||||
setPage(0);
|
||||
setOffset(0);
|
||||
}, [requestFn]);
|
||||
|
||||
return { data, next, loading, error, page, offset, end };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observable, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import queryString from "query-string";
|
||||
import * as React from "react";
|
||||
import { WithTranslation, withTranslation, Trans } from "react-i18next";
|
||||
import { RouteComponentProps, StaticContext, withRouter } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
@@ -12,8 +10,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import { hideScrollbars } from "@shared/styles";
|
||||
import { DateFilter as TDateFilter } from "@shared/types";
|
||||
import { SearchParams } from "~/stores/DocumentsStore";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
import DocumentListItem from "~/components/DocumentListItem";
|
||||
import Empty from "~/components/Empty";
|
||||
@@ -24,11 +20,13 @@ import RegisterKeyDown from "~/components/RegisterKeyDown";
|
||||
import Scene from "~/components/Scene";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import withStores from "~/components/withStores";
|
||||
import env from "~/env";
|
||||
import usePaginatedRequest from "~/hooks/usePaginatedRequest";
|
||||
import useQuery from "~/hooks/useQuery";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { hover } from "~/styles";
|
||||
import Logger from "~/utils/Logger";
|
||||
import { SearchResult } from "~/types";
|
||||
import { searchPath } from "~/utils/routeHelpers";
|
||||
import { decodeURIComponentSafe } from "~/utils/urls";
|
||||
import CollectionFilter from "./components/CollectionFilter";
|
||||
import DateFilter from "./components/DateFilter";
|
||||
import RecentSearches from "./components/RecentSearches";
|
||||
@@ -36,73 +34,102 @@ import SearchInput from "./components/SearchInput";
|
||||
import StatusFilter from "./components/StatusFilter";
|
||||
import UserFilter from "./components/UserFilter";
|
||||
|
||||
type Props = RouteComponentProps<
|
||||
{ term: string },
|
||||
StaticContext,
|
||||
{ search: string; fromMenu?: boolean }
|
||||
> &
|
||||
WithTranslation &
|
||||
RootStore & {
|
||||
notFound?: boolean;
|
||||
type Props = { notFound?: boolean };
|
||||
|
||||
function Search(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { documents, searches } = useStores();
|
||||
|
||||
// routing
|
||||
const params = useQuery();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const routeMatch = useRouteMatch<{ term: string }>();
|
||||
|
||||
// refs
|
||||
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const resultListCompositeRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const recentSearchesCompositeRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// filters
|
||||
const query = routeMatch.params.term ?? "";
|
||||
const includeArchived = params.get("includeArchived") === "true";
|
||||
const collectionId = params.get("collectionId") ?? undefined;
|
||||
const userId = params.get("userId") ?? undefined;
|
||||
const dateFilter = (params.get("dateFilter") as TDateFilter) ?? undefined;
|
||||
const titleFilter = params.get("titleFilter") === "true";
|
||||
|
||||
const filters = React.useMemo(
|
||||
() => ({
|
||||
query,
|
||||
includeArchived,
|
||||
collectionId,
|
||||
userId,
|
||||
dateFilter,
|
||||
titleFilter,
|
||||
}),
|
||||
[query, includeArchived, collectionId, userId, dateFilter, titleFilter]
|
||||
);
|
||||
|
||||
const requestFn = React.useMemo(() => {
|
||||
// Add to the searches store so this search can immediately appear in the recent searches list
|
||||
// without a flash of loading.
|
||||
if (query) {
|
||||
searches.add({
|
||||
id: uuidv4(),
|
||||
query,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return async () =>
|
||||
titleFilter
|
||||
? await documents.searchTitles(query, filters)
|
||||
: await documents.search(query, filters);
|
||||
}
|
||||
|
||||
return () => Promise.resolve([] as SearchResult[]);
|
||||
}, [query, titleFilter, filters, searches, documents]);
|
||||
|
||||
const { data, next, end, loading } = usePaginatedRequest(requestFn, {
|
||||
limit: Pagination.defaultLimit,
|
||||
});
|
||||
|
||||
const updateLocation = (query: string) => {
|
||||
history.replace({
|
||||
pathname: searchPath(query),
|
||||
search: location.search,
|
||||
});
|
||||
};
|
||||
|
||||
@observer
|
||||
class Search extends React.Component<Props> {
|
||||
resultListCompositeRef: HTMLDivElement | null | undefined;
|
||||
recentSearchesCompositeRef: HTMLDivElement | null | undefined;
|
||||
searchInputRef: HTMLInputElement | null | undefined;
|
||||
|
||||
lastQuery = "";
|
||||
|
||||
lastParams: SearchParams;
|
||||
|
||||
@observable
|
||||
query: string = decodeURIComponentSafe(this.props.match.params.term || "");
|
||||
|
||||
@observable
|
||||
params: URLSearchParams = new URLSearchParams(this.props.location.search);
|
||||
|
||||
@observable
|
||||
offset = 0;
|
||||
|
||||
@observable
|
||||
allowLoadMore = true;
|
||||
|
||||
@observable
|
||||
isLoading = false;
|
||||
|
||||
componentDidMount() {
|
||||
this.handleTermChange();
|
||||
|
||||
if (this.props.location.search) {
|
||||
this.handleQueryChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.location.search !== this.props.location.search) {
|
||||
this.handleQueryChange();
|
||||
}
|
||||
|
||||
if (prevProps.match.params.term !== this.props.match.params.term) {
|
||||
this.handleTermChange();
|
||||
}
|
||||
}
|
||||
|
||||
goBack = () => {
|
||||
this.props.history.goBack();
|
||||
// All filters go through the query string so that searches are bookmarkable, which neccesitates
|
||||
// some complexity as the query string is the source of truth for the filters.
|
||||
const handleFilterChange = (search: {
|
||||
collectionId?: string | undefined;
|
||||
userId?: string | undefined;
|
||||
dateFilter?: TDateFilter;
|
||||
includeArchived?: boolean | undefined;
|
||||
titleFilter?: boolean | undefined;
|
||||
}) => {
|
||||
history.replace({
|
||||
pathname: location.pathname,
|
||||
search: queryString.stringify(
|
||||
{ ...queryString.parse(location.search), ...search },
|
||||
{
|
||||
skipEmptyString: true,
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.key === "Enter") {
|
||||
this.updateLocation(ev.currentTarget.value);
|
||||
void this.fetchResults();
|
||||
updateLocation(ev.currentTarget.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
return this.goBack();
|
||||
return history.goBack();
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowUp") {
|
||||
@@ -130,312 +157,127 @@ class Search extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const firstItem = this.firstResultItem ?? this.firstRecentSearchItem;
|
||||
const firstResultItem = (
|
||||
resultListCompositeRef.current?.querySelectorAll(
|
||||
"[href]"
|
||||
) as NodeListOf<HTMLAnchorElement>
|
||||
)?.[0];
|
||||
|
||||
const firstRecentSearchItem = (
|
||||
recentSearchesCompositeRef.current?.querySelectorAll(
|
||||
"li > [href]"
|
||||
) as NodeListOf<HTMLAnchorElement>
|
||||
)?.[0];
|
||||
|
||||
const firstItem = firstResultItem ?? firstRecentSearchItem;
|
||||
firstItem?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleQueryChange = () => {
|
||||
this.params = new URLSearchParams(this.props.location.search);
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
// To prevent "no results" showing before debounce kicks in
|
||||
this.isLoading = true;
|
||||
void this.fetchResults();
|
||||
};
|
||||
const handleEscape = () => searchInputRef.current?.focus();
|
||||
const showEmpty = !loading && query && data?.length === 0;
|
||||
|
||||
handleTermChange = () => {
|
||||
const query = decodeURIComponentSafe(this.props.match.params.term || "");
|
||||
this.query = query ? query : "";
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
// To prevent "no results" showing before debounce kicks in
|
||||
this.isLoading = true;
|
||||
void this.fetchResults();
|
||||
};
|
||||
return (
|
||||
<Scene textTitle={query ? `${query} – ${t("Search")}` : t("Search")}>
|
||||
<RegisterKeyDown trigger="Escape" handler={history.goBack} />
|
||||
{loading && <LoadingIndicator />}
|
||||
{props.notFound && (
|
||||
<div>
|
||||
<h1>{t("Not Found")}</h1>
|
||||
<Empty>
|
||||
{t("We were unable to find the page you’re looking for.")}
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
<ResultsWrapper column auto>
|
||||
<SearchInput
|
||||
key={query ? "search" : "recent"}
|
||||
ref={searchInputRef}
|
||||
placeholder={`${t("Search")}…`}
|
||||
onKeyDown={handleKeyDown}
|
||||
defaultValue={query}
|
||||
/>
|
||||
|
||||
handleFilterChange = (search: {
|
||||
collectionId?: string | undefined;
|
||||
userId?: string | undefined;
|
||||
dateFilter?: TDateFilter;
|
||||
includeArchived?: boolean | undefined;
|
||||
titleFilter?: boolean | undefined;
|
||||
}) => {
|
||||
this.props.history.replace({
|
||||
pathname: this.props.location.pathname,
|
||||
search: queryString.stringify(
|
||||
{ ...queryString.parse(this.props.location.search), ...search },
|
||||
{
|
||||
skipEmptyString: true,
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
handleTitleFilterChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.handleFilterChange({ titleFilter: ev.target.checked });
|
||||
};
|
||||
|
||||
get firstResultItem() {
|
||||
const linkItems = this.resultListCompositeRef?.querySelectorAll(
|
||||
"[href]"
|
||||
) as NodeListOf<HTMLAnchorElement>;
|
||||
return linkItems?.[0];
|
||||
}
|
||||
|
||||
get firstRecentSearchItem() {
|
||||
const linkItems = this.recentSearchesCompositeRef?.querySelectorAll(
|
||||
"li > [href]"
|
||||
) as NodeListOf<HTMLAnchorElement>;
|
||||
return linkItems?.[0];
|
||||
}
|
||||
|
||||
get includeArchived() {
|
||||
return this.params.get("includeArchived") === "true";
|
||||
}
|
||||
|
||||
get collectionId() {
|
||||
const id = this.params.get("collectionId");
|
||||
return id ? id : undefined;
|
||||
}
|
||||
|
||||
get userId() {
|
||||
const id = this.params.get("userId");
|
||||
return id ? id : undefined;
|
||||
}
|
||||
|
||||
get dateFilter() {
|
||||
const id = this.params.get("dateFilter");
|
||||
return id ? (id as TDateFilter) : undefined;
|
||||
}
|
||||
|
||||
get titleFilter() {
|
||||
return this.params.get("titleFilter") === "true";
|
||||
}
|
||||
|
||||
get isFiltered() {
|
||||
return (
|
||||
this.dateFilter ||
|
||||
this.userId ||
|
||||
this.collectionId ||
|
||||
this.includeArchived ||
|
||||
this.titleFilter
|
||||
);
|
||||
}
|
||||
|
||||
get title() {
|
||||
const query = this.query;
|
||||
const title = this.props.t("Search");
|
||||
if (query) {
|
||||
return `${query} – ${title}`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
@action
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||
if (!this.allowLoadMore || this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch more results
|
||||
await this.fetchResults();
|
||||
};
|
||||
|
||||
@action
|
||||
fetchResults = async () => {
|
||||
if (this.query.trim()) {
|
||||
const params = {
|
||||
offset: this.offset,
|
||||
limit: Pagination.defaultLimit,
|
||||
dateFilter: this.dateFilter,
|
||||
includeArchived: this.includeArchived,
|
||||
includeDrafts: true,
|
||||
collectionId: this.collectionId,
|
||||
userId: this.userId,
|
||||
titleFilter: this.titleFilter,
|
||||
};
|
||||
|
||||
// we just requested this thing – no need to try again
|
||||
if (this.lastQuery === this.query && isEqual(params, this.lastParams)) {
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.lastQuery = this.query;
|
||||
this.lastParams = params;
|
||||
|
||||
try {
|
||||
const results = this.titleFilter
|
||||
? await this.props.documents.searchTitles(this.query, params)
|
||||
: await this.props.documents.search(this.query, params);
|
||||
|
||||
// Add to the searches store so this search can immediately appear in
|
||||
// the recent searches list without a flash of load
|
||||
this.props.searches.add({
|
||||
id: uuidv4(),
|
||||
query: this.query,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (results.length === 0 || results.length < Pagination.defaultLimit) {
|
||||
this.allowLoadMore = false;
|
||||
} else {
|
||||
this.offset += Pagination.defaultLimit;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Search query failed", error);
|
||||
this.lastQuery = "";
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
this.lastQuery = this.query;
|
||||
}
|
||||
};
|
||||
|
||||
updateLocation = (query: string) => {
|
||||
this.props.history.replace({
|
||||
pathname: searchPath(query),
|
||||
search: this.props.location.search,
|
||||
});
|
||||
};
|
||||
|
||||
setResultListCompositeRef = (ref: HTMLDivElement | null) => {
|
||||
this.resultListCompositeRef = ref;
|
||||
};
|
||||
|
||||
setRecentSearchesCompositeRef = (ref: HTMLDivElement | null) => {
|
||||
this.recentSearchesCompositeRef = ref;
|
||||
};
|
||||
|
||||
setSearchInputRef = (ref: HTMLInputElement | null) => {
|
||||
this.searchInputRef = ref;
|
||||
};
|
||||
|
||||
handleEscape = () => {
|
||||
this.searchInputRef?.focus();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents, notFound, t } = this.props;
|
||||
const results = documents.searchResults(this.query);
|
||||
const showEmpty = !this.isLoading && this.query && results?.length === 0;
|
||||
|
||||
return (
|
||||
<Scene textTitle={this.title}>
|
||||
<RegisterKeyDown trigger="Escape" handler={this.goBack} />
|
||||
{this.isLoading && <LoadingIndicator />}
|
||||
{notFound && (
|
||||
<div>
|
||||
<h1>{t("Not Found")}</h1>
|
||||
<Empty>
|
||||
{t("We were unable to find the page you’re looking for.")}
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
<ResultsWrapper column auto>
|
||||
<SearchInput
|
||||
ref={this.setSearchInputRef}
|
||||
placeholder={`${t("Search")}…`}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
defaultValue={this.query}
|
||||
/>
|
||||
|
||||
{this.query ? (
|
||||
{query ? (
|
||||
<>
|
||||
<Filters>
|
||||
<StatusFilter
|
||||
includeArchived={this.includeArchived}
|
||||
includeArchived={includeArchived}
|
||||
onSelect={(includeArchived) =>
|
||||
this.handleFilterChange({
|
||||
includeArchived,
|
||||
})
|
||||
handleFilterChange({ includeArchived })
|
||||
}
|
||||
/>
|
||||
<CollectionFilter
|
||||
collectionId={this.collectionId}
|
||||
collectionId={collectionId}
|
||||
onSelect={(collectionId) =>
|
||||
this.handleFilterChange({
|
||||
collectionId,
|
||||
})
|
||||
handleFilterChange({ collectionId })
|
||||
}
|
||||
/>
|
||||
<UserFilter
|
||||
userId={this.userId}
|
||||
onSelect={(userId) =>
|
||||
this.handleFilterChange({
|
||||
userId,
|
||||
})
|
||||
}
|
||||
userId={userId}
|
||||
onSelect={(userId) => handleFilterChange({ userId })}
|
||||
/>
|
||||
<DateFilter
|
||||
dateFilter={this.dateFilter}
|
||||
onSelect={(dateFilter) =>
|
||||
this.handleFilterChange({
|
||||
dateFilter,
|
||||
})
|
||||
}
|
||||
dateFilter={dateFilter}
|
||||
onSelect={(dateFilter) => handleFilterChange({ dateFilter })}
|
||||
/>
|
||||
<SearchTitlesFilter
|
||||
width={26}
|
||||
height={14}
|
||||
label={t("Search titles only")}
|
||||
onChange={this.handleTitleFilterChange}
|
||||
checked={this.titleFilter}
|
||||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleFilterChange({ titleFilter: ev.target.checked });
|
||||
}}
|
||||
checked={titleFilter}
|
||||
/>
|
||||
</Filters>
|
||||
) : (
|
||||
<RecentSearches
|
||||
ref={this.setRecentSearchesCompositeRef}
|
||||
onEscape={this.handleEscape}
|
||||
/>
|
||||
)}
|
||||
{showEmpty && (
|
||||
<Fade>
|
||||
<Centered column>
|
||||
<Text type="secondary">
|
||||
<Trans>No documents found for your search filters.</Trans>
|
||||
</Text>
|
||||
</Centered>
|
||||
</Fade>
|
||||
)}
|
||||
<ResultList column>
|
||||
<StyledArrowKeyNavigation
|
||||
ref={this.setResultListCompositeRef}
|
||||
onEscape={this.handleEscape}
|
||||
aria-label={t("Search Results")}
|
||||
>
|
||||
{(compositeProps) =>
|
||||
results?.map((result) => {
|
||||
const document = documents.data.get(result.document.id);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DocumentListItem
|
||||
key={document.id}
|
||||
document={document}
|
||||
highlight={this.query}
|
||||
context={result.context}
|
||||
showCollection
|
||||
showTemplate
|
||||
{...compositeProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</StyledArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||
{showEmpty && (
|
||||
<Fade>
|
||||
<Centered column>
|
||||
<Text type="secondary">
|
||||
{t("No documents found for your search filters.")}
|
||||
</Text>
|
||||
</Centered>
|
||||
</Fade>
|
||||
)}
|
||||
</ResultList>
|
||||
</ResultsWrapper>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
<ResultList column>
|
||||
<StyledArrowKeyNavigation
|
||||
ref={resultListCompositeRef}
|
||||
onEscape={handleEscape}
|
||||
aria-label={t("Search Results")}
|
||||
>
|
||||
{(compositeProps) =>
|
||||
data?.length
|
||||
? data.map((result) => (
|
||||
<DocumentListItem
|
||||
key={result.document.id}
|
||||
document={result.document}
|
||||
highlight={query}
|
||||
context={result.context}
|
||||
showCollection
|
||||
showTemplate
|
||||
{...compositeProps}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
}
|
||||
</StyledArrowKeyNavigation>
|
||||
<Waypoint
|
||||
key={data?.length}
|
||||
onEnter={end || loading ? undefined : next}
|
||||
debug={env.ENVIRONMENT === "development"}
|
||||
/>
|
||||
</ResultList>
|
||||
</>
|
||||
) : (
|
||||
<RecentSearches
|
||||
ref={recentSearchesCompositeRef}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
)}
|
||||
</ResultsWrapper>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
const Centered = styled(Flex)`
|
||||
@@ -487,4 +329,4 @@ const SearchTitlesFilter = styled(Switch)`
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
export default withTranslation()(withStores(withRouter(Search)));
|
||||
export default observer(Search);
|
||||
|
||||
@@ -43,9 +43,6 @@ export default class DocumentsStore extends Store<Document> {
|
||||
{ sharedTree: NavigationNode; team: PublicTeam } | undefined
|
||||
> = new Map();
|
||||
|
||||
@observable
|
||||
searchCache: Map<string, SearchResult[] | undefined> = new Map();
|
||||
|
||||
@observable
|
||||
backlinks: Map<string, string[]> = new Map();
|
||||
|
||||
@@ -173,10 +170,6 @@ export default class DocumentsStore extends Store<Document> {
|
||||
return naturalSort(this.inCollection(collectionId), "title");
|
||||
}
|
||||
|
||||
searchResults(query: string): SearchResult[] | undefined {
|
||||
return this.searchCache.get(query);
|
||||
}
|
||||
|
||||
@computed
|
||||
get archived(): Document[] {
|
||||
return orderBy(this.orderedData, "archivedAt", "desc").filter(
|
||||
@@ -367,7 +360,10 @@ export default class DocumentsStore extends Store<Document> {
|
||||
this.fetchNamedPage("list", options);
|
||||
|
||||
@action
|
||||
searchTitles = async (query: string, options?: SearchParams) => {
|
||||
searchTitles = async (
|
||||
query: string,
|
||||
options?: SearchParams
|
||||
): Promise<SearchResult[]> => {
|
||||
const compactedOptions = omitBy(options, (o) => !o);
|
||||
const res = await client.post("/documents.search_titles", {
|
||||
...compactedOptions,
|
||||
@@ -388,15 +384,12 @@ export default class DocumentsStore extends Store<Document> {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: document.id,
|
||||
document,
|
||||
};
|
||||
})
|
||||
);
|
||||
const existing = this.searchCache.get(query) || [];
|
||||
// splice modifies any existing results, taking into account pagination
|
||||
existing.splice(0, existing.length, ...results);
|
||||
this.searchCache.set(query, existing);
|
||||
return res.data;
|
||||
return results;
|
||||
};
|
||||
|
||||
@action
|
||||
@@ -431,11 +424,7 @@ export default class DocumentsStore extends Store<Document> {
|
||||
};
|
||||
})
|
||||
);
|
||||
const existing = this.searchCache.get(query) || [];
|
||||
// splice modifies any existing results, taking into account pagination
|
||||
existing.splice(options.offset || 0, options.limit || 0, ...results);
|
||||
this.searchCache.set(query, existing);
|
||||
return res.data;
|
||||
return results;
|
||||
};
|
||||
|
||||
@action
|
||||
|
||||
Reference in New Issue
Block a user