diff --git a/apps/web/core/components/command-palette/command-modal.tsx b/apps/web/core/components/command-palette/command-modal.tsx index e482438f679..30fad6e95ee 100644 --- a/apps/web/core/components/command-palette/command-modal.tsx +++ b/apps/web/core/components/command-palette/command-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef, useMemo, useCallback } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; @@ -17,9 +17,16 @@ import { } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { LayersIcon } from "@plane/propel/icons"; -import { IWorkspaceSearchResults } from "@plane/types"; +import { + IWorkspaceSearchResults, + ICycle, + TActivityEntityData, + TIssueEntityData, + TIssueSearchResponse, + TPartialProject, +} from "@plane/types"; import { Loader, ToggleSwitch } from "@plane/ui"; -import { cn, getTabIndex } from "@plane/utils"; +import { cn, getTabIndex, generateWorkItemLink } from "@plane/utils"; // components import { ChangeIssueAssignee, @@ -33,12 +40,20 @@ import { CommandPaletteWorkspaceSettingsActions, } from "@/components/command-palette"; import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root"; +import { + CommandPaletteProjectSelector, + CommandPaletteCycleSelector, + CommandPaletteEntityList, + useKeySequence, +} from "@/components/command-palette"; +import { COMMAND_CONFIG } from "@/components/command-palette"; // 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 { useCycle } from "@/hooks/store/use-cycle"; import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; @@ -48,6 +63,7 @@ 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"; +import type { CommandPaletteEntity } from "@/store/base-command-palette.store"; const workspaceService = new WorkspaceService(); @@ -65,6 +81,10 @@ export const CommandModal: React.FC = observer(() => { const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); const [searchInIssue, setSearchInIssue] = useState(false); + const [recentIssues, setRecentIssues] = useState([]); + const [issueResults, setIssueResults] = useState([]); + const [projectSelectionAction, setProjectSelectionAction] = useState<"navigate" | "cycle" | null>(null); + const [selectedProjectId, setSelectedProjectId] = useState(null); // plane hooks const { t } = useTranslation(); // hooks @@ -72,11 +92,18 @@ export const CommandModal: React.FC = observer(() => { issue: { getIssueById }, fetchIssueWithIdentifier, } = useIssueDetail(); - const { workspaceProjectIds } = useProject(); + const { workspaceProjectIds, joinedProjectIds, getPartialProjectById } = useProject(); + const { getProjectCycleIds, getCycleById, fetchAllCycles } = useCycle(); const { platform, isMobile } = usePlatformOS(); const { canPerformAnyCreateAction } = useUser(); - const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = - useCommandPalette(); + const { + isCommandPaletteOpen, + toggleCommandPaletteModal, + toggleCreateIssueModal, + toggleCreateProjectModal, + activeEntity, + clearActiveEntity, + } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); const projectIdentifier = workItem?.toString().split("-")[0]; const sequence_id = workItem?.toString().split("-")[1]; @@ -101,6 +128,106 @@ export const CommandModal: React.FC = observer(() => { ); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const openProjectSelection = useCallback( + (action: "navigate" | "cycle") => { + if (!workspaceSlug) return; + setPlaceholder("Search projects..."); + setSearchTerm(""); + setProjectSelectionAction(action); + setSelectedProjectId(null); + setPages((p) => [...p, "open-project"]); + }, + [workspaceSlug] + ); + + const openProjectList = useCallback(() => openProjectSelection("navigate"), [openProjectSelection]); + + const openCycleList = useCallback(() => { + if (!workspaceSlug) return; + const currentProject = projectId ? getPartialProjectById(projectId.toString()) : null; + if (currentProject && currentProject.cycle_view) { + setSelectedProjectId(projectId.toString()); + setPlaceholder("Search cycles..."); + setSearchTerm(""); + setPages((p) => [...p, "open-cycle"]); + fetchAllCycles(workspaceSlug.toString(), projectId.toString()); + } else { + openProjectSelection("cycle"); + } + }, [workspaceSlug, projectId, getPartialProjectById, fetchAllCycles, openProjectSelection]); + + const openIssueList = useCallback(() => { + if (!workspaceSlug) return; + setPlaceholder("Search issues..."); + setSearchTerm(""); + setPages((p) => [...p, "open-issue"]); + workspaceService + .fetchWorkspaceRecents(workspaceSlug.toString(), "issue") + .then((res) => + setRecentIssues(res.map((r: TActivityEntityData) => r.entity_data as TIssueEntityData).slice(0, 10)) + ) + .catch(() => setRecentIssues([])); + }, [workspaceSlug]); + + const entityHandlers = useMemo< + Partial void>> + >( + () => ({ + project: openProjectList, + cycle: openCycleList, + issue: openIssueList, + }), + [openProjectList, openCycleList, openIssueList] + ); + + const sequenceHandlers = useMemo(() => { + const handlers: Record void> = {}; + COMMAND_CONFIG.forEach((cmd) => { + if (!cmd.enabled || cmd.enabled()) { + const handler = entityHandlers[cmd.entity]; + if (handler) handlers[cmd.sequence] = handler; + } + }); + return handlers; + }, [entityHandlers]); + + const handleKeySequence = useKeySequence(sequenceHandlers); + + useEffect(() => { + if (!isCommandPaletteOpen || !activeEntity) return; + + const handler = entityHandlers[activeEntity]; + if (handler) handler(); + clearActiveEntity(); + }, [isCommandPaletteOpen, activeEntity, clearActiveEntity, entityHandlers]); + + const projectOptions = useMemo(() => { + const list: TPartialProject[] = []; + joinedProjectIds.forEach((id) => { + const project = getPartialProjectById(id); + if (project) list.push(project); + }); + return list.sort((a, b) => new Date(b.updated_at ?? 0).getTime() - new Date(a.updated_at ?? 0).getTime()); + }, [joinedProjectIds, getPartialProjectById]); + + const cycleOptions = useMemo(() => { + const cycles: ICycle[] = []; + if (selectedProjectId) { + const cycleIds = getProjectCycleIds(selectedProjectId) || []; + cycleIds.forEach((cid) => { + const cycle = getCycleById(cid); + const status = cycle?.status ? cycle.status.toLowerCase() : ""; + if (cycle && ["current", "upcoming"].includes(status)) cycles.push(cycle); + }); + } + return cycles.sort((a, b) => new Date(b.updated_at ?? 0).getTime() - new Date(a.updated_at ?? 0).getTime()); + }, [selectedProjectId, getProjectCycleIds, getCycleById]); + + useEffect(() => { + if (page !== "open-cycle" || !workspaceSlug || !selectedProjectId) return; + fetchAllCycles(workspaceSlug.toString(), selectedProjectId); + }, [page, workspaceSlug, selectedProjectId, fetchAllCycles]); + useEffect(() => { if (issueDetails && isCommandPaletteOpen) { setSearchInIssue(true); @@ -117,6 +244,10 @@ export const CommandModal: React.FC = observer(() => { const closePalette = () => { toggleCommandPaletteModal(false); + setPages([]); + setPlaceholder("Type a command or search..."); + setProjectSelectionAction(null); + setSelectedProjectId(null); }; const createNewWorkspace = () => { @@ -124,14 +255,30 @@ export const CommandModal: React.FC = observer(() => { router.push("/create-workspace"); }; - useEffect( - () => { - if (!workspaceSlug) return; + useEffect(() => { + if (!workspaceSlug) return; - setIsLoading(true); + setIsLoading(true); - if (debouncedSearchTerm) { - setIsSearching(true); + if (debouncedSearchTerm) { + setIsSearching(true); + if (page === "open-issue") { + workspaceService + .searchEntity(workspaceSlug.toString(), { + count: 10, + query: debouncedSearchTerm, + query_type: ["issue"], + ...(!isWorkspaceLevel && projectId ? { project_id: projectId.toString() } : {}), + }) + .then((res) => { + setIssueResults(res.issue || []); + setResultsCount(res.issue?.length || 0); + }) + .finally(() => { + setIsLoading(false); + setIsSearching(false); + }); + } else { workspaceService .searchWorkspace(workspaceSlug.toString(), { ...(projectId ? { project_id: projectId.toString() } : {}), @@ -150,14 +297,14 @@ export const CommandModal: React.FC = observer(() => { 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 - ); + } else { + setResults(WORKSPACE_DEFAULT_SEARCH_RESULT); + setIssueResults([]); + setIsLoading(false); + setIsSearching(false); + } + }, [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug, page]); return ( setSearchTerm("")} as={React.Fragment}> @@ -203,7 +350,11 @@ export const CommandModal: React.FC = observer(() => { }} shouldFilter={searchTerm.length > 0} onKeyDown={(e: any) => { - if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + const key = e.key.toLowerCase(); + if (!e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey && !page && searchTerm === "") { + handleKeySequence(e); + } + if ((e.metaKey || e.ctrlKey) && key === "k") { e.preventDefault(); e.stopPropagation(); closePalette(); @@ -235,20 +386,23 @@ export const CommandModal: React.FC = observer(() => { } } - if (e.key === "Escape" && searchTerm) { + if (e.key === "Escape") { e.preventDefault(); - setSearchTerm(""); - } - - if (e.key === "Escape" && !page && !searchTerm) { - e.preventDefault(); - closePalette(); + if (searchTerm) setSearchTerm(""); + else closePalette(); + return; } - if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { + if (e.key === "Backspace" && !searchTerm && page) { e.preventDefault(); - setPages((pages) => pages.slice(0, -1)); - setPlaceholder("Type a command or search..."); + const newPages = pages.slice(0, -1); + const newPage = newPages[newPages.length - 1]; + setPages(newPages); + if (!newPage) setPlaceholder("Type a command or search..."); + else if (newPage === "open-project") setPlaceholder("Search projects..."); + else if (newPage === "open-cycle") setPlaceholder("Search cycles..."); + if (page === "open-cycle") setSelectedProjectId(null); + if (page === "open-project" && !newPage) setProjectSelectionAction(null); } }} > @@ -324,7 +478,7 @@ export const CommandModal: React.FC = observer(() => { )} - {debouncedSearchTerm !== "" && ( + {debouncedSearchTerm !== "" && page !== "open-issue" && ( )} @@ -341,6 +495,27 @@ export const CommandModal: React.FC = observer(() => { setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} /> )} + {workspaceSlug && joinedProjectIds.length > 0 && ( + + {COMMAND_CONFIG.filter((cmd) => !cmd.enabled || cmd.enabled()).map((cmd) => ( + entityHandlers[cmd.entity]?.()} + className="focus:outline-none" + > +
+ + {cmd.title} +
+
+ {cmd.keys.map((k) => ( + {k} + ))} +
+
+ ))} +
+ )} {workspaceSlug && workspaceProjectIds && workspaceProjectIds.length > 0 && @@ -431,6 +606,120 @@ export const CommandModal: React.FC = observer(() => { )} + {page === "open-project" && workspaceSlug && ( + + projectSelectionAction === "cycle" ? p.cycle_view : true + )} + onSelect={(project) => { + if (projectSelectionAction === "navigate") { + closePalette(); + router.push(`/${workspaceSlug}/projects/${project.id}/issues`); + } else if (projectSelectionAction === "cycle") { + setSelectedProjectId(project.id); + setPages((p) => [...p, "open-cycle"]); + setPlaceholder("Search cycles..."); + fetchAllCycles(workspaceSlug.toString(), project.id); + } + }} + /> + )} + + {page === "open-cycle" && workspaceSlug && selectedProjectId && ( + { + closePalette(); + router.push(`/${workspaceSlug}/projects/${cycle.project_id}/cycles/${cycle.id}`); + }} + /> + )} + + {page === "open-issue" && workspaceSlug && ( + <> + {searchTerm === "" ? ( + recentIssues.length > 0 ? ( + issue.id} + getLabel={(issue) => `${issue.project_identifier}-${issue.sequence_id} ${issue.name}`} + renderItem={(issue) => ( +
+ + {issue.name} +
+ )} + onSelect={(issue) => { + closePalette(); + router.push( + generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue.project_id, + issueId: issue.id, + projectIdentifier: issue.project_identifier, + sequenceId: issue.sequence_id, + isEpic: issue.is_epic, + }) + ); + }} + emptyText="Search for issue id or issue title" + /> + ) : ( +
+ Search for issue id or issue title +
+ ) + ) : issueResults.length > 0 ? ( + issue.id} + getLabel={(issue) => `${issue.project__identifier}-${issue.sequence_id} ${issue.name}`} + renderItem={(issue) => ( +
+ + {issue.name} +
+ )} + onSelect={(issue) => { + closePalette(); + router.push( + generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue.project_id, + issueId: issue.id, + projectIdentifier: issue.project__identifier, + sequenceId: issue.sequence_id, + }) + ); + }} + emptyText={t("command_k.empty_state.search.title") as string} + /> + ) : ( + !isLoading && + !isSearching && ( +
+ +
+ ) + )} + + )} + {/* workspace settings actions */} {page === "settings" && workspaceSlug && ( diff --git a/apps/web/core/components/command-palette/command-palette.tsx b/apps/web/core/components/command-palette/command-palette.tsx index 85115a3d721..3fb18f00032 100644 --- a/apps/web/core/components/command-palette/command-palette.tsx +++ b/apps/web/core/components/command-palette/command-palette.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, FC, useMemo } from "react"; +import React, { useCallback, useEffect, FC, useMemo, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -10,11 +10,13 @@ import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { copyTextToClipboard } from "@plane/utils"; import { CommandModal, ShortcutsModal } from "@/components/command-palette"; +import { COMMAND_CONFIG, CommandConfig } from "@/components/command-palette"; // helpers // hooks import { captureClick } from "@/helpers/event-tracker.helper"; import { useAppTheme } from "@/hooks/store/use-app-theme"; import { useCommandPalette } from "@/hooks/store/use-command-palette"; +import type { CommandPaletteEntity } from "@/store/base-command-palette.store"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useUser, useUserPermissions } from "@/hooks/store/user"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -41,7 +43,8 @@ export const CommandPalette: FC = observer(() => { const { toggleSidebar } = useAppTheme(); const { platform } = usePlatformOS(); const { data: currentUser, canPerformAnyCreateAction } = useUser(); - const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette(); + const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen, activateEntity } = + useCommandPalette(); const { allowPermissions } = useUserPermissions(); // derived values @@ -158,6 +161,17 @@ export const CommandPalette: FC = observer(() => { [] ); + const keySequence = useRef(""); + const sequenceTimeout = useRef(null); + + const commandSequenceMap = useMemo(() => { + const map: Record = {}; + COMMAND_CONFIG.forEach((cmd) => { + map[cmd.sequence] = cmd; + }); + return map; + }, []); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { const { key, ctrlKey, metaKey, altKey, shiftKey } = e; @@ -186,6 +200,21 @@ export const CommandPalette: FC = observer(() => { toggleShortcutModal(true); } + if (!cmdClicked && !altKey && !shiftKey && !isAnyModalOpen) { + keySequence.current = (keySequence.current + keyPressed).slice(-2); + if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current); + sequenceTimeout.current = setTimeout(() => { + keySequence.current = ""; + }, 500); + const cmd = commandSequenceMap[keySequence.current]; + if (cmd && (!cmd.enabled || cmd.enabled())) { + e.preventDefault(); + activateEntity(cmd.entity); + keySequence.current = ""; + return; + } + } + if (deleteKey) { if (performProjectBulkDeleteActions()) { shortcutsList.project.delete.action(); @@ -240,6 +269,7 @@ export const CommandPalette: FC = observer(() => { projectId, shortcutsList, toggleCommandPaletteModal, + activateEntity, toggleShortcutModal, toggleSidebar, workspaceSlug, diff --git a/apps/web/core/components/command-palette/commands.ts b/apps/web/core/components/command-palette/commands.ts new file mode 100644 index 00000000000..063cf1d2253 --- /dev/null +++ b/apps/web/core/components/command-palette/commands.ts @@ -0,0 +1,52 @@ +import type { CommandPaletteEntity } from "@/store/base-command-palette.store"; + +export interface CommandConfig { + /** + * Unique identifier for the command + */ + id: string; + /** + * Key sequence that triggers the command. Should be lowercase. + */ + sequence: string; + /** + * Display label shown in the command palette. + */ + title: string; + /** + * Keys displayed as shortcut hint. + */ + keys: string[]; + /** + * Entity that the command opens. + */ + entity: CommandPaletteEntity; + /** + * Optional predicate controlling command availability + */ + enabled?: () => boolean; +} + +export const COMMAND_CONFIG: CommandConfig[] = [ + { + id: "open-project", + sequence: "op", + title: "Open project...", + keys: ["O", "P"], + entity: "project", + }, + { + id: "open-cycle", + sequence: "oc", + title: "Open cycle...", + keys: ["O", "C"], + entity: "cycle", + }, + { + id: "open-issue", + sequence: "oi", + title: "Open issue...", + keys: ["O", "I"], + entity: "issue", + }, +]; diff --git a/apps/web/core/components/command-palette/cycle-selector.tsx b/apps/web/core/components/command-palette/cycle-selector.tsx new file mode 100644 index 00000000000..be958dd84f2 --- /dev/null +++ b/apps/web/core/components/command-palette/cycle-selector.tsx @@ -0,0 +1,21 @@ +"use client"; + +import React from "react"; +import type { ICycle } from "@plane/types"; +import { CommandPaletteEntityList } from "./entity-list"; + +interface Props { + cycles: ICycle[]; + onSelect: (cycle: ICycle) => void; +} + +export const CommandPaletteCycleSelector: React.FC = ({ cycles, onSelect }) => ( + cycle.id} + getLabel={(cycle) => cycle.name} + onSelect={onSelect} + emptyText="No cycles found" + /> +); diff --git a/apps/web/core/components/command-palette/entity-list.tsx b/apps/web/core/components/command-palette/entity-list.tsx new file mode 100644 index 00000000000..c17167b69e6 --- /dev/null +++ b/apps/web/core/components/command-palette/entity-list.tsx @@ -0,0 +1,42 @@ +"use client"; + +import React from "react"; +import { Command } from "cmdk"; +import { cn } from "@plane/utils"; + +interface CommandPaletteEntityListProps { + heading: string; + items: T[]; + onSelect: (item: T) => void; + getKey?: (item: T) => string; + getLabel: (item: T) => string; + renderItem?: (item: T) => React.ReactNode; + emptyText?: string; +} + +export const CommandPaletteEntityList = ({ + heading, + items, + onSelect, + getKey, + getLabel, + renderItem, + emptyText = "No results found", +}: CommandPaletteEntityListProps) => { + if (items.length === 0) return
{emptyText}
; + + return ( + + {items.map((item) => ( + onSelect(item)} + className={cn("focus:outline-none")} + > + {renderItem ? renderItem(item) : getLabel(item)} + + ))} + + ); +}; diff --git a/apps/web/core/components/command-palette/index.ts b/apps/web/core/components/command-palette/index.ts index 5aee700af3e..4b53979d396 100644 --- a/apps/web/core/components/command-palette/index.ts +++ b/apps/web/core/components/command-palette/index.ts @@ -2,3 +2,8 @@ export * from "./actions"; export * from "./shortcuts-modal"; export * from "./command-modal"; export * from "./command-palette"; +export * from "./project-selector"; +export * from "./cycle-selector"; +export * from "./entity-list"; +export * from "./use-key-sequence"; +export * from "./commands"; diff --git a/apps/web/core/components/command-palette/project-selector.tsx b/apps/web/core/components/command-palette/project-selector.tsx new file mode 100644 index 00000000000..d1b2bcf8730 --- /dev/null +++ b/apps/web/core/components/command-palette/project-selector.tsx @@ -0,0 +1,21 @@ +"use client"; + +import React from "react"; +import type { TPartialProject } from "@/plane-web/types"; +import { CommandPaletteEntityList } from "./entity-list"; + +interface Props { + projects: TPartialProject[]; + onSelect: (project: TPartialProject) => void; +} + +export const CommandPaletteProjectSelector: React.FC = ({ projects, onSelect }) => ( + project.id} + getLabel={(project) => project.name} + onSelect={onSelect} + emptyText="No projects found" + /> +); diff --git a/apps/web/core/components/command-palette/use-key-sequence.ts b/apps/web/core/components/command-palette/use-key-sequence.ts new file mode 100644 index 00000000000..343a5f1a88d --- /dev/null +++ b/apps/web/core/components/command-palette/use-key-sequence.ts @@ -0,0 +1,25 @@ +"use client"; + +import { useRef } from "react"; + +export const useKeySequence = (handlers: Record void>, timeout = 500) => { + const sequence = useRef(""); + const sequenceTimeout = useRef(null); + + return (e: React.KeyboardEvent) => { + const key = e.key.toLowerCase(); + sequence.current = (sequence.current + key).slice(-2); + + if (sequenceTimeout.current) clearTimeout(sequenceTimeout.current); + sequenceTimeout.current = setTimeout(() => { + sequence.current = ""; + }, timeout); + + const action = handlers[sequence.current]; + if (action) { + e.preventDefault(); + action(); + sequence.current = ""; + } + }; +}; diff --git a/apps/web/core/store/base-command-palette.store.ts b/apps/web/core/store/base-command-palette.store.ts index f9e309814a7..6903f5f9e50 100644 --- a/apps/web/core/store/base-command-palette.store.ts +++ b/apps/web/core/store/base-command-palette.store.ts @@ -8,6 +8,8 @@ import { } from "@plane/constants"; import { EIssuesStoreType } from "@plane/types"; +export type CommandPaletteEntity = "project" | "cycle" | "module" | "issue"; + export interface ModalData { store: EIssuesStoreType; viewId: string; @@ -30,6 +32,9 @@ export interface IBaseCommandPaletteStore { allStickiesModal: boolean; projectListOpenMap: Record; getIsProjectListOpen: (projectId: string) => boolean; + activeEntity: CommandPaletteEntity | null; + activateEntity: (entity: CommandPaletteEntity) => void; + clearActiveEntity: () => void; // toggle actions toggleCommandPaletteModal: (value?: boolean) => void; toggleShortcutModal: (value?: boolean) => void; @@ -61,6 +66,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createWorkItemAllowedProjectIds: IBaseCommandPaletteStore["createWorkItemAllowedProjectIds"] = undefined; allStickiesModal: boolean = false; projectListOpenMap: Record = {}; + activeEntity: CommandPaletteEntity | null = null; constructor() { makeObservable(this, { @@ -79,6 +85,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createWorkItemAllowedProjectIds: observable, allStickiesModal: observable, projectListOpenMap: observable, + activeEntity: observable, // projectPages: computed, // toggle actions toggleCommandPaletteModal: action, @@ -93,6 +100,8 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor toggleBulkDeleteIssueModal: action, toggleAllStickiesModal: action, toggleProjectListOpen: action, + activateEntity: action, + clearActiveEntity: action, }); } @@ -127,6 +136,22 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor else this.projectListOpenMap[projectId] = !this.projectListOpenMap[projectId]; }; + /** + * Opens the command palette with a specific entity pre-selected + * @param entity + */ + activateEntity = (entity: CommandPaletteEntity) => { + this.isCommandPaletteOpen = true; + this.activeEntity = entity; + }; + + /** + * Clears the active entity trigger + */ + clearActiveEntity = () => { + this.activeEntity = null; + }; + /** * Toggles the command palette modal * @param value