web: add command palette (#7314)

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-02-19 15:48:44 +05:00
committed by GitHub
parent 53ba5562d3
commit a47967dd53
20 changed files with 2035 additions and 37 deletions

View File

@@ -249,3 +249,26 @@ textarea,
background-color: color-mix(in srgb, var(--accent) 5%, transparent); 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;
}
}

View File

@@ -21,6 +21,7 @@ import hotkeys from "hotkeys-js";
import { useEditorStore } from "../stores/editor-store"; import { useEditorStore } from "../stores/editor-store";
import { useStore as useSearchStore } from "../stores/search-store"; import { useStore as useSearchStore } from "../stores/search-store";
import { useEditorManager } from "../components/editor/manager"; import { useEditorManager } from "../components/editor/manager";
import { CommandPaletteDialog } from "../dialogs/command-palette";
function isInEditor(e: KeyboardEvent) { function isInEditor(e: KeyboardEvent) {
return ( return (
@@ -123,7 +124,7 @@ const KEYMAP = [
useSearchStore.setState({ isSearching: true, searchType: "notes" }); useSearchStore.setState({ isSearching: true, searchType: "notes" });
} }
} },
// { // {
// keys: ["alt+n"], // keys: ["alt+n"],
// description: "Go to Notes", // description: "Go to Notes",
@@ -187,6 +188,16 @@ const KEYMAP = [
// themestore.get().toggleNightMode(); // 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() { export function registerKeyMap() {

View File

@@ -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/>. 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; export const MAX_AUTO_SAVEABLE_WORDS = IS_TESTING ? 100 : 100_000;

View File

@@ -220,7 +220,8 @@ import {
mdiTagOutline, mdiTagOutline,
mdiChatQuestionOutline, mdiChatQuestionOutline,
mdiNoteRemoveOutline, mdiNoteRemoveOutline,
mdiTabPlus mdiTabPlus,
mdiRadar
} from "@mdi/js"; } from "@mdi/js";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
@@ -563,3 +564,4 @@ export const OpenInNew = createIcon(mdiOpenInNew);
export const Coupon = createIcon(mdiTagOutline); export const Coupon = createIcon(mdiTagOutline);
export const Support = createIcon(mdiChatQuestionOutline); export const Support = createIcon(mdiChatQuestionOutline);
export const NewTab = createIcon(mdiTabPlus); export const NewTab = createIcon(mdiTabPlus);
export const Radar = createIcon(mdiRadar);

View File

@@ -26,7 +26,12 @@ export type NotebookContext = {
}; };
export type Context = export type Context =
| { | {
type: "tag" | "color"; type: "tag";
id: string;
item?: Tag;
}
| {
type: "color";
id: string; id: string;
} }
| NotebookContext | NotebookContext

View File

@@ -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; if (!(await showMultiPermanentDeleteConfirmation(ids.length))) return;
await store.delete(...ids); await store.delete(...ids);
showToast("success", `${pluralize(ids.length, "item")} permanently deleted`); showToast("success", `${pluralize(ids.length, "item")} permanently deleted`);

View 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();
}

View 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()
}
];

View 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";

View File

