"use client"; import React, { useEffect, useState } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; // plane imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS, WORK_ITEM_TRACKER_ELEMENTS, WORKSPACE_DEFAULT_SEARCH_RESULT, } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { WorkItemsIcon } from "@plane/propel/icons"; import type { IWorkspaceSearchResults } from "@plane/types"; import { Loader, ToggleSwitch } from "@plane/ui"; import { cn, getTabIndex } from "@plane/utils"; // components import { ChangeIssueAssignee, ChangeIssuePriority, ChangeIssueState, CommandPaletteHelpActions, CommandPaletteIssueActions, CommandPaletteProjectActions, CommandPaletteSearchResults, CommandPaletteThemeActions, CommandPaletteWorkspaceSettingsActions, } from "@/components/command-palette"; import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; // helpers // hooks import { captureClick } from "@/helpers/event-tracker.helper"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useProject } from "@/hooks/store/use-project"; import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier"; // plane web services import { WorkspaceService } from "@/plane-web/services"; const workspaceService = new WorkspaceService(); export const CommandModal: React.FC = observer(() => { // router const router = useAppRouter(); const { workspaceSlug, projectId: routerProjectId, workItem } = useParams(); // 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(WORKSPACE_DEFAULT_SEARCH_RESULT); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); const [searchInIssue, setSearchInIssue] = useState(false); // plane hooks const { t } = useTranslation(); // hooks const { issue: { getIssueById }, fetchIssueWithIdentifier, } = useIssueDetail(); const { workspaceProjectIds } = useProject(); const { platform, isMobile } = usePlatformOS(); const { canPerformAnyCreateAction } = useUser(); const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); const projectIdentifier = workItem?.toString().split("-")[0]; const sequence_id = workItem?.toString().split("-")[1]; // fetch work item details using identifier const { data: workItemDetailsSWR } = useSWR( workspaceSlug && workItem ? `ISSUE_DETAIL_${workspaceSlug}_${projectIdentifier}_${sequence_id}` : null, workspaceSlug && workItem ? () => fetchIssueWithIdentifier(workspaceSlug.toString(), projectIdentifier, sequence_id) : null ); // derived values const issueDetails = workItemDetailsSWR ? getIssueById(workItemDetailsSWR?.id) : null; const issueId = issueDetails?.id; const projectId = issueDetails?.project_id ?? routerProjectId; const page = pages[pages.length - 1]; const debouncedSearchTerm = useDebounce(searchTerm, 500); const { baseTabIndex } = getTabIndex(undefined, isMobile); const canPerformWorkspaceActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE ); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); useEffect(() => { if (issueDetails && isCommandPaletteOpen) { setSearchInIssue(true); } }, [issueDetails, isCommandPaletteOpen]); useEffect(() => { if (!projectId && !isWorkspaceLevel) { setIsWorkspaceLevel(true); } else { setIsWorkspaceLevel(false); } }, [projectId]); 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(WORKSPACE_DEFAULT_SEARCH_RESULT); setIsLoading(false); setIsSearching(false); } }, [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); return ( setSearchTerm("")} as={React.Fragment}> { closePalette(); if (searchInIssue) { setSearchInIssue(true); } }} >
{ if (value.toLowerCase().includes(search.toLowerCase())) return 1; return 0; }} shouldFilter={searchTerm.length > 0} onKeyDown={(e: any) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); e.stopPropagation(); closePalette(); return; } if (e.key === "Tab") { e.preventDefault(); const commandList = document.querySelector("[cmdk-list]"); const items = commandList?.querySelectorAll("[cmdk-item]") || []; const selectedItem = commandList?.querySelector('[aria-selected="true"]'); if (items.length === 0) return; const currentIndex = Array.from(items).indexOf(selectedItem as Element); let nextIndex; if (e.shiftKey) { nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; } else { nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; } const nextItem = items[nextIndex] as HTMLElement; if (nextItem) { nextItem.setAttribute("aria-selected", "true"); selectedItem?.setAttribute("aria-selected", "false"); nextItem.focus(); nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } if (e.key === "Escape" && searchTerm) { e.preventDefault(); setSearchTerm(""); } if (e.key === "Escape" && !page && !searchTerm) { e.preventDefault(); closePalette(); } if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { e.preventDefault(); setPages((pages) => pages.slice(0, -1)); setPlaceholder("Type a command or search..."); } }} >
setSearchTerm(e)} autoFocus tabIndex={baseTabIndex} />
{searchTerm !== "" && (
Search results for{" "} {'"'} {searchTerm} {'"'} {" "} in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
)} {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
)} {(isLoading || isSearching) && ( )} {debouncedSearchTerm !== "" && ( )} {!page && ( <> {/* issue actions */} {issueId && issueDetails && searchInIssue && ( setPages(newPages)} setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} /> )} {workspaceSlug && workspaceProjectIds && workspaceProjectIds.length > 0 && canPerformAnyCreateAction && ( { closePalette(); captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON, }); toggleCreateIssueModal(true); }} className="focus:bg-custom-background-80" >
Create new work item
C
)} {workspaceSlug && canPerformWorkspaceActions && ( { closePalette(); captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON }); toggleCreateProjectModal(true); }} className="focus:outline-none" >
Create new project
P
)} {/* project actions */} {projectId && canPerformAnyCreateAction && ( )} {canPerformWorkspaceActions && ( { 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)); }} /> )}
{/* Bottom overlay */}
Actions
{platform === "MacOS" ? : "Ctrl"}
K
Workspace Level setIsWorkspaceLevel((prevData) => !prevData)} disabled={!projectId} size="sm" />
); });