mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-21 22:19:41 +01:00
web: add support for sorting in search
This commit is contained in:
@@ -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,21 +284,15 @@ 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 groupItems = await groups();
|
||||
const items: MenuItem[] = groupItems.map(({ group, index }) => {
|
||||
const groupTitle = group.title.toString();
|
||||
return {
|
||||
type: "button",
|
||||
@@ -294,9 +302,7 @@ function GroupHeader(props: GroupHeaderProps) {
|
||||
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,
|
||||
|
||||
@@ -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) => (
|
||||
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}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
lookup(query, sortOptions || db.settings.getGroupOptions("search")).then(
|
||||
(items) => setFilteredItems(items)
|
||||
);
|
||||
}, [isSearching, query, searchType, type, sortOptions, ...deps]);
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user