diff --git a/src/i18n/en.json b/src/i18n/en.json index 4eecf561..acf6022e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -138,11 +138,6 @@ "Options": "Options", "Sync notes to and from Google Drive": "Sync notes to and from Google Drive", "Command palette": "Command palette", - "Command palette detail": { - "line1": "By default, Command palette looks for notes by their name.", - "line2": "Type {{symbol}} first, and continue to start search for commands.", - "line3": "Type {{symbol}} first, and continue to start search for notes by their content." - }, "Repeat last command": "Repeat last command", "Toggle Focus mode": "Toggle Focus mode", "Toggle Sidebar": "Toggle Sidebar", diff --git a/src/notes.tsx b/src/notes.tsx index 8b7a7983..cc7811ff 100644 --- a/src/notes.tsx +++ b/src/notes.tsx @@ -2,9 +2,8 @@ /* eslint-disable react/jsx-pascal-case */ import { h, render, Fragment } from "preact"; import { - useState, useEffect, useRef, useCallback, useMemo, + useState, useEffect, useRef, useCallback, } from "preact/hooks"; - import { Os, Storage, @@ -49,12 +48,9 @@ import getFirstAvailableNoteName from "notes/filters/get-first-available-note-na import notesHistory from "notes/history"; import keyboardShortcuts, { KeyboardShortcut } from "notes/keyboard-shortcuts"; import { useKeyboardShortcut } from "notes/hooks/use-keyboard-shortcut"; -import { - Command, commands, toggleSidebar, toggleToolbar, -} from "notes/commands"; +import { Command, toggleSidebar, toggleToolbar } from "notes/commands"; import { exportNote } from "notes/export"; import { notesToSidebarNotes } from "notes/adapters"; -import { t } from "i18n"; let autoSyncIntervalId: number | undefined; @@ -504,45 +500,12 @@ const Notes = (): h.JSX.Element => { chrome.storage.local.set({ active: noteName }); }, [notesProps]); - // Command Palette - const commandPaletteCommands: { name: string, translation: h.JSX.Element, command: Command }[] = useMemo(() => [ - { - name: "Insert current Date", - translation: t("Insert current Date"), - command: commands.InsertCurrentDate, - }, - { - name: "Insert current Time", - translation: t("Insert current Time"), - command: commands.InsertCurrentTime, - }, - { - name: "Insert current Date and Time", - translation: t("Insert current Date and Time"), - command: commands.InsertCurrentDateAndTime, - }, - { - name: "Toggle Sidebar", - translation: t("Shortcuts descriptions.Toggle Sidebar"), - command: toggleSidebar, - }, - { - name: "Toggle Toolbar", - translation: t("Shortcuts descriptions.Toggle Toolbar"), - command: toggleToolbar, - }, - ], []); - // Repeat last executed command const [setOnRepeatLastExecutedCommandHandler] = useKeyboardShortcut(KeyboardShortcut.OnRepeatLastExecutedCommand); // Command Palette const [setOnToggleCommandPaletteHandler] = useKeyboardShortcut(KeyboardShortcut.OnToggleCommandPalette); useEffect(() => { - if (!notesOrder) { - return; - } - // Detach when there are no notes if (!Object.keys(notesProps.notes).length) { setOnToggleCommandPaletteHandler(undefined); @@ -550,27 +513,16 @@ const Notes = (): h.JSX.Element => { return; } - // Start preparing props for Command Palette - const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true; - const newCommands = currentNoteLocked ? [] : commandPaletteCommands; - // Props for Command Palette + const currentNoteLocked: boolean = notesProps.active in notesProps.notes && notesProps.notes[notesProps.active].locked === true; const props: CommandPaletteProps = { - notes: notesToSidebarNotes(notesProps.notes, notesOrder, order), - commands: newCommands, - onActivateNote: (noteName: string) => { + includeContentCommands: !currentNoteLocked, + onCommandToExecute: (command: Command) => { setCommandPaletteProps(null); - range.restore(() => handleOnActivateNote(noteName)); - }, - onExecuteCommand: (commandName: string) => { - const foundCommand = commandPaletteCommands.find((command) => command.name === commandName); - if (foundCommand) { - setCommandPaletteProps(null); - range.restore(() => { - foundCommand.command(); - setOnRepeatLastExecutedCommandHandler(foundCommand.command); - }); - } + range.restore(() => { + command(); + setOnRepeatLastExecutedCommandHandler(command); + }); }, }; @@ -589,7 +541,7 @@ const Notes = (): h.JSX.Element => { // Update props for already visible Command Palette setCommandPaletteProps((prev) => (!prev ? prev : props)); - }, [os, notesProps, notesOrder, order, handleOnActivateNote, commandPaletteCommands]); + }, [notesProps]); // Automatically show modal to create a new note if there are 0 notes useEffect(() => { diff --git a/src/notes/components/CommandPalette.tsx b/src/notes/components/CommandPalette.tsx index 2c65636f..7d785b69 100644 --- a/src/notes/components/CommandPalette.tsx +++ b/src/notes/components/CommandPalette.tsx @@ -1,136 +1,77 @@ import { h } from "preact"; import { - useRef, useState, useMemo, useCallback, useEffect, + useRef, useState, useEffect, useMemo, } from "preact/hooks"; import clsx from "clsx"; import useBodyClass from "notes/hooks/use-body-class"; -import { SidebarNote } from "notes/adapters"; import { t, tString } from "i18n"; +import { + Command, commands as contentCommands, toggleSidebar, toggleToolbar, +} from "notes/commands"; -export interface CommandPaletteProps { - notes: SidebarNote[] - commands: { name: string, translation: h.JSX.Element }[] - onActivateNote: (noteName: string) => void - onExecuteCommand: (commandName: string) => void -} - -export enum FilterType { - CommandsByName, - NotesByContent, - NotesByName, -} - -export interface Filter { - type: FilterType - input: string -} - -/** - * Returns filter to be used based on the input. - * - * We can filter: - * A) CommandsByName - * => filter commands by name, when input starts with ">" (whitespace before and after ">" is allowed, whitespace at the end is allowed) - * - * B) NotesByContent - * => filter notes by the content, when input starts with "?" (whitespace before and after "?" is allowed, whitespace at the end is allowed) - * - * C) NotesByName - * => filter notes by their name, when input does NOT start with [">", "?"] (whitespace before is allowed, whitespace at the end is allowed) - */ -export const prepareFilter = (rawInput: string): Filter => { - const input = rawInput.trim().toLowerCase().replace(/^([>?])\s*(.*)/, "$1$2"); // trim whitespace, remove whitespace after [">", "?"] - - // A) CommandsByName - if (input.startsWith(">")) { - return { - type: FilterType.CommandsByName, - input: input.slice(1), - }; - } - - // B) NotesByContent - if (input.startsWith("?")) { - return { - type: FilterType.NotesByContent, - input: input.slice(1), - }; - } - - // C) NotesByName - return { - type: FilterType.NotesByName, - input, - }; +type CommandPaletteCommand = { + title: string + command: Command + isContentCommand?: boolean }; -export const prepareItems = (notes: SidebarNote[], commands: string[], filter: Filter | undefined): string[] => { - const noteNames = notes.map((note) => note.name); +const allCommands: CommandPaletteCommand[] = [ + { + title: tString("Insert current Date"), + command: contentCommands.InsertCurrentDate, + isContentCommand: true, + }, + { + title: tString("Insert current Time"), + command: contentCommands.InsertCurrentTime, + isContentCommand: true, + }, + { + title: tString("Insert current Date and Time"), + command: contentCommands.InsertCurrentDateAndTime, + isContentCommand: true, + }, + { + title: tString("Shortcuts descriptions.Toggle Sidebar"), + command: toggleSidebar, + }, + { + title: tString("Shortcuts descriptions.Toggle Toolbar"), + command: toggleToolbar, + }, +]; - if (!filter) { - return noteNames; - } - - const input = filter.input.trim(); - const prepareFilterPredicate = (givenInput: string) => (item: string) => (givenInput ? item.toLowerCase().includes(givenInput) : item); - - // A) CommandsByName - if (filter.type === FilterType.CommandsByName) { - return commands.filter(prepareFilterPredicate(input)); - } - - // B) NotesByContent - if (filter.type === FilterType.NotesByContent) { - const filterPredicate = prepareFilterPredicate(input); - return input - ? noteNames.filter((noteName) => { - const foundNote = notes.find((note) => note.name === noteName); - return foundNote && filterPredicate(foundNote.content); - }) - : noteNames; - } - - // C) NotesByName - return noteNames.filter(prepareFilterPredicate(input)); -}; +export interface CommandPaletteProps { + includeContentCommands: boolean + onCommandToExecute: (command: Command) => void +} const CommandPalette = ({ - notes, commands, onActivateNote, onExecuteCommand, + includeContentCommands, + onCommandToExecute, }: CommandPaletteProps): h.JSX.Element => { useBodyClass("with-command-palette"); const inputRef = useRef(null); - const [filter, setFilter] = useState(undefined); - - // Items to show in Command Palette - const items: string[] = useMemo(() => prepareItems(notes, commands.map((command) => command.name), filter), [notes, commands, filter]); - - // Selected item; can be changed with Up / Down arrow keys - const [selectedItemIndex, setSelectedItemIndex] = useState(-1); - - // What to do on Enter - const handleItem = useCallback((name: string) => { - if (filter?.type === FilterType.CommandsByName) { - onExecuteCommand(name); - return; - } - - onActivateNote(name); - }, [filter, onActivateNote, onExecuteCommand]); + useEffect(() => inputRef.current?.focus(), []); - // auto-focus the input when Command Palette is open - useEffect(() => inputRef.current?.focus(), [inputRef]); + const [filter, setFilter] = useState(""); + const filteredCommands = useMemo(() => ( + allCommands + .filter((x) => (includeContentCommands ? true : (!x.isContentCommand))) + .filter((x) => (filter ? x.title.toLowerCase().includes(filter.trim().toLowerCase()) : true)) + ), [includeContentCommands, filter]); - // reset selected item when props change; auto-select it when there's just one - useEffect(() => setSelectedItemIndex(items.length === 1 ? 0 : -1), [filter, items]); + const [selectedIndex, setSelectedIndex] = useState(-1); + useEffect(() => setSelectedIndex(filteredCommands.length === 1 ? 0 : -1), [filteredCommands]); return (
{ - if (event.key === "Enter" && selectedItemIndex !== -1) { - const item = items[selectedItemIndex]; - handleItem(item); + if (event.key === "Enter" && selectedIndex !== -1) { + const { command } = filteredCommands[selectedIndex]; + onCommandToExecute(command); return; } @@ -140,10 +81,10 @@ const CommandPalette = ({ } event.preventDefault(); - const activeIndexCandidate = ((selectedItemIndex + direction) % items.length); - setSelectedItemIndex( + const activeIndexCandidate = ((selectedIndex + direction) % filteredCommands.length); + setSelectedIndex( activeIndexCandidate < 0 && direction === -1 - ? items.length - 1 + ? filteredCommands.length - 1 : activeIndexCandidate, ); }} @@ -155,34 +96,29 @@ const CommandPalette = ({ ref={inputRef} onBlur={() => inputRef.current?.focus()} onInput={(event) => { - const newFilter = prepareFilter((event.target as HTMLInputElement).value); - setFilter(newFilter); + setFilter((event.target as HTMLInputElement).value); }} autoComplete="off" /> - {items.length > 0 && ( + {filteredCommands.length > 0 && (
- {items.map((name, index) => ( + {filteredCommands.map((item, index) => (
handleItem(name)} - onMouseEnter={() => setSelectedItemIndex(index)} + className={clsx("command-palette-list-item", index === selectedIndex && "active")} + onClick={() => onCommandToExecute(item.command)} + onMouseEnter={() => setSelectedIndex(index)} > - {(filter?.type === FilterType.CommandsByName) - ? commands.find((command) => command.name === name)?.translation - : name} + {item.title}
))}
)} - {items.length === 0 && filter && ( + {filteredCommands.length === 0 && (
- {filter.type === FilterType.CommandsByName && t("(No matching commands)")} - {filter.type === FilterType.NotesByContent && t("(No matching notes)")} - {filter.type === FilterType.NotesByName && t("(No matching notes)")} + {t("(No matching commands)")}
)} diff --git a/src/notes/components/__tests__/CommandPalette.test.tsx b/src/notes/components/__tests__/CommandPalette.test.tsx deleted file mode 100644 index 88a77583..00000000 --- a/src/notes/components/__tests__/CommandPalette.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { SidebarNote } from "notes/adapters"; -import { - Filter, FilterType, prepareFilter, prepareItems, -} from "../CommandPalette"; - -describe("prepareFilter", () => { - it("detects CommandsByName filter", () => { - expect(prepareFilter(" > time ")).toEqual({ - type: FilterType.CommandsByName, - input: "time", - }); - - expect(prepareFilter(" > current time ")).toEqual({ - type: FilterType.CommandsByName, - input: "current time", - }); - }); - - it("detects NotesByContent filter", () => { - expect(prepareFilter(" ? muffins ")).toEqual({ - type: FilterType.NotesByContent, - input: "muffins", - }); - - expect(prepareFilter(" ? best ever muffins ")).toEqual({ - type: FilterType.NotesByContent, - input: "best ever muffins", - }); - }); - - it("detects NotesByName filter", () => { - expect(prepareFilter(" TODO ")).toEqual({ - type: FilterType.NotesByName, - input: "todo", - }); - }); -}); - -describe("prepareItems", () => { - const createdTime = "CT"; // not relevant for the test - const modifiedTime = "MT"; // not relevant for the test - - const notes: SidebarNote[] = [ - { - name: "Clipboard", - content: "", - createdTime, - modifiedTime, - }, - { - name: "Article", - content: "This is an interesting article", - createdTime, - modifiedTime, - }, - { - name: "TODO", - content: "buy milk, buy coffee", - createdTime, - modifiedTime, - }, - ]; - - const commands = [ - "Insert current Date", - "Insert current Time", - "Insert current Date and Time", - ]; - - it("returns notes by default", () => { - const items = prepareItems(notes, commands, undefined); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - - describe("CommandsByName filter", () => { - it("returns commands that include input in their name", () => { - const filter: Filter = { type: FilterType.CommandsByName, input: "date" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Insert current Date", - "Insert current Date and Time", - ]); - }); - - it("returns all commands when no input is provided", () => { - const filter: Filter = { type: FilterType.CommandsByName, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual(commands); - }); - }); - - describe("NotesByContent filter", () => { - it("returns notes that include input in their content", () => { - const filter: Filter = { type: FilterType.NotesByContent, input: "interesting" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Article", - ]); - }); - - it("returns all notes when no input is provided", () => { - const filter: Filter = { type: FilterType.NotesByContent, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - }); - - describe("NotesByName filter", () => { - it("returns notes that include input in their name", () => { - const filter: Filter = { type: FilterType.NotesByName, input: "ar" }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - ]); - }); - - it("returns all notes when no input is provided", () => { - const filter: Filter = { type: FilterType.NotesByName, input: " " }; - const items = prepareItems(notes, commands, filter); - expect(items).toEqual([ - "Clipboard", - "Article", - "TODO", - ]); - }); - }); -}); diff --git a/src/options/KeyboardShortcuts.tsx b/src/options/KeyboardShortcuts.tsx index c24407d1..b07bcaf2 100644 --- a/src/options/KeyboardShortcuts.tsx +++ b/src/options/KeyboardShortcuts.tsx @@ -40,14 +40,6 @@ const KeyboardShortcuts = ({ os }: KeyboardShortcutsProps): h.JSX.Element => ( {t("Shortcuts other.if enabled")} )} - - {shortcut.description === "Command palette" && ( -
-
{t("Shortcuts descriptions.Command palette detail.line1")}
-
{t("Shortcuts descriptions.Command palette detail.line2", { symbol: ">" })}
-
{t("Shortcuts descriptions.Command palette detail.line3", { symbol: "?" })}
-
- )} ))}