import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Command } from "cmdk"; import { Dialog, Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite"; import { FolderPlus, Search, Settings } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services import { WorkspaceService } from "services/workspace.service"; import { IssueService } from "services/issue"; // hooks import useDebounce from "hooks/use-debounce"; // components import { CommandPaletteThemeActions, ChangeIssueAssignee, ChangeIssuePriority, ChangeIssueState, CommandPaletteHelpActions, CommandPaletteIssueActions, CommandPaletteProjectActions, CommandPaletteWorkspaceSettingsActions, CommandPaletteSearchResults, } from "components/command-palette"; import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // types import { IWorkspaceSearchResults } from "types"; // fetch-keys import { ISSUE_DETAILS } from "constants/fetch-keys"; // services const workspaceService = new WorkspaceService(); const issueService = new IssueService(); export const CommandModal: React.FC = observer(() => { // states 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 { commandPalette: { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal, }, trackEvent: { setTrackElement }, } = useMobxStore(); // router const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; const page = pages[pages.length - 1]; const debouncedSearchTerm = useDebounce(searchTerm, 500); // TODO: update this to mobx store const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null, workspaceSlug && projectId && issueId ? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString()) : null ); const closePalette = () => { toggleCommandPaletteModal(false); }; const createNewWorkspace = () => { closePalette(); router.push("/create-workspace"); }; useEffect( () => { if (!workspaceSlug) return; setIsLoading(true); if (debouncedSearchTerm) { setIsSearching(true); workspaceService .searchWorkspace(workspaceSlug.toString(), { ...(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 ); return ( setSearchTerm("")} as={React.Fragment}> closePalette()}>
{ 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) closePalette(); // 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 !== "" && ( )} {!page && ( <> {/* issue actions */} {issueId && ( setPages(newPages)} setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} /> )} { closePalette(); setTrackElement("COMMAND_PALETTE"); toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" >
Create new issue
C
{workspaceSlug && ( { closePalette(); setTrackElement("COMMAND_PALETTE"); toggleCreateProjectModal(true); }} className="focus:outline-none" >
Create new project
P
)} {/* project actions */} {projectId && } { 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...
{/* help options */} )} {/* workspace settings actions */} {page === "settings" && workspaceSlug && ( )} {/* 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)); }} /> )}
); });