mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-21 14:09:34 +01:00
web: add command palette (#7314)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -249,3 +249,26 @@ textarea,
|
||||
background-color: color-mix(in srgb, var(--accent) 5%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: var(--background);
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
color: var(--paragraph-secondary);
|
||||
}
|
||||
|
||||
.ping {
|
||||
animation: ping 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import hotkeys from "hotkeys-js";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { useStore as useSearchStore } from "../stores/search-store";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
import { CommandPaletteDialog } from "../dialogs/command-palette";
|
||||
|
||||
function isInEditor(e: KeyboardEvent) {
|
||||
return (
|
||||
@@ -123,7 +124,7 @@ const KEYMAP = [
|
||||
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
}
|
||||
}
|
||||
},
|
||||
// {
|
||||
// keys: ["alt+n"],
|
||||
// description: "Go to Notes",
|
||||
@@ -187,6 +188,16 @@ const KEYMAP = [
|
||||
// themestore.get().toggleNightMode();
|
||||
// },
|
||||
// },
|
||||
{
|
||||
keys: ["ctrl+k", "cmd+k", "ctrl+p", "cmd+p"],
|
||||
description: "Open command palette",
|
||||
action: (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
CommandPaletteDialog.show({
|
||||
isCommandMode: e.key === "k"
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function registerKeyMap() {
|
||||
|
||||
@@ -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 { Attachment } from "@notesnook/editor";
|
||||
import { Attachment, Editor } from "@notesnook/editor";
|
||||
|
||||
export const MAX_AUTO_SAVEABLE_WORDS = IS_TESTING ? 100 : 100_000;
|
||||
|
||||
|
||||
@@ -220,7 +220,8 @@ import {
|
||||
mdiTagOutline,
|
||||
mdiChatQuestionOutline,
|
||||
mdiNoteRemoveOutline,
|
||||
mdiTabPlus
|
||||
mdiTabPlus,
|
||||
mdiRadar
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -563,3 +564,4 @@ export const OpenInNew = createIcon(mdiOpenInNew);
|
||||
export const Coupon = createIcon(mdiTagOutline);
|
||||
export const Support = createIcon(mdiChatQuestionOutline);
|
||||
export const NewTab = createIcon(mdiTabPlus);
|
||||
export const Radar = createIcon(mdiRadar);
|
||||
|
||||
@@ -26,7 +26,12 @@ export type NotebookContext = {
|
||||
};
|
||||
export type Context =
|
||||
| {
|
||||
type: "tag" | "color";
|
||||
type: "tag";
|
||||
id: string;
|
||||
item?: Tag;
|
||||
}
|
||||
| {
|
||||
type: "color";
|
||||
id: string;
|
||||
}
|
||||
| NotebookContext
|
||||
|
||||
@@ -100,7 +100,7 @@ const menuItems: (item: TrashItemType, ids?: string[]) => MenuItem[] = (
|
||||
];
|
||||
};
|
||||
|
||||
async function deleteTrash(ids: string[]) {
|
||||
export async function deleteTrash(ids: string[]) {
|
||||
if (!(await showMultiPermanentDeleteConfirmation(ids.length))) return;
|
||||
await store.delete(...ids);
|
||||
showToast("success", `${pluralize(ids.length, "item")} permanently deleted`);
|
||||
|
||||
623
apps/web/src/dialogs/command-palette/command-palette-dialog.tsx
Normal file
623
apps/web/src/dialogs/command-palette/command-palette-dialog.tsx
Normal file
@@ -0,0 +1,623 @@
|
||||
/*
|
||||
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 { debounce, toTitleCase } from "@notesnook/common";
|
||||
import { fuzzy } from "@notesnook/core";
|
||||
import { Box, Button, Flex, Text } from "@theme-ui/components";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { GroupedVirtuoso, GroupedVirtuosoHandle } from "react-virtuoso";
|
||||
import { db } from "../../common/db";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
import Dialog from "../../components/dialog";
|
||||
import Field from "../../components/field";
|
||||
import {
|
||||
Cross,
|
||||
Icon,
|
||||
Notebook as NotebookIcon,
|
||||
Note as NoteIcon,
|
||||
Reminder as ReminderIcon,
|
||||
Tag as TagIcon
|
||||
} from "../../components/icons";
|
||||
import { CustomScrollbarsVirtualList } from "../../components/list-container";
|
||||
import { hashNavigate, navigate } from "../../navigation";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import Config from "../../utils/config";
|
||||
import { commands as COMMANDS } from "./commands";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
interface Command {
|
||||
id: string;
|
||||
title: string;
|
||||
highlightedTitle?: string;
|
||||
type:
|
||||
| "command"
|
||||
| "command-dynamic"
|
||||
| "note"
|
||||
| "notebook"
|
||||
| "tag"
|
||||
| "reminder";
|
||||
group: string;
|
||||
}
|
||||
|
||||
type GroupedCommands = { group: string; count: number }[];
|
||||
|
||||
type CommandPaletteDialogProps = BaseDialogProps<boolean> & {
|
||||
isCommandMode: boolean;
|
||||
};
|
||||
|
||||
type Coords = Record<"x" | "y", number>;
|
||||
|
||||
export const CommandPaletteDialog = DialogManager.register(
|
||||
function CommandPaletteDialog(props: CommandPaletteDialogProps) {
|
||||
const [commands, setCommands] = useState<Command[]>(
|
||||
props.isCommandMode ? getDefaultCommands() : getSessionsAsCommands()
|
||||
);
|
||||
const [selected, setSelected] = useState<Coords>({ x: 0, y: 0 });
|
||||
const [query, setQuery] = useState(props.isCommandMode ? ">" : "");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
virtuosoRef.current?.scrollToIndex({
|
||||
index: selected.y,
|
||||
align: "end",
|
||||
behavior: "auto"
|
||||
});
|
||||
}, [selected]);
|
||||
|
||||
const onChange = useCallback(async function onChange(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) {
|
||||
try {
|
||||
setSelected({ x: 0, y: 0 });
|
||||
const query = e.target.value;
|
||||
setQuery(query);
|
||||
if (!isCommandMode(query)) {
|
||||
setLoading(true);
|
||||
}
|
||||
const res = await search(query);
|
||||
const highlighted = fuzzy(
|
||||
prepareQuery(query),
|
||||
res.map((r) => ({
|
||||
...r,
|
||||
highlightedTitle: r.title
|
||||
})) ?? [],
|
||||
/**
|
||||
* we use a separate key for highlighted title
|
||||
* so that when we save recent commands to local storage
|
||||
* we can save the original title instead of the highlighted one
|
||||
*/
|
||||
"highlightedTitle",
|
||||
{
|
||||
prefix: "<b style='color: var(--accent-foreground)'>",
|
||||
suffix: "</b>"
|
||||
}
|
||||
);
|
||||
setCommands(sortCommands(highlighted));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]);
|
||||
|
||||
const grouped = useMemo(
|
||||
() =>
|
||||
commands.reduce((acc, command) => {
|
||||
const item = acc.find((c) => c.group === command.group);
|
||||
if (item) {
|
||||
item.count++;
|
||||
} else {
|
||||
acc.push({ group: command.group, count: 1 });
|
||||
}
|
||||
return acc;
|
||||
}, [] as GroupedCommands),
|
||||
[commands]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
width={650}
|
||||
onClose={() => {
|
||||
props.onClose(false);
|
||||
}}
|
||||
noScroll
|
||||
sx={{
|
||||
fontFamily: "body"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="ping"
|
||||
sx={{
|
||||
height: 4,
|
||||
bg: loading ? "accent" : "background",
|
||||
transition: "background 0.2s"
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
variant="columnFill"
|
||||
sx={{ mx: 3, overflow: "hidden", height: 400 }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
e.preventDefault();
|
||||
const command = commands[selected.y];
|
||||
if (!command) return;
|
||||
if (selected.x === 1) {
|
||||
setSelected({ x: 0, y: 0 });
|
||||
removeRecentCommand(command.id);
|
||||
setCommands((commands) =>
|
||||
commands.filter((c) => c.id !== command.id)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const action = getCommandAction({
|
||||
id: command.id,
|
||||
type: command.type
|
||||
});
|
||||
action?.(command.id);
|
||||
addRecentCommand(command);
|
||||
props.onClose(false);
|
||||
setSelected({ x: 0, y: 0 });
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelected(moveSelectionDown(selected, commands));
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelected(moveSelectionUp(selected, commands));
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
setSelected(moveSelectionRight(selected, commands));
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
setSelected(moveSelectionLeft(selected, commands));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder={strings.searchInNotesNotebooksAndTags()}
|
||||
sx={{ mx: 0, my: 2 }}
|
||||
defaultValue={query}
|
||||
onChange={isCommandMode(query) ? onChange : debounce(onChange, 500)}
|
||||
/>
|
||||
{query && commands.length === 0 && (
|
||||
<Box>
|
||||
<Text variant="subBody">
|
||||
{strings.noResultsFound(prepareQuery(query))}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ marginY: "10px", height: "100%" }}>
|
||||
<GroupedVirtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ overflow: "hidden" }}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList
|
||||
}}
|
||||
groupCounts={grouped.map((g) => g.count)}
|
||||
groupContent={(groupIndex) => {
|
||||
const label =
|
||||
grouped[groupIndex].group === "recent"
|
||||
? strings.recent()
|
||||
: grouped[groupIndex].group;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
py: 0.5,
|
||||
bg: "background",
|
||||
px: 1,
|
||||
borderRadius: "2px"
|
||||
}}
|
||||
>
|
||||
<Text variant="subBody" bg="">
|
||||
{toTitleCase(label)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
itemContent={(index) => {
|
||||
const command = commands[index];
|
||||
if (!command) return null;
|
||||
|
||||
const Icon = getCommandIcon({
|
||||
id: command.id,
|
||||
type: command.type
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
gap: 1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={command.title}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
const action = getCommandAction({
|
||||
id: command.id,
|
||||
type: command.type
|
||||
});
|
||||
action?.(command.id);
|
||||
addRecentCommand(command);
|
||||
props.onClose(false);
|
||||
}}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: 2,
|
||||
py: 1,
|
||||
bg:
|
||||
selected.x === 0 && index === selected.y
|
||||
? "hover"
|
||||
: "transparent",
|
||||
".chip": {
|
||||
bg:
|
||||
selected.x === 0 && index === selected.y
|
||||
? "color-mix(in srgb, var(--accent) 20%, transparent)"
|
||||
: "var(--background-secondary)"
|
||||
},
|
||||
":hover:not(:disabled):not(:active)": {
|
||||
bg: "hover"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
size={18}
|
||||
color={
|
||||
selected.x === 0 && index === selected.y
|
||||
? "icon-selected"
|
||||
: "icon"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{["note", "notebook", "reminder", "tag"].includes(
|
||||
command.type
|
||||
) ? (
|
||||
<Text
|
||||
className="chip"
|
||||
sx={{
|
||||
px: 1,
|
||||
borderRadius: "4px",
|
||||
border: "1px solid",
|
||||
borderColor: "border",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: command?.highlightedTitle ?? command.title
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: command?.highlightedTitle ?? command.title
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
{command.group === "recent" && (
|
||||
<Button
|
||||
title={strings.removeFromRecent()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeRecentCommand(command.id);
|
||||
setCommands((commands) =>
|
||||
commands.filter((c) => c.id !== command.id)
|
||||
);
|
||||
}}
|
||||
variant="icon"
|
||||
sx={{
|
||||
bg:
|
||||
selected.x === 1 && index === selected.y
|
||||
? "hover"
|
||||
: "transparent",
|
||||
p: 1,
|
||||
mr: 1,
|
||||
":hover:not(:disabled):not(:active)": {
|
||||
bg: "hover"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Cross size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{ flexDirection: "row", bg: "hover", justifyContent: "center" }}
|
||||
>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{ m: 1 }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: strings.commandPaletteDescription()
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function moveSelectionDown(selected: Coords, commands: Command[]) {
|
||||
const currentCommand = commands[selected.y];
|
||||
const nextIndex = (selected.y + 1) % commands.length;
|
||||
const nextCommand = commands[nextIndex];
|
||||
if (currentCommand.group === "recent" && nextCommand.group === "recent") {
|
||||
return { x: selected.x, y: nextIndex };
|
||||
}
|
||||
return { x: 0, y: nextIndex };
|
||||
}
|
||||
|
||||
function moveSelectionUp(selected: Coords, commands: Command[]) {
|
||||
const currentCommand = commands[selected.y];
|
||||
const nextIndex = (selected.y - 1 + commands.length) % commands.length;
|
||||
const nextCommand = commands[nextIndex];
|
||||
if (currentCommand.group === "recent" && nextCommand.group === "recent") {
|
||||
return { x: selected.x, y: nextIndex };
|
||||
}
|
||||
return { x: 0, y: nextIndex };
|
||||
}
|
||||
|
||||
function moveSelectionRight(selected: Coords, commands: Command[]) {
|
||||
const currentCommand = commands[selected.y];
|
||||
if (currentCommand.group !== "recent") return selected;
|
||||
const nextIndex = (selected.x + 1) % 2;
|
||||
return { x: nextIndex, y: selected.y };
|
||||
}
|
||||
|
||||
function moveSelectionLeft(selected: Coords, commands: Command[]) {
|
||||
const currentCommand = commands[selected.y];
|
||||
if (currentCommand.group !== "recent") return selected;
|
||||
const nextIndex = (selected.x - 1 + 2) % 2;
|
||||
return { x: nextIndex, y: selected.y };
|
||||
}
|
||||
|
||||
const CommandIconMap = COMMANDS.reduce((acc, command) => {
|
||||
acc.set(command.id, command.icon);
|
||||
return acc;
|
||||
}, new Map<string, Icon>());
|
||||
|
||||
const CommandActionMap = COMMANDS.reduce((acc, command) => {
|
||||
acc.set(command.id, command.action);
|
||||
return acc;
|
||||
}, new Map<string, (arg?: any) => void>());
|
||||
|
||||
function resolveCommands() {
|
||||
return COMMANDS.reduce((acc, command) => {
|
||||
if (acc.find((c) => c.id === command.id)) return acc;
|
||||
|
||||
const hidden = command.hidden ? command.hidden() : false;
|
||||
const group =
|
||||
typeof command.group === "function" ? command.group() : command.group;
|
||||
const title =
|
||||
typeof command.title === "function" ? command.title() : command.title;
|
||||
if (hidden || group === undefined || title === undefined) return acc;
|
||||
return acc.concat({
|
||||
id: command.id,
|
||||
title: title,
|
||||
type: command.dynamic
|
||||
? ("command-dynamic" as const)
|
||||
: ("command" as const),
|
||||
group: group
|
||||
});
|
||||
}, [] as Command[]);
|
||||
}
|
||||
|
||||
function getDefaultCommands() {
|
||||
return getRecentCommands().concat(resolveCommands());
|
||||
}
|
||||
|
||||
function getRecentCommands() {
|
||||
return Config.get<Command[]>("commandPalette:recent", []);
|
||||
}
|
||||
|
||||
function addRecentCommand(command: Command) {
|
||||
if (command.type === "command-dynamic") return;
|
||||
let commands = getRecentCommands();
|
||||
const index = commands.findIndex((c) => c.id === command.id);
|
||||
if (index > -1) {
|
||||
commands.splice(index, 1);
|
||||
}
|
||||
commands.unshift({
|
||||
...command,
|
||||
highlightedTitle: undefined,
|
||||
group: "recent"
|
||||
});
|
||||
if (commands.length > 3) {
|
||||
commands = commands.slice(0, 3);
|
||||
}
|
||||
Config.set("commandPalette:recent", commands);
|
||||
}
|
||||
|
||||
function removeRecentCommand(id: Command["id"]) {
|
||||
let commands = getRecentCommands();
|
||||
const index = commands.findIndex((c) => c.id === id);
|
||||
if (index > -1) {
|
||||
commands.splice(index, 1);
|
||||
Config.set("commandPalette:recent", commands);
|
||||
}
|
||||
}
|
||||
|
||||
function getCommandAction({
|
||||
id,
|
||||
type
|
||||
}: {
|
||||
id: Command["id"];
|
||||
type: Command["type"];
|
||||
}) {
|
||||
switch (type) {
|
||||
case "command":
|
||||
case "command-dynamic":
|
||||
return CommandActionMap.get(id);
|
||||
case "note":
|
||||
return (noteId: string) => useEditorStore.getState().openSession(noteId);
|
||||
case "notebook":
|
||||
return (notebookId: string) => navigate(`/notebooks/${notebookId}`);
|
||||
case "tag":
|
||||
return (tagId: string) => navigate(`/tags/${tagId}`);
|
||||
case "reminder":
|
||||
return (reminderId: string) =>
|
||||
hashNavigate(`/reminders/${reminderId}/edit`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCommandIcon({
|
||||
id,
|
||||
type
|
||||
}: {
|
||||
id: Command["id"];
|
||||
type: Command["type"];
|
||||
}) {
|
||||
switch (type) {
|
||||
case "command":
|
||||
case "command-dynamic":
|
||||
return CommandIconMap.get(id);
|
||||
case "note":
|
||||
return NoteIcon;
|
||||
case "notebook":
|
||||
return NotebookIcon;
|
||||
case "tag":
|
||||
return TagIcon;
|
||||
case "reminder":
|
||||
return ReminderIcon;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionsAsCommands() {
|
||||
const sessions = useEditorStore.getState().get().sessions;
|
||||
return sessions
|
||||
.filter((s) => s.type !== "new")
|
||||
.map((session) => {
|
||||
return {
|
||||
id: session.id,
|
||||
title: session.note.title,
|
||||
group: strings.dataTypesCamelCase.note(),
|
||||
type: "note" as const
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* commands need to be sorted wrt groups,
|
||||
* meaning commands of same group should be next to each other,
|
||||
* and recent commands should be at the top
|
||||
*/
|
||||
function sortCommands(commands: Command[]) {
|
||||
const recent: Command[] = [];
|
||||
const sortedWrtGroups: Command[][] = [];
|
||||
for (const command of commands) {
|
||||
const group = command.group;
|
||||
if (group === "recent") {
|
||||
recent.push(command);
|
||||
continue;
|
||||
}
|
||||
const index = sortedWrtGroups.findIndex((c) => c[0].group === group);
|
||||
if (index === -1) {
|
||||
sortedWrtGroups.push([command]);
|
||||
} else {
|
||||
sortedWrtGroups[index].push(command);
|
||||
}
|
||||
}
|
||||
return recent.concat(sortedWrtGroups.flat());
|
||||
}
|
||||
|
||||
function search(query: string) {
|
||||
const prepared = prepareQuery(query);
|
||||
if (isCommandMode(query)) {
|
||||
return commandSearch(prepared);
|
||||
}
|
||||
if (prepared.length < 1) {
|
||||
return getSessionsAsCommands();
|
||||
}
|
||||
return dbSearch(prepared);
|
||||
}
|
||||
|
||||
function commandSearch(query: string) {
|
||||
const commands = getDefaultCommands();
|
||||
const result = fuzzy(query, commands, "title", {
|
||||
matchOnly: true
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function dbSearch(query: string) {
|
||||
const notes = db.lookup.notes(query, undefined, {
|
||||
titleOnly: true
|
||||
});
|
||||
const notebooks = db.lookup.notebooks(query, {
|
||||
titleOnly: true
|
||||
});
|
||||
const tags = db.lookup.tags(query);
|
||||
const reminders = db.lookup.reminders(query, {
|
||||
titleOnly: true
|
||||
});
|
||||
const list = (
|
||||
await Promise.all([
|
||||
notes.items(),
|
||||
notebooks.items(),
|
||||
tags.items(),
|
||||
reminders.items()
|
||||
])
|
||||
).flat();
|
||||
const commands = list.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
group: strings.dataTypesCamelCase[item.type](),
|
||||
type: item.type
|
||||
};
|
||||
});
|
||||
return commands;
|
||||
}
|
||||
|
||||
function isCommandMode(query: string) {
|
||||
return query.startsWith(">");
|
||||
}
|
||||
|
||||
function prepareQuery(query: string) {
|
||||
return isCommandMode(query) ? query.substring(1).trim() : query.trim();
|
||||
}
|
||||
816
apps/web/src/dialogs/command-palette/commands.ts
Normal file
816
apps/web/src/dialogs/command-palette/commands.ts
Normal file
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
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 { createInternalLink, hosts } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { db } from "../../common/db";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { useEditorManager } from "../../components/editor/manager";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowTopRight,
|
||||
Copy,
|
||||
DeleteForver,
|
||||
Duplicate,
|
||||
Edit,
|
||||
Editor,
|
||||
InternalLink,
|
||||
Notebook,
|
||||
NotebookEdit,
|
||||
OpenInNew,
|
||||
Pin,
|
||||
Plus,
|
||||
Publish,
|
||||
Radar,
|
||||
Readonly,
|
||||
Reminder,
|
||||
Restore,
|
||||
Shortcut,
|
||||
Star,
|
||||
Sync,
|
||||
Tag,
|
||||
Trash
|
||||
} from "../../components/icons";
|
||||
import { showPublishView } from "../../components/publish-view";
|
||||
import { deleteTrash } from "../../components/trash-item";
|
||||
import { hashNavigate, navigate } from "../../navigation";
|
||||
import { store as appStore } from "../../stores/app-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { store as monographStore } from "../../stores/monograph-store";
|
||||
import { store as noteStore } from "../../stores/note-store";
|
||||
import { store as notebookStore } from "../../stores/notebook-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { store as trashStore } from "../../stores/trash-store";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import { AddNotebookDialog } from "../add-notebook-dialog";
|
||||
import { AddReminderDialog } from "../add-reminder-dialog";
|
||||
import { AddTagsDialog } from "../add-tags-dialog";
|
||||
import { AttachmentsDialog } from "../attachments-dialog";
|
||||
import { ConfirmDialog } from "../confirm";
|
||||
import { CreateColorDialog } from "../create-color-dialog";
|
||||
import { EditTagDialog } from "../item-dialog";
|
||||
import { MoveNoteDialog } from "../move-note-dialog";
|
||||
|
||||
function getLabelForActiveNoteGroup() {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.actionsForNote(note.title) : undefined;
|
||||
}
|
||||
|
||||
function getLabelForActiveNotebookGroup() {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook" && context.item?.title
|
||||
? strings.actionsForNotebook(context.item.title)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getLabelForActiveTagGroup() {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "tag" && context.item?.title
|
||||
? strings.actionsForTag(context.item.title)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const commands = [
|
||||
{
|
||||
id: "pin-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? (note.pinned ? strings.unpin() : strings.pin()) : undefined;
|
||||
},
|
||||
icon: Pin,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
noteStore.get().pin(!note.pinned, note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "readonly-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.toggleReadonly() : undefined;
|
||||
},
|
||||
icon: Readonly,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
noteStore.get().readonly(!note.readonly, note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "favorite-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note
|
||||
? note.favorite
|
||||
? strings.unfavorite()
|
||||
: strings.favorite()
|
||||
: undefined;
|
||||
},
|
||||
icon: Star,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
noteStore.get().favorite(!note.favorite, note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "remind-me-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.remindMe() : undefined;
|
||||
},
|
||||
icon: Reminder,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
if (note.type === "trash") return;
|
||||
AddReminderDialog.show({ note: note });
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "link-notebooks-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.linkNotebooks() : undefined;
|
||||
},
|
||||
icon: Notebook,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
MoveNoteDialog.show({ noteIds: [note.id] });
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "add-tags-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.addTags() : undefined;
|
||||
},
|
||||
icon: Tag,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
AddTagsDialog.show({ noteIds: [note.id] });
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "publish-on-monograph-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.publishOnMonograph() : undefined;
|
||||
},
|
||||
icon: Publish,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || note.type === "trash") return;
|
||||
const isPublished = db.monographs.isPublished(note.id);
|
||||
if (isPublished) return;
|
||||
showPublishView(note);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return (
|
||||
!note || note.type === "trash" || db.monographs.isPublished(note.id)
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "open-in-monograph-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.openInMonograph() : undefined;
|
||||
},
|
||||
icon: OpenInNew,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || note.type === "trash") return;
|
||||
const isPublished = db.monographs.isPublished(note.id);
|
||||
if (!isPublished) return;
|
||||
const url = `${hosts.MONOGRAPH_HOST}/${note.id}`;
|
||||
window.open(url, "_blank");
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return (
|
||||
!note || note.type === "trash" || !db.monographs.isPublished(note.id)
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "copy-monograph-link-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.copyMonographLink() : undefined;
|
||||
},
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || note.type === "trash") return;
|
||||
const isPublished = db.monographs.isPublished(note.id);
|
||||
if (!isPublished) return;
|
||||
const url = `${hosts.MONOGRAPH_HOST}/${note.id}`;
|
||||
writeToClipboard({
|
||||
"text/plain": url,
|
||||
"text/html": `<a href="${url}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${url})`
|
||||
});
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return (
|
||||
!note || note.type === "trash" || !db.monographs.isPublished(note.id)
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "toggle-sync-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note
|
||||
? note?.localOnly
|
||||
? strings.turnSyncOn()
|
||||
: strings.turnSyncOff()
|
||||
: undefined;
|
||||
},
|
||||
icon: Sync,
|
||||
action: async () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || note.type === "trash") return;
|
||||
if (
|
||||
note.localOnly ||
|
||||
(await ConfirmDialog.show({
|
||||
title: strings.syncOffConfirm(1),
|
||||
message: strings.syncOffDesc(1),
|
||||
positiveButtonText: strings.yes(),
|
||||
negativeButtonText: strings.no()
|
||||
}))
|
||||
) {
|
||||
await noteStore.localOnly(!note.localOnly, note.id);
|
||||
}
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "unpublish-on-monograph-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.unpublishOnMonograph() : undefined;
|
||||
},
|
||||
icon: Publish,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || note.type === "trash") return;
|
||||
monographStore.get().unpublish(note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return (
|
||||
!note || note.type === "trash" || !db.monographs.isPublished(note.id)
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "copy-link-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.copyLink() : undefined;
|
||||
},
|
||||
icon: InternalLink,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (note) {
|
||||
const link = createInternalLink("note", note.id);
|
||||
writeToClipboard({
|
||||
"text/plain": link,
|
||||
"text/html": `<a href="${link}">${note.title}</a>`,
|
||||
"text/markdown": `[${note.title}](${link})`
|
||||
});
|
||||
}
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "duplicate-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.duplicate() : undefined;
|
||||
},
|
||||
icon: Duplicate,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
noteStore.get().duplicate(note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type === "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "move-to-trash-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.moveToTrash() : undefined;
|
||||
},
|
||||
icon: Trash,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note || db.monographs.isPublished(note.id)) return;
|
||||
Multiselect.moveNotesToTrash([note.id], false);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return (
|
||||
!note || note.type === "trash" || db.monographs.isPublished(note.id)
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "restore-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.restore() : undefined;
|
||||
},
|
||||
icon: Restore,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
trashStore.restore(note.id);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type !== "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "delete-active-note",
|
||||
title: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return note ? strings.delete() : undefined;
|
||||
},
|
||||
icon: DeleteForver,
|
||||
action: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
if (!note) return;
|
||||
deleteTrash([note.id]);
|
||||
},
|
||||
group: getLabelForActiveNoteGroup,
|
||||
hidden: () => {
|
||||
const note = useEditorStore.getState().getActiveNote();
|
||||
return !note || note.type !== "trash";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "add-subnotebook-active-notebook",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook"
|
||||
? strings.addSubnotebook()
|
||||
: undefined;
|
||||
},
|
||||
icon: Plus,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "notebook") return;
|
||||
AddNotebookDialog.show({ parentId: context.id });
|
||||
},
|
||||
group: getLabelForActiveNotebookGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return (
|
||||
context?.type !== "notebook" || !context.item || context.item.deleted
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "edit-active-notebook",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook" ? strings.edit() : undefined;
|
||||
},
|
||||
icon: NotebookEdit,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "notebook") return;
|
||||
hashNavigate(`/notebooks/${context.id}/edit`);
|
||||
},
|
||||
group: getLabelForActiveNotebookGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return (
|
||||
context?.type !== "notebook" || !context.item || context.item.deleted
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "pin-active-notebook",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook"
|
||||
? context.item?.pinned
|
||||
? strings.unpin()
|
||||
: strings.pin()
|
||||
: undefined;
|
||||
},
|
||||
icon: Pin,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "notebook") return;
|
||||
notebookStore.pin(!context.item?.pinned, context.id);
|
||||
},
|
||||
group: getLabelForActiveNotebookGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "notebook";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "add-shortcut-active-notebook",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook" && context.item
|
||||
? db.shortcuts.exists(context.item.id)
|
||||
? strings.removeShortcut()
|
||||
: strings.addShortcut()
|
||||
: undefined;
|
||||
},
|
||||
icon: Shortcut,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "notebook" || !context.item) return;
|
||||
appStore.addToShortcuts(context.item);
|
||||
},
|
||||
group: getLabelForActiveNotebookGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "notebook";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "move-to-trash-active-notebook",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "notebook" ? strings.moveToTrash() : undefined;
|
||||
},
|
||||
icon: Trash,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "notebook") return;
|
||||
Multiselect.moveNotebooksToTrash([context.id]).then(() => {
|
||||
navigate("/notebooks");
|
||||
});
|
||||
},
|
||||
group: getLabelForActiveNotebookGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "notebook";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "rename-active-tag",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "tag" ? strings.rename() : undefined;
|
||||
},
|
||||
icon: Edit,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type === "tag" && context.item) {
|
||||
EditTagDialog.show(context.item);
|
||||
}
|
||||
},
|
||||
group: getLabelForActiveTagGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "tag";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "add-shortcut-active-tag",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "tag" && context.item
|
||||
? db.shortcuts.exists(context.item.id)
|
||||
? strings.removeShortcut()
|
||||
: strings.addShortcut()
|
||||
: undefined;
|
||||
},
|
||||
icon: Shortcut,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (context?.type !== "tag" || !context.item) return;
|
||||
appStore.addToShortcuts(context.item);
|
||||
},
|
||||
group: getLabelForActiveTagGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "tag";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "delete-active-tag",
|
||||
title: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type === "tag" ? strings.delete() : undefined;
|
||||
},
|
||||
icon: DeleteForver,
|
||||
action: () => {
|
||||
const context = noteStore.get().context;
|
||||
if (!context || context.type !== "tag" || !context.item) return;
|
||||
Multiselect.deleteTags([context.item.id]);
|
||||
},
|
||||
group: getLabelForActiveTagGroup,
|
||||
hidden: () => {
|
||||
const context = noteStore.get().context;
|
||||
return context?.type !== "tag";
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "undo",
|
||||
title: strings.undo(),
|
||||
icon: Editor,
|
||||
action: () => {
|
||||
const session = useEditorStore.getState().getActiveSession();
|
||||
if (!session) return;
|
||||
useEditorManager.getState().editors[session.id].editor?.undo();
|
||||
},
|
||||
group: strings.editor(),
|
||||
hidden: () => {
|
||||
const session = useEditorStore.getState().getActiveSession();
|
||||
return (
|
||||
!session ||
|
||||
!useEditorManager.getState().editors[session.id].canUndo ||
|
||||
session.type === "readonly"
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
title: strings.redo(),
|
||||
icon: Editor,
|
||||
action: () => {
|
||||
const session = useEditorStore.getState().getActiveSession();
|
||||
if (!session) return;
|
||||
useEditorManager.getState().editors[session.id].editor?.redo();
|
||||
},
|
||||
group: strings.editor(),
|
||||
hidden: () => {
|
||||
const session = useEditorStore.getState().getActiveSession();
|
||||
return (
|
||||
!session ||
|
||||
!useEditorManager.getState().editors[session.id].canRedo ||
|
||||
session.type === "readonly"
|
||||
);
|
||||
},
|
||||
dynamic: true
|
||||
},
|
||||
{
|
||||
id: "next-tab",
|
||||
title: strings.nextTab(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => useEditorStore.getState().focusNextTab(),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "previous-tab",
|
||||
title: strings.previousTab(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => useEditorStore.getState().focusPreviousTab(),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "go-forward-in-tab",
|
||||
title: strings.goForwardInTab(),
|
||||
icon: ArrowRight,
|
||||
action: () => useEditorStore.getState().goForward(),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "go-back-in-tab",
|
||||
title: strings.goBackInTab(),
|
||||
icon: ArrowLeft,
|
||||
action: () => useEditorStore.getState().goBack(),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "notes",
|
||||
title: strings.dataTypesPluralCamelCase.note(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "notebooks",
|
||||
title: strings.dataTypesPluralCamelCase.notebook(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/notebooks"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
title: strings.dataTypesPluralCamelCase.tag(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/tags"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "favorites",
|
||||
title: strings.dataTypesPluralCamelCase.favorite(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/favorites"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "reminders",
|
||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/reminders"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "monographs",
|
||||
title: strings.dataTypesPluralCamelCase.monograph(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/monographs"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "trash",
|
||||
title: strings.trash(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => navigate("/trash"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
title: strings.settings(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => hashNavigate("/settings", { replace: true }),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
title: strings.helpAndSupport(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => (window.location.href = "https://help.notesnook.com"),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "attachment-manager",
|
||||
title: strings.attachmentManager(),
|
||||
icon: ArrowTopRight,
|
||||
action: () => AttachmentsDialog.show({}),
|
||||
group: strings.navigate()
|
||||
},
|
||||
{
|
||||
id: "new-tab",
|
||||
title: strings.newTab(),
|
||||
icon: Plus,
|
||||
action: () => useEditorStore.getState().addTab(),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "new-note",
|
||||
title: strings.newNote(),
|
||||
icon: Plus,
|
||||
action: () => useEditorStore.getState().newSession(),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "new-notebook",
|
||||
title: strings.newNotebook(),
|
||||
icon: Plus,
|
||||
action: () => hashNavigate("/notebooks/create", { replace: true }),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "new-tag",
|
||||
title: strings.newTag(),
|
||||
icon: Plus,
|
||||
action: () => hashNavigate("/tags/create", { replace: true }),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "new-reminder",
|
||||
title: strings.newReminder(),
|
||||
icon: Plus,
|
||||
action: () => hashNavigate(`/reminders/create`, { replace: true }),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "new-color",
|
||||
title: strings.newColor(),
|
||||
icon: Plus,
|
||||
action: () => CreateColorDialog.show(true),
|
||||
group: strings.create()
|
||||
},
|
||||
{
|
||||
id: "close-tab",
|
||||
title: strings.closeCurrentTab(),
|
||||
icon: Radar,
|
||||
action: () => useEditorStore.getState().closeActiveTab(),
|
||||
group: strings.general()
|
||||
},
|
||||
{
|
||||
id: "close-all-tabs",
|
||||
title: strings.closeAllTabs(),
|
||||
icon: Radar,
|
||||
action: () => useEditorStore.getState().closeAllTabs(),
|
||||
group: strings.general()
|
||||
},
|
||||
{
|
||||
id: "toggle-theme",
|
||||
title: strings.toggleTheme(),
|
||||
icon: Radar,
|
||||
action: () => useThemeStore.getState().toggleColorScheme(),
|
||||
group: strings.general()
|
||||
}
|
||||
];
|
||||
20
apps/web/src/dialogs/command-palette/index.ts
Normal file
20
apps/web/src/dialogs/command-palette/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export * from "./command-palette-dialog";
|
||||
@@ -168,6 +168,11 @@ const routes = defineRoutes({
|
||||
title: async () => {
|
||||
const tag = await db.tags.tag(tagId);
|
||||
if (!tag) return;
|
||||
notestore.setContext({
|
||||
type: "tag",
|
||||
id: tagId,
|
||||
item: tag
|
||||
});
|
||||
return `#${tag.title}`;
|
||||
},
|
||||
component: Notes,
|
||||
|
||||
@@ -81,12 +81,17 @@ function Notebook(props: NotebookProps) {
|
||||
Promise.all([
|
||||
!!notebookId && db.notebooks.exists(notebookId),
|
||||
db.notebooks.exists(rootId)
|
||||
]).then((exists) => {
|
||||
]).then(async (exists) => {
|
||||
if (exists.every((e) => !e)) {
|
||||
navigate(`/notebooks`, { replace: true });
|
||||
return;
|
||||
}
|
||||
setContext({ type: "notebook", id: notebookId || rootId });
|
||||
const notebook = await db.notebooks.notebook(notebookId || rootId);
|
||||
setContext({
|
||||
type: "notebook",
|
||||
id: notebookId || rootId,
|
||||
item: notebook
|
||||
});
|
||||
});
|
||||
}, [rootId, notebookId]);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
TEST_NOTEBOOK2,
|
||||
databaseTest
|
||||
} from "./utils/index.ts";
|
||||
import { test, expect } from "vitest";
|
||||
import { test, expect, describe } from "vitest";
|
||||
|
||||
const content = {
|
||||
...TEST_NOTE.content,
|
||||
@@ -81,6 +81,22 @@ test("search notes with an empty note", () =>
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("search notes with opts.titleOnly should not search in descriptions", () =>
|
||||
noteTest({
|
||||
content: content
|
||||
}).then(async ({ db }) => {
|
||||
await db.notes.add({
|
||||
title: "note of the world",
|
||||
content: { type: "tiptap", data: "<p>hello<br></p>" }
|
||||
});
|
||||
let filtered = await db.lookup
|
||||
.notes("note of the world", undefined, {
|
||||
titleOnly: true
|
||||
})
|
||||
.ids();
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("search notebooks", () =>
|
||||
notebookTest().then(async ({ db }) => {
|
||||
await db.notebooks.add(TEST_NOTEBOOK2);
|
||||
@@ -88,6 +104,16 @@ test("search notebooks", () =>
|
||||
expect(filtered.length).toBeGreaterThan(0);
|
||||
}));
|
||||
|
||||
test("search notebook with titleOnly option should not search in descriptions", () =>
|
||||
notebookTest().then(async ({ db }) => {
|
||||
await db.notebooks.add({ title: "Description" });
|
||||
await db.notebooks.add(TEST_NOTEBOOK2);
|
||||
let filtered = await db.lookup
|
||||
.notebooks("Description", { titleOnly: true })
|
||||
.ids();
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("search should not return trashed notes", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const id = await db.notes.add({
|
||||
@@ -112,3 +138,36 @@ test("search should return restored notes", () =>
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("search reminders", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
await db.reminders.add({
|
||||
title: "remind me",
|
||||
description: "please do",
|
||||
date: Date.now()
|
||||
});
|
||||
|
||||
const titleSearch = await db.lookup.reminders("remind me").ids();
|
||||
expect(titleSearch).toHaveLength(1);
|
||||
const descriptionSearch = await db.lookup.reminders("please do").ids();
|
||||
expect(descriptionSearch).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("search reminders with titleOnly option should not search in descriptions", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
await db.reminders.add({
|
||||
title: "idc",
|
||||
description: "desc",
|
||||
date: Date.now()
|
||||
});
|
||||
await db.reminders.add({
|
||||
title: "remind me",
|
||||
description: "idc",
|
||||
date: Date.now()
|
||||
});
|
||||
|
||||
const filtered = await db.lookup
|
||||
.reminders("idc", { titleOnly: true })
|
||||
.ids();
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
@@ -19,7 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { match } from "fuzzyjs";
|
||||
import Database from "./index.js";
|
||||
import { Item, Note, SortOptions, TrashItem } from "../types.js";
|
||||
import {
|
||||
Item,
|
||||
Note,
|
||||
Notebook,
|
||||
Reminder,
|
||||
SortOptions,
|
||||
TrashItem
|
||||
} from "../types.js";
|
||||
import { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
|
||||
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
|
||||
import { FilteredSelector } from "../database/sql-collection.js";
|
||||
@@ -43,7 +50,11 @@ type FuzzySearchField<T> = {
|
||||
export default class Lookup {
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
notes(query: string, notes?: FilteredSelector<Note>): SearchResults<Note> {
|
||||
notes(
|
||||
query: string,
|
||||
notes?: FilteredSelector<Note>,
|
||||
opts?: { titleOnly?: boolean }
|
||||
): SearchResults<Note> {
|
||||
return this.toSearchResults(async (limit, sortOptions) => {
|
||||
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
|
||||
const excludedIds = this.db.trash.cache.notes;
|
||||
@@ -61,21 +72,23 @@ export default class Lookup {
|
||||
)
|
||||
.where("title", "match", query)
|
||||
.select(["id", sql<number>`rank * 10`.as("rank")])
|
||||
.unionAll((eb) =>
|
||||
eb
|
||||
.selectFrom("content_fts")
|
||||
.$if(!!notes, (eb) =>
|
||||
eb.where("noteId", "in", notes!.filter.select("id"))
|
||||
)
|
||||
.$if(excludedIds.length > 0, (eb) =>
|
||||
eb.where("id", "not in", excludedIds)
|
||||
)
|
||||
.where("data", "match", query)
|
||||
.select(["noteId as id", "rank"])
|
||||
.$castTo<{
|
||||
id: string;
|
||||
rank: number;
|
||||
}>()
|
||||
.$if(!opts?.titleOnly, (eb) =>
|
||||
eb.unionAll((eb) =>
|
||||
eb
|
||||
.selectFrom("content_fts")
|
||||
.$if(!!notes, (eb) =>
|
||||
eb.where("noteId", "in", notes!.filter.select("id"))
|
||||
)
|
||||
.$if(excludedIds.length > 0, (eb) =>
|
||||
eb.where("id", "not in", excludedIds)
|
||||
)
|
||||
.where("data", "match", query)
|
||||
.select(["noteId as id", "rank"])
|
||||
.$castTo<{
|
||||
id: string;
|
||||
rank: number;
|
||||
}>()
|
||||
)
|
||||
)
|
||||
.as("results")
|
||||
)
|
||||
@@ -99,12 +112,18 @@ export default class Lookup {
|
||||
}, notes || this.db.notes.all);
|
||||
}
|
||||
|
||||
notebooks(query: string) {
|
||||
return this.search(this.db.notebooks.all, query, [
|
||||
notebooks(query: string, opts: { titleOnly?: boolean } = {}) {
|
||||
const fields: FuzzySearchField<Notebook>[] = [
|
||||
{ name: "id", column: "notebooks.id", weight: -100 },
|
||||
{ name: "title", column: "notebooks.title", weight: 10 },
|
||||
{ name: "description", column: "notebooks.description" }
|
||||
]);
|
||||
{ name: "title", column: "notebooks.title", weight: 10 }
|
||||
];
|
||||
if (!opts.titleOnly) {
|
||||
fields.push({
|
||||
name: "description",
|
||||
column: "notebooks.description"
|
||||
});
|
||||
}
|
||||
return this.search(this.db.notebooks.all, query, fields);
|
||||
}
|
||||
|
||||
tags(query: string) {
|
||||
@@ -114,12 +133,18 @@ export default class Lookup {
|
||||
]);
|
||||
}
|
||||
|
||||
reminders(query: string) {
|
||||
return this.search(this.db.reminders.all, query, [
|
||||
reminders(query: string, opts: { titleOnly?: boolean } = {}) {
|
||||
const fields: FuzzySearchField<Reminder>[] = [
|
||||
{ name: "id", column: "reminders.id", weight: -100 },
|
||||
{ name: "title", column: "reminders.title", weight: 10 },
|
||||
{ name: "description", column: "reminders.description" }
|
||||
]);
|
||||
{ name: "title", column: "reminders.title", weight: 10 }
|
||||
];
|
||||
if (!opts.titleOnly) {
|
||||
fields.push({
|
||||
name: "description",
|
||||
column: "reminders.description"
|
||||
});
|
||||
}
|
||||
return this.search(this.db.reminders.all, query, fields);
|
||||
}
|
||||
|
||||
trash(query: string): SearchResults<TrashItem> {
|
||||
|
||||
113
packages/core/src/utils/__tests__/fuzzy.test.ts
Normal file
113
packages/core/src/utils/__tests__/fuzzy.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
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 { fuzzy } from "../fuzzy";
|
||||
import { test, expect, describe } from "vitest";
|
||||
|
||||
describe("lookup.fuzzy", () => {
|
||||
test("should sort items by score when sort", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "system"
|
||||
},
|
||||
{
|
||||
title: "hello"
|
||||
},
|
||||
{
|
||||
title: "items"
|
||||
}
|
||||
];
|
||||
const query = "ems";
|
||||
expect(fuzzy(query, items, "title")).toStrictEqual([
|
||||
items[2],
|
||||
items[0],
|
||||
items[1]
|
||||
]);
|
||||
});
|
||||
describe("opts.matchOnly", () => {
|
||||
test("should return all items when matchOnly is false", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "hello"
|
||||
},
|
||||
{
|
||||
title: "world"
|
||||
}
|
||||
];
|
||||
const successQuery = "o";
|
||||
const failureQuery = "i";
|
||||
expect(fuzzy(successQuery, items, "title")).toStrictEqual(items);
|
||||
expect(fuzzy(failureQuery, items, "title")).toStrictEqual(items);
|
||||
});
|
||||
test("should return only matching items when matchOnly is true", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "hello"
|
||||
},
|
||||
{
|
||||
title: "world"
|
||||
}
|
||||
];
|
||||
const successQuery = "or";
|
||||
const failureQuery = "i";
|
||||
expect(
|
||||
fuzzy(successQuery, items, "title", { matchOnly: true })
|
||||
).toStrictEqual([items[1]]);
|
||||
expect(
|
||||
fuzzy(failureQuery, items, "title", { matchOnly: true })
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe("opts.prefix", () => {
|
||||
test("should prefix matched field with provided value when given", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "hello"
|
||||
},
|
||||
{
|
||||
title: "world"
|
||||
}
|
||||
];
|
||||
const query = "d";
|
||||
expect(
|
||||
fuzzy(query, items, "title", {
|
||||
prefix: "prefix-"
|
||||
})
|
||||
).toStrictEqual([items[0], { title: "worlprefix-d" }]);
|
||||
});
|
||||
});
|
||||
describe("opt.suffix", () => {
|
||||
test("should suffix matched field with provided value when given", () => {
|
||||
const items = [
|
||||
{
|
||||
title: "hello"
|
||||
},
|
||||
{
|
||||
title: "world"
|
||||
}
|
||||
];
|
||||
const query = "llo";
|
||||
expect(
|
||||
fuzzy(query, items, "title", {
|
||||
suffix: "-suffix"
|
||||
})
|
||||
).toStrictEqual([{ title: "hello-suffix" }, items[1]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
64
packages/core/src/utils/fuzzy.ts
Normal file
64
packages/core/src/utils/fuzzy.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 { match, surround } from "fuzzyjs";
|
||||
|
||||
export function fuzzy<T>(
|
||||
query: string,
|
||||
items: T[],
|
||||
key: keyof T,
|
||||
opts?: {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
/**
|
||||
* If true, only items that match the query will be returned
|
||||
*/
|
||||
matchOnly?: boolean;
|
||||
}
|
||||
): T[] {
|
||||
if (query === "") return items;
|
||||
|
||||
const fuzzied: [T, number][] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = match(query, `${item[key]}`);
|
||||
if (!result.match) {
|
||||
if (opts?.matchOnly) continue;
|
||||
fuzzied.push([item, result.score]);
|
||||
continue;
|
||||
}
|
||||
if (opts?.prefix || opts?.suffix) {
|
||||
fuzzied.push([
|
||||
{
|
||||
...item,
|
||||
[key]: surround(`${item[key]}`, {
|
||||
result: result,
|
||||
prefix: opts?.prefix,
|
||||
suffix: opts?.suffix
|
||||
})
|
||||
},
|
||||
result.score
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
fuzzied.push([item, result.score]);
|
||||
}
|
||||
|
||||
return fuzzied.sort((a, b) => b[1] - a[1]).map((f) => f[0]);
|
||||
}
|
||||
@@ -41,3 +41,4 @@ export * from "./set.js";
|
||||
export * from "./title-format.js";
|
||||
export * from "./virtualized-grouping.js";
|
||||
export * from "./crypto.js";
|
||||
export * from "./fuzzy";
|
||||
|
||||
@@ -67,7 +67,7 @@ export const KeyMap = Extension.create({
|
||||
});
|
||||
return true;
|
||||
},
|
||||
"Mod-k": ({ editor }) => {
|
||||
"Shift-Mod-k": ({ editor }) => {
|
||||
showLinkPopup(editor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -580,6 +580,10 @@ msgstr "{words, plural, other {# selected}}"
|
||||
msgid "#notesnook"
|
||||
msgstr "#notesnook"
|
||||
|
||||
#: src/strings.ts:2417
|
||||
msgid "<kbd>{\">\"}</kbd> for command mode · remove <kbd>{\">\"}</kbd> for search mode · <kbd>⏎</kbd> to select · <kbd>↑</kbd> <kbd>↓</kbd> to navigate"
|
||||
msgstr "<kbd>{\">\"}</kbd> for command mode · remove <kbd>{\">\"}</kbd> for search mode · <kbd>⏎</kbd> to select · <kbd>↑</kbd> <kbd>↓</kbd> to navigate"
|
||||
|
||||
#: src/strings.ts:1564
|
||||
msgid "12-hour"
|
||||
msgstr "12-hour"
|
||||
@@ -628,6 +632,18 @@ msgstr "Account"
|
||||
msgid "Account password"
|
||||
msgstr "Account password"
|
||||
|
||||
#: src/strings.ts:2437
|
||||
msgid "Actions for note: {title}"
|
||||
msgstr "Actions for note: {title}"
|
||||
|
||||
#: src/strings.ts:2438
|
||||
msgid "Actions for notebook: {title}"
|
||||
msgstr "Actions for notebook: {title}"
|
||||
|
||||
#: src/strings.ts:2439
|
||||
msgid "Actions for tag: {title}"
|
||||
msgstr "Actions for tag: {title}"
|
||||
|
||||
#: src/strings.ts:2018
|
||||
msgid "Activate"
|
||||
msgstr "Activate"
|
||||
@@ -672,6 +688,10 @@ msgstr "Add shortcut"
|
||||
msgid "Add shortcuts for notebooks and tags here."
|
||||
msgstr "Add shortcuts for notebooks and tags here."
|
||||
|
||||
#: src/strings.ts:2426
|
||||
msgid "Add subnotebook"
|
||||
msgstr "Add subnotebook"
|
||||
|
||||
#: src/strings.ts:564
|
||||
msgid "Add tag"
|
||||
msgstr "Add tag"
|
||||
@@ -907,6 +927,10 @@ msgstr "attachment"
|
||||
msgid "Attachment"
|
||||
msgstr "Attachment"
|
||||
|
||||
#: src/strings.ts:2432
|
||||
msgid "Attachment manager"
|
||||
msgstr "Attachment manager"
|
||||
|
||||
#: src/strings.ts:1916
|
||||
msgid "Attachment preview failed"
|
||||
msgstr "Attachment preview failed"
|
||||
@@ -1480,6 +1504,14 @@ msgstr "Close"
|
||||
msgid "Close all"
|
||||
msgstr "Close all"
|
||||
|
||||
#: src/strings.ts:2435
|
||||
msgid "Close all tabs"
|
||||
msgstr "Close all tabs"
|
||||
|
||||
#: src/strings.ts:2434
|
||||
msgid "Close current tab"
|
||||
msgstr "Close current tab"
|
||||
|
||||
#: src/strings.ts:1997
|
||||
msgid "Close others"
|
||||
msgstr "Close others"
|
||||
@@ -1650,6 +1682,10 @@ msgstr "Copy link"
|
||||
msgid "Copy link text"
|
||||
msgstr "Copy link text"
|
||||
|
||||
#: src/strings.ts:2422
|
||||
msgid "Copy monograph link"
|
||||
msgstr "Copy monograph link"
|
||||
|
||||
#: src/strings.ts:447
|
||||
msgid "Copy note"
|
||||
msgstr "Copy note"
|
||||
@@ -2812,6 +2848,10 @@ msgstr "Getting recovery codes"
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
msgstr "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
|
||||
#: src/strings.ts:2431
|
||||
msgid "Go back in tab"
|
||||
msgstr "Go back in tab"
|
||||
|
||||
#: src/strings.ts:2206
|
||||
msgid "Go back to notebooks"
|
||||
msgstr "Go back to notebooks"
|
||||
@@ -2820,6 +2860,10 @@ msgstr "Go back to notebooks"
|
||||
msgid "Go back to tags"
|
||||
msgstr "Go back to tags"
|
||||
|
||||
#: src/strings.ts:2430
|
||||
msgid "Go forward in tab"
|
||||
msgstr "Go forward in tab"
|
||||
|
||||
#: src/strings.ts:1794
|
||||
msgid "Go to"
|
||||
msgstr "Go to"
|
||||
@@ -3672,6 +3716,10 @@ msgstr "Name"
|
||||
msgid "Native high-performance encryption"
|
||||
msgstr "Native high-performance encryption"
|
||||
|
||||
#: src/strings.ts:2427
|
||||
msgid "Navigate"
|
||||
msgstr "Navigate"
|
||||
|
||||
#: src/strings.ts:390
|
||||
msgid "Never"
|
||||
msgstr "Never"
|
||||
@@ -3728,6 +3776,10 @@ msgstr "New reminder"
|
||||
msgid "New tab"
|
||||
msgstr "New tab"
|
||||
|
||||
#: src/strings.ts:2433
|
||||
msgid "New tag"
|
||||
msgstr "New tag"
|
||||
|
||||
#: src/strings.ts:1351
|
||||
msgid "New update available"
|
||||
msgstr "New update available"
|
||||
@@ -3752,6 +3804,10 @@ msgstr "Next"
|
||||
msgid "Next match"
|
||||
msgstr "Next match"
|
||||
|
||||
#: src/strings.ts:2428
|
||||
msgid "Next tab"
|
||||
msgstr "Next tab"
|
||||
|
||||
#: src/strings.ts:539
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
@@ -4033,6 +4089,10 @@ msgstr "Open in browser"
|
||||
msgid "Open in browser to manage subscription"
|
||||
msgstr "Open in browser to manage subscription"
|
||||
|
||||
#: src/strings.ts:2421
|
||||
msgid "Open in monograph"
|
||||
msgstr "Open in monograph"
|
||||
|
||||
#: src/strings.ts:2304
|
||||
msgid "Open in new tab"
|
||||
msgstr "Open in new tab"
|
||||
@@ -4430,6 +4490,10 @@ msgstr "Preview not available, content is encrypted."
|
||||
msgid "Previous match"
|
||||
msgstr "Previous match"
|
||||
|
||||
#: src/strings.ts:2429
|
||||
msgid "Previous tab"
|
||||
msgstr "Previous tab"
|
||||
|
||||
#: src/strings.ts:2015
|
||||
msgid "Print"
|
||||
msgstr "Print"
|
||||
@@ -4518,6 +4582,10 @@ msgstr "Publish"
|
||||
msgid "Publish note"
|
||||
msgstr "Publish note"
|
||||
|
||||
#: src/strings.ts:2420
|
||||
msgid "Publish on monograph"
|
||||
msgstr "Publish on monograph"
|
||||
|
||||
#: src/strings.ts:481
|
||||
msgid "Publish your note to share it with others. You can set a password to protect it."
|
||||
msgstr "Publish your note to share it with others. You can set a password to protect it."
|
||||
@@ -4598,6 +4666,10 @@ msgstr "Reading backup file..."
|
||||
msgid "Receipt"
|
||||
msgstr "Receipt"
|
||||
|
||||
#: src/strings.ts:2440
|
||||
msgid "Recent"
|
||||
msgstr "Recent"
|
||||
|
||||
#: src/strings.ts:1592
|
||||
msgid "RECENT BACKUPS"
|
||||
msgstr "RECENT BACKUPS"
|
||||
@@ -4788,6 +4860,10 @@ msgstr "Remove from all"
|
||||
msgid "Remove from notebook"
|
||||
msgstr "Remove from notebook"
|
||||
|
||||
#: src/strings.ts:2441
|
||||
msgid "Remove from recent"
|
||||
msgstr "Remove from recent"
|
||||
|
||||
#: src/strings.ts:1030
|
||||
msgid "Remove full name"
|
||||
msgstr "Remove full name"
|
||||
@@ -5146,6 +5222,10 @@ msgstr "Search in Notebooks"
|
||||
msgid "Search in Notes"
|
||||
msgstr "Search in Notes"
|
||||
|
||||
#: src/strings.ts:2418
|
||||
msgid "Search in notes, notebooks, and tags"
|
||||
msgstr "Search in notes, notebooks, and tags"
|
||||
|
||||
#: src/strings.ts:31
|
||||
msgid "Search in Reminders"
|
||||
msgstr "Search in Reminders"
|
||||
@@ -6085,10 +6165,18 @@ msgstr "Toggle dark/light mode"
|
||||
msgid "Toggle indentation mode"
|
||||
msgstr "Toggle indentation mode"
|
||||
|
||||
#: src/strings.ts:2419
|
||||
msgid "Toggle readonly"
|
||||
msgstr "Toggle readonly"
|
||||
|
||||
#: src/strings.ts:2360
|
||||
msgid "Toggle replace"
|
||||
msgstr "Toggle replace"
|
||||
|
||||
#: src/strings.ts:2436
|
||||
msgid "Toggle theme"
|
||||
msgstr "Toggle theme"
|
||||
|
||||
#: src/strings.ts:2113
|
||||
msgid "Toolbar"
|
||||
msgstr "Toolbar"
|
||||
@@ -6142,6 +6230,14 @@ msgstr "Turn off reminder"
|
||||
msgid "Turn on reminder"
|
||||
msgstr "Turn on reminder"
|
||||
|
||||
#: src/strings.ts:2424
|
||||
msgid "Turn sync off"
|
||||
msgstr "Turn sync off"
|
||||
|
||||
#: src/strings.ts:2423
|
||||
msgid "Turn sync on"
|
||||
msgstr "Turn sync on"
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "Turns off syncing completely on this device. Any changes made will remain local only and new changes from your other devices won't sync to this device."
|
||||
msgstr "Turns off syncing completely on this device. Any changes made will remain local only and new changes from your other devices won't sync to this device."
|
||||
@@ -6255,6 +6351,10 @@ msgstr "Unpublish"
|
||||
msgid "Unpublish notes to delete them"
|
||||
msgstr "Unpublish notes to delete them"
|
||||
|
||||
#: src/strings.ts:2425
|
||||
msgid "Unpublish on monograph"
|
||||
msgstr "Unpublish on monograph"
|
||||
|
||||
#: src/strings.ts:2067
|
||||
msgid "Unregister"
|
||||
msgstr "Unregister"
|
||||
|
||||
@@ -580,6 +580,10 @@ msgstr ""
|
||||
msgid "#notesnook"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2417
|
||||
msgid "<kbd>{\">\"}</kbd> for command mode · remove <kbd>{\">\"}</kbd> for search mode · <kbd>⏎</kbd> to select · <kbd>↑</kbd> <kbd>↓</kbd> to navigate"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1564
|
||||
msgid "12-hour"
|
||||
msgstr ""
|
||||
@@ -628,6 +632,18 @@ msgstr ""
|
||||
msgid "Account password"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2437
|
||||
msgid "Actions for note: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2438
|
||||
msgid "Actions for notebook: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2439
|
||||
msgid "Actions for tag: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2018
|
||||
msgid "Activate"
|
||||
msgstr ""
|
||||
@@ -672,6 +688,10 @@ msgstr ""
|
||||
msgid "Add shortcuts for notebooks and tags here."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2426
|
||||
msgid "Add subnotebook"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:564
|
||||
msgid "Add tag"
|
||||
msgstr ""
|
||||
@@ -907,6 +927,10 @@ msgstr ""
|
||||
msgid "Attachment"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2432
|
||||
msgid "Attachment manager"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1916
|
||||
msgid "Attachment preview failed"
|
||||
msgstr ""
|
||||
@@ -1469,6 +1493,14 @@ msgstr ""
|
||||
msgid "Close all"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2435
|
||||
msgid "Close all tabs"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2434
|
||||
msgid "Close current tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1997
|
||||
msgid "Close others"
|
||||
msgstr ""
|
||||
@@ -1639,6 +1671,10 @@ msgstr ""
|
||||
msgid "Copy link text"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2422
|
||||
msgid "Copy monograph link"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:447
|
||||
msgid "Copy note"
|
||||
msgstr ""
|
||||
@@ -2794,6 +2830,10 @@ msgstr ""
|
||||
msgid "GNU GENERAL PUBLIC LICENSE Version 3"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2431
|
||||
msgid "Go back in tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2206
|
||||
msgid "Go back to notebooks"
|
||||
msgstr ""
|
||||
@@ -2802,6 +2842,10 @@ msgstr ""
|
||||
msgid "Go back to tags"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2430
|
||||
msgid "Go forward in tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1794
|
||||
msgid "Go to"
|
||||
msgstr ""
|
||||
@@ -3652,6 +3696,10 @@ msgstr ""
|
||||
msgid "Native high-performance encryption"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2427
|
||||
msgid "Navigate"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:390
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
@@ -3708,6 +3756,10 @@ msgstr ""
|
||||
msgid "New tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2433
|
||||
msgid "New tag"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1351
|
||||
msgid "New update available"
|
||||
msgstr ""
|
||||
@@ -3732,6 +3784,10 @@ msgstr ""
|
||||
msgid "Next match"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2428
|
||||
msgid "Next tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:539
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
@@ -4007,6 +4063,10 @@ msgstr ""
|
||||
msgid "Open in browser to manage subscription"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2421
|
||||
msgid "Open in monograph"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2304
|
||||
msgid "Open in new tab"
|
||||
msgstr ""
|
||||
@@ -4404,6 +4464,10 @@ msgstr ""
|
||||
msgid "Previous match"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2429
|
||||
msgid "Previous tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2015
|
||||
msgid "Print"
|
||||
msgstr ""
|
||||
@@ -4492,6 +4556,10 @@ msgstr ""
|
||||
msgid "Publish note"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2420
|
||||
msgid "Publish on monograph"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:481
|
||||
msgid "Publish your note to share it with others. You can set a password to protect it."
|
||||
msgstr ""
|
||||
@@ -4572,6 +4640,10 @@ msgstr ""
|
||||
msgid "Receipt"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2440
|
||||
msgid "Recent"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1592
|
||||
msgid "RECENT BACKUPS"
|
||||
msgstr ""
|
||||
@@ -4762,6 +4834,10 @@ msgstr ""
|
||||
msgid "Remove from notebook"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2441
|
||||
msgid "Remove from recent"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1030
|
||||
msgid "Remove full name"
|
||||
msgstr ""
|
||||
@@ -5120,6 +5196,10 @@ msgstr ""
|
||||
msgid "Search in Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2418
|
||||
msgid "Search in notes, notebooks, and tags"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:31
|
||||
msgid "Search in Reminders"
|
||||
msgstr ""
|
||||
@@ -6044,10 +6124,18 @@ msgstr ""
|
||||
msgid "Toggle indentation mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2419
|
||||
msgid "Toggle readonly"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2360
|
||||
msgid "Toggle replace"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2436
|
||||
msgid "Toggle theme"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2113
|
||||
msgid "Toolbar"
|
||||
msgstr ""
|
||||
@@ -6101,6 +6189,14 @@ msgstr ""
|
||||
msgid "Turn on reminder"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2424
|
||||
msgid "Turn sync off"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2423
|
||||
msgid "Turn sync on"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1078
|
||||
msgid "Turns off syncing completely on this device. Any changes made will remain local only and new changes from your other devices won't sync to this device."
|
||||
msgstr ""
|
||||
@@ -6214,6 +6310,10 @@ msgstr ""
|
||||
msgid "Unpublish notes to delete them"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2425
|
||||
msgid "Unpublish on monograph"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2067
|
||||
msgid "Unregister"
|
||||
msgstr ""
|
||||
|
||||
@@ -2412,5 +2412,31 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
redeemGiftCode: () => t`Redeem gift code`,
|
||||
redeemGiftCodeDesc: () => t`Enter the gift code to redeem your subscription.`,
|
||||
redeemingGiftCode: () => t`Redeeming gift code`,
|
||||
redeem: () => t`Redeem`
|
||||
redeem: () => t`Redeem`,
|
||||
commandPaletteDescription: () =>
|
||||
t`<kbd>{">"}</kbd> for command mode · remove <kbd>{">"}</kbd> for search mode · <kbd>⏎</kbd> to select · <kbd>↑</kbd> <kbd>↓</kbd> to navigate`,
|
||||
searchInNotesNotebooksAndTags: () => t`Search in notes, notebooks, and tags`,
|
||||
toggleReadonly: () => t`Toggle readonly`,
|
||||
publishOnMonograph: () => t`Publish on monograph`,
|
||||
openInMonograph: () => t`Open in monograph`,
|
||||
copyMonographLink: () => t`Copy monograph link`,
|
||||
turnSyncOn: () => t`Turn sync on`,
|
||||
turnSyncOff: () => t`Turn sync off`,
|
||||
unpublishOnMonograph: () => t`Unpublish on monograph`,
|
||||
addSubnotebook: () => t`Add subnotebook`,
|
||||
navigate: () => t`Navigate`,
|
||||
nextTab: () => t`Next tab`,
|
||||
previousTab: () => t`Previous tab`,
|
||||
goForwardInTab: () => t`Go forward in tab`,
|
||||
goBackInTab: () => t`Go back in tab`,
|
||||
attachmentManager: () => t`Attachment manager`,
|
||||
newTag: () => t`New tag`,
|
||||
closeCurrentTab: () => t`Close current tab`,
|
||||
closeAllTabs: () => t`Close all tabs`,
|
||||
toggleTheme: () => t`Toggle theme`,
|
||||
actionsForNote: (title: string) => t`Actions for note: ${title}`,
|
||||
actionsForNotebook: (title: string) => t`Actions for notebook: ${title}`,
|
||||
actionsForTag: (title: string) => t`Actions for tag: ${title}`,
|
||||
recent: () => t`Recent`,
|
||||
removeFromRecent: () => t`Remove from recent`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user