From 7aaf840fb11064b912fd044f5a3358e5b4dae91e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:46:41 +0530 Subject: [PATCH] refactor: command k modal (#2803) * refactor: command palette file structure * fix: identifier search --- .../command-palette/actions/help-actions.tsx | 83 +++ .../command-palette/actions/index.ts | 6 + .../actions/issue-actions/actions-list.tsx | 166 ++++++ .../actions/issue-actions/change-assignee.tsx | 79 +++ .../actions/issue-actions/change-priority.tsx | 56 ++ .../actions/issue-actions/change-state.tsx | 65 +++ .../actions/issue-actions/index.ts | 4 + .../actions/project-actions.tsx | 83 +++ .../actions/search-results.tsx | 49 ++ .../theme-actions.tsx} | 21 +- .../actions/workspace-settings-actions.tsx | 61 +++ .../command-palette/command-modal.tsx | 499 +++--------------- .../command-palette/command-pallette.tsx | 14 +- web/components/command-palette/helpers.tsx | 21 +- web/components/command-palette/index.ts | 3 +- .../issue/change-issue-assignee.tsx | 111 ---- .../issue/change-issue-priority.tsx | 78 --- .../issue/change-issue-state.tsx | 93 ---- web/components/command-palette/issue/index.ts | 3 - 19 files changed, 741 insertions(+), 754 deletions(-) create mode 100644 web/components/command-palette/actions/help-actions.tsx create mode 100644 web/components/command-palette/actions/index.ts create mode 100644 web/components/command-palette/actions/issue-actions/actions-list.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-assignee.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-priority.tsx create mode 100644 web/components/command-palette/actions/issue-actions/change-state.tsx create mode 100644 web/components/command-palette/actions/issue-actions/index.ts create mode 100644 web/components/command-palette/actions/project-actions.tsx create mode 100644 web/components/command-palette/actions/search-results.tsx rename web/components/command-palette/{change-interface-theme.tsx => actions/theme-actions.tsx} (74%) create mode 100644 web/components/command-palette/actions/workspace-settings-actions.tsx delete mode 100644 web/components/command-palette/issue/change-issue-assignee.tsx delete mode 100644 web/components/command-palette/issue/change-issue-priority.tsx delete mode 100644 web/components/command-palette/issue/change-issue-state.tsx delete mode 100644 web/components/command-palette/issue/index.ts diff --git a/web/components/command-palette/actions/help-actions.tsx b/web/components/command-palette/actions/help-actions.tsx new file mode 100644 index 000000000..859a6d23a --- /dev/null +++ b/web/components/command-palette/actions/help-actions.tsx @@ -0,0 +1,83 @@ +import { Command } from "cmdk"; +import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { DiscordIcon } from "@plane/ui"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteHelpActions: React.FC = (props) => { + const { closePalette } = props; + + const { + commandPalette: { toggleShortcutModal }, + } = useMobxStore(); + + return ( + + { + closePalette(); + toggleShortcutModal(true); + }} + className="focus:outline-none" + > +
+ + Open keyboard shortcuts +
+
+ { + closePalette(); + window.open("https://docs.plane.so/", "_blank"); + }} + className="focus:outline-none" + > +
+ + Open Plane documentation +
+
+ { + closePalette(); + window.open("https://discord.com/invite/A92xrEGCge", "_blank"); + }} + className="focus:outline-none" + > +
+ + Join our Discord +
+
+ { + closePalette(); + window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); + }} + className="focus:outline-none" + > +
+ + Report a bug +
+
+ { + closePalette(); + (window as any)?.$crisp.push(["do", "chat:open"]); + }} + className="focus:outline-none" + > +
+ + Chat with us +
+
+
+ ); +}; diff --git a/web/components/command-palette/actions/index.ts b/web/components/command-palette/actions/index.ts new file mode 100644 index 000000000..7c3af470e --- /dev/null +++ b/web/components/command-palette/actions/index.ts @@ -0,0 +1,6 @@ +export * from "./issue-actions"; +export * from "./help-actions"; +export * from "./project-actions"; +export * from "./search-results"; +export * from "./theme-actions"; +export * from "./workspace-settings-actions"; diff --git a/web/components/command-palette/actions/issue-actions/actions-list.tsx b/web/components/command-palette/actions/issue-actions/actions-list.tsx new file mode 100644 index 000000000..73a021cf8 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/actions-list.tsx @@ -0,0 +1,166 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issueDetails: IIssue | undefined; + pages: string[]; + setPages: (pages: string[]) => void; + setPlaceholder: (placeholder: string) => void; + setSearchTerm: (searchTerm: string) => void; +}; + +export const CommandPaletteIssueActions: React.FC = observer((props) => { + const { closePalette, issueDetails, pages, setPages, setPlaceholder, setSearchTerm } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal }, + issueDetail: { updateIssue }, + user: { currentUser }, + } = useMobxStore(); + + const { setToastAlert } = useToast(); + + const handleUpdateIssue = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issueDetails) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issueDetails.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueAssignees = (assignee: string) => { + if (!issueDetails || !assignee) return; + + closePalette(); + const updatedAssignees = issueDetails.assignees ?? []; + + if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + else updatedAssignees.push(assignee); + + handleUpdateIssue({ assignees: updatedAssignees }); + }; + + const deleteIssue = () => { + toggleCommandPaletteModal(false); + toggleDeleteIssueModal(true); + }; + + const copyIssueUrlToClipboard = () => { + if (!router.query.issueId) return; + + const url = new URL(window.location.href); + copyTextToClipboard(url.href) + .then(() => { + setToastAlert({ + type: "success", + title: "Copied to clipboard", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Some error occurred", + }); + }); + }; + + return ( + + { + setPlaceholder("Change state..."); + setSearchTerm(""); + setPages([...pages, "change-issue-state"]); + }} + className="focus:outline-none" + > +
+ + Change state... +
+
+ { + setPlaceholder("Change priority..."); + setSearchTerm(""); + setPages([...pages, "change-issue-priority"]); + }} + className="focus:outline-none" + > +
+ + Change priority... +
+
+ { + setPlaceholder("Assign to..."); + setSearchTerm(""); + setPages([...pages, "change-issue-assignee"]); + }} + className="focus:outline-none" + > +
+ + Assign to... +
+
+ { + handleIssueAssignees(currentUser?.id ?? ""); + setSearchTerm(""); + }} + className="focus:outline-none" + > +
+ {issueDetails?.assignees.includes(currentUser?.id ?? "") ? ( + <> + + Un-assign from me + + ) : ( + <> + + Assign to me + + )} +
+
+ +
+ + Delete issue +
+
+ { + closePalette(); + copyIssueUrlToClipboard(); + }} + className="focus:outline-none" + > +
+ + Copy issue URL +
+
+
+ ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/components/command-palette/actions/issue-actions/change-assignee.tsx new file mode 100644 index 000000000..4b7ee6b99 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -0,0 +1,79 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { Check } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssueAssignee: React.FC = observer((props) => { + const { closePalette, issue } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store + const { + issueDetail: { updateIssue }, + projectMember: { projectMembers }, + } = useMobxStore(); + + const options = + projectMembers?.map(({ member }) => ({ + value: member.id, + query: member.display_name, + content: ( + <> +
+ + {member.display_name} +
+ {issue.assignees.includes(member.id) && ( +
+ +
+ )} + + ), + })) ?? []; + + const handleUpdateIssue = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueAssignees = (assignee: string) => { + const updatedAssignees = issue.assignees ?? []; + + if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + else updatedAssignees.push(assignee); + + handleUpdateIssue({ assignees: updatedAssignees }); + closePalette(); + }; + + return ( + <> + {options.map((option: any) => ( + handleIssueAssignees(option.value)} + className="focus:outline-none" + > + {option.content} + + ))} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-priority.tsx b/web/components/command-palette/actions/issue-actions/change-priority.tsx new file mode 100644 index 000000000..bda152c33 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-priority.tsx @@ -0,0 +1,56 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +import { Command } from "cmdk"; +import { Check } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { PriorityIcon } from "@plane/ui"; +// types +import { IIssue, TIssuePriorities } from "types"; +// constants +import { PRIORITIES } from "constants/project"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssuePriority: React.FC = observer((props) => { + const { closePalette, issue } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + issueDetail: { updateIssue }, + } = useMobxStore(); + + const submitChanges = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueState = (priority: TIssuePriorities) => { + submitChanges({ priority }); + closePalette(); + }; + + return ( + <> + {PRIORITIES.map((priority) => ( + handleIssueState(priority)} className="focus:outline-none"> +
+ + {priority ?? "None"} +
+
{priority === issue.priority && }
+
+ ))} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/change-state.tsx b/web/components/command-palette/actions/issue-actions/change-state.tsx new file mode 100644 index 000000000..5b147e499 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/change-state.tsx @@ -0,0 +1,65 @@ +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// cmdk +import { Command } from "cmdk"; +// ui +import { Spinner, StateGroupIcon } from "@plane/ui"; +// icons +import { Check } from "lucide-react"; +// types +import { IIssue } from "types"; + +type Props = { + closePalette: () => void; + issue: IIssue; +}; + +export const ChangeIssueState: React.FC = observer((props) => { + const { closePalette, issue } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + projectState: { projectStates }, + issueDetail: { updateIssue }, + } = useMobxStore(); + + const submitChanges = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !issue) return; + + const payload = { ...formData }; + await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => { + console.error(e); + }); + }; + + const handleIssueState = (stateId: string) => { + submitChanges({ state: stateId }); + closePalette(); + }; + + return ( + <> + {projectStates ? ( + projectStates.length > 0 ? ( + projectStates.map((state) => ( + handleIssueState(state.id)} className="focus:outline-none"> +
+ +