@@ -168,6 +168,11 @@ const routes = defineRoutes({
title: async () => { title: async () => {
const tag = await db.tags.tag(tagId); const tag = await db.tags.tag(tagId);
if (!tag) return; if (!tag) return;
notestore.setContext({
type: "tag",
id: tagId,
item: tag
});
return `#${tag.title}`; return `#${tag.title}`;
}, },
component: Notes, component: Notes,

View File

@@ -81,12 +81,17 @@ function Notebook(props: NotebookProps) {
Promise.all([ Promise.all([
!!notebookId && db.notebooks.exists(notebookId), !!notebookId && db.notebooks.exists(notebookId),
db.notebooks.exists(rootId) db.notebooks.exists(rootId)
]).then((exists) => { ]).then(async (exists) => {
if (exists.every((e) => !e)) { if (exists.every((e) => !e)) {
navigate(`/notebooks`, { replace: true }); navigate(`/notebooks`, { replace: true });
return; 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]); }, [rootId, notebookId]);

View File

@@ -24,7 +24,7 @@ import {
TEST_NOTEBOOK2, TEST_NOTEBOOK2,
databaseTest databaseTest
} from "./utils/index.ts"; } from "./utils/index.ts";
import { test, expect } from "vitest"; import { test, expect, describe } from "vitest";
const content = { const content = {
...TEST_NOTE.content, ...TEST_NOTE.content,
@@ -81,6 +81,22 @@ test("search notes with an empty note", () =>
expect(filtered).toHaveLength(1); 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", () => test("search notebooks", () =>
notebookTest().then(async ({ db }) => { notebookTest().then(async ({ db }) => {
await db.notebooks.add(TEST_NOTEBOOK2); await db.notebooks.add(TEST_NOTEBOOK2);
@@ -88,6 +104,16 @@ test("search notebooks", () =>
expect(filtered.length).toBeGreaterThan(0); 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", () => test("search should not return trashed notes", () =>
databaseTest().then(async (db) => { databaseTest().then(async (db) => {
const id = await db.notes.add({ const id = await db.notes.add({
@@ -112,3 +138,36 @@ test("search should return restored notes", () =>
expect(filtered).toHaveLength(1); 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);
}));

View File

@@ -19,7 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { match } from "fuzzyjs"; import { match } from "fuzzyjs";
import Database from "./index.js"; 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 { DatabaseSchema, RawDatabaseSchema } from "../database/index.js";
import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely"; import { AnyColumnWithTable, Kysely, sql } from "@streetwriters/kysely";
import { FilteredSelector } from "../database/sql-collection.js"; import { FilteredSelector } from "../database/sql-collection.js";
@@ -43,7 +50,11 @@ type FuzzySearchField<T> = {
export default class Lookup { export default class Lookup {
constructor(private readonly db: Database) {} 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) => { return this.toSearchResults(async (limit, sortOptions) => {
const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>; const db = this.db.sql() as unknown as Kysely<RawDatabaseSchema>;
const excludedIds = this.db.trash.cache.notes; const excludedIds = this.db.trash.cache.notes;
@@ -61,21 +72,23 @@ export default class Lookup {
) )
.where("title", "match", query) .where("title", "match", query)
.select(["id", sql<number>`rank * 10`.as("rank")]) .select(["id", sql<number>`rank * 10`.as("rank")])
.unionAll((eb) => .$if(!opts?.titleOnly, (eb) =>
eb eb.unionAll((eb) =>
.selectFrom("content_fts") eb
.$if(!!notes, (eb) => .selectFrom("content_fts")
eb.where("noteId", "in", notes!.filter.select("id")) .$if(!!notes, (eb) =>
) eb.where("noteId", "in", notes!.filter.select("id"))
.$if(excludedIds.length > 0, (eb) => )
eb.where("id", "not in", excludedIds) .$if(excludedIds.length > 0, (eb) =>
) eb.where("id", "not in", excludedIds)
.where("data", "match", query) )
.select(["noteId as id", "rank"]) .where("data", "match", query)
.$castTo<{ .select(["noteId as id", "rank"])
id: string; .$castTo<{
rank: number; id: string;
}>() rank: number;
}>()
)
) )
.as("results") .as("results")
) )
@@ -99,12 +112,18 @@ export default class Lookup {
}, notes || this.db.notes.all); }, notes || this.db.notes.all);
} }
notebooks(query: string) { notebooks(query: string, opts: { titleOnly?: boolean } = {}) {
return this.search(this.db.notebooks.all, query, [ const fields: FuzzySearchField<Notebook>[] = [
{ name: "id", column: "notebooks.id", weight: -100 }, { name: "id", column: "notebooks.id", weight: -100 },
{ name: "title", column: "notebooks.title", weight: 10 }, { name: "title", column: "notebooks.title", weight: 10 }
{ name: "description", column: "notebooks.description" } ];
]); if (!opts.titleOnly) {
fields.push({
name: "description",
column: "notebooks.description"
});
}
return this.search(this.db.notebooks.all, query, fields);
} }
tags(query: string) { tags(query: string) {
@@ -114,12 +133,18 @@ export default class Lookup {
]); ]);
} }
reminders(query: string) { reminders(query: string, opts: { titleOnly?: boolean } = {}) {
return this.search(this.db.reminders.all, query, [ const fields: FuzzySearchField<Reminder>[] = [
{ name: "id", column: "reminders.id", weight: -100 }, { name: "id", column: "reminders.id", weight: -100 },
{ name: "title", column: "reminders.title", weight: 10 }, { name: "title", column: "reminders.title", weight: 10 }
{ name: "description", column: "reminders.description" } ];
]); if (!opts.titleOnly) {
fields.push({
name: "description",
column: "reminders.description"
});
}
return this.search(this.db.reminders.all, query, fields);
} }
trash(query: string): SearchResults<TrashItem> { trash(query: string): SearchResults<TrashItem> {

View 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]]);
});
});
});

View 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]);
}

View File

@@ -41,3 +41,4 @@ export * from "./set.js";
export * from "./title-format.js"; export * from "./title-format.js";
export * from "./virtualized-grouping.js"; export * from "./virtualized-grouping.js";
export * from "./crypto.js"; export * from "./crypto.js";
export * from "./fuzzy";

View File

@@ -67,7 +67,7 @@ export const KeyMap = Extension.create({
}); });
return true; return true;
}, },
"Mod-k": ({ editor }) => { "Shift-Mod-k": ({ editor }) => {
showLinkPopup(editor); showLinkPopup(editor);
return true; return true;
} }

