mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 20:07:56 +01:00
refactor command palette into modular command system
This commit is contained in:
135
apps/web/core/components/command-palette/command-config.ts
Normal file
135
apps/web/core/components/command-palette/command-config.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FolderPlus, Settings } from "lucide-react";
|
||||||
|
import { LayersIcon } from "@plane/propel/icons";
|
||||||
|
import {
|
||||||
|
PROJECT_TRACKER_ELEMENTS,
|
||||||
|
WORK_ITEM_TRACKER_ELEMENTS,
|
||||||
|
} from "@plane/constants";
|
||||||
|
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||||
|
import { PaletteCommandGroup } from "./types";
|
||||||
|
|
||||||
|
interface BuildParams {
|
||||||
|
workspaceSlug?: string | string[];
|
||||||
|
pages: string[];
|
||||||
|
workspaceProjectIds?: string[];
|
||||||
|
canPerformAnyCreateAction: boolean;
|
||||||
|
canPerformWorkspaceActions: boolean;
|
||||||
|
closePalette: () => void;
|
||||||
|
toggleCreateIssueModal: (v: boolean) => void;
|
||||||
|
toggleCreateProjectModal: (v: boolean) => void;
|
||||||
|
setPages: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
setPlaceholder: (v: string) => void;
|
||||||
|
setSearchTerm: (v: string) => void;
|
||||||
|
createNewWorkspace: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildCommandGroups = (params: BuildParams): PaletteCommandGroup[] => {
|
||||||
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
pages,
|
||||||
|
workspaceProjectIds,
|
||||||
|
canPerformAnyCreateAction,
|
||||||
|
canPerformWorkspaceActions,
|
||||||
|
closePalette,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
toggleCreateProjectModal,
|
||||||
|
setPages,
|
||||||
|
setPlaceholder,
|
||||||
|
setSearchTerm,
|
||||||
|
createNewWorkspace,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const groups: PaletteCommandGroup[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
workspaceSlug &&
|
||||||
|
workspaceProjectIds &&
|
||||||
|
workspaceProjectIds.length > 0 &&
|
||||||
|
canPerformAnyCreateAction
|
||||||
|
) {
|
||||||
|
groups.push({
|
||||||
|
id: "work-item",
|
||||||
|
heading: "Work item",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "create-work-item",
|
||||||
|
label: "Create new work item",
|
||||||
|
shortcut: "C",
|
||||||
|
icon: LayersIcon,
|
||||||
|
perform: () => {
|
||||||
|
closePalette();
|
||||||
|
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_BUTTON });
|
||||||
|
toggleCreateIssueModal(true);
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceSlug && canPerformWorkspaceActions) {
|
||||||
|
groups.push({
|
||||||
|
id: "project",
|
||||||
|
heading: "Project",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "create-project",
|
||||||
|
label: "Create new project",
|
||||||
|
shortcut: "P",
|
||||||
|
icon: FolderPlus,
|
||||||
|
perform: () => {
|
||||||
|
closePalette();
|
||||||
|
captureClick({ elementName: PROJECT_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON });
|
||||||
|
toggleCreateProjectModal(true);
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.push({
|
||||||
|
id: "workspace-settings",
|
||||||
|
heading: "Workspace Settings",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "search-settings",
|
||||||
|
label: "Search settings...",
|
||||||
|
icon: Settings,
|
||||||
|
perform: () => {
|
||||||
|
setPlaceholder("Search workspace settings...");
|
||||||
|
setSearchTerm("");
|
||||||
|
setPages((p) => [...p, "settings"]);
|
||||||
|
},
|
||||||
|
enabled: !!(canPerformWorkspaceActions && workspaceSlug),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
groups.push({
|
||||||
|
id: "account",
|
||||||
|
heading: "Account",
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
id: "create-workspace",
|
||||||
|
label: "Create new workspace",
|
||||||
|
icon: FolderPlus,
|
||||||
|
perform: createNewWorkspace,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "change-interface-theme",
|
||||||
|
label: "Change interface theme...",
|
||||||
|
icon: Settings,
|
||||||
|
perform: () => {
|
||||||
|
setPlaceholder("Change interface theme...");
|
||||||
|
setSearchTerm("");
|
||||||
|
setPages((p) => [...p, "change-interface-theme"]);
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
||||||
// helpers
|
// helpers
|
||||||
// hooks
|
// hooks
|
||||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
|
||||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
@@ -46,6 +45,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
|||||||
// plane web components
|
// plane web components
|
||||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
|
||||||
|
import { buildCommandGroups } from "./command-config";
|
||||||
|
import { PaletteCommandGroup } from "./types";
|
||||||
// plane web services
|
// plane web services
|
||||||
import { WorkspaceService } from "@/plane-web/services";
|
import { WorkspaceService } from "@/plane-web/services";
|
||||||
|
|
||||||
@@ -124,6 +125,38 @@ export const CommandModal: React.FC = observer(() => {
|
|||||||
router.push("/create-workspace");
|
router.push("/create-workspace");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const commandGroups: PaletteCommandGroup[] = useMemo(
|
||||||
|
() =>
|
||||||
|
buildCommandGroups({
|
||||||
|
workspaceSlug,
|
||||||
|
pages,
|
||||||
|
workspaceProjectIds,
|
||||||
|
canPerformAnyCreateAction,
|
||||||
|
canPerformWorkspaceActions,
|
||||||
|
closePalette,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
toggleCreateProjectModal,
|
||||||
|
setPages,
|
||||||
|
setPlaceholder,
|
||||||
|
setSearchTerm,
|
||||||
|
createNewWorkspace,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
workspaceSlug,
|
||||||
|
pages,
|
||||||
|
workspaceProjectIds,
|
||||||
|
canPerformAnyCreateAction,
|
||||||
|
canPerformWorkspaceActions,
|
||||||
|
closePalette,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
toggleCreateProjectModal,
|
||||||
|
setPages,
|
||||||
|
setPlaceholder,
|
||||||
|
setSearchTerm,
|
||||||
|
createNewWorkspace,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
@@ -341,91 +374,32 @@ export const CommandModal: React.FC = observer(() => {
|
|||||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{workspaceSlug &&
|
{commandGroups.map((group) => (
|
||||||
workspaceProjectIds &&
|
<React.Fragment key={group.id}>
|
||||||
workspaceProjectIds.length > 0 &&
|
{group.id === "workspace-settings" &&
|
||||||
|
projectId &&
|
||||||
canPerformAnyCreateAction && (
|
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} />
|
<CommandPaletteProjectActions closePalette={closePalette} />
|
||||||
)}
|
)}
|
||||||
{canPerformWorkspaceActions && (
|
<Command.Group heading={group.heading}>
|
||||||
<Command.Group heading="Workspace Settings">
|
{group.commands
|
||||||
|
.filter((c) => c.enabled !== false)
|
||||||
|
.map((c) => (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
key={c.id}
|
||||||
setPlaceholder("Search workspace settings...");
|
onSelect={() => c.perform()}
|
||||||
setSearchTerm("");
|
|
||||||
setPages([...pages, "settings"]);
|
|
||||||
}}
|
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
<Settings className="h-3.5 w-3.5" />
|
{c.icon && <c.icon className="h-3.5 w-3.5" />}
|
||||||
Search settings...
|
{c.label}
|
||||||
</div>
|
</div>
|
||||||
|
{c.shortcut && <kbd>{c.shortcut}</kbd>}
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
))}
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
)}
|
</React.Fragment>
|
||||||
<Command.Group heading="Account">
|
))}
|
||||||
<Command.Item onSelect={createNewWorkspace} 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 workspace
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => {
|
|
||||||
setPlaceholder("Change interface theme...");
|
|
||||||
setSearchTerm("");
|
|
||||||
setPages([...pages, "change-interface-theme"]);
|
|
||||||
}}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<Settings className="h-3.5 w-3.5" />
|
|
||||||
Change interface theme...
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
</Command.Group>
|
|
||||||
|
|
||||||
{/* help options */}
|
{/* help options */}
|
||||||
<CommandPaletteHelpActions closePalette={closePalette} />
|
<CommandPaletteHelpActions closePalette={closePalette} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback, useEffect, FC, useMemo } from "react";
|
import React, { useCallback, FC, useMemo } from "react";
|
||||||
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";
|
||||||
@@ -10,6 +10,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
|||||||
// components
|
// components
|
||||||
import { copyTextToClipboard } from "@plane/utils";
|
import { copyTextToClipboard } from "@plane/utils";
|
||||||
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
||||||
|
import { useShortcuts, Shortcut } from "./use-shortcuts";
|
||||||
// helpers
|
// helpers
|
||||||
// hooks
|
// hooks
|
||||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||||
@@ -158,98 +159,119 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const isEditable = (e: KeyboardEvent) => {
|
||||||
(e: KeyboardEvent) => {
|
const target = e.target as HTMLElement;
|
||||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
return (
|
||||||
if (!key) return;
|
target instanceof HTMLTextAreaElement ||
|
||||||
|
target instanceof HTMLInputElement ||
|
||||||
|
target.isContentEditable ||
|
||||||
|
target.classList.contains("ProseMirror")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const keyPressed = key.toLowerCase();
|
const shortcuts: Shortcut[] = useMemo(() => {
|
||||||
const cmdClicked = ctrlKey || metaKey;
|
const list: Shortcut[] = [
|
||||||
const shiftClicked = shiftKey;
|
{
|
||||||
const deleteKey = keyPressed === "backspace" || keyPressed === "delete";
|
keys: ["meta", "k"],
|
||||||
|
handler: () => toggleCommandPaletteModal(true),
|
||||||
if (cmdClicked && keyPressed === "k" && !isAnyModalOpen) {
|
enabled: () => !isAnyModalOpen,
|
||||||
e.preventDefault();
|
|
||||||
toggleCommandPaletteModal(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if on input, textarea or editor, don't do anything
|
|
||||||
if (
|
|
||||||
e.target instanceof HTMLTextAreaElement ||
|
|
||||||
e.target instanceof HTMLInputElement ||
|
|
||||||
(e.target as Element)?.classList?.contains("ProseMirror")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (shiftClicked && (keyPressed === "?" || keyPressed === "/") && !isAnyModalOpen) {
|
|
||||||
e.preventDefault();
|
|
||||||
toggleShortcutModal(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteKey) {
|
|
||||||
if (performProjectBulkDeleteActions()) {
|
|
||||||
shortcutsList.project.delete.action();
|
|
||||||
}
|
|
||||||
} else if (cmdClicked) {
|
|
||||||
if (keyPressed === "c" && ((platform === "MacOS" && ctrlKey) || altKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
copyIssueUrlToClipboard();
|
|
||||||
} else 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Additional keydown events
|
|
||||||
handleAdditionalKeyDownEvents(e);
|
|
||||||
},
|
},
|
||||||
[
|
{
|
||||||
|
keys: ["control", "k"],
|
||||||
|
handler: () => toggleCommandPaletteModal(true),
|
||||||
|
enabled: () => !isAnyModalOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["shift", "?"],
|
||||||
|
handler: () => toggleShortcutModal(true),
|
||||||
|
enabled: () => !isAnyModalOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["shift", "/"],
|
||||||
|
handler: () => toggleShortcutModal(true),
|
||||||
|
enabled: () => !isAnyModalOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: platform === "MacOS" ? ["control", "meta", "c"] : ["control", "alt", "c"],
|
||||||
|
handler: () => copyIssueUrlToClipboard(),
|
||||||
|
enabled: () => !!workItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["meta", "b"],
|
||||||
|
handler: () => toggleSidebar(),
|
||||||
|
enabled: (e) => !isEditable(e),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["control", "b"],
|
||||||
|
handler: () => toggleSidebar(),
|
||||||
|
enabled: (e) => !isEditable(e),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Object.keys(shortcutsList.global).forEach((k) => {
|
||||||
|
list.push({
|
||||||
|
sequence: [k],
|
||||||
|
handler: () => {
|
||||||
|
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
|
||||||
|
shortcutsList.global[k].action();
|
||||||
|
},
|
||||||
|
enabled: (e) =>
|
||||||
|
!isEditable(e) &&
|
||||||
|
!isAnyModalOpen &&
|
||||||
|
(((!projectId && performAnyProjectCreateActions()) || performProjectCreateActions())),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(shortcutsList.workspace).forEach((k) => {
|
||||||
|
list.push({
|
||||||
|
sequence: [k],
|
||||||
|
handler: () => {
|
||||||
|
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
|
||||||
|
shortcutsList.workspace[k].action();
|
||||||
|
},
|
||||||
|
enabled: (e) =>
|
||||||
|
!isEditable(e) &&
|
||||||
|
!isAnyModalOpen &&
|
||||||
|
!!workspaceSlug &&
|
||||||
|
performWorkspaceCreateActions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(shortcutsList.project).forEach(([k, v]) => {
|
||||||
|
const isDeleteKey = k === "delete" || k === "backspace";
|
||||||
|
list.push({
|
||||||
|
sequence: [k],
|
||||||
|
handler: () => {
|
||||||
|
captureClick({ elementName: COMMAND_PALETTE_TRACKER_ELEMENTS.COMMAND_PALETTE_SHORTCUT_KEY });
|
||||||
|
v.action();
|
||||||
|
},
|
||||||
|
enabled: (e) =>
|
||||||
|
!isEditable(e) &&
|
||||||
|
!isAnyModalOpen &&
|
||||||
|
!!projectId &&
|
||||||
|
(isDeleteKey ? performProjectBulkDeleteActions() : performProjectCreateActions()),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}, [
|
||||||
copyIssueUrlToClipboard,
|
copyIssueUrlToClipboard,
|
||||||
isAnyModalOpen,
|
isAnyModalOpen,
|
||||||
platform,
|
|
||||||
performAnyProjectCreateActions,
|
performAnyProjectCreateActions,
|
||||||
performProjectBulkDeleteActions,
|
performProjectBulkDeleteActions,
|
||||||
performProjectCreateActions,
|
performProjectCreateActions,
|
||||||
performWorkspaceCreateActions,
|
performWorkspaceCreateActions,
|
||||||
|
platform,
|
||||||
projectId,
|
projectId,
|
||||||
shortcutsList,
|
shortcutsList,
|
||||||
toggleCommandPaletteModal,
|
toggleCommandPaletteModal,
|
||||||
toggleShortcutModal,
|
toggleShortcutModal,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
]
|
workItem,
|
||||||
);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useShortcuts(shortcuts, { additional: handleAdditionalKeyDownEvents });
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [handleKeyDown]);
|
|
||||||
|
|
||||||
if (!currentUser) return null;
|
if (!currentUser) return null;
|
||||||
|
|
||||||
|
|||||||
16
apps/web/core/components/command-palette/types.ts
Normal file
16
apps/web/core/components/command-palette/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface PaletteCommand {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
shortcut?: string;
|
||||||
|
icon?: React.ComponentType<{ className?: string }>;
|
||||||
|
perform: () => void;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaletteCommandGroup {
|
||||||
|
id: string;
|
||||||
|
heading: string;
|
||||||
|
commands: PaletteCommand[];
|
||||||
|
}
|
||||||
67
apps/web/core/components/command-palette/use-shortcuts.ts
Normal file
67
apps/web/core/components/command-palette/use-shortcuts.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export interface Shortcut {
|
||||||
|
keys?: string[]; // simultaneous combination
|
||||||
|
sequence?: string[]; // sequential keys
|
||||||
|
handler: (e: KeyboardEvent) => void;
|
||||||
|
enabled?: (e: KeyboardEvent) => boolean;
|
||||||
|
preventDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchCombo = (pressed: string[], combo: string[]) => {
|
||||||
|
if (pressed.length !== combo.length) return false;
|
||||||
|
return combo.every((k) => pressed.includes(k));
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchSequence = (buffer: string[], sequence: string[]) => {
|
||||||
|
if (buffer.length < sequence.length) return false;
|
||||||
|
const start = buffer.length - sequence.length;
|
||||||
|
return sequence.every((k, i) => buffer[start + i] === k);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShortcuts = (
|
||||||
|
shortcuts: Shortcut[],
|
||||||
|
options?: { filter?: (e: KeyboardEvent) => boolean; additional?: (e: KeyboardEvent) => void }
|
||||||
|
) => {
|
||||||
|
const bufferRef = useRef<string[]>([]);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const listener = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (options?.filter && !options.filter(e)) return;
|
||||||
|
|
||||||
|
const pressed: string[] = [];
|
||||||
|
if (e.metaKey) pressed.push("meta");
|
||||||
|
if (e.ctrlKey) pressed.push("control");
|
||||||
|
if (e.altKey) pressed.push("alt");
|
||||||
|
if (e.shiftKey) pressed.push("shift");
|
||||||
|
pressed.push(e.key.toLowerCase());
|
||||||
|
|
||||||
|
bufferRef.current.push(e.key.toLowerCase());
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
bufferRef.current = [];
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
shortcuts.forEach((s) => {
|
||||||
|
if (s.enabled && !s.enabled(e)) return;
|
||||||
|
if (s.keys && matchCombo(pressed, s.keys)) {
|
||||||
|
if (s.preventDefault ?? true) e.preventDefault();
|
||||||
|
s.handler(e);
|
||||||
|
} else if (s.sequence && matchSequence(bufferRef.current, s.sequence)) {
|
||||||
|
if (s.preventDefault ?? true) e.preventDefault();
|
||||||
|
bufferRef.current = [];
|
||||||
|
s.handler(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
options?.additional?.(e);
|
||||||
|
},
|
||||||
|
[shortcuts, options]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener("keydown", listener);
|
||||||
|
return () => document.removeEventListener("keydown", listener);
|
||||||
|
}, [listener]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user