{state.name}

+
+
{state.id === issue.state && }
+
+ )) + ) : ( +
No states found
+ ) + ) : ( + + )} + + ); +}); diff --git a/web/components/command-palette/actions/issue-actions/index.ts b/web/components/command-palette/actions/issue-actions/index.ts new file mode 100644 index 000000000..305107d60 --- /dev/null +++ b/web/components/command-palette/actions/issue-actions/index.ts @@ -0,0 +1,4 @@ +export * from "./actions-list"; +export * from "./change-state"; +export * from "./change-priority"; +export * from "./change-assignee"; diff --git a/web/components/command-palette/actions/project-actions.tsx b/web/components/command-palette/actions/project-actions.tsx new file mode 100644 index 000000000..5db9b2c4e --- /dev/null +++ b/web/components/command-palette/actions/project-actions.tsx @@ -0,0 +1,83 @@ +import { Command } from "cmdk"; +import { ContrastIcon, FileText } from "lucide-react"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// ui +import { DiceIcon, PhotoFilterIcon } from "@plane/ui"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteProjectActions: React.FC = (props) => { + const { closePalette } = props; + + const { + commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal }, + } = useMobxStore(); + + return ( + <> + + { + closePalette(); + toggleCreateCycleModal(true); + }} + className="focus:outline-none" + > +
+ + Create new cycle +
+ Q +
+
+ + { + closePalette(); + toggleCreateModuleModal(true); + }} + className="focus:outline-none" + > +
+ + Create new module +
+ M +
+
+ + { + closePalette(); + toggleCreateViewModal(true); + }} + className="focus:outline-none" + > +
+ + Create new view +
+ V +
+
+ + { + closePalette(); + toggleCreatePageModal(true); + }} + className="focus:outline-none" + > +
+ + Create new page +
+ D +
+
+ + ); +}; diff --git a/web/components/command-palette/actions/search-results.tsx b/web/components/command-palette/actions/search-results.tsx new file mode 100644 index 000000000..791c62656 --- /dev/null +++ b/web/components/command-palette/actions/search-results.tsx @@ -0,0 +1,49 @@ +import { useRouter } from "next/router"; +import { Command } from "cmdk"; +// helpers +import { commandGroups } from "components/command-palette"; +// types +import { IWorkspaceSearchResults } from "types"; + +type Props = { + closePalette: () => void; + results: IWorkspaceSearchResults; +}; + +export const CommandPaletteSearchResults: React.FC = (props) => { + const { closePalette, results } = props; + + const router = useRouter(); + + return ( + <> + {Object.keys(results.results).map((key) => { + const section = (results.results as any)[key]; + const currentSection = commandGroups[key]; + + if (section.length > 0) { + return ( + + {section.map((item: any) => ( + { + closePalette(); + router.push(currentSection.path(item)); + }} + value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`} + className="focus:outline-none" + > +
+ {currentSection.icon} +

