diff --git a/apps/app/components/command-palette/command-k.tsx b/apps/app/components/command-palette/command-k.tsx new file mode 100644 index 000000000..75d7e5bcc --- /dev/null +++ b/apps/app/components/command-palette/command-k.tsx @@ -0,0 +1,776 @@ +import React, { useCallback, useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// cmdk +import { Command } from "cmdk"; +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// services +import workspaceService from "services/workspace.service"; +import issuesService from "services/issues.service"; +import inboxService from "services/inbox.service"; +// hooks +import useProjectDetails from "hooks/use-project-details"; +import useDebounce from "hooks/use-debounce"; +import useUser from "hooks/use-user"; +import useToast from "hooks/use-toast"; +// components +import { + ChangeInterfaceTheme, + ChangeIssueAssignee, + ChangeIssuePriority, + ChangeIssueState, + commandGroups, +} from "components/command-palette"; +// ui +import { Icon, Loader, ToggleSwitch, Tooltip } from "components/ui"; +// icons +import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons"; +import { InboxIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { IIssue, IWorkspaceSearchResults } from "types"; +// fetch-keys +import { INBOX_LIST, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; + +type Props = { + deleteIssue: () => void; + isPaletteOpen: boolean; + setIsPaletteOpen: React.Dispatch>; +}; + +export const CommandK: React.FC = ({ deleteIssue, isPaletteOpen, setIsPaletteOpen }) => { + const [placeholder, setPlaceholder] = useState("Type a command or search..."); + + const [resultsCount, setResultsCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [results, setResults] = useState({ + results: { + workspace: [], + project: [], + issue: [], + cycle: [], + module: [], + issue_view: [], + page: [], + }, + }); + const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); + + const [pages, setPages] = useState([]); + const page = pages[pages.length - 1]; + + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + + const { setToastAlert } = useToast(); + + const { user } = useUser(); + const { projectDetails } = useProjectDetails(); + + const { data: issueDetails } = useSWR( + workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, + workspaceSlug && projectId && issueId + ? () => + issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string) + : null + ); + + const { data: inboxList } = useSWR( + workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) + : 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 issuesService + .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) + .then(() => { + mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); + mutate(ISSUE_DETAILS(issueId as string)); + }) + .catch((e) => { + console.error(e); + }); + }, + [workspaceSlug, issueId, projectId, user] + ); + + const handleIssueAssignees = (assignee: string) => { + if (!issueDetails) return; + + setIsPaletteOpen(false); + const updatedAssignees = issueDetails.assignees ?? []; + + if (updatedAssignees.includes(assignee)) { + updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); + } else { + updatedAssignees.push(assignee); + } + updateIssue({ assignees_list: updatedAssignees }); + }; + + const redirect = (path: string) => { + setIsPaletteOpen(false); + router.push(path); + }; + + const createNewWorkspace = () => { + setIsPaletteOpen(false); + 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; + + setIsLoading(true); + + if (debouncedSearchTerm) { + setIsSearching(true); + workspaceService + .searchWorkspace(workspaceSlug as string, { + ...(projectId ? { project_id: projectId.toString() } : {}), + search: debouncedSearchTerm, + workspace_search: !projectId ? true : isWorkspaceLevel, + }) + .then((results) => { + setResults(results); + const count = Object.keys(results.results).reduce( + (accumulator, key) => (results.results as any)[key].length + accumulator, + 0 + ); + setResultsCount(count); + }) + .finally(() => { + setIsLoading(false); + setIsSearching(false); + }); + } else { + setResults({ + results: { + workspace: [], + project: [], + issue: [], + cycle: [], + module: [], + issue_view: [], + page: [], + }, + }); + setIsLoading(false); + setIsSearching(false); + } + }, + [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes + ); + + if (!user) return null; + + return ( + { + setSearchTerm(""); + }} + as={React.Fragment} + > + setIsPaletteOpen(false)}> + +
+ + +
+ + + { + if (value.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} + 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) { + setIsPaletteOpen(false); + } + // Escape goes to previous page + // Backspace goes to previous page when search is empty + if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { + e.preventDefault(); + setPages((pages) => pages.slice(0, -1)); + setPlaceholder("Type a command or search..."); + } + }} + > +
+ {issueDetails && ( +
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "} + {issueDetails.name} +
+ )} + {projectId && ( + +
+ + setIsWorkspaceLevel((prevData) => !prevData)} + /> +
+
+ )} +
+
+
+ + + {searchTerm !== "" && ( +
+ Search results for{" "} + + {'"'} + {searchTerm} + {'"'} + {" "} + in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: +
+ )} + + {!isLoading && + resultsCount === 0 && + searchTerm !== "" && + debouncedSearchTerm !== "" && ( +
No results found.
+ )} + + {(isLoading || isSearching) && ( + + + + + + + + + )} + + {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) => ( + { + router.push(currentSection.path(item)); + setIsPaletteOpen(false); + }} + value={`${key}-${item?.name}`} + className="focus:outline-none" + > +
+ +