View File

@@ -580,6 +580,10 @@ msgstr "{words, plural, other {# selected}}"
msgid "#notesnook" msgid "#notesnook"
msgstr "#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 #: src/strings.ts:1564
msgid "12-hour" msgid "12-hour"
msgstr "12-hour" msgstr "12-hour"
@@ -628,6 +632,18 @@ msgstr "Account"
msgid "Account password" msgid "Account password"
msgstr "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 #: src/strings.ts:2018
msgid "Activate" msgid "Activate"
msgstr "Activate" msgstr "Activate"
@@ -672,6 +688,10 @@ msgstr "Add shortcut"
msgid "Add shortcuts for notebooks and tags here." msgid "Add shortcuts for notebooks and tags here."
msgstr "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 #: src/strings.ts:564
msgid "Add tag" msgid "Add tag"
msgstr "Add tag" msgstr "Add tag"
@@ -907,6 +927,10 @@ msgstr "attachment"
msgid "Attachment" msgid "Attachment"
msgstr "Attachment" msgstr "Attachment"
#: src/strings.ts:2432
msgid "Attachment manager"
msgstr "Attachment manager"
#: src/strings.ts:1916 #: src/strings.ts:1916
msgid "Attachment preview failed" msgid "Attachment preview failed"
msgstr "Attachment preview failed" msgstr "Attachment preview failed"
@@ -1480,6 +1504,14 @@ msgstr "Close"
msgid "Close all" msgid "Close all"
msgstr "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 #: src/strings.ts:1997
msgid "Close others" msgid "Close others"
msgstr "Close others" msgstr "Close others"
@@ -1650,6 +1682,10 @@ msgstr "Copy link"
msgid "Copy link text" msgid "Copy link text"
msgstr "Copy link text" msgstr "Copy link text"
#: src/strings.ts:2422
msgid "Copy monograph link"
msgstr "Copy monograph link"
#: src/strings.ts:447 #: src/strings.ts:447
msgid "Copy note" msgid "Copy note"
msgstr "Copy note" msgstr "Copy note"
@@ -2812,6 +2848,10 @@ msgstr "Getting recovery codes"
msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgid "GNU GENERAL PUBLIC LICENSE Version 3"
msgstr "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 #: src/strings.ts:2206
msgid "Go back to notebooks" msgid "Go back to notebooks"
msgstr "Go back to notebooks" msgstr "Go back to notebooks"
@@ -2820,6 +2860,10 @@ msgstr "Go back to notebooks"
msgid "Go back to tags" msgid "Go back to tags"
msgstr "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 #: src/strings.ts:1794
msgid "Go to" msgid "Go to"
msgstr "Go to" msgstr "Go to"
@@ -3672,6 +3716,10 @@ msgstr "Name"
msgid "Native high-performance encryption" msgid "Native high-performance encryption"
msgstr "Native high-performance encryption" msgstr "Native high-performance encryption"
#: src/strings.ts:2427
msgid "Navigate"
msgstr "Navigate"
#: src/strings.ts:390 #: src/strings.ts:390
msgid "Never" msgid "Never"
msgstr "Never" msgstr "Never"
@@ -3728,6 +3776,10 @@ msgstr "New reminder"
msgid "New tab" msgid "New tab"
msgstr "New tab" msgstr "New tab"
#: src/strings.ts:2433
msgid "New tag"
msgstr "New tag"
#: src/strings.ts:1351 #: src/strings.ts:1351
msgid "New update available" msgid "New update available"
msgstr "New update available" msgstr "New update available"
@@ -3752,6 +3804,10 @@ msgstr "Next"
msgid "Next match" msgid "Next match"
msgstr "Next match" msgstr "Next match"
#: src/strings.ts:2428
msgid "Next tab"
msgstr "Next tab"
#: src/strings.ts:539 #: src/strings.ts:539
msgid "No" msgid "No"
msgstr "No" msgstr "No"
@@ -4033,6 +4089,10 @@ msgstr "Open in browser"
msgid "Open in browser to manage subscription" msgid "Open in browser to manage subscription"
msgstr "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 #: src/strings.ts:2304
msgid "Open in new tab" msgid "Open in new tab"
msgstr "Open in new tab" msgstr "Open in new tab"
@@ -4430,6 +4490,10 @@ msgstr "Preview not available, content is encrypted."
msgid "Previous match" msgid "Previous match"
msgstr "Previous match" msgstr "Previous match"
#: src/strings.ts:2429
msgid "Previous tab"
msgstr "Previous tab"
#: src/strings.ts:2015 #: src/strings.ts:2015
msgid "Print" msgid "Print"
msgstr "Print" msgstr "Print"
@@ -4518,6 +4582,10 @@ msgstr "Publish"
msgid "Publish note" msgid "Publish note"
msgstr "Publish note" msgstr "Publish note"
#: src/strings.ts:2420
msgid "Publish on monograph"
msgstr "Publish on monograph"
#: src/strings.ts:481 #: src/strings.ts:481
msgid "Publish your note to share it with others. You can set a password to protect it." 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." 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" msgid "Receipt"
msgstr "Receipt" msgstr "Receipt"
#: src/strings.ts:2440
msgid "Recent"
msgstr "Recent"
#: src/strings.ts:1592 #: src/strings.ts:1592
msgid "RECENT BACKUPS" msgid "RECENT BACKUPS"
msgstr "RECENT BACKUPS" msgstr "RECENT BACKUPS"
@@ -4788,6 +4860,10 @@ msgstr "Remove from all"
msgid "Remove from notebook" msgid "Remove from notebook"
msgstr "Remove from notebook" msgstr "Remove from notebook"
#: src/strings.ts:2441
msgid "Remove from recent"
msgstr "Remove from recent"
#: src/strings.ts:1030 #: src/strings.ts:1030
msgid "Remove full name" msgid "Remove full name"
msgstr "Remove full name" msgstr "Remove full name"
@@ -5146,6 +5222,10 @@ msgstr "Search in Notebooks"
msgid "Search in Notes" msgid "Search in Notes"
msgstr "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 #: src/strings.ts:31
msgid "Search in Reminders" msgid "Search in Reminders"
msgstr "Search in Reminders" msgstr "Search in Reminders"
@@ -6085,10 +6165,18 @@ msgstr "Toggle dark/light mode"
msgid "Toggle indentation mode" msgid "Toggle indentation mode"
msgstr "Toggle indentation mode" msgstr "Toggle indentation mode"
#: src/strings.ts:2419
msgid "Toggle readonly"
msgstr "Toggle readonly"
#: src/strings.ts:2360 #: src/strings.ts:2360
msgid "Toggle replace" msgid "Toggle replace"
msgstr "Toggle replace" msgstr "Toggle replace"
#: src/strings.ts:2436
msgid "Toggle theme"
msgstr "Toggle theme"
#: src/strings.ts:2113 #: src/strings.ts:2113
msgid "Toolbar" msgid "Toolbar"
msgstr "Toolbar" msgstr "Toolbar"
@@ -6142,6 +6230,14 @@ msgstr "Turn off reminder"
msgid "Turn on reminder" msgid "Turn on reminder"
msgstr "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 #: 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." 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." 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" msgid "Unpublish notes to delete them"
msgstr "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 #: src/strings.ts:2067
msgid "Unregister" msgid "Unregister"
msgstr "Unregister" msgstr "Unregister"

