From c87d70195dadbff3fa4640c83bf343efc4d32979 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:52:21 +0530 Subject: [PATCH] refactor: command k and enable workspace level search (#1664) * refactor: command k and enable workspace level search * fix: global level search --- .../command-palette/command-pallette.tsx | 377 ++++++++++-------- apps/app/components/command-palette/index.ts | 6 +- .../{ => issue}/change-issue-assignee.tsx | 25 +- .../{ => issue}/change-issue-priority.tsx | 0 .../{ => issue}/change-issue-state.tsx | 0 .../components/command-palette/issue/index.ts | 3 + .../modals/existing-issues-list-modal.tsx | 2 +- apps/app/components/ui/icon.tsx | 2 +- apps/app/services/issues.service.ts | 2 +- apps/app/services/workspace.service.ts | 13 +- apps/app/types/workspace.d.ts | 18 +- 11 files changed, 248 insertions(+), 200 deletions(-) rename apps/app/components/command-palette/{ => issue}/change-issue-assignee.tsx (86%) rename apps/app/components/command-palette/{ => issue}/change-issue-priority.tsx (100%) rename apps/app/components/command-palette/{ => issue}/change-issue-state.tsx (100%) create mode 100644 apps/app/components/command-palette/issue/index.ts diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 5ce741ae8..37091fb75 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -1,35 +1,12 @@ -import { useRouter } from "next/router"; import React, { useCallback, useEffect, useState } from "react"; + +import { useRouter } from "next/router"; + import useSWR, { mutate } from "swr"; // icons -import { - ArrowRightIcon, - ChartBarIcon, - ChatBubbleOvalLeftEllipsisIcon, - DocumentTextIcon, - FolderPlusIcon, - InboxIcon, - LinkIcon, - MagnifyingGlassIcon, - RocketLaunchIcon, - Squares2X2Icon, - TrashIcon, - UserMinusIcon, - UserPlusIcon, - UsersIcon, -} from "@heroicons/react/24/outline"; -import { - AssignmentClipboardIcon, - ContrastIcon, - DiscordIcon, - DocumentIcon, - GithubIcon, - LayerDiagonalIcon, - PeopleGroupIcon, - SettingIcon, - ViewListIcon, -} from "components/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 @@ -56,22 +33,83 @@ import { CreateProjectModal } from "components/project"; import { CreateUpdateViewModal } from "components/views"; import { CreateUpdatePageModal } from "components/pages"; -import { Spinner } from "components/ui"; +import { Icon, Loader, ToggleSwitch, Tooltip } from "components/ui"; // helpers -import { - capitalizeFirstLetter, - copyTextToClipboard, - replaceUnderscoreIfSnakeCase, -} from "helpers/string.helper"; +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, IWorkspaceSearchResults } from "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", + }, +}; + export const CommandPalette: React.FC = () => { const [isPaletteOpen, setIsPaletteOpen] = useState(false); const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); @@ -101,9 +139,11 @@ export const CommandPalette: React.FC = () => { const [isSearching, setIsSearching] = useState(false); const debouncedSearchTerm = useDebounce(searchTerm, 500); const [placeholder, setPlaceholder] = React.useState("Type a command or search..."); - const [pages, setPages] = React.useState([]); + 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; @@ -113,7 +153,7 @@ export const CommandPalette: React.FC = () => { const { setToastAlert } = useToast(); const { toggleCollapsed } = useTheme(); - const { data: issueDetails } = useSWR( + const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, workspaceSlug && projectId && issueId ? () => @@ -246,20 +286,24 @@ export const CommandPalette: React.FC = () => { useEffect(() => { document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); useEffect( () => { - if (!workspaceSlug || !projectId) return; + if (!workspaceSlug) return; setIsLoading(true); - // this is done prevent subsequent api request - // or searchTerm has not been updated within last 500ms. + if (debouncedSearchTerm) { setIsSearching(true); workspaceService - .searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm) + .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( @@ -288,7 +332,7 @@ export const CommandPalette: React.FC = () => { setIsSearching(false); } }, - [debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes + [debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes ); if (!user) return null; @@ -441,118 +485,57 @@ export const CommandPalette: React.FC = () => { } }} > - {issueId && issueDetails && ( -
-

- {issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "} - {issueDetails?.name} -

-
- )} +
+ {issueDetails && ( +
+ {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "} + {issueDetails.name} +
+ )} + {projectId && ( + +
+ + setIsWorkspaceLevel((prevData) => !prevData)} + /> +
+
+ )} +
+ - {!isLoading && - resultsCount === 0 && - searchTerm !== "" && - debouncedSearchTerm !== "" && ( -
- No results found. -
- )} - - {(isLoading || isSearching) && ( - -
- -
-
- )} - - {debouncedSearchTerm !== "" && ( - <> - {Object.keys(results.results).map((key) => { - const section = (results.results as any)[key]; - if (section.length > 0) { - return ( - - {section.map((item: any) => { - let path = ""; - let value = item.name; - let Icon: any = ArrowRightIcon; - - if (key === "workspace") { - path = `/${item.slug}`; - Icon = FolderPlusIcon; - } else if (key == "project") { - path = `/${item.workspace__slug}/projects/${item.id}/issues`; - Icon = AssignmentClipboardIcon; - } else if (key === "issue") { - path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`; - // user can search id-num idnum or issue name - value = `${item.project__identifier}-${item.sequence_id} ${item.project__identifier}${item.sequence_id} ${item.name}`; - Icon = LayerDiagonalIcon; - } else if (key === "issue_view") { - path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`; - Icon = ViewListIcon; - } else if (key === "module") { - path = `/${item.workspace__slug}/projects/${item.project_id}/modules/${item.id}`; - Icon = PeopleGroupIcon; - } else if (key === "page") { - path = `/${item.workspace__slug}/projects/${item.project_id}/pages/${item.id}`; - Icon = DocumentTextIcon; - } else if (key === "cycle") { - path = `/${item.workspace__slug}/projects/${item.project_id}/cycles/${item.id}`; - Icon = ContrastIcon; - } - - return ( - { - router.push(path); - setIsPaletteOpen(false); - }} - value={value} - className="focus:outline-none" - > -
- -

{item.name}

-
-
- ); - })} -
- ); - } - })} - - )} - {!page && ( <> {issueId && ( - <> + { setPlaceholder("Change state..."); @@ -562,7 +545,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Change state...
@@ -575,7 +558,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Change priority...
@@ -588,7 +571,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Assign to...
@@ -602,21 +585,20 @@ export const CommandPalette: React.FC = () => {
{issueDetails?.assignees.includes(user.id) ? ( <> - + Un-assign from me ) : ( <> - + Assign to me )}
-
- + Delete issue
@@ -628,11 +610,11 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- - Copy issue URL to clipboard + + Copy issue URL
- +
)} { className="focus:bg-custom-background-80" >
- + Create new issue
C @@ -654,7 +636,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Create new project
P @@ -670,46 +652,42 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Create new cycle
Q
-
- + Create new module
M
-
- + Create new view
V
-
- + Create new page
D
- {projectDetails && projectDetails.inbox_view && ( { className="focus:outline-none" >
- + Search settings...
@@ -751,7 +729,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Create new workspace
@@ -764,7 +742,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Change interface theme...
@@ -781,7 +759,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Open keyboard shortcuts
@@ -793,7 +771,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Open Plane documentation
@@ -820,7 +798,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Report a bug
@@ -832,7 +810,7 @@ export const CommandPalette: React.FC = () => { className="focus:outline-none" >
- + Chat with us
@@ -840,6 +818,67 @@ export const CommandPalette: React.FC = () => { )} + {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 && ( <> { )} {page === "change-issue-state" && issueDetails && ( - <> - - + )} {page === "change-issue-priority" && issueDetails && ( >; @@ -25,12 +29,7 @@ export const ChangeIssueAssignee: React.FC = ({ setIsPaletteOpen, issue, const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; - const { data: members } = useSWR( - projectId ? PROJECT_MEMBERS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) - : null - ); + const { members } = useProjectMembers(workspaceSlug as string, projectId as string); const options = members?.map(({ member }) => ({ diff --git a/apps/app/components/command-palette/change-issue-priority.tsx b/apps/app/components/command-palette/issue/change-issue-priority.tsx similarity index 100% rename from apps/app/components/command-palette/change-issue-priority.tsx rename to apps/app/components/command-palette/issue/change-issue-priority.tsx diff --git a/apps/app/components/command-palette/change-issue-state.tsx b/apps/app/components/command-palette/issue/change-issue-state.tsx similarity index 100% rename from apps/app/components/command-palette/change-issue-state.tsx rename to apps/app/components/command-palette/issue/change-issue-state.tsx diff --git a/apps/app/components/command-palette/issue/index.ts b/apps/app/components/command-palette/issue/index.ts new file mode 100644 index 000000000..7d0bbd05d --- /dev/null +++ b/apps/app/components/command-palette/issue/index.ts @@ -0,0 +1,3 @@ +export * from "./change-issue-state"; +export * from "./change-issue-priority"; +export * from "./change-issue-assignee"; diff --git a/apps/app/components/core/modals/existing-issues-list-modal.tsx b/apps/app/components/core/modals/existing-issues-list-modal.tsx index 897e094af..5a9f68d8b 100644 --- a/apps/app/components/core/modals/existing-issues-list-modal.tsx +++ b/apps/app/components/core/modals/existing-issues-list-modal.tsx @@ -161,7 +161,7 @@ export const ExistingIssuesListModal: React.FC = ({ aria-hidden="true" /> setSearchTerm(e.target.value)} diff --git a/apps/app/components/ui/icon.tsx b/apps/app/components/ui/icon.tsx index aefc24dff..ff093a1ce 100644 --- a/apps/app/components/ui/icon.tsx +++ b/apps/app/components/ui/icon.tsx @@ -6,7 +6,7 @@ type Props = { }; export const Icon: React.FC = ({ iconName, className = "" }) => ( - + {iconName} ); diff --git a/apps/app/services/issues.service.ts b/apps/app/services/issues.service.ts index 42596fa83..1b32f04e7 100644 --- a/apps/app/services/issues.service.ts +++ b/apps/app/services/issues.service.ts @@ -60,7 +60,7 @@ class ProjectIssuesServices extends APIService { }); } - async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise { + async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`) .then((response) => response?.data) .catch((error) => { diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index e44b6f4eb..e2598e1be 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -231,12 +231,15 @@ class WorkspaceService extends APIService { async searchWorkspace( workspaceSlug: string, - projectId: string, - query: string + params: { + project_id?: string; + search: string; + workspace_search: boolean; + } ): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/search/?search=${query}` - ) + return this.get(`/api/workspaces/${workspaceSlug}/search/`, { + params, + }) .then((res) => res?.data) .catch((error) => { throw error?.response?.data; diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index adc7b359d..619d6c31c 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -74,29 +74,37 @@ export interface ILastActiveWorkspaceDetails { } export interface IWorkspaceDefaultSearchResult { - name: string; id: string; + name: string; project_id: string; workspace__slug: string; } export interface IWorkspaceSearchResult { - name: string; id: string; + name: string; slug: string; } export interface IWorkspaceIssueSearchResult { - name: string; id: string; - sequence_id: number; + name: string; project__identifier: string; project_id: string; + sequence_id: number; workspace__slug: string; } + +export interface IWorkspaceProjectSearchResult { + id: string; + identifier: string; + name: string; + workspace__slug: string; +} + export interface IWorkspaceSearchResults { results: { workspace: IWorkspaceSearchResult[]; - project: IWorkspaceDefaultSearchResult[]; + project: IWorkspaceProjectSearchResult[]; issue: IWorkspaceIssueSearchResult[]; cycle: IWorkspaceDefaultSearchResult[]; module: IWorkspaceDefaultSearchResult[];