mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
web: add support for highlighting in search results
This commit is contained in:
committed by
Abdullah Atta
parent
33b6e3a19d
commit
2e8173f6ca
@@ -134,18 +134,19 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
treeRef={treeRef}
|
||||
rootId="root"
|
||||
itemHeight={27}
|
||||
getChildNodes={async (id, depth) => {
|
||||
getChildNodes={async (parent) => {
|
||||
const remainingToc =
|
||||
id === "root"
|
||||
parent.id === "root"
|
||||
? tableOfContents
|
||||
: tableOfContents.slice(
|
||||
tableOfContents.findIndex((item) => item.id === id)
|
||||
);
|
||||
|
||||
let items: typeof remainingToc = [];
|
||||
const items: typeof remainingToc = [];
|
||||
let added = false;
|
||||
for (let i = 0; i < remainingToc.length; i++) {
|
||||
if (added && depth + 1 > remainingToc[i].level) break;
|
||||
if (added && parent.depth + 1 > remainingToc[i].level)
|
||||
break;
|
||||
|
||||
items.push(remainingToc[i]);
|
||||
added = true;
|
||||
@@ -154,12 +155,12 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
const nodes: TreeNode<TOCItem>[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.level !== depth + 1) continue;
|
||||
if (item.level !== parent.depth + 1) continue;
|
||||
nodes.push({
|
||||
id: item.id,
|
||||
data: item,
|
||||
depth: depth + 1,
|
||||
parentId: id,
|
||||
depth: parent.depth + 1,
|
||||
parentId: parent.id,
|
||||
hasChildren:
|
||||
i + 1 < items.length && items[i + 1].level > item.level,
|
||||
expanded: true
|
||||
|
||||
137
apps/web/src/components/search-result/index.tsx
Normal file
137
apps/web/src/components/search-result/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { HighlightedResult, Match } from "@notesnook/core";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import React from "react";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
|
||||
import ListItem from "../list-item";
|
||||
import { ChevronDown, ChevronRight } from "../icons";
|
||||
|
||||
type SearchResultProps = {
|
||||
item: HighlightedResult;
|
||||
match?: Match[];
|
||||
depth: number;
|
||||
isExpandable: boolean;
|
||||
isExpanded: boolean;
|
||||
collapse: () => void;
|
||||
expand: () => void;
|
||||
};
|
||||
|
||||
function SearchResult(props: SearchResultProps) {
|
||||
const { item, match, collapse, depth, expand, isExpandable, isExpanded } =
|
||||
props;
|
||||
|
||||
const isOpened = useEditorStore((store) => store.isNoteOpen(item.id));
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
isFocused={isOpened}
|
||||
isCompact={!match}
|
||||
item={item}
|
||||
onClick={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.openSession(item.id, { considerPinnedTab: true })
|
||||
}
|
||||
onMiddleClick={() =>
|
||||
useEditorStore.getState().openSession(item.id, { openInNewTab: true })
|
||||
}
|
||||
title={
|
||||
<Flex sx={{ alignItems: "center", gap: "small" }}>
|
||||
{isExpandable ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
isExpanded ? collapse() : expand();
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
color={isOpened ? "icon-selected" : "icon"}
|
||||
/>
|
||||
) : (
|
||||
<ChevronRight
|
||||
size={14}
|
||||
color={isOpened ? "icon-selected" : "icon"}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
<Text
|
||||
data-test-id={`title`}
|
||||
variant={"body"}
|
||||
color={isOpened ? "paragraph-selected" : "paragraph"}
|
||||
sx={{
|
||||
...(match
|
||||
? {
|
||||
whiteSpace: "pre-wrap"
|
||||
}
|
||||
: {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}),
|
||||
fontWeight: "body",
|
||||
display: "block",
|
||||
".match": {
|
||||
bg: "accent-secondary",
|
||||
color: "accentForeground-secondary"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{match
|
||||
? match.map((match) => (
|
||||
<>
|
||||
<span>{match.prefix}</span>
|
||||
<span className="match">{match.match}</span>
|
||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||
</>
|
||||
))
|
||||
: item.title.map((match) => (
|
||||
<>
|
||||
<span>{match.prefix}</span>
|
||||
<span className="match">{match.match}</span>
|
||||
{match.suffix ? <span>{match.suffix}</span> : null}
|
||||
</>
|
||||
))}
|
||||
</Text>
|
||||
</Flex>
|
||||
}
|
||||
footer={
|
||||
match ? undefined : (
|
||||
<Text variant="subBody">
|
||||
{item.content?.reduce((count, next) => next.length + count, 0) || 0}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
mb: "small",
|
||||
borderRadius: "default",
|
||||
paddingLeft: `${5 + (depth === 0 ? 0 : 15 * depth)}px`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(SearchResult);
|
||||
@@ -39,7 +39,7 @@ export type VirtualizedTreeHandle<T> = {
|
||||
};
|
||||
export type TreeNode<T = any> = {
|
||||
id: string;
|
||||
parentId: string;
|
||||
parentId?: string;
|
||||
depth: number;
|
||||
hasChildren: boolean;
|
||||
data: T;
|
||||
@@ -49,8 +49,8 @@ type ExpandedIds = Record<string, boolean>;
|
||||
type TreeViewProps<T> = {
|
||||
treeRef?: React.Ref<VirtualizedTreeHandle<T>> | null;
|
||||
rootId: string;
|
||||
itemHeight: number;
|
||||
getChildNodes: (id: string, depth: number) => Promise<TreeNode<T>[]>;
|
||||
itemHeight?: number;
|
||||
getChildNodes: (parent: TreeNode<T>) => Promise<TreeNode<T>[]>;
|
||||
renderItem: (props: {
|
||||
item: TreeNode<T>;
|
||||
index: number;
|
||||
@@ -101,8 +101,7 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
() => ({
|
||||
async refresh() {
|
||||
const { children } = await fetchChildren(
|
||||
rootId,
|
||||
-1,
|
||||
{ id: rootId, depth: -1, data: {} as T, hasChildren: true },
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
);
|
||||
@@ -117,7 +116,7 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
const node = nodes[index];
|
||||
const removeIds: string[] = [node.id];
|
||||
for (const treeNode of nodes) {
|
||||
if (removeIds.includes(treeNode.parentId)) {
|
||||
if (treeNode.parentId && removeIds.includes(treeNode.parentId)) {
|
||||
removeIds.push(treeNode.id);
|
||||
}
|
||||
}
|
||||
@@ -128,14 +127,13 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childNodes = await getChildNodes(node.id, node.depth);
|
||||
const childNodes = await getChildNodes(node);
|
||||
if (childNodes.length > 0 && itemOptions?.expand) {
|
||||
expandedIds[node.id] = true;
|
||||
}
|
||||
|
||||
const { children } = await fetchChildren(
|
||||
node.id,
|
||||
node.depth,
|
||||
node,
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
);
|
||||
@@ -202,12 +200,14 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchChildren(rootId, -1, expandedIds, getChildNodes).then(
|
||||
({ children, expandedIds }) => {
|
||||
setNodes(children);
|
||||
setExpandedIds(expandedIds);
|
||||
}
|
||||
);
|
||||
fetchChildren(
|
||||
{ depth: -1, id: rootId, data: {} as T, hasChildren: true },
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
).then(({ children, expandedIds }) => {
|
||||
setNodes(children);
|
||||
setExpandedIds(expandedIds);
|
||||
});
|
||||
console.log("fetching");
|
||||
}, [rootId]);
|
||||
|
||||
@@ -242,8 +242,9 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
const removeIds: string[] = [];
|
||||
for (const treeNode of tree) {
|
||||
if (
|
||||
treeNode.parentId === node.id ||
|
||||
removeIds.includes(treeNode.parentId)
|
||||
treeNode.parentId &&
|
||||
(treeNode.parentId === node.id ||
|
||||
removeIds.includes(treeNode.parentId))
|
||||
) {
|
||||
removeIds.push(treeNode.id);
|
||||
}
|
||||
@@ -256,8 +257,7 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
if (expandedIds[node.id]) return;
|
||||
|
||||
const { children } = await fetchChildren(
|
||||
node.id,
|
||||
node.depth,
|
||||
node,
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
);
|
||||
@@ -298,12 +298,11 @@ function VirtuosoItem({
|
||||
}
|
||||
|
||||
async function fetchChildren<T>(
|
||||
id: string,
|
||||
depth: number,
|
||||
node: TreeNode<T>,
|
||||
expandedIds: ExpandedIds,
|
||||
getChildNodes: (id: string, depth: number) => Promise<TreeNode<T>[]>
|
||||
getChildNodes: TreeViewProps<T>["getChildNodes"]
|
||||
) {
|
||||
const children = await getChildNodes(id, depth);
|
||||
const children = await getChildNodes(node);
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childNode = children[i];
|
||||
if (
|
||||
@@ -314,8 +313,7 @@ async function fetchChildren<T>(
|
||||
) {
|
||||
expandedIds[childNode.id] = true;
|
||||
const { children: nodes } = await fetchChildren(
|
||||
childNode.id,
|
||||
childNode.depth,
|
||||
childNode,
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
);
|
||||
|
||||
@@ -219,7 +219,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
|
||||
rootId={"root"}
|
||||
itemHeight={30}
|
||||
treeRef={treeRef}
|
||||
getChildNodes={async (id, depth) => {
|
||||
getChildNodes={async ({ id, depth }) => {
|
||||
const nodes: TreeNode<Notebook>[] = [];
|
||||
if (id === "root") {
|
||||
for (const id of notebooks) {
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useStore } from "../stores/note-store";
|
||||
import ListContainer from "../components/list-container";
|
||||
import useNavigate from "../hooks/use-navigate";
|
||||
@@ -26,8 +26,24 @@ import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
import { Box, Text } from "@theme-ui/components";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
TreeNode,
|
||||
VirtualizedTree,
|
||||
VirtualizedTreeHandle
|
||||
} from "../components/virtualized-tree";
|
||||
import SearchResult from "../components/search-result";
|
||||
import { HighlightedResult, Match } from "@notesnook/core";
|
||||
import GroupHeader from "../components/group-header";
|
||||
|
||||
function Home() {
|
||||
const treeRef = useRef<
|
||||
VirtualizedTreeHandle<{
|
||||
item: HighlightedResult;
|
||||
match?: Match[];
|
||||
}>
|
||||
>(null);
|
||||
const notes = useStore((store) => store.notes);
|
||||
const isCompact = useStore((store) => store.viewMode === "compact");
|
||||
const refresh = useStore((store) => store.refresh);
|
||||
@@ -36,7 +52,7 @@ function Home() {
|
||||
"notes",
|
||||
async (query, sortOptions) => {
|
||||
if (useStore.getState().context) return;
|
||||
return await db.lookup.notes(query).sorted(sortOptions);
|
||||
return await db.lookup.notes(query, sortOptions);
|
||||
},
|
||||
[notes]
|
||||
);
|
||||
@@ -47,6 +63,10 @@ function Home() {
|
||||
useStore.getState().refresh();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
treeRef.current?.resetAndRefresh();
|
||||
}, [filteredItems]);
|
||||
|
||||
// useEffect(() => {
|
||||
// (async function () {
|
||||
|
||||
@@ -64,6 +84,94 @@ function Home() {
|
||||
// })();
|
||||
// }, []);
|
||||
|
||||
if (filteredItems) {
|
||||
return (
|
||||
<Box
|
||||
id="search-results"
|
||||
sx={{
|
||||
flex: 1,
|
||||
'[data-viewport-type="element"]': {
|
||||
px: 1,
|
||||
width: `calc(100% - ${2 * 6}px) !important`
|
||||
}
|
||||
}}
|
||||
>
|
||||
{filteredItems.length === 0 ? (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ color: "paragraph-secondary", mx: 1 }}
|
||||
data-test-id="list-placeholder"
|
||||
>
|
||||
{strings.noResultsFound()}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<GroupHeader
|
||||
groupingKey={"search"}
|
||||
isSearching={true}
|
||||
refresh={refresh}
|
||||
title={`${filteredItems.length} results`}
|
||||
isFocused={false}
|
||||
index={0}
|
||||
onSelectGroup={() => {}}
|
||||
groups={async () => []}
|
||||
onJump={() => {}}
|
||||
/>
|
||||
<VirtualizedTree
|
||||
treeRef={treeRef}
|
||||
testId="search-results-list"
|
||||
rootId={"root"}
|
||||
getChildNodes={async (parent) => {
|
||||
const nodes: TreeNode<{
|
||||
item: HighlightedResult;
|
||||
match?: Match[];
|
||||
}>[] = [];
|
||||
if (parent.id === "root") {
|
||||
for (let i = 0; i < filteredItems.length; ++i) {
|
||||
const result = await filteredItems.item(i);
|
||||
if (!result.item) continue;
|
||||
nodes.push({
|
||||
data: { item: result.item },
|
||||
depth: parent.depth + 1,
|
||||
hasChildren: !!result.item.content?.length,
|
||||
id: result.item.id,
|
||||
parentId: parent.id,
|
||||
expanded: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let i = 0;
|
||||
for (const match of parent.data.item.content || []) {
|
||||
nodes.push({
|
||||
data: { item: parent.data.item, match },
|
||||
depth: parent.depth + 1,
|
||||
parentId: parent.id,
|
||||
id: parent.id + i++,
|
||||
hasChildren: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}}
|
||||
renderItem={({ collapse, expand, expanded, item: node }) => (
|
||||
<SearchResult
|
||||
key={node.id}
|
||||
depth={node.depth}
|
||||
isExpandable={node.hasChildren}
|
||||
item={node.data.item}
|
||||
match={node.data.match}
|
||||
isExpanded={expanded}
|
||||
collapse={collapse}
|
||||
expand={expand}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!notes) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
@@ -72,7 +180,6 @@ function Home() {
|
||||
compact={isCompact}
|
||||
refresh={refresh}
|
||||
items={filteredItems || notes}
|
||||
isSearching={!!filteredItems}
|
||||
placeholder={<Placeholder context={filteredItems ? "search" : "notes"} />}
|
||||
button={{
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
|
||||
@@ -18,18 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Notebook as NotebookType, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Box, Flex, Input, Text } from "@theme-ui/components";
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Box, Input, Text } from "@theme-ui/components";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { store, useStore } from "../stores/notebook-store";
|
||||
import { useStore as useSelectionStore } from "../stores/selection-store";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { Notebook } from "../components/notebook";
|
||||
import {
|
||||
TreeNode,
|
||||
@@ -39,9 +32,6 @@ import {
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
import { debounce } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { CustomScrollbarsVirtualList } from "../components/list-container";
|
||||
import { ScrollerProps } from "react-virtuoso";
|
||||
import ScrollContainer from "../components/scroll-container";
|
||||
import { SidebarScroller } from "../components/sidebar-scroller";
|
||||
|
||||
export function Notebooks() {
|
||||
@@ -107,16 +97,16 @@ export function Notebooks() {
|
||||
onDeselect={deselectItem}
|
||||
onSelect={selectItem}
|
||||
saveKey="notebook-tree"
|
||||
getChildNodes={async (parentId, depth) => {
|
||||
getChildNodes={async (parent) => {
|
||||
const nodes: TreeNode<{
|
||||
notebook: NotebookType;
|
||||
totalNotes: number;
|
||||
}>[] = [];
|
||||
const grouping =
|
||||
parentId === "root"
|
||||
parent.id === "root"
|
||||
? notebooks
|
||||
: await db.relations
|
||||
.from({ type: "notebook", id: parentId }, "notebook")
|
||||
.from({ type: "notebook", id: parent.id }, "notebook")
|
||||
.selector.sorted(
|
||||
db.settings.getGroupOptions("notebooks")
|
||||
);
|
||||
@@ -125,10 +115,10 @@ export function Notebooks() {
|
||||
if (!notebook.item) continue;
|
||||
nodes.push({
|
||||
data: { notebook: notebook.item, totalNotes: 0 },
|
||||
depth: depth + 1,
|
||||
depth: parent.depth + 1,
|
||||
hasChildren: false,
|
||||
id: notebook.item.id,
|
||||
parentId
|
||||
parentId: parent.id
|
||||
});
|
||||
}
|
||||
const allRelations = await db.relations
|
||||
|
||||
Reference in New Issue
Block a user