{currentSection.itemName(item)}

+
+
+ ))} +
+ ); + } + })} + + ); +}; diff --git a/web/components/command-palette/change-interface-theme.tsx b/web/components/command-palette/actions/theme-actions.tsx similarity index 74% rename from web/components/command-palette/change-interface-theme.tsx rename to web/components/command-palette/actions/theme-actions.tsx index 0b899f811..f7266a48a 100644 --- a/web/components/command-palette/change-interface-theme.tsx +++ b/web/components/command-palette/actions/theme-actions.tsx @@ -1,4 +1,4 @@ -import React, { FC, Dispatch, SetStateAction, useEffect, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; import { Command } from "cmdk"; import { useTheme } from "next-themes"; import { Settings } from "lucide-react"; @@ -10,22 +10,25 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { THEME_OPTIONS } from "constants/themes"; type Props = { - setIsPaletteOpen: Dispatch>; + closePalette: () => void; }; -export const ChangeInterfaceTheme: FC = observer((props) => { - const { setIsPaletteOpen } = props; - // store - const { user: userStore } = useMobxStore(); +export const CommandPaletteThemeActions: FC = observer((props) => { + const { closePalette } = props; // states const [mounted, setMounted] = useState(false); + // store + const { + user: { updateCurrentUserTheme }, + } = useMobxStore(); // hooks const { setTheme } = useTheme(); const { setToastAlert } = useToast(); - const updateUserTheme = (newTheme: string) => { + const updateUserTheme = async (newTheme: string) => { setTheme(newTheme); - return userStore.updateCurrentUserTheme(newTheme).catch(() => { + + return updateCurrentUserTheme(newTheme).catch(() => { setToastAlert({ title: "Failed to save user theme settings!", type: "error", @@ -47,7 +50,7 @@ export const ChangeInterfaceTheme: FC = observer((props) => { key={theme.value} onSelect={() => { updateUserTheme(theme.value); - setIsPaletteOpen(false); + closePalette(); }} className="focus:outline-none" > diff --git a/web/components/command-palette/actions/workspace-settings-actions.tsx b/web/components/command-palette/actions/workspace-settings-actions.tsx new file mode 100644 index 000000000..84e62593a --- /dev/null +++ b/web/components/command-palette/actions/workspace-settings-actions.tsx @@ -0,0 +1,61 @@ +import { useRouter } from "next/router"; +import { Command } from "cmdk"; +// icons +import { SettingIcon } from "components/icons"; + +type Props = { + closePalette: () => void; +}; + +export const CommandPaletteWorkspaceSettingsActions: React.FC = (props) => { + const { closePalette } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const redirect = (path: string) => { + closePalette(); + router.push(path); + }; + + return ( + <> + redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none"> +
+ + General +
+
+ redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none"> +
+ + Members +
+
+ redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none"> +
+ + Billing and Plans +
+
+ redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none"> +
+ + Integrations +
+
+ redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none"> +
+ + Import +
+
+ redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none"> +
+ + Export +
+
+ + ); +}; diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index bd066326d..f43032cf2 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -1,22 +1,10 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; -import { - FileText, - FolderPlus, - LinkIcon, - MessageSquare, - Rocket, - Search, - Settings, - Signal, - Trash2, - UserMinus2, - UserPlus2, -} from "lucide-react"; +import { FolderPlus, Search, Settings } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services @@ -24,47 +12,29 @@ import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; // hooks import useDebounce from "hooks/use-debounce"; -import useToast from "hooks/use-toast"; // components import { - ChangeInterfaceTheme, + CommandPaletteThemeActions, ChangeIssueAssignee, ChangeIssuePriority, ChangeIssueState, - commandGroups, + CommandPaletteHelpActions, + CommandPaletteIssueActions, + CommandPaletteProjectActions, + CommandPaletteWorkspaceSettingsActions, + CommandPaletteSearchResults, } from "components/command-palette"; -import { - ContrastIcon, - DiceIcon, - DoubleCircleIcon, - LayersIcon, - Loader, - PhotoFilterIcon, - ToggleSwitch, - Tooltip, - UserGroupIcon, -} from "@plane/ui"; -// icons -import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons"; -// helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types -import { IIssue, IWorkspaceSearchResults } from "types"; +import { IWorkspaceSearchResults } from "types"; // fetch-keys -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; - -type Props = { - deleteIssue: () => void; - isPaletteOpen: boolean; - closePalette: () => void; -}; +import { ISSUE_DETAILS } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); const issueService = new IssueService(); -export const CommandModal: React.FC = observer((props) => { - const { deleteIssue, isPaletteOpen, closePalette } = props; +export const CommandModal: React.FC = observer(() => { // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -85,8 +55,14 @@ export const CommandModal: React.FC = observer((props) => { const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); - const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore(); - const user = userStore.currentUser ?? undefined; + const { + commandPalette: { + isCommandPaletteOpen, + toggleCommandPaletteModal, + toggleCreateIssueModal, + toggleCreateProjectModal, + }, + } = useMobxStore(); // router const router = useRouter(); @@ -96,64 +72,16 @@ export const CommandModal: React.FC = observer((props) => { const debouncedSearchTerm = useDebounce(searchTerm, 500); - const { setToastAlert } = useToast(); - + // TODO: update this to mobx store const { data: issueDetails } = useSWR( - workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, workspaceSlug && projectId && issueId - ? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + ? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString()) : null ); - const updateIssue = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - - (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - mutate(ISSUE_DETAILS(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueAssignees = (assignee: string) => { - if (!issueDetails) return; - - closePalette(); - const updatedAssignees = issueDetails.assignees ?? []; - - if (updatedAssignees.includes(assignee)) { - updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); - } else { - updatedAssignees.push(assignee); - } - updateIssue({ assignees: updatedAssignees }); - }; - - const redirect = (path: string) => { - closePalette(); - router.push(path); + const closePalette = () => { + toggleCommandPaletteModal(false); }; const createNewWorkspace = () => { @@ -161,25 +89,6 @@ export const CommandModal: React.FC = observer((props) => { router.push("/create-workspace"); }; - const copyIssueUrlToClipboard = useCallback(() => { - if (!router.query.issueId) return; - - const url = new URL(window.location.href); - copyTextToClipboard(url.href) - .then(() => { - setToastAlert({ - type: "success", - title: "Copied to clipboard", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Some error occurred", - }); - }); - }, [router, setToastAlert]); - useEffect( () => { if (!workspaceSlug) return; @@ -189,7 +98,7 @@ export const CommandModal: React.FC = observer((props) => { if (debouncedSearchTerm) { setIsSearching(true); workspaceService - .searchWorkspace(workspaceSlug as string, { + .searchWorkspace(workspaceSlug.toString(), { ...(projectId ? { project_id: projectId.toString() } : {}), search: debouncedSearchTerm, workspace_search: !projectId ? true : isWorkspaceLevel, @@ -225,16 +134,8 @@ export const CommandModal: React.FC = observer((props) => { [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); - if (!user) return null; - return ( - { - setSearchTerm(""); - }} - as={React.Fragment} - > + setSearchTerm("")} as={React.Fragment}> closePalette()}> = observer((props) => { onKeyDown={(e) => { // when search is empty and page is undefined // when user tries to close the modal with esc - if (e.key === "Escape" && !page && !searchTerm) { - closePalette(); - } + if (e.key === "Escape" && !page && !searchTerm) closePalette(); + // Escape goes to previous page // Backspace goes to previous page when search is empty if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { @@ -318,9 +218,7 @@ export const CommandModal: React.FC = observer((props) => { className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm" placeholder={placeholder} value={searchTerm} - onValueChange={(e) => { - setSearchTerm(e); - }} + onValueChange={(e) => setSearchTerm(e)} autoFocus tabIndex={1} /> @@ -340,7 +238,7 @@ export const CommandModal: React.FC = observer((props) => { )} {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
No results found.
+
No results found.
)} {(isLoading || isSearching) && ( @@ -354,125 +252,28 @@ export const CommandModal: React.FC = observer((props) => { )} - {debouncedSearchTerm !== "" && - Object.keys(results.results).map((key) => { - const section = (results.results as any)[key]; - const currentSection = commandGroups[key]; - - if (section.length > 0) { - return ( - - {section.map((item: any) => ( - { - closePalette(); - router.push(currentSection.path(item)); - }} - value={`${key}-${item?.name}`} - className="focus:outline-none" - > -
- {currentSection.icon} -

{currentSection.itemName(item)}

-
-
- ))} -
- ); - } - })} + {debouncedSearchTerm !== "" && ( + + )} {!page && ( <> + {/* issue actions */} {issueId && ( - - { - closePalette(); - setPlaceholder("Change state..."); - setSearchTerm(""); - setPages([...pages, "change-issue-state"]); - }} - className="focus:outline-none" - > -
- - Change state... -
-
- { - setPlaceholder("Change priority..."); - setSearchTerm(""); - setPages([...pages, "change-issue-priority"]); - }} - className="focus:outline-none" - > -
- - Change priority... -
-
- { - setPlaceholder("Assign to..."); - setSearchTerm(""); - setPages([...pages, "change-issue-assignee"]); - }} - className="focus:outline-none" - > -
- - Assign to... -
-
- { - handleIssueAssignees(user.id); - setSearchTerm(""); - }} - className="focus:outline-none" - > -
- {issueDetails?.assignees.includes(user.id) ? ( - <> - - Un-assign from me - - ) : ( - <> - - Assign to me - - )} -
-
- -
- - Delete issue -
-
- { - closePalette(); - copyIssueUrlToClipboard(); - }} - className="focus:outline-none" - > -
- - Copy issue URL -
-
-
+ setPages(newPages)} + setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} + setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} + /> )} { closePalette(); - commandPaletteStore.toggleCreateIssueModal(true); + toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" > @@ -489,7 +290,7 @@ export const CommandModal: React.FC = observer((props) => { { closePalette(); - commandPaletteStore.toggleCreateProjectModal(true); + toggleCreateProjectModal(true); }} className="focus:outline-none" > @@ -502,70 +303,8 @@ export const CommandModal: React.FC = observer((props) => { )} - {projectId && ( - <> - - { - closePalette(); - commandPaletteStore.toggleCreateCycleModal(true); - }} - className="focus:outline-none" - > -
- - Create new cycle -
- Q -
-
- - { - closePalette(); - commandPaletteStore.toggleCreateModuleModal(true); - }} - className="focus:outline-none" - > -
- - Create new module -
- M -
-
- - { - closePalette(); - commandPaletteStore.toggleCreateViewModal(true); - }} - className="focus:outline-none" - > -
- - Create new view -
- V -
-
- - { - closePalette(); - commandPaletteStore.toggleCreatePageModal(true); - }} - className="focus:outline-none" - > -
- - Create new page -
- D -
-
- - )} + {/* project actions */} + {projectId && } = observer((props) => { - - { - closePalette(); - commandPaletteStore.toggleShortcutModal(true); - }} - className="focus:outline-none" - > -
- - Open keyboard shortcuts -
-
- { - closePalette(); - window.open("https://docs.plane.so/", "_blank"); - }} - className="focus:outline-none" - > -
- - Open Plane documentation -
-
- { - closePalette(); - window.open("https://discord.com/invite/A92xrEGCge", "_blank"); - }} - className="focus:outline-none" - > -
- - Join our Discord -
-
- { - closePalette(); - window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"); - }} - className="focus:outline-none" - > -
- - Report a bug -
-
- { - closePalette(); - (window as any).$crisp.push(["do", "chat:open"]); - }} - className="focus:outline-none" - > -
- - Chat with us -
-
-
+ + {/* help options */} + )} + {/* workspace settings actions */} {page === "settings" && workspaceSlug && ( - <> - redirect(`/${workspaceSlug}/settings`)} - className="focus:outline-none" - > -
- - General -
-
- redirect(`/${workspaceSlug}/settings/members`)} - className="focus:outline-none" - > -
- - Members -
-
- redirect(`/${workspaceSlug}/settings/billing`)} - className="focus:outline-none" - > -
- - Billing and Plans -
-
- redirect(`/${workspaceSlug}/settings/integrations`)} - className="focus:outline-none" - > -
- - Integrations -
-
- redirect(`/${workspaceSlug}/settings/imports`)} - className="focus:outline-none" - > -
- - Import -
-
- redirect(`/${workspaceSlug}/settings/exports`)} - className="focus:outline-none" - > -
- - Export -
-
- + )} + + {/* issue details page actions */} {page === "change-issue-state" && issueDetails && ( - + )} {page === "change-issue-priority" && issueDetails && ( - + )} {page === "change-issue-assignee" && issueDetails && ( - + + )} + + {/* theme actions */} + {page === "change-interface-theme" && ( + { + closePalette(); + setPages((pages) => pages.slice(0, -1)); + }} + /> )} - {page === "change-interface-theme" && } diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 0ca293475..7708ff926 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -32,7 +32,6 @@ export const CommandPalette: FC = observer(() => { // store const { commandPalette, theme: themeStore } = useMobxStore(); const { - isCommandPaletteOpen, toggleCommandPaletteModal, isCreateIssueModalOpen, toggleCreateIssueModal, @@ -156,11 +155,6 @@ export const CommandPalette: FC = observer(() => { if (!user) return null; - const deleteIssue = () => { - toggleCommandPaletteModal(false); - toggleDeleteIssueModal(true); - }; - return ( <> { }} user={user} /> - { - toggleCommandPaletteModal(false); - }} - /> + ); }); diff --git a/web/components/command-palette/helpers.tsx b/web/components/command-palette/helpers.tsx index 90511f907..cfa21cede 100644 --- a/web/components/command-palette/helpers.tsx +++ b/web/components/command-palette/helpers.tsx @@ -20,9 +20,7 @@ export const commandGroups: { icon: , itemName: (cycle: IWorkspaceDefaultSearchResult) => (
- {cycle.project__identifier} - {"- "} - {cycle.name} + {cycle.project__identifier} {cycle.name}
), path: (cycle: IWorkspaceDefaultSearchResult) => @@ -33,8 +31,9 @@ export const commandGroups: { icon: , itemName: (issue: IWorkspaceIssueSearchResult) => (
- {issue.project__identifier} - {"- "} + + {issue.project__identifier}-{issue.sequence_id} + {" "} {issue.name}
), @@ -46,9 +45,7 @@ export const commandGroups: { icon: , itemName: (view: IWorkspaceDefaultSearchResult) => (
- {view.project__identifier} - {"- "} - {view.name} + {view.project__identifier} {view.name}
), path: (view: IWorkspaceDefaultSearchResult) => @@ -59,9 +56,7 @@ export const commandGroups: { icon: , itemName: (module: IWorkspaceDefaultSearchResult) => (
- {module.project__identifier} - {"- "} - {module.name} + {module.project__identifier} {module.name}
), path: (module: IWorkspaceDefaultSearchResult) => @@ -72,9 +67,7 @@ export const commandGroups: { icon: , itemName: (page: IWorkspaceDefaultSearchResult) => (
- {page.project__identifier} - {"- "} - {page.name} + {page.project__identifier} {page.name}
), path: (page: IWorkspaceDefaultSearchResult) => diff --git a/web/components/command-palette/index.ts b/web/components/command-palette/index.ts index a1230ca32..1fac3f134 100644 --- a/web/components/command-palette/index.ts +++ b/web/components/command-palette/index.ts @@ -1,5 +1,4 @@ -export * from "./issue"; -export * from "./change-interface-theme"; +export * from "./actions"; export * from "./command-modal"; export * from "./command-pallette"; export * from "./helpers"; diff --git a/web/components/command-palette/issue/change-issue-assignee.tsx b/web/components/command-palette/issue/change-issue-assignee.tsx deleted file mode 100644 index 1693a6b94..000000000 --- a/web/components/command-palette/issue/change-issue-assignee.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { Dispatch, SetStateAction, useCallback, FC } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; -import { Command } from "cmdk"; -import { Check } from "lucide-react"; -// mobx store -import { useMobxStore } from "lib/mobx/store-provider"; -// services -import { IssueService } from "services/issue"; -// ui -import { Avatar } from "@plane/ui"; -// types -import { IUser, IIssue } from "types"; -// constants -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser | undefined; -}; - -// services -const issueService = new IssueService(); - -export const ChangeIssueAssignee: FC = observer((props) => { - const { setIsPaletteOpen, issue } = props; - // router - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - // store - const { - projectMember: { projectMembers }, - } = useMobxStore(); - - const options = - projectMembers?.map(({ member }) => ({ - value: member.id, - query: member.display_name, - content: ( - <> -
- - {member.display_name} -
- {issue.assignees.includes(member.id) && ( -
- -
- )} - - ), - })) ?? []; - - const updateIssue = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueAssignees = (assignee: string) => { - const updatedAssignees = issue.assignees ?? []; - - if (updatedAssignees.includes(assignee)) { - updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); - } else { - updatedAssignees.push(assignee); - } - - updateIssue({ assignees: updatedAssignees }); - setIsPaletteOpen(false); - }; - - return ( - <> - {options.map((option: any) => ( - handleIssueAssignees(option.value)} - className="focus:outline-none" - > - {option.content} - - ))} - - ); -}); diff --git a/web/components/command-palette/issue/change-issue-priority.tsx b/web/components/command-palette/issue/change-issue-priority.tsx deleted file mode 100644 index c5e4bfa2a..000000000 --- a/web/components/command-palette/issue/change-issue-priority.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Dispatch, SetStateAction, useCallback } from "react"; -import { useRouter } from "next/router"; -import { mutate } from "swr"; -// cmdk -import { Command } from "cmdk"; -// services -import { IssueService } from "services/issue"; -// types -import { IIssue, IUser, TIssuePriorities } from "types"; -// constants -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; -import { PRIORITIES } from "constants/project"; -// icons -import { PriorityIcon } from "@plane/ui"; -import { Check } from "lucide-react"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser; -}; - -// services -const issueService = new IssueService(); - -export const ChangeIssuePriority: React.FC = ({ setIsPaletteOpen, issue }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId] - ); - - const handleIssueState = (priority: TIssuePriorities) => { - submitChanges({ priority }); - setIsPaletteOpen(false); - }; - - return ( - <> - {PRIORITIES.map((priority) => ( - handleIssueState(priority)} className="focus:outline-none"> -
- - {priority ?? "None"} -
-
{priority === issue.priority && }
-
- ))} - - ); -}; diff --git a/web/components/command-palette/issue/change-issue-state.tsx b/web/components/command-palette/issue/change-issue-state.tsx deleted file mode 100644 index cbddfb688..000000000 --- a/web/components/command-palette/issue/change-issue-state.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { Dispatch, SetStateAction, useCallback } from "react"; -import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; -// cmdk -import { Command } from "cmdk"; -// services -import { IssueService } from "services/issue"; -import { ProjectStateService } from "services/project"; -// ui -import { Spinner, StateGroupIcon } from "@plane/ui"; -// icons -import { Check } from "lucide-react"; -// types -import { IUser, IIssue } from "types"; -// fetch keys -import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys"; - -type Props = { - setIsPaletteOpen: Dispatch>; - issue: IIssue; - user: IUser | undefined; -}; - -// services -const issueService = new IssueService(); -const stateService = new ProjectStateService(); - -export const ChangeIssueState: React.FC = ({ setIsPaletteOpen, issue }) => { - const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; - - const { data: states, mutate: mutateStates } = useSWR( - workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, - workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null - ); - - const submitChanges = useCallback( - async (formData: Partial) => { - if (!workspaceSlug || !projectId || !issueId) return; - - mutate( - ISSUE_DETAILS(issueId as string), - async (prevData) => { - if (!prevData) return prevData; - return { - ...prevData, - ...formData, - }; - }, - false - ); - - const payload = { ...formData }; - await issueService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) - .then(() => { - mutateStates(); - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId, mutateStates] - ); - - const handleIssueState = (stateId: string) => { - submitChanges({ state: stateId }); - setIsPaletteOpen(false); - }; - - return ( - <> - {states ? ( - states.length > 0 ? ( - states.map((state) => ( - handleIssueState(state.id)} className="focus:outline-none"> -
- -

{state.name}

-
-
{state.id === issue.state && }
-
- )) - ) : ( -
No states found
- ) - ) : ( - - )} - - ); -}; diff --git a/web/components/command-palette/issue/index.ts b/web/components/command-palette/issue/index.ts deleted file mode 100644 index 7d0bbd05d..000000000 --- a/web/components/command-palette/issue/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./change-issue-state"; -export * from "./change-issue-priority"; -export * from "./change-issue-assignee";