+ {currentSection.itemName(item)} +

+
+
+ ))} +
+ ); + } + })} + + {!page && ( + <> + {issueId && ( + + { + 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 +
+
+ { + setIsPaletteOpen(false); + copyIssueUrlToClipboard(); + }} + className="focus:outline-none" + > +
+ + Copy issue URL +
+
+
+ )} + + { + const e = new KeyboardEvent("keydown", { + key: "c", + }); + document.dispatchEvent(e); + }} + className="focus:bg-custom-background-80" + > +
+ + Create new issue +
+ C +
+
+ + {workspaceSlug && ( + + { + const e = new KeyboardEvent("keydown", { + key: "p", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new project +
+ P +
+
+ )} + + {projectId && ( + <> + + { + const e = new KeyboardEvent("keydown", { + key: "q", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new cycle +
+ Q +
+
+ + { + const e = new KeyboardEvent("keydown", { + key: "m", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new module +
+ M +
+
+ + { + const e = new KeyboardEvent("keydown", { + key: "v", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new view +
+ V +
+
+ + { + const e = new KeyboardEvent("keydown", { + key: "d", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Create new page +
+ D +
+
+ {projectDetails && projectDetails.inbox_view && ( + + + redirect( + `/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}` + ) + } + className="focus:outline-none" + > +
+ + Open inbox +
+
+
+ )} + + )} + + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:outline-none" + > +
+ + Search settings... +
+
+
+ + +
+ + Create new workspace +
+
+ { + setPlaceholder("Change interface theme..."); + setSearchTerm(""); + setPages([...pages, "change-interface-theme"]); + }} + className="focus:outline-none" + > +
+ + Change interface theme... +
+
+
+ + { + setIsPaletteOpen(false); + const e = new KeyboardEvent("keydown", { + key: "h", + }); + document.dispatchEvent(e); + }} + className="focus:outline-none" + > +
+ + Open keyboard shortcuts +
+
+ { + setIsPaletteOpen(false); + window.open("https://docs.plane.so/", "_blank"); + }} + className="focus:outline-none" + > +
+ + Open Plane documentation +
+
+ { + setIsPaletteOpen(false); + window.open("https://discord.com/invite/A92xrEGCge", "_blank"); + }} + className="focus:outline-none" + > +
+ + Join our Discord +
+
+ { + setIsPaletteOpen(false); + window.open( + "https://github.com/makeplane/plane/issues/new/choose", + "_blank" + ); + }} + className="focus:outline-none" + > +
+ + Report a bug +
+
+ { + setIsPaletteOpen(false); + (window as any).$crisp.push(["do", "chat:open"]); + }} + className="focus:outline-none" + > +
+ + Chat with us +
+
+
+ + )} + + {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/import-export`)} + className="focus:outline-none" + > +
+ + Import/Export +
+
+ + )} + {page === "change-issue-state" && issueDetails && ( + + )} + {page === "change-issue-priority" && issueDetails && ( + + )} + {page === "change-issue-assignee" && issueDetails && ( + + )} + {page === "change-interface-theme" && ( + + )} +
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 37091fb75..0b4c9577b 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -2,29 +2,14 @@ import React, { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; -// icons -import { InboxIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons"; -// headless ui -import { Dialog, Transition } from "@headlessui/react"; -// cmdk -import { Command } from "cmdk"; // hooks -import useProjectDetails from "hooks/use-project-details"; import useTheme from "hooks/use-theme"; import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; -import useDebounce from "hooks/use-debounce"; // components -import { - ShortcutsModal, - ChangeIssueState, - ChangeIssuePriority, - ChangeIssueAssignee, - ChangeInterfaceTheme, -} from "components/command-palette"; +import { CommandK, ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; import { CreateUpdateCycleModal } from "components/cycles"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; @@ -32,83 +17,13 @@ import { CreateUpdateModuleModal } from "components/modules"; import { CreateProjectModal } from "components/project"; import { CreateUpdateViewModal } from "components/views"; import { CreateUpdatePageModal } from "components/pages"; - -import { Icon, Loader, ToggleSwitch, Tooltip } from "components/ui"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; // services import issuesService from "services/issues.service"; -import workspaceService from "services/workspace.service"; import inboxService from "services/inbox.service"; -// types -import { - IIssue, - IWorkspaceDefaultSearchResult, - IWorkspaceIssueSearchResult, - IWorkspaceProjectSearchResult, - IWorkspaceSearchResult, - IWorkspaceSearchResults, -} from "types"; // fetch keys -import { INBOX_LIST, ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys"; - -const commandGroups: { - [key: string]: { - icon: string; - itemName: (item: any) => React.ReactNode; - path: (item: any) => string; - title: string; - }; -} = { - cycle: { - icon: "contrast", - itemName: (cycle: IWorkspaceDefaultSearchResult) => cycle?.name, - path: (cycle: IWorkspaceDefaultSearchResult) => - `/${cycle?.workspace__slug}/projects/${cycle?.project_id}/cycles/${cycle?.id}`, - title: "Cycles", - }, - issue: { - icon: "stack", - itemName: (issue: IWorkspaceIssueSearchResult) => issue?.name, - path: (issue: IWorkspaceIssueSearchResult) => - `/${issue?.workspace__slug}/projects/${issue?.project_id}/issues/${issue?.id}`, - title: "Issues", - }, - issue_view: { - icon: "photo_filter", - itemName: (view: IWorkspaceDefaultSearchResult) => view?.name, - path: (view: IWorkspaceDefaultSearchResult) => - `/${view?.workspace__slug}/projects/${view?.project_id}/views/${view?.id}`, - title: "Views", - }, - module: { - icon: "dataset", - itemName: (module: IWorkspaceDefaultSearchResult) => module?.name, - path: (module: IWorkspaceDefaultSearchResult) => - `/${module?.workspace__slug}/projects/${module?.project_id}/modules/${module?.id}`, - title: "Modules", - }, - page: { - icon: "article", - itemName: (page: IWorkspaceDefaultSearchResult) => page?.name, - path: (page: IWorkspaceDefaultSearchResult) => - `/${page?.workspace__slug}/projects/${page?.project_id}/pages/${page?.id}`, - title: "Pages", - }, - project: { - icon: "work", - itemName: (project: IWorkspaceProjectSearchResult) => project?.name, - path: (project: IWorkspaceProjectSearchResult) => - `/${project?.workspace__slug}/projects/${project?.id}/issues/`, - title: "Projects", - }, - workspace: { - icon: "grid_view", - itemName: (workspace: IWorkspaceSearchResult) => workspace?.name, - path: (workspace: IWorkspaceSearchResult) => `/${workspace?.slug}/`, - title: "Workspaces", - }, -}; +import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; export const CommandPalette: React.FC = () => { const [isPaletteOpen, setIsPaletteOpen] = useState(false); @@ -122,33 +37,10 @@ export const CommandPalette: React.FC = () => { const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [results, setResults] = useState({ - results: { - workspace: [], - project: [], - issue: [], - cycle: [], - module: [], - issue_view: [], - page: [], - }, - }); - const [resultsCount, setResultsCount] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [isSearching, setIsSearching] = useState(false); - const debouncedSearchTerm = useDebounce(searchTerm, 500); - const [placeholder, setPlaceholder] = React.useState("Type a command or search..."); - const [pages, setPages] = useState([]); - const page = pages[pages.length - 1]; - - const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); - const router = useRouter(); const { workspaceSlug, projectId, issueId, inboxId } = router.query; const { user } = useUser(); - const { projectDetails } = useProjectDetails(); const { setToastAlert } = useToast(); const { toggleCollapsed } = useTheme(); @@ -161,59 +53,6 @@ export const CommandPalette: React.FC = () => { : null ); - const { data: inboxList } = useSWR( - workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) - : 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 issuesService - .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload, user) - .then(() => { - mutate(PROJECT_ISSUES_ACTIVITY(issueId as string)); - mutate(ISSUE_DETAILS(issueId as string)); - }) - .catch((e) => { - console.error(e); - }); - }, - [workspaceSlug, issueId, projectId, user] - ); - - const handleIssueAssignees = (assignee: string) => { - if (!issueDetails) return; - - setIsPaletteOpen(false); - const updatedAssignees = issueDetails.assignees ?? []; - - if (updatedAssignees.includes(assignee)) { - updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1); - } else { - updatedAssignees.push(assignee); - } - updateIssue({ assignees_list: updatedAssignees }); - }; - const copyIssueUrlToClipboard = useCallback(() => { if (!router.query.issueId) return; @@ -290,98 +129,13 @@ export const CommandPalette: React.FC = () => { return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); - useEffect( - () => { - if (!workspaceSlug) return; - - setIsLoading(true); - - if (debouncedSearchTerm) { - setIsSearching(true); - workspaceService - .searchWorkspace(workspaceSlug as string, { - ...(projectId ? { project_id: projectId.toString() } : {}), - search: debouncedSearchTerm, - workspace_search: !projectId ? true : isWorkspaceLevel, - }) - .then((results) => { - setResults(results); - const count = Object.keys(results.results).reduce( - (accumulator, key) => (results.results as any)[key].length + accumulator, - 0 - ); - setResultsCount(count); - }) - .finally(() => { - setIsLoading(false); - setIsSearching(false); - }); - } else { - setResults({ - results: { - workspace: [], - project: [], - issue: [], - cycle: [], - module: [], - issue_view: [], - page: [], - }, - }); - setIsLoading(false); - setIsSearching(false); - } - }, - [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes - ); - if (!user) return null; - const createNewWorkspace = () => { - setIsPaletteOpen(false); - router.push("/create-workspace"); - }; - - const createNewProject = () => { - setIsPaletteOpen(false); - setIsProjectModalOpen(true); - }; - - const createNewIssue = () => { - setIsPaletteOpen(false); - setIsIssueModalOpen(true); - }; - - const createNewCycle = () => { - setIsPaletteOpen(false); - setIsCreateCycleModalOpen(true); - }; - - const createNewView = () => { - setIsPaletteOpen(false); - setIsCreateViewModalOpen(true); - }; - - const createNewPage = () => { - setIsPaletteOpen(false); - setIsCreateUpdatePageModalOpen(true); - }; - - const createNewModule = () => { - setIsPaletteOpen(false); - setIsCreateModuleModalOpen(true); - }; - const deleteIssue = () => { setIsPaletteOpen(false); setDeleteIssueModal(true); }; - const redirect = (path: string) => { - setIsPaletteOpen(false); - router.push(path); - }; - return ( <> @@ -434,531 +188,11 @@ export const CommandPalette: React.FC = () => { setIsOpen={setIsBulkDeleteIssuesModalOpen} user={user} /> - { - setSearchTerm(""); - }} - as={React.Fragment} - > - setIsPaletteOpen(false)}> - -
- - -
- - - { - if (value.toLowerCase().includes(search.toLowerCase())) return 1; - return 0; - }} - 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) { - setIsPaletteOpen(false); - } - // Escape goes to previous page - // Backspace goes to previous page when search is empty - if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { - e.preventDefault(); - setPages((pages) => pages.slice(0, -1)); - setPlaceholder("Type a command or search..."); - } - }} - > -
- {issueDetails && ( -
- {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "} - {issueDetails.name} -
- )} - {projectId && ( - -
- - setIsWorkspaceLevel((prevData) => !prevData)} - /> -
-
- )} -
-
-
- - - {!page && ( - <> - {issueId && ( - - { - 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 -
-
- { - setIsPaletteOpen(false); - copyIssueUrlToClipboard(); - }} - className="focus:outline-none" - > -
- - Copy issue URL -
-
-
- )} - - -
- - Create new issue -
- C -
-
- - {workspaceSlug && ( - - -
- - Create new project -
- P -
-
- )} - - {projectId && ( - <> - - -
- - Create new cycle -
- Q -
-
- - -
- - Create new module -
- M -
-
- - -
- - Create new view -
- V -
-
- - -
- - Create new page -
- D -
-
- {projectDetails && projectDetails.inbox_view && ( - - - redirect( - `/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}` - ) - } - className="focus:outline-none" - > -
- - Open inbox -
-
-
- )} - - )} - - - { - setPlaceholder("Search workspace settings..."); - setSearchTerm(""); - setPages([...pages, "settings"]); - }} - className="focus:outline-none" - > -
- - Search settings... -
-
-
- - -
- - Create new workspace -
-
- { - setPlaceholder("Change interface theme..."); - setSearchTerm(""); - setPages([...pages, "change-interface-theme"]); - }} - className="focus:outline-none" - > -
- - Change interface theme... -
-
-
- - { - setIsPaletteOpen(false); - const e = new KeyboardEvent("keydown", { - key: "h", - }); - document.dispatchEvent(e); - }} - className="focus:outline-none" - > -
- - Open keyboard shortcuts -
-
- { - setIsPaletteOpen(false); - window.open("https://docs.plane.so/", "_blank"); - }} - className="focus:outline-none" - > -
- - Open Plane documentation -
-
- { - setIsPaletteOpen(false); - window.open("https://discord.com/invite/A92xrEGCge", "_blank"); - }} - className="focus:outline-none" - > -
- - Join our Discord -
-
- { - setIsPaletteOpen(false); - window.open( - "https://github.com/makeplane/plane/issues/new/choose", - "_blank" - ); - }} - className="focus:outline-none" - > -
- - Report a bug -
-
- { - setIsPaletteOpen(false); - (window as any).$crisp.push(["do", "chat:open"]); - }} - className="focus:outline-none" - > -
- - Chat with us -
-
-
- - )} - - {searchTerm !== "" && ( -
- Search results for{" "} - - {'"'} - {searchTerm} - {'"'} - {" "} - in project: -
- )} - - {!isLoading && - resultsCount === 0 && - searchTerm !== "" && - debouncedSearchTerm !== "" && ( -
- No results found. -
- )} - - {(isLoading || isSearching) && ( - - - - - - - - - )} - - {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) => ( - { - router.push(currentSection.path(item)); - setIsPaletteOpen(false); - }} - value={`${key}-${item?.name}`} - className="focus:outline-none" - > -
- -

{item.name}

-
-
- ))} -
- ); - } - })} - - {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/import-export`)} - className="focus:outline-none" - > -
- - Import/Export -
-
- - )} - {page === "change-issue-state" && issueDetails && ( - - )} - {page === "change-issue-priority" && issueDetails && ( - - )} - {page === "change-issue-assignee" && issueDetails && ( - - )} - {page === "change-interface-theme" && ( - - )} -
-
-
-
-
-
-
+ ); }; diff --git a/apps/app/components/command-palette/helpers.tsx b/apps/app/components/command-palette/helpers.tsx new file mode 100644 index 000000000..0b232b7dc --- /dev/null +++ b/apps/app/components/command-palette/helpers.tsx @@ -0,0 +1,95 @@ +// types +import { + IWorkspaceDefaultSearchResult, + IWorkspaceIssueSearchResult, + IWorkspaceProjectSearchResult, + IWorkspaceSearchResult, +} from "types"; + +export const commandGroups: { + [key: string]: { + icon: string; + itemName: (item: any) => React.ReactNode; + path: (item: any) => string; + title: string; + }; +} = { + cycle: { + icon: "contrast", + itemName: (cycle: IWorkspaceDefaultSearchResult) => ( +
+ {cycle.project__identifier} + {"- "} + {cycle.name} +
+ ), + path: (cycle: IWorkspaceDefaultSearchResult) => + `/${cycle?.workspace__slug}/projects/${cycle?.project_id}/cycles/${cycle?.id}`, + title: "Cycles", + }, + issue: { + icon: "stack", + itemName: (issue: IWorkspaceIssueSearchResult) => ( +
+ {issue.project__identifier} + {"- "} + {issue.name} +
+ ), + path: (issue: IWorkspaceIssueSearchResult) => + `/${issue?.workspace__slug}/projects/${issue?.project_id}/issues/${issue?.id}`, + title: "Issues", + }, + issue_view: { + icon: "photo_filter", + itemName: (view: IWorkspaceDefaultSearchResult) => ( +
+ {view.project__identifier} + {"- "} + {view.name} +
+ ), + path: (view: IWorkspaceDefaultSearchResult) => + `/${view?.workspace__slug}/projects/${view?.project_id}/views/${view?.id}`, + title: "Views", + }, + module: { + icon: "dataset", + itemName: (module: IWorkspaceDefaultSearchResult) => ( +
+ {module.project__identifier} + {"- "} + {module.name} +
+ ), + path: (module: IWorkspaceDefaultSearchResult) => + `/${module?.workspace__slug}/projects/${module?.project_id}/modules/${module?.id}`, + title: "Modules", + }, + page: { + icon: "article", + itemName: (page: IWorkspaceDefaultSearchResult) => ( +
+ {page.project__identifier} + {"- "} + {page.name} +
+ ), + path: (page: IWorkspaceDefaultSearchResult) => + `/${page?.workspace__slug}/projects/${page?.project_id}/pages/${page?.id}`, + title: "Pages", + }, + project: { + icon: "work", + itemName: (project: IWorkspaceProjectSearchResult) => project?.name, + path: (project: IWorkspaceProjectSearchResult) => + `/${project?.workspace__slug}/projects/${project?.id}/issues/`, + title: "Projects", + }, + workspace: { + icon: "grid_view", + itemName: (workspace: IWorkspaceSearchResult) => workspace?.name, + path: (workspace: IWorkspaceSearchResult) => `/${workspace?.slug}/`, + title: "Workspaces", + }, +}; diff --git a/apps/app/components/command-palette/index.ts b/apps/app/components/command-palette/index.ts index 9e441e2bb..6c137d6df 100644 --- a/apps/app/components/command-palette/index.ts +++ b/apps/app/components/command-palette/index.ts @@ -1,4 +1,6 @@ export * from "./issue"; export * from "./change-interface-theme"; +export * from "./command-k"; export * from "./command-pallette"; +export * from "./helpers"; export * from "./shortcuts-modal"; diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index 619d6c31c..cbc93c6b9 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -77,6 +77,7 @@ export interface IWorkspaceDefaultSearchResult { id: string; name: string; project_id: string; + project__identifier: string; workspace__slug: string; } export interface IWorkspaceSearchResult {