refactor: command k and enable workspace level search (#1664)

* refactor: command k and enable workspace level search

* fix: global level search
This commit is contained in:
Aaryan Khandelwal 2023-07-25 13:52:21 +05:30 committed by GitHub
parent c2327fa538
commit c87d70195d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 248 additions and 200 deletions

View File

@ -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<string[]>([]);
const [pages, setPages] = useState<string[]>([]);
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<IIssue | undefined>(
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 && (
<div className="flex p-3">
<p className="overflow-hidden truncate rounded-md bg-custom-background-90 p-1 px-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails?.name}
</p>
</div>
)}
<div
className={`flex sm:items-center gap-4 p-3 pb-0 ${
issueDetails ? "flex-col sm:flex-row justify-between" : "justify-end"
}`}
>
{issueDetails && (
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails.name}
</div>
)}
{projectId && (
<Tooltip tooltipContent="Toggle workspace level search">
<div className="flex-shrink-0 self-end sm:self-center flex items-center gap-1 text-xs cursor-pointer">
<button
type="button"
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
className="flex-shrink-0"
>
Workspace Level
</button>
<ToggleSwitch
value={isWorkspaceLevel}
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
/>
</div>
</Tooltip>
)}
</div>
<div className="relative">
<MagnifyingGlassIcon
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-custom-text-200"
aria-hidden="true"
/>
<Command.Input
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 outline-none focus:ring-0 sm:text-sm"
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm"
placeholder={placeholder}
value={searchTerm}
onValueChange={(e) => {
setSearchTerm(e);
}}
autoFocus
tabIndex={1}
/>
</div>
<Command.List className="max-h-96 overflow-scroll p-2">
{!isLoading &&
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">
No results found.
</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
<div className="flex h-full w-full items-center justify-center py-8">
<Spinner />
</div>
</Command.Loading>
)}
{debouncedSearchTerm !== "" && (
<>
{Object.keys(results.results).map((key) => {
const section = (results.results as any)[key];
if (section.length > 0) {
return (
<Command.Group
heading={capitalizeFirstLetter(replaceUnderscoreIfSnakeCase(key))}
key={key}
>
{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 (
<Command.Item
key={item.id}
onSelect={() => {
router.push(path);
setIsPaletteOpen(false);
}}
value={value}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
<Icon
className="h-4 w-4 text-custom-text-200"
color="#6b7280"
/>
<p className="block flex-1 truncate">{item.name}</p>
</div>
</Command.Item>
);
})}
</Command.Group>
);
}
})}
</>
)}
{!page && (
<>
{issueId && (
<>
<Command.Group heading="Issue actions">
<Command.Item
onSelect={() => {
setPlaceholder("Change state...");
@ -562,7 +545,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<Squares2X2Icon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="grid_view" />
Change state...
</div>
</Command.Item>
@ -575,7 +558,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<ChartBarIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="bar_chart" />
Change priority...
</div>
</Command.Item>
@ -588,7 +571,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<UsersIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="group" />
Assign to...
</div>
</Command.Item>
@ -602,21 +585,20 @@ export const CommandPalette: React.FC = () => {
<div className="flex items-center gap-2 text-custom-text-200">
{issueDetails?.assignees.includes(user.id) ? (
<>
<UserMinusIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="person_remove" />
Un-assign from me
</>
) : (
<>
<UserPlusIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="person_add" />
Assign to me
</>
)}
</div>
</Command.Item>
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<TrashIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="delete" />
Delete issue
</div>
</Command.Item>
@ -628,11 +610,11 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LinkIcon className="h-4 w-4 text-custom-text-200" />
Copy issue URL to clipboard
<Icon iconName="link" />
Copy issue URL
</div>
</Command.Item>
</>
</Command.Group>
)}
<Command.Group heading="Issue">
<Command.Item
@ -640,7 +622,7 @@ export const CommandPalette: React.FC = () => {
className="focus:bg-custom-background-80"
>
<div className="flex items-center gap-2 text-custom-text-200">
<LayerDiagonalIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="stack" />
Create new issue
</div>
<kbd>C</kbd>
@ -654,7 +636,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<AssignmentClipboardIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="create_new_folder" />
Create new project
</div>
<kbd>P</kbd>
@ -670,46 +652,42 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<ContrastIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="contrast" />
Create new cycle
</div>
<kbd>Q</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Module">
<Command.Item
onSelect={createNewModule}
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<PeopleGroupIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="dataset" />
Create new module
</div>
<kbd>M</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="View">
<Command.Item onSelect={createNewView} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<ViewListIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="photo_filter" />
Create new view
</div>
<kbd>V</kbd>
</Command.Item>
</Command.Group>
<Command.Group heading="Page">
<Command.Item onSelect={createNewPage} className="focus:outline-none">
<div className="flex items-center gap-2 text-custom-text-200">
<DocumentTextIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="article" />
Create new page
</div>
<kbd>D</kbd>
</Command.Item>
</Command.Group>
{projectDetails && projectDetails.inbox_view && (
<Command.Group heading="Inbox">
<Command.Item
@ -740,7 +718,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4" color="#6b7280" />
<Icon iconName="settings" />
Search settings...
</div>
</Command.Item>
@ -751,7 +729,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<FolderPlusIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="create_new_folder" />
Create new workspace
</div>
</Command.Item>
@ -764,7 +742,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<SettingIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="settings" />
Change interface theme...
</div>
</Command.Item>
@ -781,7 +759,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<RocketLaunchIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="rocket_launch" />
Open keyboard shortcuts
</div>
</Command.Item>
@ -793,7 +771,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DocumentIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="article" />
Open Plane documentation
</div>
</Command.Item>
@ -820,7 +798,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<GithubIcon className="h-4 w-4" color="#6b7280" />
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
Report a bug
</div>
</Command.Item>
@ -832,7 +810,7 @@ export const CommandPalette: React.FC = () => {
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<ChatBubbleOvalLeftEllipsisIcon className="h-4 w-4 text-custom-text-200" />
<Icon iconName="sms" />
Chat with us
</div>
</Command.Item>
@ -840,6 +818,67 @@ export const CommandPalette: React.FC = () => {
</>
)}
{searchTerm !== "" && (
<h5 className="text-xs text-custom-text-100 mx-[3px] my-4">
Search results for{" "}
<span className="font-medium">
{'"'}
{searchTerm}
{'"'}
</span>{" "}
in project:
</h5>
)}
{!isLoading &&
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">
No results found.
</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
<Loader className="space-y-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
<Loader.Item height="40px" />
</Loader>
</Command.Loading>
)}
{debouncedSearchTerm !== "" &&
Object.keys(results.results).map((key) => {
const section = (results.results as any)[key];
const currentSection = commandGroups[key];
if (section.length > 0) {
return (
<Command.Group key={key} heading={currentSection.title}>
{section.map((item: any) => (
<Command.Item
key={item.id}
onSelect={() => {
router.push(currentSection.path(item));
setIsPaletteOpen(false);
}}
value={`${key}-${item?.name}`}
className="focus:outline-none"
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
<Icon iconName={currentSection.icon} />
<p className="block flex-1 truncate">{item.name}</p>
</div>
</Command.Item>
))}
</Command.Group>
);
}
})}
{page === "settings" && workspaceSlug && (
<>
<Command.Item
@ -890,13 +929,11 @@ export const CommandPalette: React.FC = () => {
</>
)}
{page === "change-issue-state" && issueDetails && (
<>
<ChangeIssueState
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
</>
<ChangeIssueState
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority

View File

@ -1,6 +1,4 @@
export * from "./issue";
export * from "./change-interface-theme";
export * from "./command-pallette";
export * from "./shortcuts-modal";
export * from "./change-issue-state";
export * from "./change-issue-priority";
export * from "./change-issue-assignee";
export * from "./change-interface-theme";

View File

@ -1,19 +1,23 @@
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import useSWR, { mutate } from "swr";
import { useRouter } from "next/router";
import { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue } from "types";
// hooks
import useProjectMembers from "hooks/use-project-members";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, PROJECT_MEMBERS } from "constants/fetch-keys";
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
// ui
import { Avatar } from "components/ui";
// icons
import { CheckIcon } from "components/icons";
import projectService from "services/project.service";
import { Avatar } from "components/ui";
// types
import { ICurrentUserResponse, IIssue } from "types";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@ -25,12 +29,7 @@ export const ChangeIssueAssignee: React.FC<Props> = ({ 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 }) => ({

View File

@ -0,0 +1,3 @@
export * from "./change-issue-state";
export * from "./change-issue-priority";
export * from "./change-issue-assignee";

View File

@ -161,7 +161,7 @@ export const ExistingIssuesListModal: React.FC<Props> = ({
aria-hidden="true"
/>
<Combobox.Input
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 sm:text-sm placeholder:text-custom-text-400"
className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-custom-text-100 outline-none focus:ring-0 text-sm placeholder:text-custom-text-400"
placeholder="Type to search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}

View File

@ -6,7 +6,7 @@ type Props = {
};
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span className={`material-symbols-rounded text-lg leading-5 font-light ${className}`}>
<span className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}>
{iconName}
</span>
);

View File

@ -60,7 +60,7 @@ class ProjectIssuesServices extends APIService {
});
}
async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise<IIssue> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`)
.then((response) => response?.data)
.catch((error) => {

View File

@ -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<IWorkspaceSearchResults> {
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;

View File

@ -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[];