Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0844d58899 | ||
|
|
7c2dd9d6ad | ||
|
|
b5bc2dc07f |
@@ -119,4 +119,23 @@ export const getCommonShortcutsList = (platform: string): TCommandPaletteShortcu
|
||||
},
|
||||
];
|
||||
|
||||
export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => [];
|
||||
export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => [
|
||||
{
|
||||
key: "issue",
|
||||
title: "Issue detail",
|
||||
shortcuts: [
|
||||
{ keys: "A", description: "Assign issue" },
|
||||
{ keys: "I", description: "Assign to me" },
|
||||
{ keys: "S", description: "Change state/status" },
|
||||
{ keys: "P", description: "Set priority" },
|
||||
{ keys: "L", description: "Manage labels" },
|
||||
{ keys: "Shift,E", description: "Set estimate" },
|
||||
{ keys: "Shift,D", description: "Set due date" },
|
||||
{ keys: "Ctrl,Shift,D", description: "Clear due date" },
|
||||
{ keys: "Shift,C", description: "Add to cycle" },
|
||||
{ keys: "Shift,M", description: "Add to module" },
|
||||
{ keys: "Ctrl,M", description: "Add a comment on the issue" },
|
||||
{ keys: "Ctrl,Delete", description: "Delete issue" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,15 +8,8 @@ import useSWR from "swr";
|
||||
import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import {
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
PROJECT_TRACKER_ELEMENTS,
|
||||
WORK_ITEM_TRACKER_ELEMENTS,
|
||||
WORKSPACE_DEFAULT_SEARCH_RESULT,
|
||||
} from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { LayersIcon } from "@plane/propel/icons";
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||
import { cn, getTabIndex } from "@plane/utils";
|
||||
@@ -27,7 +20,6 @@ import {
|
||||
ChangeIssueState,
|
||||
CommandPaletteHelpActions,
|
||||
CommandPaletteIssueActions,
|
||||
CommandPaletteProjectActions,
|
||||
CommandPaletteSearchResults,
|
||||
CommandPaletteThemeActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
@@ -35,23 +27,24 @@ import {
|
||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
||||
// helpers
|
||||
// hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
||||
import type { CommandGroupMap } from "./utils/registry";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const CommandModal: React.FC = observer(() => {
|
||||
type Props = { commandGroups: CommandGroupMap };
|
||||
|
||||
export const CommandModal: React.FC<Props> = observer(({ commandGroups }) => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId: routerProjectId, workItem } = useParams();
|
||||
@@ -72,11 +65,8 @@ export const CommandModal: React.FC = observer(() => {
|
||||
issue: { getIssueById },
|
||||
fetchIssueWithIdentifier,
|
||||
} = useIssueDetail();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
const { platform, isMobile } = usePlatformOS();
|
||||
const { canPerformAnyCreateAction } = useUser();
|
||||
const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } =
|
||||
useCommandPalette();
|
||||
const { isCommandPaletteOpen, toggleCommandPaletteModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const projectIdentifier = workItem?.toString().split("-")[0];
|
||||
const sequence_id = workItem?.toString().split("-")[1];
|
||||
@@ -341,52 +331,31 @@ export const CommandModal: React.FC = observer(() => {
|
||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||
/>
|
||||
)}
|
||||
{workspaceSlug &&
|
||||
workspaceProjectIds &&
|
||||
workspaceProjectIds.length > 0 &&
|
||||
canPerformAnyCreateAction && (
|
||||
<Command.Group heading="Work item">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
captureClick({
|
||||
elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON,
|
||||
});
|
||||
toggleCreateIssueModal(true);
|
||||
}}
|
||||
className="focus:bg-custom-background-80"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LayersIcon className="h-3.5 w-3.5" />
|
||||
Create new work item
|
||||
</div>
|
||||
<kbd>C</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
{workspaceSlug && canPerformWorkspaceActions && (
|
||||
<Command.Group heading="Project">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
|
||||
toggleCreateProjectModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FolderPlus className="h-3.5 w-3.5" />
|
||||
Create new project
|
||||
</div>
|
||||
<kbd>P</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{/* project actions */}
|
||||
{projectId && canPerformAnyCreateAction && (
|
||||
<CommandPaletteProjectActions closePalette={closePalette} />
|
||||
)}
|
||||
{Object.entries(commandGroups).map(([heading, cmds]) => {
|
||||
const enabledCmds = cmds.filter((cmd) => !cmd.enabled || cmd.enabled());
|
||||
if (enabledCmds.length === 0) return null;
|
||||
return (
|
||||
<Command.Group heading={heading} key={heading}>
|
||||
{enabledCmds.map((cmd) => (
|
||||
<Command.Item
|
||||
key={cmd.id}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
cmd.run();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
{cmd.icon}
|
||||
{cmd.label}
|
||||
</div>
|
||||
{cmd.shortcut && <kbd>{cmd.shortcut}</kbd>}
|
||||
</Command.Item>
|
||||
))}
|
||||
</Command.Group>
|
||||
);
|
||||
})}
|
||||
{canPerformWorkspaceActions && (
|
||||
<Command.Group heading="Workspace Settings">
|
||||
<Command.Item
|
||||
|
||||
@@ -5,19 +5,19 @@ import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
import { COMMAND_PALETTE_TRACKER_ELEMENTS, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { copyTextToClipboard } from "@plane/utils";
|
||||
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
||||
// helpers
|
||||
// hooks
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
// plane web components
|
||||
import {
|
||||
IssueLevelModals,
|
||||
@@ -26,12 +26,9 @@ import {
|
||||
} from "@/plane-web/components/command-palette/modals";
|
||||
// plane web constants
|
||||
// plane web helpers
|
||||
import {
|
||||
getGlobalShortcutsList,
|
||||
getProjectShortcutsList,
|
||||
getWorkspaceShortcutsList,
|
||||
handleAdditionalKeyDownEvents,
|
||||
} from "@/plane-web/helpers/command-palette";
|
||||
import { handleAdditionalKeyDownEvents } from "@/plane-web/helpers/command-palette";
|
||||
import { useCommandRegistry } from "./utils/use-command-registry";
|
||||
import { getDefaultCommands } from "./utils/commands";
|
||||
|
||||
export const CommandPalette: FC = observer(() => {
|
||||
// router params
|
||||
@@ -41,8 +38,21 @@ export const CommandPalette: FC = observer(() => {
|
||||
const { toggleSidebar } = useAppTheme();
|
||||
const { platform } = usePlatformOS();
|
||||
const { data: currentUser, canPerformAnyCreateAction } = useUser();
|
||||
const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette();
|
||||
const {
|
||||
toggleCommandPaletteModal,
|
||||
isShortcutModalOpen,
|
||||
toggleShortcutModal,
|
||||
isAnyModalOpen,
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateViewModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
} = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { workspaceProjectIds } = useProject();
|
||||
|
||||
// derived values
|
||||
const projectIdentifier = workItem?.toString().split("-")[0];
|
||||
@@ -145,19 +155,44 @@ export const CommandPalette: FC = observer(() => {
|
||||
[canPerformAnyCreateAction]
|
||||
);
|
||||
|
||||
const shortcutsList: {
|
||||
global: Record<string, { title: string; description: string; action: () => void }>;
|
||||
workspace: Record<string, { title: string; description: string; action: () => void }>;
|
||||
project: Record<string, { title: string; description: string; action: () => void }>;
|
||||
} = useMemo(
|
||||
() => ({
|
||||
global: getGlobalShortcutsList(),
|
||||
workspace: getWorkspaceShortcutsList(),
|
||||
project: getProjectShortcutsList(),
|
||||
}),
|
||||
[]
|
||||
const commandActions = useMemo(
|
||||
() =>
|
||||
getDefaultCommands({
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateViewModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
performAnyProjectCreateActions: () => performAnyProjectCreateActions(),
|
||||
performWorkspaceCreateActions: () => performWorkspaceCreateActions(),
|
||||
performProjectCreateActions: () => performProjectCreateActions(),
|
||||
performProjectBulkDeleteActions: () => performProjectBulkDeleteActions(),
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: projectId?.toString(),
|
||||
hasProjects: workspaceProjectIds && workspaceProjectIds.length > 0,
|
||||
}),
|
||||
[
|
||||
performAnyProjectCreateActions,
|
||||
performProjectBulkDeleteActions,
|
||||
performProjectCreateActions,
|
||||
performWorkspaceCreateActions,
|
||||
workspaceProjectIds,
|
||||
projectId,
|
||||
toggleBulkDeleteIssueModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreateViewModal,
|
||||
workspaceSlug,
|
||||
]
|
||||
);
|
||||
|
||||
const { groups: commandGroups, handleKeyDown: handleCommandKeyDown } = useCommandRegistry(commandActions);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
@@ -166,83 +201,51 @@ export const CommandPalette: FC = observer(() => {
|
||||
const keyPressed = key.toLowerCase();
|
||||
const cmdClicked = ctrlKey || metaKey;
|
||||
const shiftClicked = shiftKey;
|
||||
const deleteKey = keyPressed === "backspace" || keyPressed === "delete";
|
||||
|
||||
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
|
||||
e.preventDefault();
|
||||
toggleCommandPaletteModal(true);
|
||||
}
|
||||
|
||||
// if on input, textarea or editor, don't do anything
|
||||
const target = e.target as Element;
|
||||
if (
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
(e.target as Element)?.classList?.contains("ProseMirror")
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target.classList.contains("ProseMirror")
|
||||
)
|
||||
return;
|
||||
|
||||
if (shiftClicked && (keyPressed === "?" || keyPressed === "/") && !isAnyModalOpen) {
|
||||
e.preventDefault();
|
||||
toggleShortcutModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteKey) {
|
||||
if (performProjectBulkDeleteActions()) {
|
||||
shortcutsList.project.delete.action();
|
||||
}
|
||||
} else if (cmdClicked) {
|
||||
if (cmdClicked) {
|
||||
if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) {
|
||||
e.preventDefault();
|
||||
copyIssueUrlToClipboard();
|
||||
} else if (keyPressed === "b") {
|
||||
return;
|
||||
}
|
||||
if (keyPressed === "b") {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
} else if (!isAnyModalOpen) {
|
||||
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
|
||||
if (
|
||||
Object.keys(shortcutsList.global).includes(keyPressed) &&
|
||||
((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions())
|
||||
) {
|
||||
shortcutsList.global[keyPressed].action();
|
||||
}
|
||||
// workspace authorized actions
|
||||
else if (
|
||||
Object.keys(shortcutsList.workspace).includes(keyPressed) &&
|
||||
workspaceSlug &&
|
||||
performWorkspaceCreateActions()
|
||||
) {
|
||||
e.preventDefault();
|
||||
shortcutsList.workspace[keyPressed].action();
|
||||
}
|
||||
// project authorized actions
|
||||
else if (
|
||||
Object.keys(shortcutsList.project).includes(keyPressed) &&
|
||||
projectId &&
|
||||
performProjectCreateActions()
|
||||
) {
|
||||
e.preventDefault();
|
||||
// actions that can be performed only inside a project
|
||||
shortcutsList.project[keyPressed].action();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Additional keydown events
|
||||
|
||||
if (handleCommandKeyDown(e)) return;
|
||||
|
||||
handleAdditionalKeyDownEvents(e);
|
||||
},
|
||||
[
|
||||
copyIssueUrlToClipboard,
|
||||
handleCommandKeyDown,
|
||||
isAnyModalOpen,
|
||||
platform,
|
||||
performAnyProjectCreateActions,
|
||||
performProjectBulkDeleteActions,
|
||||
performProjectCreateActions,
|
||||
performWorkspaceCreateActions,
|
||||
projectId,
|
||||
shortcutsList,
|
||||
toggleCommandPaletteModal,
|
||||
toggleShortcutModal,
|
||||
toggleSidebar,
|
||||
workspaceSlug,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -261,7 +264,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
<IssueLevelModals projectId={projectId} issueId={issueId} />
|
||||
<CommandModal />
|
||||
<CommandModal commandGroups={commandGroups} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
CYCLE_TRACKER_ELEMENTS,
|
||||
MODULE_TRACKER_ELEMENTS,
|
||||
PROJECT_PAGE_TRACKER_ELEMENTS,
|
||||
PROJECT_TRACKER_ELEMENTS,
|
||||
PROJECT_VIEW_TRACKER_ELEMENTS,
|
||||
WORK_ITEM_TRACKER_ELEMENTS,
|
||||
} from "@plane/constants";
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { LayersIcon, DiceIcon } from "@plane/propel/icons";
|
||||
import { ContrastIcon, FileText, FolderPlus, Layers } from "lucide-react";
|
||||
import { CommandAction } from "./registry";
|
||||
|
||||
export interface CommandDeps {
|
||||
toggleCreateIssueModal: (v: boolean) => void;
|
||||
toggleCreateProjectModal: (v: boolean) => void;
|
||||
toggleCreatePageModal: (v: { isOpen: boolean }) => void;
|
||||
toggleCreateModuleModal: (v: boolean) => void;
|
||||
toggleCreateCycleModal: (v: boolean) => void;
|
||||
toggleCreateViewModal: (v: boolean) => void;
|
||||
toggleBulkDeleteIssueModal: (v: boolean) => void;
|
||||
performAnyProjectCreateActions: () => boolean;
|
||||
performWorkspaceCreateActions: () => boolean;
|
||||
performProjectCreateActions: () => boolean;
|
||||
performProjectBulkDeleteActions: () => boolean;
|
||||
workspaceSlug?: string;
|
||||
projectId?: string;
|
||||
hasProjects?: boolean;
|
||||
}
|
||||
|
||||
export const getDefaultCommands = (deps: CommandDeps): CommandAction[] => {
|
||||
const {
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateViewModal,
|
||||
toggleBulkDeleteIssueModal,
|
||||
performAnyProjectCreateActions,
|
||||
performWorkspaceCreateActions,
|
||||
performProjectCreateActions,
|
||||
performProjectBulkDeleteActions,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
hasProjects,
|
||||
} = deps;
|
||||
|
||||
const commands: CommandAction[] = [
|
||||
{
|
||||
id: "create-issue",
|
||||
keys: ["c"],
|
||||
run: () => {
|
||||
toggleCreateIssueModal(true);
|
||||
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON });
|
||||
},
|
||||
enabled: () =>
|
||||
(hasProjects ? (!projectId && performAnyProjectCreateActions()) || performProjectCreateActions() : false),
|
||||
group: "Work item",
|
||||
label: "Create new work item",
|
||||
shortcut: "C",
|
||||
icon: <LayersIcon className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "create-project",
|
||||
keys: ["p"],
|
||||
run: () => {
|
||||
toggleCreateProjectModal(true);
|
||||
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_CREATE_BUTTON });
|
||||
},
|
||||
enabled: () => !!workspaceSlug && performWorkspaceCreateActions(),
|
||||
group: "Project",
|
||||
label: "Create new project",
|
||||
shortcut: "P",
|
||||
icon: <FolderPlus className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "create-cycle",
|
||||
keys: ["q"],
|
||||
run: () => {
|
||||
toggleCreateCycleModal(true);
|
||||
captureClick({ elementName: CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
|
||||
},
|
||||
enabled: () => !!projectId && performProjectCreateActions(),
|
||||
group: "Cycle",
|
||||
label: "Create new cycle",
|
||||
shortcut: "Q",
|
||||
icon: <ContrastIcon className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "create-module",
|
||||
keys: ["m"],
|
||||
run: () => {
|
||||
toggleCreateModuleModal(true);
|
||||
captureClick({ elementName: MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
|
||||
},
|
||||
enabled: () => !!projectId && performProjectCreateActions(),
|
||||
group: "Module",
|
||||
label: "Create new module",
|
||||
shortcut: "M",
|
||||
icon: <DiceIcon className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "create-view",
|
||||
keys: ["v"],
|
||||
run: () => {
|
||||
toggleCreateViewModal(true);
|
||||
captureClick({ elementName: PROJECT_VIEW_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM });
|
||||
},
|
||||
enabled: () => !!projectId && performProjectCreateActions(),
|
||||
group: "View",
|
||||
label: "Create new view",
|
||||
shortcut: "V",
|
||||
icon: <Layers className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "create-page",
|
||||
keys: ["d"],
|
||||
run: () => {
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
|
||||
},
|
||||
enabled: () => !!projectId && performProjectCreateActions(),
|
||||
group: "Page",
|
||||
label: "Create new page",
|
||||
shortcut: "D",
|
||||
icon: <FileText className="h-3.5 w-3.5" />,
|
||||
showInPalette: true,
|
||||
},
|
||||
{
|
||||
id: "bulk-delete-backspace",
|
||||
keys: ["backspace"],
|
||||
run: () => toggleBulkDeleteIssueModal(true),
|
||||
enabled: () => !!projectId && performProjectBulkDeleteActions(),
|
||||
showInPalette: false,
|
||||
},
|
||||
{
|
||||
id: "bulk-delete-delete",
|
||||
keys: ["delete"],
|
||||
run: () => toggleBulkDeleteIssueModal(true),
|
||||
enabled: () => !!projectId && performProjectBulkDeleteActions(),
|
||||
showInPalette: false,
|
||||
},
|
||||
];
|
||||
|
||||
return commands;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type CommandAction = {
|
||||
id: string;
|
||||
keys: string[]; // key sequence
|
||||
run: () => void;
|
||||
enabled?: () => boolean;
|
||||
group?: string; // grouping label for palette display
|
||||
label?: string; // user visible label
|
||||
shortcut?: string; // display shortcut key
|
||||
icon?: ReactNode; // optional icon for display
|
||||
showInPalette?: boolean; // show in command modal
|
||||
};
|
||||
|
||||
export type CommandGroupMap = Record<string, CommandAction[]>;
|
||||
|
||||
export const groupCommands = (commands: CommandAction[]): CommandGroupMap =>
|
||||
commands.reduce((acc, cmd) => {
|
||||
if (!cmd.group || cmd.showInPalette === false) return acc;
|
||||
if (!acc[cmd.group]) acc[cmd.group] = [];
|
||||
acc[cmd.group].push(cmd);
|
||||
return acc;
|
||||
}, {} as CommandGroupMap);
|
||||
|
||||
export const matchSequence = (sequence: string[], keys: string[]): boolean => {
|
||||
if (sequence.length !== keys.length) return false;
|
||||
return sequence.every((key, idx) => key === keys[idx]);
|
||||
};
|
||||
|
||||
export const isSequencePrefix = (sequence: string[], keys: string[]): boolean => {
|
||||
if (keys.length > sequence.length) return false;
|
||||
return keys.every((key, idx) => sequence[idx] === key);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { CommandAction, CommandGroupMap, groupCommands, isSequencePrefix, matchSequence } from "./registry";
|
||||
|
||||
export const useCommandRegistry = (commands: CommandAction[]) => {
|
||||
const sequenceRef = useRef<string[]>([]);
|
||||
|
||||
const groups: CommandGroupMap = useMemo(() => groupCommands(commands), [commands]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent): boolean => {
|
||||
const key = e.key.toLowerCase();
|
||||
sequenceRef.current.push(key);
|
||||
|
||||
const current = sequenceRef.current;
|
||||
const match = commands.find(
|
||||
(cmd) => matchSequence(cmd.keys, current) && (cmd.enabled ? cmd.enabled() : true)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
e.preventDefault();
|
||||
match.run();
|
||||
sequenceRef.current = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasPrefix = commands.some((cmd) => isSequencePrefix(cmd.keys, current));
|
||||
if (!hasPrefix) sequenceRef.current = [];
|
||||
return false;
|
||||
},
|
||||
[commands]
|
||||
);
|
||||
|
||||
return { groups, handleKeyDown };
|
||||
};
|
||||
@@ -50,7 +50,7 @@ export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) =>
|
||||
disabled={disableSelect}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-full"
|
||||
buttonContainerClassName="w-full text-left rounded"
|
||||
buttonContainerClassName="w-full text-left rounded js-issue-cycle"
|
||||
buttonClassName={`text-sm justify-between ${issue?.cycle_id ? "" : "text-custom-text-400"}`}
|
||||
placeholder={t("cycle.no_cycle")}
|
||||
hideIcon
|
||||
|
||||
@@ -24,6 +24,7 @@ export type TIssueLabel = {
|
||||
isInboxIssue?: boolean;
|
||||
onLabelUpdate?: (labelIds: string[]) => void;
|
||||
issueServiceType?: TIssueServiceType;
|
||||
triggerClassName?: string;
|
||||
};
|
||||
|
||||
export type TLabelOperations = {
|
||||
@@ -40,6 +41,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
isInboxIssue = false,
|
||||
onLabelUpdate,
|
||||
issueServiceType = EIssueServiceType.ISSUES,
|
||||
triggerClassName,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
@@ -111,6 +113,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||
issueId={issueId}
|
||||
values={issue?.label_ids || []}
|
||||
labelOperations={labelOperations}
|
||||
triggerClassName={triggerClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EUserPermissionsLevel, getRandomLabelColor } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserProjectRoles, IIssueLabel } from "@plane/types";
|
||||
// helpers
|
||||
import { getTabIndex } from "@plane/utils";
|
||||
import { cn, getTabIndex } from "@plane/utils";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
@@ -21,10 +21,11 @@ export interface IIssueLabelSelect {
|
||||
values: string[];
|
||||
onSelect: (_labelIds: string[]) => void;
|
||||
onAddLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<any>;
|
||||
triggerClassName?: string;
|
||||
}
|
||||
|
||||
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel } = props;
|
||||
const { workspaceSlug, projectId, issueId, values, onSelect, onAddLabel, triggerClassName } = props;
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
@@ -130,7 +131,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className="cursor-pointer rounded"
|
||||
className={cn("cursor-pointer rounded", triggerClassName)}
|
||||
onClick={() => !projectLabels && fetchLabels()}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -10,10 +10,11 @@ type TIssueLabelSelectRoot = {
|
||||
issueId: string;
|
||||
values: string[];
|
||||
labelOperations: TLabelOperations;
|
||||
triggerClassName?: string;
|
||||
};
|
||||
|
||||
export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
|
||||
const { workspaceSlug, projectId, issueId, values, labelOperations } = props;
|
||||
const { workspaceSlug, projectId, issueId, values, labelOperations, triggerClassName } = props;
|
||||
|
||||
const handleLabel = async (_labelIds: string[]) => {
|
||||
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: _labelIds });
|
||||
@@ -27,6 +28,7 @@ export const IssueLabelSelectRoot: FC<TIssueLabelSelectRoot> = (props) => {
|
||||
values={values}
|
||||
onSelect={handleLabel}
|
||||
onAddLabel={labelOperations.createLabel}
|
||||
triggerClassName={triggerClassName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
||||
placeholder={t("module.no_module")}
|
||||
disabled={disableSelect}
|
||||
className="group h-full w-full"
|
||||
buttonContainerClassName="w-full rounded"
|
||||
buttonContainerClassName="w-full rounded js-issue-module"
|
||||
buttonClassName={`min-h-8 text-sm justify-between ${issue?.module_ids?.length ? "" : "text-custom-text-400"}`}
|
||||
buttonVariant="transparent-with-text"
|
||||
hideIcon
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssueDetailShortcuts } from "@/hooks/use-issue-detail-shortcuts";
|
||||
// images
|
||||
import emptyIssue from "@/public/empty-state/issue.svg";
|
||||
// local components
|
||||
@@ -278,14 +279,16 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
);
|
||||
|
||||
// issue details
|
||||
const issue = getIssueById(issueId);
|
||||
// checking if issue is editable, based on user role
|
||||
const isEditable = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
const issue = getIssueById(issueId);
|
||||
// checking if issue is editable, based on user role
|
||||
const isEditable = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
|
||||
useIssueDetailShortcuts({ workspaceSlug, projectId, issueId, issueOperations });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -77,18 +77,18 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{t("common.state")}</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="text-sm"
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
<StateDropdown
|
||||
value={issue?.state_id}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left js-issue-state"
|
||||
buttonClassName="text-sm"
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
@@ -96,23 +96,23 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{t("common.assignees")}</span>
|
||||
</div>
|
||||
<MemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!isEditable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder={t("issue.add.assignee")}
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids?.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
<MemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!isEditable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder={t("issue.add.assignee")}
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left js-issue-assignee"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids?.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
@@ -120,15 +120,15 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{t("common.priority")}</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="border-with-text"
|
||||
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="border-with-text"
|
||||
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
|
||||
buttonContainerClassName="w-full text-left js-issue-priority"
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{createdByDetails && (
|
||||
@@ -175,28 +175,28 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{t("common.order_by.due_date")}</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder={t("issue.add.due_date")}
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={cn("text-sm", {
|
||||
"text-custom-text-400": !issue.target_date,
|
||||
"text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
|
||||
})}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100"
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
/>
|
||||
<DateDropdown
|
||||
placeholder={t("issue.add.due_date")}
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left js-issue-due-date"
|
||||
buttonClassName={cn("text-sm", {
|
||||
"text-custom-text-400": !issue.target_date,
|
||||
"text-red-500": shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group),
|
||||
})}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline !text-custom-text-100"
|
||||
// TODO: add this logic
|
||||
// showPlaceholderIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
{projectId && areEstimateEnabledByProjectId(projectId) && (
|
||||
@@ -205,22 +205,22 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<Triangle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>{t("common.estimate")}</span>
|
||||
</div>
|
||||
<EstimateDropdown
|
||||
value={issue?.estimate_point ?? undefined}
|
||||
onChange={(val: string | undefined) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||
}
|
||||
projectId={projectId}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
|
||||
placeholder={t("common.none")}
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
<EstimateDropdown
|
||||
value={issue?.estimate_point ?? undefined}
|
||||
onChange={(val: string | undefined) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||
}
|
||||
projectId={projectId}
|
||||
disabled={!isEditable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="group w-3/5 flex-grow"
|
||||
buttonContainerClassName="w-full text-left js-issue-estimate"
|
||||
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
|
||||
placeholder={t("common.none")}
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -284,6 +284,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!isEditable}
|
||||
triggerClassName="js-issue-label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
// types
|
||||
import type { TIssueOperations } from "@/components/issues/issue-detail/root";
|
||||
|
||||
interface IUseIssueDetailShortcuts {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
issueOperations: TIssueOperations;
|
||||
}
|
||||
|
||||
export const useIssueDetailShortcuts = ({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueOperations,
|
||||
}: IUseIssueDetailShortcuts) => {
|
||||
const { data: currentUser } = useUser();
|
||||
const { toggleDeleteIssueModal, isAnyModalOpen } = useCommandPalette();
|
||||
|
||||
const assignToMe = useCallback(() => {
|
||||
if (!currentUser) return;
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
assignee_ids: [currentUser.id],
|
||||
});
|
||||
}, [currentUser, issueOperations, workspaceSlug, projectId, issueId]);
|
||||
|
||||
useEffect(() => {
|
||||
const stopEvent = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
isAnyModalOpen ||
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target.isContentEditable ||
|
||||
target.closest(".ProseMirror")
|
||||
)
|
||||
return;
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const cmdOrCtrl = e.metaKey || e.ctrlKey;
|
||||
const shift = e.shiftKey;
|
||||
|
||||
const clickByClass = (className: string) => {
|
||||
const el = document.querySelector(`.${className}`) as HTMLElement | null;
|
||||
el?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
};
|
||||
|
||||
if (cmdOrCtrl && (key === "delete" || key === "backspace")) {
|
||||
stopEvent(e);
|
||||
toggleDeleteIssueModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdOrCtrl && key === "m") {
|
||||
stopEvent(e);
|
||||
const editor = document.querySelector(`#add_comment_${issueId} .ProseMirror`) as HTMLElement | null;
|
||||
editor?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdOrCtrl && shift && key === "d") {
|
||||
stopEvent(e);
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { target_date: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (shift && key === "d") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-due-date");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shift && key === "e") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-estimate");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shift && key === "c") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-cycle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shift && key === "m") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-module");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "a") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-assignee");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "i") {
|
||||
stopEvent(e);
|
||||
assignToMe();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "s") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-state");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "p") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-priority");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "l") {
|
||||
stopEvent(e);
|
||||
clickByClass("js-issue-label");
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handler, true);
|
||||
return () => document.removeEventListener("keydown", handler, true);
|
||||
}, [
|
||||
assignToMe,
|
||||
issueOperations,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
toggleDeleteIssueModal,
|
||||
isAnyModalOpen,
|
||||
]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user