View File

@@ -580,6 +580,10 @@ msgstr ""
msgid "#notesnook" msgid "#notesnook"
msgstr "" 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 #: src/strings.ts:1564
msgid "12-hour" msgid "12-hour"
msgstr "" msgstr ""
@@ -628,6 +632,18 @@ msgstr ""
msgid "Account password" msgid "Account password"
msgstr "" 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 #: src/strings.ts:2018
msgid "Activate" msgid "Activate"
msgstr "" msgstr ""
@@ -672,6 +688,10 @@ msgstr ""
msgid "Add shortcuts for notebooks and tags here." msgid "Add shortcuts for notebooks and tags here."
msgstr "" msgstr ""
#: src/strings.ts:2426
msgid "Add subnotebook"
msgstr ""
#: src/strings.ts:564 #: src/strings.ts:564
msgid "Add tag" msgid "Add tag"
msgstr "" msgstr ""
@@ -907,6 +927,10 @@ msgstr ""
msgid "Attachment" msgid "Attachment"
msgstr "" msgstr ""
#: src/strings.ts:2432
msgid "Attachment manager"
msgstr ""
#: src/strings.ts:1916 #: src/strings.ts:1916
msgid "Attachment preview failed" msgid "Attachment preview failed"
msgstr "" msgstr ""
@@ -1469,6 +1493,14 @@ msgstr ""
msgid "Close all" msgid "Close all"
msgstr "" msgstr ""
#: src/strings.ts:2435
msgid "Close all tabs"
msgstr ""
#: src/strings.ts:2434
msgid "Close current tab"
msgstr ""
#: src/strings.ts:1997 #: src/strings.ts:1997
msgid "Close others" msgid "Close others"
msgstr "" msgstr ""
@@ -1639,6 +1671,10 @@ msgstr ""
msgid "Copy link text" msgid "Copy link text"
msgstr "" msgstr ""
#: src/strings.ts:2422
msgid "Copy monograph link"
msgstr ""
#: src/strings.ts:447 #: src/strings.ts:447
msgid "Copy note" msgid "Copy note"
msgstr "" msgstr ""
@@ -2794,6 +2830,10 @@ msgstr ""
msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgid "GNU GENERAL PUBLIC LICENSE Version 3"
msgstr "" msgstr ""
#: src/strings.ts:2431
msgid "Go back in tab"
msgstr ""
#: src/strings.ts:2206 #: src/strings.ts:2206
msgid "Go back to notebooks" msgid "Go back to notebooks"
msgstr "" msgstr ""
@@ -2802,6 +2842,10 @@ msgstr ""
msgid "Go back to tags" msgid "Go back to tags"
msgstr "" msgstr ""
#: src/strings.ts:2430
msgid "Go forward in tab"
msgstr ""
#: src/strings.ts:1794 #: src/strings.ts:1794
msgid "Go to" msgid "Go to"
msgstr "" msgstr ""
@@ -3652,6 +3696,10 @@ msgstr ""
msgid "Native high-performance encryption" msgid "Native high-performance encryption"
msgstr "" msgstr ""
#: src/strings.ts:2427
msgid "Navigate"
msgstr ""
#: src/strings.ts:390 #: src/strings.ts:390
msgid "Never" msgid "Never"
msgstr "" msgstr ""
@@ -3708,6 +3756,10 @@ msgstr ""
msgid "New tab" msgid "New tab"
msgstr "" msgstr ""
#: src/strings.ts:2433
msgid "New tag"
msgstr ""
#: src/strings.ts:1351 #: src/strings.ts:1351
msgid "New update available" msgid "New update available"
msgstr "" msgstr ""
@@ -3732,6 +3784,10 @@ msgstr ""
msgid "Next match" msgid "Next match"
msgstr "" msgstr ""
#: src/strings.ts:2428
msgid "Next tab"
msgstr ""
#: src/strings.ts:539 #: src/strings.ts:539
msgid "No" msgid "No"
msgstr "" msgstr ""
@@ -4007,6 +4063,10 @@ msgstr ""
msgid "Open in browser to manage subscription" msgid "Open in browser to manage subscription"
msgstr "" msgstr ""
#: src/strings.ts:2421
msgid "Open in monograph"
msgstr ""
#: src/strings.ts:2304 #: src/strings.ts:2304
msgid "Open in new tab" msgid "Open in new tab"
msgstr "" msgstr ""
@@ -4404,6 +4464,10 @@ msgstr ""
msgid "Previous match" msgid "Previous match"
msgstr "" msgstr ""
#: src/strings.ts:2429
msgid "Previous tab"
msgstr ""
#: src/strings.ts:2015 #: src/strings.ts:2015
msgid "Print" msgid "Print"
msgstr "" msgstr ""
@@ -4492,6 +4556,10 @@ msgstr ""
msgid "Publish note" msgid "Publish note"
msgstr "" msgstr ""
#: src/strings.ts:2420
msgid "Publish on monograph"
msgstr ""
#: src/strings.ts:481 #: src/strings.ts:481
msgid "Publish your note to share it with others. You can set a password to protect it." msgid "Publish your note to share it with others. You can set a password to protect it."
msgstr "" msgstr ""
@@ -4572,6 +4640,10 @@ msgstr ""
msgid "Receipt" msgid "Receipt"
msgstr "" msgstr ""
#: src/strings.ts:2440
msgid "Recent"
msgstr ""
#: src/strings.ts:1592 #: src/strings.ts:1592
msgid "RECENT BACKUPS" msgid "RECENT BACKUPS"
msgstr "" msgstr ""
@@ -4762,6 +4834,10 @@ msgstr ""
msgid "Remove from notebook" msgid "Remove from notebook"
msgstr "" msgstr ""
#: src/strings.ts:2441
msgid "Remove from recent"
msgstr ""
#: src/strings.ts:1030 #: src/strings.ts:1030
msgid "Remove full name" msgid "Remove full name"
msgstr "" msgstr ""
@@ -5120,6 +5196,10 @@ msgstr ""
msgid "Search in Notes" msgid "Search in Notes"
msgstr "" msgstr ""
#: src/strings.ts:2418
msgid "Search in notes, notebooks, and tags"
msgstr ""
#: src/strings.ts:31 #: src/strings.ts:31
msgid "Search in Reminders" msgid "Search in Reminders"
msgstr "" msgstr ""
@@ -6044,10 +6124,18 @@ msgstr ""
msgid "Toggle indentation mode" msgid "Toggle indentation mode"
msgstr "" msgstr ""
#: src/strings.ts:2419
msgid "Toggle readonly"
msgstr ""
#: src/strings.ts:2360 #: src/strings.ts:2360
msgid "Toggle replace" msgid "Toggle replace"
msgstr "" msgstr ""
#: src/strings.ts:2436
msgid "Toggle theme"
msgstr ""
#: src/strings.ts:2113 #: src/strings.ts:2113
msgid "Toolbar" msgid "Toolbar"
msgstr "" msgstr ""
@@ -6101,6 +6189,14 @@ msgstr ""
msgid "Turn on reminder" msgid "Turn on reminder"
msgstr "" msgstr ""
#: src/strings.ts:2424
msgid "Turn sync off"
msgstr ""
#: src/strings.ts:2423
msgid "Turn sync on"
msgstr ""
#: src/strings.ts:1078 #: 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." 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 "" msgstr ""
@@ -6214,6 +6310,10 @@ msgstr ""
msgid "Unpublish notes to delete them" msgid "Unpublish notes to delete them"
msgstr "" msgstr ""
#: src/strings.ts:2425
msgid "Unpublish on monograph"
msgstr ""
#: src/strings.ts:2067 #: src/strings.ts:2067
msgid "Unregister" msgid "Unregister"
msgstr "" msgstr ""

View File

@@ -2412,5 +2412,31 @@ Use this if changes from other devices are not appearing on this device. This wi
redeemGiftCode: () => t`Redeem gift code`, redeemGiftCode: () => t`Redeem gift code`,
redeemGiftCodeDesc: () => t`Enter the gift code to redeem your subscription.`, redeemGiftCodeDesc: () => t`Enter the gift code to redeem your subscription.`,
redeemingGiftCode: () => t`Redeeming gift code`, 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`
}; };