web: add support for sorting in search

This commit is contained in:
Abdullah Atta
2025-05-17 11:03:54 +05:00
parent 22b037ea1d
commit 1ca30b8a5b
12 changed files with 2780 additions and 2710 deletions

View File

@@ -28,8 +28,7 @@ import {
SortDesc,
DetailedView,
CompactView,
Icon,
Loading
Icon
} from "../icons";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
@@ -45,6 +44,7 @@ import {
GroupingKey
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useStore as useSearchStore } from "../../stores/search-store";
const groupByToTitleMap = {
none: "None",
@@ -60,12 +60,13 @@ type GroupingMenuOptions = {
parentKey: keyof GroupOptions;
groupingKey: GroupingKey;
refresh: () => void;
isSearching?: boolean;
};
const groupByMenu: (options: GroupingMenuOptions) => MenuItem | null = (
options
) =>
options.groupingKey === "reminders"
options.groupingKey === "reminders" || options.isSearching
? null
: {
type: "button",
@@ -105,6 +106,8 @@ const orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
? strings.aToZ()
: options.groupOptions.sortBy === "dueDate"
? strings.earliestFirst()
: options.groupOptions.sortBy === "relevance"
? strings.leastRelevantFirst()
: strings.oldestToNewest()
},
{
@@ -114,6 +117,8 @@ const orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
? strings.zToA()
: options.groupOptions.sortBy === "dueDate"
? strings.latestFirst()
: options.groupOptions.sortBy === "relevance"
? strings.mostRelevantFirst()
: strings.newestToOldest()
}
])
@@ -156,6 +161,11 @@ const sortByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
{
key: "title",
title: strings.sortByStrings.title()
},
{
key: "relevance",
title: strings.sortByStrings.relevance(),
isHidden: options.groupingKey !== "search"
}
])
}
@@ -198,6 +208,8 @@ async function changeGroupOptions(
: groupOptions.sortBy;
}
await db.settings.setGroupOptions(options.groupingKey, groupOptions);
if (options.groupingKey === "search")
useSearchStore.setState({ sortOptions: groupOptions });
options.refresh();
}
@@ -209,7 +221,7 @@ function map(
item.isChecked = options.groupOptions[options.parentKey] === item.key;
item.onClick = () => changeGroupOptions(options, item);
return { ...item, type: "button" };
}, []);
});
}
type GroupHeaderProps = {
@@ -222,6 +234,7 @@ type GroupHeaderProps = {
refresh: () => void;
onSelectGroup: () => void;
isFocused: boolean;
isSearching?: boolean;
};
function GroupHeader(props: GroupHeaderProps) {
const {
@@ -232,10 +245,11 @@ function GroupHeader(props: GroupHeaderProps) {
groupingKey,
refresh,
onSelectGroup,
isFocused
isFocused,
isSearching
} = props;
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(groupingKey)
db.settings.getGroupOptions(isSearching ? "search" : groupingKey)
);
const groupHeaderRef = useRef<HTMLDivElement>(null);
const { openMenu, target } = useMenuTrigger();
@@ -270,33 +284,25 @@ function GroupHeader(props: GroupHeaderProps) {
return (
<Flex
ref={groupHeaderRef}
onClick={(e) => {
onClick={async (e) => {
if (e.ctrlKey) {
onSelectGroup();
return;
}
e.stopPropagation();
const items: MenuItem[] = [
{
key: "groups",
type: "lazy-loader",
loader: <Loading sx={{ my: 2 }} />,
async items() {
const items = await groups();
return items.map(({ group, index }) => {
const groupTitle = group.title.toString();
return {
type: "button",
key: groupTitle,
title: groupTitle,
onClick: () => onJump(index),
checked: group.title === title
} as MenuItem;
});
}
}
];
const groupItems = await groups();
const items: MenuItem[] = groupItems.map(({ group, index }) => {
const groupTitle = group.title.toString();
return {
type: "button",
key: groupTitle,
title: groupTitle,
onClick: () => onJump(index),
checked: group.title === title
} as MenuItem;
});
if (!items.length) return;
openMenu(items, {
title: strings.jumpToGroup(),
@@ -352,13 +358,16 @@ function GroupHeader(props: GroupHeaderProps) {
groupByToTitleMap[groupOptions.groupBy || "default"]
}`}
onClick={() => {
const groupOptions = db.settings!.getGroupOptions(groupingKey);
const groupOptions = db.settings.getGroupOptions(
isSearching ? "search" : groupingKey
);
setGroupOptions(groupOptions);
const menuOptions: Omit<GroupingMenuOptions, "parentKey"> = {
groupingKey,
groupingKey: isSearching ? "search" : groupingKey,
groupOptions,
refresh
refresh,
isSearching
};
const groupBy = groupByMenu({
...menuOptions,

View File

@@ -74,6 +74,7 @@ type ListContainerProps = {
header?: JSX.Element;
placeholder: JSX.Element;
isLoading?: boolean;
isSearching?: boolean;
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
button?: {
onClick: () => void;
@@ -91,7 +92,8 @@ function ListContainer(props: ListContainerProps) {
button,
compact,
sx,
Scroller
Scroller,
isSearching
} = props;
const [focusedGroupIndex, setFocusedGroupIndex] = useState(-1);
@@ -243,11 +245,14 @@ function ListContainer(props: ListContainerProps) {
focusGroup: setFocusedGroupIndex,
context,
compact,
onMouseUp
onMouseUp,
isSearching
}}
itemContent={(index, _data, context) => (
<ItemRenderer context={context} index={index} />
)}
itemContent={(index, _data, context) =>
context ? (
<ItemRenderer context={context} index={index} />
) : null
}
/>
</Flex>
</>
@@ -293,6 +298,7 @@ type ListContext = {
focusGroup: (index: number) => void;
context?: Context;
compact?: boolean;
isSearching?: boolean;
onMouseUp: (e: MouseEvent, itemIndex: number) => void;
};
@@ -312,8 +318,10 @@ function ItemRenderer({
selectItems,
scrollToIndex,
context: itemContext,
compact
compact,
isSearching
} = context;
const resolvedItem = useResolvedItem({ index, items });
if (!resolvedItem || !resolvedItem.item) {
const placeholderData = getListItemPlaceholderData(group, compact);
@@ -351,6 +359,7 @@ function ItemRenderer({
{resolvedItem.group && group ? (
<GroupHeader
groupingKey={group}
isSearching={isSearching}
refresh={refresh}
title={resolvedItem.group.title}
isFocused={index === focusedGroupIndex}

View File

@@ -18,25 +18,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DependencyList, useEffect, useState } from "react";
import { useStore as useSearchStore } from "../stores/search-store";
import { VirtualizedGrouping } from "@notesnook/core";
import { SortOptions, VirtualizedGrouping } from "@notesnook/core";
import { db } from "../common/db";
export function useSearch<T>(
type: "notes" | "notebooks" | "notebook" | "reminders" | "trash" | "tags",
lookup: (query: string) => Promise<VirtualizedGrouping<T>> | undefined,
lookup: (
query: string,
sortOptions?: SortOptions
) => Promise<VirtualizedGrouping<T> | undefined>,
deps: DependencyList = []
) {
const isSearching = useSearchStore((store) => store.isSearching);
const query = useSearchStore((store) => store.query);
const searchType = useSearchStore((store) => store.searchType);
const sortOptions = useSearchStore((store) => store.sortOptions);
const [filteredItems, setFilteredItems] = useState<VirtualizedGrouping<T>>();
useEffect(() => {
(async function () {
if (!query || !isSearching) return setFilteredItems(undefined);
if (searchType !== type) return;
setFilteredItems(await lookup(query));
})();
}, [isSearching, query, searchType, type, ...deps]);
if (!query || !isSearching) return setFilteredItems(undefined);
if (searchType !== type) return;
lookup(query, sortOptions || db.settings.getGroupOptions("search")).then(
(items) => setFilteredItems(items)
);
}, [isSearching, query, searchType, type, sortOptions, ...deps]);
return filteredItems;
}

View File

@@ -109,6 +109,10 @@ export const useTip = (
};
const tips: Tip[] = [
{
text: `Wrap a query in double quotes to search for an exact match.`,
contexts: ["search"]
},
{
text: "Hold Ctrl/Cmd & click on multiple items to select them.",
contexts: ["notes", "notebooks", "tags"]

View File

@@ -17,6 +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 { SortOptions } from "@notesnook/core";
import createStore from "../common/store";
import BaseStore from "./index";
@@ -24,9 +25,15 @@ class SearchStore extends BaseStore<SearchStore> {
isSearching = false;
query?: string;
searchType?: string;
sortOptions?: SortOptions;
resetSearch = () => {
this.set({ isSearching: false, query: undefined, searchType: undefined });
this.set({
isSearching: false,
query: undefined,
searchType: undefined,
sortOptions: undefined
});
};
}

View File

@@ -34,9 +34,9 @@ function Home() {
const setContext = useStore((store) => store.setContext);
const filteredItems = useSearch(
"notes",
(query) => {
async (query, sortOptions) => {
if (useStore.getState().context) return;
return db.lookup.notes(query).sorted();
return await db.lookup.notes(query).sorted(sortOptions);
},
[notes]
);
@@ -72,7 +72,8 @@ function Home() {
compact={isCompact}
refresh={refresh}
items={filteredItems || notes}
placeholder={<Placeholder context="notes" />}
isSearching={!!filteredItems}
placeholder={<Placeholder context={filteredItems ? "search" : "notes"} />}
button={{
onClick: () => useEditorStore.getState().newSession()
}}

View File

@@ -44,10 +44,10 @@ function Notes(props: NotesProps) {
const isCompact = useNotesStore((store) => store.viewMode === "compact");
const filteredItems = useSearch(
context?.type === "notebook" ? "notebook" : "notes",
(query) => {
async (query, sortOptions) => {
if (!context || !contextNotes) return;
const notes = notesFromContext(context);
return db.lookup.notes(query, notes).sorted();
return await db.lookup.notes(query, notes).sorted(sortOptions);
},
[context, contextNotes]
);
@@ -58,6 +58,7 @@ function Notes(props: NotesProps) {
type={type}
group={type}
refresh={refreshContext}
isSearching={!!filteredItems}
compact={isCompact}
context={context}
items={filteredItems || contextNotes}
@@ -65,7 +66,9 @@ function Notes(props: NotesProps) {
placeholder={
<Placeholder
context={
context.type === "favorite"
filteredItems
? "search"
: context.type === "favorite"
? "favorites"
: context.type === "archive"
? "archive"

View File

@@ -30,8 +30,8 @@ function Reminders() {
useNavigate("reminders", () => store.refresh());
const reminders = useStore((state) => state.reminders);
const refresh = useStore((state) => state.refresh);
const filteredItems = useSearch("reminders", (query) =>
db.lookup.reminders(query).sorted()
const filteredItems = useSearch("reminders", (query, sortOptions) =>
db.lookup.reminders(query).sorted(sortOptions)
);
if (!reminders) return <ListLoader />;
@@ -42,7 +42,10 @@ function Reminders() {
group="reminders"
refresh={refresh}
items={filteredItems || reminders}
placeholder={<Placeholder context="reminders" />}
isSearching={!!filteredItems}
placeholder={
<Placeholder context={filteredItems ? "search" : "reminders"} />
}
button={{
onClick: () => hashNavigate("/reminders/create")
}}

View File

@@ -33,8 +33,8 @@ function Trash() {
const items = useStore((store) => store.trash);
const refresh = useStore((store) => store.refresh);
const clearTrash = useStore((store) => store.clear);
const filteredItems = useSearch("trash", (query) =>
db.lookup.trash(query).sorted()
const filteredItems = useSearch("trash", (query, sortOptions) =>
db.lookup.trash(query).sorted(sortOptions)
);
if (!items) return <ListLoader />;
@@ -43,7 +43,8 @@ function Trash() {
type="trash"
group="trash"
refresh={refresh}
placeholder={<Placeholder context="trash" />}
isSearching={!!filteredItems}
placeholder={<Placeholder context={filteredItems ? "search" : "trash"} />}
items={filteredItems || items}
button={{
onClick: function () {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -636,6 +636,8 @@ $headline$: Use starting line of the note as title.`,
newOld: () => t`New - old`,
latestFirst: () => t`Latest first`,
earliestFirst: () => t`Earliest first`,
mostRelevantFirst: () => t`Most relevant first`,
leastRelevantFirst: () => t`Least relevant first`,
aToZ: () => t`A to Z`,
zToA: () => t`Z to A`,
title: () => t`Title`,
@@ -645,7 +647,8 @@ $headline$: Use starting line of the note as title.`,
dateCreated: () => t`Date created`,
title: () => t`Title`,
dueDate: () => t`Due date`,
dateDeleted: () => t`Date deleted`
dateDeleted: () => t`Date deleted`,
relevance: () => t`Relevance`
},
groupByStrings: {
default: () => t`Default`,