From 3a1b722d31dd165fc9a4e15f0e92f514e7e1aecb Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:41:59 +0530 Subject: [PATCH] refactor: keyboard shortcuts modal (#2822) * refactor: keyboard shortcuts modal * chore: updated search logic * refactor: divided the modal component into granular components --- web/components/command-palette/index.ts | 2 +- .../command-palette/shortcuts-modal.tsx | 200 ------------------ .../shortcuts-modal/commands-list.tsx | 98 +++++++++ .../command-palette/shortcuts-modal/index.ts | 2 + .../command-palette/shortcuts-modal/modal.tsx | 81 +++++++ web/constants/{cycle.tsx => cycle.ts} | 0 web/helpers/string.helper.ts | 26 +++ 7 files changed, 208 insertions(+), 201 deletions(-) delete mode 100644 web/components/command-palette/shortcuts-modal.tsx create mode 100644 web/components/command-palette/shortcuts-modal/commands-list.tsx create mode 100644 web/components/command-palette/shortcuts-modal/index.ts create mode 100644 web/components/command-palette/shortcuts-modal/modal.tsx rename web/constants/{cycle.tsx => cycle.ts} (100%) diff --git a/web/components/command-palette/index.ts b/web/components/command-palette/index.ts index 1fac3f134..192ef8ef9 100644 --- a/web/components/command-palette/index.ts +++ b/web/components/command-palette/index.ts @@ -1,5 +1,5 @@ export * from "./actions"; +export * from "./shortcuts-modal"; export * from "./command-modal"; export * from "./command-pallette"; export * from "./helpers"; -export * from "./shortcuts-modal"; diff --git a/web/components/command-palette/shortcuts-modal.tsx b/web/components/command-palette/shortcuts-modal.tsx deleted file mode 100644 index 40317013c..000000000 --- a/web/components/command-palette/shortcuts-modal.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { FC, useEffect, useState, Fragment } from "react"; -import { Dialog, Transition } from "@headlessui/react"; -// icons -import { Command, Search, X } from "lucide-react"; -// ui -import { Input } from "@plane/ui"; - -type Props = { - isOpen: boolean; - onClose: () => void; -}; - -const shortcuts = [ - { - title: "Navigation", - shortcuts: [ - { keys: "Ctrl,K", description: "To open navigator" }, - { keys: "↑", description: "Move up" }, - { keys: "↓", description: "Move down" }, - { keys: "←", description: "Move left" }, - { keys: "→", description: "Move right" }, - ], - }, - { - title: "Common", - shortcuts: [ - { keys: "P", description: "To create project" }, - { keys: "C", description: "To create issue" }, - { keys: "Q", description: "To create cycle" }, - { keys: "M", description: "To create module" }, - { keys: "V", description: "To create view" }, - { keys: "D", description: "To create page" }, - { keys: "Delete", description: "To bulk delete issues" }, - { keys: "H", description: "To open shortcuts guide" }, - { - keys: "Ctrl,Alt,C", - description: "To copy issue url when on issue detail page", - }, - ], - }, -]; - -const allShortcuts = shortcuts.map((i) => i.shortcuts).flat(1); - -export const ShortcutsModal: FC = (props) => { - const { isOpen, onClose } = props; - // states - const [query, setQuery] = useState(""); - // computed - const filteredShortcuts = allShortcuts.filter((shortcut) => - shortcut.description.toLowerCase().includes(query.trim().toLowerCase()) || query === "" ? true : false - ); - - useEffect(() => { - if (!isOpen) setQuery(""); - }, [isOpen]); - - return ( - - - -
- - -
-
- - -
-
-
- - Keyboard Shortcuts - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search for shortcuts" - className="w-full border-none bg-transparent py-1 px-2 text-xs text-custom-text-200 focus:outline-none" - /> -
-
-
- {query.trim().length > 0 ? ( - filteredShortcuts.length > 0 ? ( - filteredShortcuts.map((shortcut) => ( -
-
-
-

{shortcut.description}

-
- {shortcut.keys.split(",").map((key, index) => ( - - {key === "Ctrl" ? ( - - - - ) : key === "Ctrl" ? ( - - - - ) : ( - - {key} - - )} - - ))} -
-
-
-
- )) - ) : ( -
-

- No shortcuts found for{" "} - - {`"`} - {query} - {`"`} - -

-
- ) - ) : ( - shortcuts.map(({ title, shortcuts }) => ( -
-

{title}

-
- {shortcuts.map(({ keys, description }, index) => ( -
-

{description}

-
- {keys.split(",").map((key, index) => ( - - {key === "Ctrl" ? ( - - - - ) : key === "Ctrl" ? ( - - - - ) : ( - - {key} - - )} - - ))} -
-
- ))} -
-
- )) - )} -
-
-
-
-
-
-
-
-
-
- ); -}; diff --git a/web/components/command-palette/shortcuts-modal/commands-list.tsx b/web/components/command-palette/shortcuts-modal/commands-list.tsx new file mode 100644 index 000000000..d43043683 --- /dev/null +++ b/web/components/command-palette/shortcuts-modal/commands-list.tsx @@ -0,0 +1,98 @@ +import { Command } from "lucide-react"; +// helpers +import { substringMatch } from "helpers/string.helper"; + +type Props = { + searchQuery: string; +}; + +const KEYBOARD_SHORTCUTS = [ + { + key: "navigation", + title: "Navigation", + shortcuts: [{ keys: "Ctrl,K", description: "Open command menu" }], + }, + { + key: "common", + title: "Common", + shortcuts: [ + { keys: "P", description: "Create project" }, + { keys: "C", description: "Create issue" }, + { keys: "Q", description: "Create cycle" }, + { keys: "M", description: "Create module" }, + { keys: "V", description: "Create view" }, + { keys: "D", description: "Create page" }, + { keys: "Delete", description: "Bulk delete issues" }, + { keys: "H", description: "Open shortcuts guide" }, + { + keys: "Ctrl,Alt,C", + description: "Copy issue URL from the issue details page", + }, + ], + }, +]; + +export const ShortcutCommandsList: React.FC = (props) => { + const { searchQuery } = props; + + const filteredShortcuts = KEYBOARD_SHORTCUTS.map((category) => { + const newCategory = { ...category }; + + newCategory.shortcuts = newCategory.shortcuts.filter((shortcut) => + substringMatch(shortcut.description, searchQuery) + ); + + return newCategory; + }); + + const isShortcutsEmpty = filteredShortcuts.every((category) => category.shortcuts.length === 0); + + return ( +
+ {!isShortcutsEmpty ? ( + filteredShortcuts.map((category) => { + if (category.shortcuts.length === 0) return; + + return ( +
+
{category.title}
+
+ {category.shortcuts.map((shortcut) => ( +
+
+

{shortcut.description}

+
+ {shortcut.keys.split(",").map((key) => ( +
+ {key === "Ctrl" ? ( +
+ +
+ ) : ( + + {key} + + )} +
+ ))} +
+
+
+ ))} +
+
+ ); + }) + ) : ( +

+ No shortcuts found for{" "} + + {`"`} + {searchQuery} + {`"`} + +

+ )} +
+ ); +}; diff --git a/web/components/command-palette/shortcuts-modal/index.ts b/web/components/command-palette/shortcuts-modal/index.ts new file mode 100644 index 000000000..9346fb2b4 --- /dev/null +++ b/web/components/command-palette/shortcuts-modal/index.ts @@ -0,0 +1,2 @@ +export * from "./commands-list"; +export * from "./modal"; diff --git a/web/components/command-palette/shortcuts-modal/modal.tsx b/web/components/command-palette/shortcuts-modal/modal.tsx new file mode 100644 index 000000000..0f316b14d --- /dev/null +++ b/web/components/command-palette/shortcuts-modal/modal.tsx @@ -0,0 +1,81 @@ +import { FC, useState, Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +import { Search, X } from "lucide-react"; +// components +import { ShortcutCommandsList } from "components/command-palette"; +// ui +import { Input } from "@plane/ui"; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export const ShortcutsModal: FC = (props) => { + const { isOpen, onClose } = props; + // states + const [query, setQuery] = useState(""); + + const handleClose = () => { + onClose(); + setQuery(""); + }; + + return ( + + + +
+ + +
+ + +
+
+ + Keyboard shortcuts + + +
+ + setQuery(e.target.value)} + placeholder="Search for shortcuts" + className="w-full border-none bg-transparent py-1 text-xs text-custom-text-200 outline-none" + autoFocus + tabIndex={1} + /> +
+ +
+
+
+
+
+
+
+ ); +}; diff --git a/web/constants/cycle.tsx b/web/constants/cycle.ts similarity index 100% rename from web/constants/cycle.tsx rename to web/constants/cycle.ts diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 778ec602a..d402de873 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -180,3 +180,29 @@ export const getFetchKeysForIssueMutation = (options: { ...ganttFetchKey, }; }; + +/** + * @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise + * @description Returns true if searchQuery is substring of text in the same order, false otherwise + * @param {string} text string to compare from + * @param {string} searchQuery + * @example substringMatch("hello world", "hlo") => true + * @example substringMatch("hello world", "hoe") => false + */ +export const substringMatch = (text: string, searchQuery: string): boolean => { + try { + let searchIndex = 0; + + for (let i = 0; i < text.length; i++) { + if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++; + + // All characters of searchQuery found in order + if (searchIndex === searchQuery.length) return true; + } + + // Not all characters of searchQuery found in order + return false; + } catch (error) { + return false; + } +};