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:
Saheb Giri 2023-03-29 00:51:47 +05:30 committed by GitHub
parent 628591854d
commit dd3bca9a32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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>
); );