forked from github/plane
fix: cmdk integration (#567)
* fix: issues not showing on cmd k * fix: text overflows on longer issue title * fix: add loading state whenever there is a network call * fix: minor ux changes * feat: replace loading with spinner
This commit is contained in:
parent
628591854d
commit
dd3bca9a32
@ -27,7 +27,7 @@ import {
|
|||||||
PeopleGroupIcon,
|
PeopleGroupIcon,
|
||||||
SettingIcon,
|
SettingIcon,
|
||||||
ViewListIcon,
|
ViewListIcon,
|
||||||
PencilScribbleIcon
|
PencilScribbleIcon,
|
||||||
} from "components/icons";
|
} from "components/icons";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
@ -37,6 +37,7 @@ import { Command } from "cmdk";
|
|||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
|
import useDebounce from "hooks/use-debounce";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
ShortcutsModal,
|
ShortcutsModal,
|
||||||
@ -50,6 +51,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
|||||||
import { CreateUpdateModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal } from "components/modules";
|
||||||
import { CreateProjectModal } from "components/project";
|
import { CreateProjectModal } from "components/project";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateViewModal } from "components/views";
|
||||||
|
import { Spinner } from "components/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
@ -58,12 +60,11 @@ import {
|
|||||||
} from "helpers/string.helper";
|
} from "helpers/string.helper";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
// types
|
// types
|
||||||
import { IIssue, IWorkspaceSearchResults } from "types";
|
import { IIssue, IWorkspaceSearchResults } from "types";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
import useDebounce from "hooks/use-debounce";
|
|
||||||
import workspaceService from "services/workspace.service";
|
|
||||||
|
|
||||||
export const CommandPalette: React.FC = () => {
|
export const CommandPalette: React.FC = () => {
|
||||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||||
@ -88,7 +89,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
page: [],
|
page: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [isPendingAPIRequest, setIsPendingAPIRequest] = useState(false);
|
const [resultsCount, setResultsCount] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||||
const [placeholder, setPlaceholder] = React.useState("Type a command or search...");
|
const [placeholder, setPlaceholder] = React.useState("Type a command or search...");
|
||||||
const [pages, setPages] = React.useState<string[]>([]);
|
const [pages, setPages] = React.useState<string[]>([]);
|
||||||
@ -220,18 +223,39 @@ export const CommandPalette: React.FC = () => {
|
|||||||
() => {
|
() => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
// this is done prevent api request when user is clearing input
|
setIsLoading(true);
|
||||||
|
// this is done prevent subsequent api request
|
||||||
// or searchTerm has not been updated within last 500ms.
|
// or searchTerm has not been updated within last 500ms.
|
||||||
if (debouncedSearchTerm) {
|
if (debouncedSearchTerm) {
|
||||||
setIsPendingAPIRequest(true);
|
setIsSearching(true);
|
||||||
workspaceService
|
workspaceService
|
||||||
.searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm)
|
.searchWorkspace(workspaceSlug as string, projectId as string, debouncedSearchTerm)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
setIsPendingAPIRequest(false);
|
|
||||||
setResults(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 {
|
} else {
|
||||||
setIsPendingAPIRequest(false);
|
setResults({
|
||||||
|
results: {
|
||||||
|
workspace: [],
|
||||||
|
project: [],
|
||||||
|
issue: [],
|
||||||
|
cycle: [],
|
||||||
|
module: [],
|
||||||
|
issue_view: [],
|
||||||
|
page: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsSearching(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes
|
[debouncedSearchTerm, workspaceSlug, projectId] // Only call effect if debounced search term changes
|
||||||
@ -369,11 +393,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{issueId && issueDetails && (
|
{issueId && issueDetails && (
|
||||||
<div className="p-3">
|
<div className="flex p-3">
|
||||||
<span className="rounded-md bg-slate-100 p-1 px-2 text-xs font-medium text-slate-500">
|
<p className="overflow-hidden truncate rounded-md bg-slate-100 p-1 px-2 text-xs font-medium text-slate-500">
|
||||||
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
{issueDetails.project_detail?.identifier}-{issueDetails.sequence_id}{" "}
|
||||||
{issueDetails?.name}
|
{issueDetails?.name}
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -392,9 +416,20 @@ export const CommandPalette: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Command.List className="max-h-96 overflow-scroll p-2">
|
<Command.List className="max-h-96 overflow-scroll p-2">
|
||||||
<Command.Empty className="my-4 text-center text-gray-500">
|
{!isLoading &&
|
||||||
No results found.
|
resultsCount === 0 &&
|
||||||
</Command.Empty>
|
searchTerm !== "" &&
|
||||||
|
debouncedSearchTerm !== "" && (
|
||||||
|
<div className="my-4 text-center text-gray-500">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 !== "" && (
|
{debouncedSearchTerm !== "" && (
|
||||||
<>
|
<>
|
||||||
@ -419,7 +454,8 @@ export const CommandPalette: React.FC = () => {
|
|||||||
Icon = AssignmentClipboardIcon;
|
Icon = AssignmentClipboardIcon;
|
||||||
} else if (key === "issue") {
|
} else if (key === "issue") {
|
||||||
path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`;
|
path = `/${item.workspace__slug}/projects/${item.project_id}/issues/${item.id}`;
|
||||||
value = `${item.project__identifier}-${item.sequence_id} item.name`;
|
// 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;
|
Icon = LayerDiagonalIcon;
|
||||||
} else if (key === "issue_view") {
|
} else if (key === "issue_view") {
|
||||||
path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`;
|
path = `/${item.workspace__slug}/projects/${item.project_id}/views/${item.id}`;
|
||||||
@ -446,9 +482,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
className="focus:bg-slate-200 focus:outline-none"
|
className="focus:bg-slate-200 focus:outline-none"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-slate-700">
|
<div className="flex items-center gap-2 overflow-hidden text-slate-700">
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{item.name}
|
<p className="block flex-1 truncate">{item.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user