diff --git a/apps/web/core/components/command-palette/command-modal.tsx b/apps/web/core/components/command-palette/command-modal.tsx index 9bf8a18b47..db256d1e31 100644 --- a/apps/web/core/components/command-palette/command-modal.tsx +++ b/apps/web/core/components/command-palette/command-modal.tsx @@ -17,9 +17,15 @@ import { } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { LayersIcon } from "@plane/propel/icons"; -import { IWorkspaceSearchResults, ICycle } from "@plane/types"; +import { + IWorkspaceSearchResults, + ICycle, + TActivityEntityData, + TIssueEntityData, + TIssueSearchResponse, +} 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, @@ -67,6 +73,8 @@ 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 keySequence = useRef(""); const sequenceTimeout = useRef(null); // plane hooks @@ -128,11 +136,25 @@ export const CommandModal: React.FC = observer(() => { else fetchAllCycles(workspaceSlug.toString(), projectId.toString()); }; + const openIssueList = () => { + 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([])); + }; + useEffect(() => { if (!isCommandPaletteOpen || !activeEntity) return; if (activeEntity === "project") openProjectList(); if (activeEntity === "cycle") openCycleList(); + if (activeEntity === "issue") openIssueList(); clearActiveEntity(); }, [isCommandPaletteOpen, activeEntity, clearActiveEntity]); @@ -190,14 +212,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() } : {}), @@ -216,14 +254,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}> @@ -289,6 +327,12 @@ export const CommandModal: React.FC = observer(() => { keySequence.current = ""; return; } + if (keySequence.current === "oi") { + e.preventDefault(); + openIssueList(); + keySequence.current = ""; + return; + } } } if ((e.metaKey || e.ctrlKey) && key === "k") { @@ -409,7 +453,7 @@ export const CommandModal: React.FC = observer(() => { )} - {debouncedSearchTerm !== "" && ( + {debouncedSearchTerm !== "" && page !== "open-issue" && ( )} @@ -448,6 +492,16 @@ export const CommandModal: React.FC = observer(() => { C + +
+ + Open issue... +
+
+ O + I +
+
)} {workspaceSlug && @@ -583,6 +637,93 @@ export const CommandModal: React.FC = observer(() => { )} + {page === "open-issue" && workspaceSlug && ( + <> + {searchTerm === "" ? ( + recentIssues.length > 0 ? ( + + {recentIssues.map((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, + }) + ); + }} + className="focus:outline-none" + > +
+ + {issue.name} +
+
+ ))} +
+ ) : ( +
+ Search for issue id or issue title +
+ ) + ) : issueResults.length > 0 ? ( + + {issueResults.map((issue) => ( + { + closePalette(); + router.push( + generateWorkItemLink({ + workspaceSlug: workspaceSlug.toString(), + projectId: issue.project_id, + issueId: issue.id, + projectIdentifier: issue.project__identifier, + sequenceId: issue.sequence_id, + }) + ); + }} + className="focus:outline-none" + > +
+ + {issue.name} +
+
+ ))} +
+ ) : ( + !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 a27c64e2c0..dc2f0e47b2 100644 --- a/apps/web/core/components/command-palette/command-palette.tsx +++ b/apps/web/core/components/command-palette/command-palette.tsx @@ -201,6 +201,7 @@ export const CommandPalette: FC = observer(() => { op: "project", oc: "cycle", om: "module", + oi: "issue", }; const entity = entityShortcutMap[keySequence.current]; if (entity) { diff --git a/apps/web/core/store/base-command-palette.store.ts b/apps/web/core/store/base-command-palette.store.ts index 55e025f081..6903f5f9e5 100644 --- a/apps/web/core/store/base-command-palette.store.ts +++ b/apps/web/core/store/base-command-palette.store.ts @@ -8,7 +8,7 @@ import { } from "@plane/constants"; import { EIssuesStoreType } from "@plane/types"; -export type CommandPaletteEntity = "project" | "cycle" | "module"; +export type CommandPaletteEntity = "project" | "cycle" | "module" | "issue"; export interface ModalData { store: EIssuesStoreType;