[WEB-3357 | WEB-3363 | WEB-3370] chore: command-k enhancement and fixes (#6600)

* fix: command-k work item actions

* chore: command k work item context indicator improvement and default vale for workspace toggle updated

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2025-02-14 19:04:08 +05:30 committed by GitHub
parent bf1f12378e
commit 82eea3e802
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 79 additions and 92 deletions

View file

@ -4,14 +4,13 @@ import { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react"; import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
import { EIssuesStoreType } from "@plane/constants";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// hooks // hooks
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui"; import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
// helpers // helpers
import { copyTextToClipboard } from "@/helpers/string.helper"; import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks // hooks
import { useCommandPalette, useIssues, useUser } from "@/hooks/store"; import { useCommandPalette, useIssueDetail, useUser } from "@/hooks/store";
type Props = { type Props = {
closePalette: () => void; closePalette: () => void;
@ -27,9 +26,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
// router // router
const { workspaceSlug, projectId, issueId } = useParams(); const { workspaceSlug, projectId, issueId } = useParams();
// hooks // hooks
const { const { updateIssue } = useIssueDetail();
issues: { updateIssue },
} = useIssues(EIssuesStoreType.PROJECT);
const { toggleCommandPaletteModal, toggleDeleteIssueModal } = useCommandPalette(); const { toggleCommandPaletteModal, toggleDeleteIssueModal } = useCommandPalette();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
@ -65,16 +62,10 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
copyTextToClipboard(url.href) copyTextToClipboard(url.href)
.then(() => { .then(() => {
setToast({ setToast({ type: TOAST_TYPE.SUCCESS, title: "Copied to clipboard" });
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
});
}) })
.catch(() => { .catch(() => {
setToast({ setToast({ type: TOAST_TYPE.ERROR, title: "Some error occurred" });
type: TOAST_TYPE.ERROR,
title: "Some error occurred",
});
}); });
}; };

View file

@ -4,8 +4,6 @@ import { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
// plane constants
import { EIssuesStoreType } from "@plane/constants";
// plane types // plane types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
// plane ui // plane ui
@ -13,21 +11,16 @@ import { Avatar } from "@plane/ui";
// helpers // helpers
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
// hooks // hooks
import { useIssues, useMember } from "@/hooks/store"; import { useIssueDetail, useMember } from "@/hooks/store";
type Props = { type Props = { closePalette: () => void; issue: TIssue };
closePalette: () => void;
issue: TIssue;
};
export const ChangeIssueAssignee: React.FC<Props> = observer((props) => { export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
const { closePalette, issue } = props; const { closePalette, issue } = props;
// router params // router params
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store // store
const { const { updateIssue } = useIssueDetail();
issues: { updateIssue },
} = useIssues(EIssuesStoreType.PROJECT);
const { const {
project: { projectMemberIds, getProjectMemberDetails }, project: { projectMemberIds, getProjectMemberDetails },
} = useMember(); } = useMember();

View file

@ -5,28 +5,23 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
// plane constants // plane constants
import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants"; import { ISSUE_PRIORITIES } from "@plane/constants";
// plane types // plane types
import { TIssue, TIssuePriorities } from "@plane/types"; import { TIssue, TIssuePriorities } from "@plane/types";
// mobx store // mobx store
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";
import { useIssues } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// ui // ui
// types // types
// constants // constants
type Props = { type Props = { closePalette: () => void; issue: TIssue };
closePalette: () => void;
issue: TIssue;
};
export const ChangeIssuePriority: React.FC<Props> = observer((props) => { export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
const { closePalette, issue } = props; const { closePalette, issue } = props;
// router params // router params
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
const { const { updateIssue } = useIssueDetail();
issues: { updateIssue },
} = useIssues(EIssuesStoreType.PROJECT);
const submitChanges = async (formData: Partial<TIssue>) => { const submitChanges = async (formData: Partial<TIssue>) => {
if (!workspaceSlug || !projectId || !issue) return; if (!workspaceSlug || !projectId || !issue) return;

View file

@ -5,27 +5,21 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// hooks // hooks
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { EIssuesStoreType } from "@plane/constants";
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
import { Spinner, StateGroupIcon } from "@plane/ui"; import { Spinner, StateGroupIcon } from "@plane/ui";
import { useProjectState, useIssues } from "@/hooks/store"; import { useProjectState, useIssueDetail } from "@/hooks/store";
// ui // ui
// icons // icons
// types // types
type Props = { type Props = { closePalette: () => void; issue: TIssue };
closePalette: () => void;
issue: TIssue;
};
export const ChangeIssueState: React.FC<Props> = observer((props) => { export const ChangeIssueState: React.FC<Props> = observer((props) => {
const { closePalette, issue } = props; const { closePalette, issue } = props;
// router params // router params
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
// store hooks // store hooks
const { const { updateIssue } = useIssueDetail();
issues: { updateIssue },
} = useIssues(EIssuesStoreType.PROJECT);
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const submitChanges = async (formData: Partial<TIssue>) => { const submitChanges = async (formData: Partial<TIssue>) => {

View file

@ -5,13 +5,14 @@ import { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
import { CommandIcon, FolderPlus, Search, Settings } from "lucide-react"; import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// plane imports // plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IWorkspaceSearchResults } from "@plane/types"; import { IWorkspaceSearchResults } from "@plane/types";
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui"; import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
import { cn } from "@plane/utils";
// components // components
import { import {
ChangeIssueAssignee, ChangeIssueAssignee,
@ -56,18 +57,11 @@ export const CommandModal: React.FC = observer(() => {
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<IWorkspaceSearchResults>({ const [results, setResults] = useState<IWorkspaceSearchResults>({
results: { results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
}); });
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(true); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const [pages, setPages] = useState<string[]>([]); const [pages, setPages] = useState<string[]>([]);
const [searchInIssue, setSearchInIssue] = useState(false);
// plane hooks // plane hooks
const { t } = useTranslation(); const { t } = useTranslation();
// hooks // hooks
@ -96,6 +90,20 @@ export const CommandModal: React.FC = observer(() => {
: null : null
); );
useEffect(() => {
if (issueDetails && isCommandPaletteOpen) {
setSearchInIssue(true);
}
}, [issueDetails, isCommandPaletteOpen]);
useEffect(() => {
if (!projectId && !isWorkspaceLevel) {
setIsWorkspaceLevel(true);
} else {
setIsWorkspaceLevel(false);
}
}, [projectId]);
const closePalette = () => { const closePalette = () => {
toggleCommandPaletteModal(false); toggleCommandPaletteModal(false);
}; };
@ -133,15 +141,7 @@ export const CommandModal: React.FC = observer(() => {
}); });
} else { } else {
setResults({ setResults({
results: { results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
}); });
setIsLoading(false); setIsLoading(false);
setIsSearching(false); setIsSearching(false);
@ -152,7 +152,16 @@ export const CommandModal: React.FC = observer(() => {
return ( return (
<Transition.Root show={isCommandPaletteOpen} afterLeave={() => setSearchTerm("")} as={React.Fragment}> <Transition.Root show={isCommandPaletteOpen} afterLeave={() => setSearchTerm("")} as={React.Fragment}>
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}> <Dialog
as="div"
className="relative z-30"
onClose={() => {
closePalette();
if (searchInIssue) {
setSearchInIssue(true);
}
}}
>
<Transition.Child <Transition.Child
as={React.Fragment} as={React.Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -213,10 +222,7 @@ export const CommandModal: React.FC = observer(() => {
nextItem.setAttribute("aria-selected", "true"); nextItem.setAttribute("aria-selected", "true");
selectedItem?.setAttribute("aria-selected", "false"); selectedItem?.setAttribute("aria-selected", "false");
nextItem.focus(); nextItem.focus();
nextItem.scrollIntoView({ nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
behavior: "smooth",
block: "nearest",
});
} }
} }
@ -237,32 +243,40 @@ export const CommandModal: React.FC = observer(() => {
} }
}} }}
> >
<div <div className="relative flex items-center px-4 border-0 border-b border-custom-border-200">
className={`flex gap-4 pb-0 sm:items-center ${ <div className="flex items-center gap-2 flex-shrink-0">
issueDetails ? "flex-col justify-between sm:flex-row" : "justify-end" <Search
}`} className="h-4 w-4 text-custom-text-200 flex-shrink-0"
> aria-hidden="true"
{issueDetails && ( strokeWidth={2}
<div className="flex gap-2 items-center overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200"> />
{searchInIssue && issueDetails && (
<>
<span className="flex items-center text-sm">Update in:</span>
<span className="flex items-center gap-1 rounded px-1.5 py-1 text-sm bg-custom-primary-100/10 ">
{issueDetails.project_id && ( {issueDetails.project_id && (
<IssueIdentifier <IssueIdentifier
issueId={issueDetails.id} issueId={issueDetails.id}
projectId={issueDetails.project_id} projectId={issueDetails.project_id}
textContainerClassName="text-xs font-medium text-custom-text-200" textContainerClassName="text-sm text-custom-primary-200"
/> />
)} )}
{issueDetails.name} <X
</div> size={12}
)}
</div>
<div className="relative">
<Search
className="pointer-events-none absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-custom-text-200"
aria-hidden="true"
strokeWidth={2} strokeWidth={2}
className="flex-shrink-0 cursor-pointer"
onClick={() => {
setSearchInIssue(false);
}}
/> />
</span>
</>
)}
</div>
<Command.Input <Command.Input
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0" className={cn(
"w-full bg-transparent p-4 text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
)}
placeholder={placeholder} placeholder={placeholder}
value={searchTerm} value={searchTerm}
onValueChange={(e) => setSearchTerm(e)} onValueChange={(e) => setSearchTerm(e)}
@ -308,7 +322,7 @@ export const CommandModal: React.FC = observer(() => {
{!page && ( {!page && (
<> <>
{/* issue actions */} {/* issue actions */}
{issueId && ( {issueId && issueDetails && searchInIssue && (
<CommandPaletteIssueActions <CommandPaletteIssueActions
closePalette={closePalette} closePalette={closePalette}
issueDetails={issueDetails} issueDetails={issueDetails}