mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
5 Commits
chore-refa
...
dev/power-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0106689c89 | ||
|
|
31dc1f193f | ||
|
|
c2070a09ed | ||
|
|
5b7ee22c02 | ||
|
|
35b552d6f8 |
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
@@ -26,6 +26,7 @@ export * from "./waitlist";
|
||||
export * from "./webhook";
|
||||
export * from "./workspace-views";
|
||||
export * from "./common";
|
||||
export * from "./power-k";
|
||||
export * from "./pragmatic";
|
||||
export * from "./publish";
|
||||
export * from "./search";
|
||||
|
||||
24
packages/types/src/power-k.d.ts
vendored
Normal file
24
packages/types/src/power-k.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export type TPowerKPageKeys =
|
||||
// work-item actions
|
||||
| "change-work-item-assignee"
|
||||
| "change-work-item-priority"
|
||||
| "change-work-item-state"
|
||||
// module actions
|
||||
| "change-module-member"
|
||||
| "change-module-status"
|
||||
// configs
|
||||
| "workspace-settings"
|
||||
| "project-settings"
|
||||
| "profile-settings"
|
||||
// personalization
|
||||
| "change-theme";
|
||||
|
||||
export type TPowerKCreateActionKeys = "cycle" | "issue" | "module" | "page" | "project" | "view" | "workspace";
|
||||
export type TPowerKCreateAction = {
|
||||
key: TPowerKCreateActionKeys;
|
||||
icon: any;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
shortcut?: string;
|
||||
shouldRender?: boolean;
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Metadata, Viewport } from "next";
|
||||
import Script from "next/script";
|
||||
// styles
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/command-pallette.css";
|
||||
import "@/styles/emoji.css";
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/power-k.css";
|
||||
import "@/styles/react-day-picker.css";
|
||||
// meta data info
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useParams } from "next/navigation";
|
||||
// plane types
|
||||
import { TPowerKPageKeys } from "@plane/types";
|
||||
// components
|
||||
import { PowerKIssueActionsMenu } from "@/components/command-palette/power-k/context-based-actions";
|
||||
|
||||
type Props = {
|
||||
activePage: TPowerKPageKeys | undefined;
|
||||
handleClose: () => void;
|
||||
handleUpdateSearchTerm: (searchTerm: string) => void;
|
||||
handleUpdatePage: (page: TPowerKPageKeys) => void;
|
||||
};
|
||||
|
||||
export const PowerKContextBasedActions: React.FC<Props> = (props) => {
|
||||
const { activePage, handleClose, handleUpdateSearchTerm, handleUpdatePage } = props;
|
||||
// navigation
|
||||
const { issueId } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
{issueId && (
|
||||
<PowerKIssueActionsMenu
|
||||
handleClose={handleClose}
|
||||
activePage={activePage}
|
||||
handleUpdatePage={handleUpdatePage}
|
||||
issueId={issueId.toString()}
|
||||
handleUpdateSearchTerm={handleUpdateSearchTerm}
|
||||
/>
|
||||
)}
|
||||
{/* {moduleId && (
|
||||
<PowerKModuleActionsMenu
|
||||
handleClose={handleClose}
|
||||
activePage={activePage}
|
||||
handleUpdatePage={handleUpdatePage}
|
||||
moduleId={moduleId.toString()}
|
||||
handleUpdateSearchTerm={handleUpdateSearchTerm}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
105
web/ce/components/command-palette/power-k/create-actions.ts
Normal file
105
web/ce/components/command-palette/power-k/create-actions.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { FileText, FolderPlus, Layers, SquarePlus } from "lucide-react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TPowerKCreateAction, TPowerKCreateActionKeys } from "@plane/types";
|
||||
import { ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
|
||||
// lib
|
||||
import { TAppRouterInstance } from "@/lib/n-progress/AppProgressBar";
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const commonCreateActions = (
|
||||
router: TAppRouterInstance
|
||||
): Record<TPowerKCreateActionKeys, TPowerKCreateAction> => {
|
||||
// store
|
||||
const {
|
||||
canPerformAnyCreateAction,
|
||||
permission: { allowPermissions },
|
||||
} = store.user;
|
||||
const { workspaceProjectIds, currentProjectDetails } = store.projectRoot.project;
|
||||
const {
|
||||
toggleCreateCycleModal,
|
||||
toggleCreateIssueModal,
|
||||
toggleCreateModuleModal,
|
||||
toggleCreatePageModal,
|
||||
toggleCreateProjectModal,
|
||||
toggleCreateViewModal,
|
||||
} = store.commandPalette;
|
||||
// derived values
|
||||
const canCreateIssue = workspaceProjectIds && workspaceProjectIds.length > 0;
|
||||
const canCreateProject = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
const options: Record<TPowerKCreateActionKeys, TPowerKCreateAction> = {
|
||||
issue: {
|
||||
key: "issue",
|
||||
onClick: () => toggleCreateIssueModal(true),
|
||||
label: "New work item",
|
||||
icon: LayersIcon,
|
||||
shortcut: "C",
|
||||
shouldRender: canCreateIssue,
|
||||
},
|
||||
page: {
|
||||
key: "page",
|
||||
onClick: () => toggleCreatePageModal({ isOpen: true }),
|
||||
label: "New page",
|
||||
icon: FileText,
|
||||
shortcut: "D",
|
||||
shouldRender: currentProjectDetails?.page_view && canPerformAnyCreateAction,
|
||||
},
|
||||
view: {
|
||||
key: "view",
|
||||
onClick: () => toggleCreateViewModal(true),
|
||||
label: "New view",
|
||||
icon: Layers,
|
||||
shortcut: "V",
|
||||
shouldRender: currentProjectDetails?.issue_views_view && canPerformAnyCreateAction,
|
||||
},
|
||||
cycle: {
|
||||
key: "cycle",
|
||||
onClick: () => toggleCreateCycleModal(true),
|
||||
label: "New cycle",
|
||||
icon: ContrastIcon,
|
||||
shortcut: "Q",
|
||||
shouldRender: currentProjectDetails?.cycle_view && canPerformAnyCreateAction,
|
||||
},
|
||||
module: {
|
||||
key: "module",
|
||||
onClick: () => toggleCreateModuleModal(true),
|
||||
label: "New module",
|
||||
icon: DiceIcon,
|
||||
shortcut: "M",
|
||||
shouldRender: currentProjectDetails?.module_view && canPerformAnyCreateAction,
|
||||
},
|
||||
project: {
|
||||
key: "project",
|
||||
onClick: () => toggleCreateProjectModal(true),
|
||||
label: "New project",
|
||||
icon: FolderPlus,
|
||||
shortcut: "P",
|
||||
shouldRender: canCreateProject,
|
||||
},
|
||||
workspace: {
|
||||
key: "workspace",
|
||||
onClick: () => router.push("/create-workspace"),
|
||||
label: "New workspace",
|
||||
icon: SquarePlus,
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
export const getCreateActionsList = (router: TAppRouterInstance): TPowerKCreateAction[] => {
|
||||
const optionsList = commonCreateActions(router);
|
||||
return [
|
||||
optionsList["issue"],
|
||||
optionsList["page"],
|
||||
optionsList["view"],
|
||||
optionsList["cycle"],
|
||||
optionsList["module"],
|
||||
optionsList["project"],
|
||||
optionsList["workspace"],
|
||||
];
|
||||
};
|
||||
3
web/ce/components/command-palette/power-k/index.ts
Normal file
3
web/ce/components/command-palette/power-k/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./context-based-actions";
|
||||
export * from "./create-actions";
|
||||
export * from "./placeholder";
|
||||
19
web/ce/components/command-palette/power-k/placeholder.ts
Normal file
19
web/ce/components/command-palette/power-k/placeholder.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// plane types
|
||||
import { TPowerKPageKeys } from "@plane/types";
|
||||
|
||||
export const POWER_K_PLACEHOLDER_TEXT: Record<TPowerKPageKeys | "default", string> = {
|
||||
// issue actions
|
||||
"change-issue-assignee": "Assign to",
|
||||
"change-issue-priority": "Change priority",
|
||||
"change-issue-state": "Change state",
|
||||
// module actions
|
||||
"change-module-member": "Add/remove members",
|
||||
"change-module-status": "Change status",
|
||||
// configs
|
||||
"workspace-settings": "Search workspace settings",
|
||||
"project-settings": "Search project settings",
|
||||
"profile-settings": "Search profile settings",
|
||||
// personalization
|
||||
"change-theme": "Change theme",
|
||||
default: "Type a command or search",
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
"use client";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
|
||||
// ui
|
||||
import { DiscordIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useCommandPalette, useTransient } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteHelpActions: React.FC<Props> = observer((props) => {
|
||||
const { closePalette } = props;
|
||||
// hooks
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { toggleIntercom } = useTransient();
|
||||
|
||||
return (
|
||||
<Command.Group heading="Help">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleShortcutModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
Open keyboard shortcuts
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://docs.plane.so/", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Open Plane documentation
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Join our Discord
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<GithubIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
|
||||
Report a bug
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleIntercom(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
Chat with us
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./issue-actions";
|
||||
export * from "./help-actions";
|
||||
export * from "./project-actions";
|
||||
export * from "./search-results";
|
||||
export * from "./theme-actions";
|
||||
export * from "./workspace-settings-actions";
|
||||
@@ -1,163 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useIssues, useUser } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issueDetails: TIssue | undefined;
|
||||
pages: string[];
|
||||
setPages: (pages: string[]) => void;
|
||||
setPlaceholder: (placeholder: string) => void;
|
||||
setSearchTerm: (searchTerm: string) => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issueDetails, pages, setPages, setPlaceholder, setSearchTerm } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// hooks
|
||||
const {
|
||||
issues: { updateIssue },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { toggleCommandPaletteModal, toggleDeleteIssueModal } = useCommandPalette();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const handleUpdateIssue = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issueDetails.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
if (!issueDetails || !assignee) return;
|
||||
|
||||
closePalette();
|
||||
const updatedAssignees = issueDetails.assignee_ids ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
else updatedAssignees.push(assignee);
|
||||
|
||||
handleUpdateIssue({ assignee_ids: updatedAssignees });
|
||||
};
|
||||
|
||||
const deleteIssue = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
toggleDeleteIssueModal(true);
|
||||
};
|
||||
|
||||
const copyIssueUrlToClipboard = () => {
|
||||
if (!issueId) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
copyTextToClipboard(url.href)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Some error occurred",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Command.Group heading="Work item actions">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change state...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-state"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DoubleCircleIcon className="h-3.5 w-3.5" />
|
||||
Change state...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Change priority...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-priority"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Signal className="h-3.5 w-3.5" />
|
||||
Change priority...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Assign to...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "change-issue-assignee"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
handleIssueAssignees(currentUser?.id ?? "");
|
||||
setSearchTerm("");
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
{issueDetails?.assignee_ids.includes(currentUser?.id ?? "") ? (
|
||||
<>
|
||||
<UserMinus2 className="h-3.5 w-3.5" />
|
||||
Un-assign from me
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus2 className="h-3.5 w-3.5" />
|
||||
Assign to me
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={deleteIssue} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
Delete work item
|
||||
</div>
|
||||
</Command.Item>
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
copyIssueUrlToClipboard();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
Copy work item URL
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
// plane types
|
||||
import { TIssue } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useIssues, useMember } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store
|
||||
const {
|
||||
issues: { updateIssue },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const {
|
||||
project: { projectMemberIds, getProjectMemberDetails },
|
||||
} = useMember();
|
||||
|
||||
const options =
|
||||
projectMemberIds
|
||||
?.map((userId) => {
|
||||
if (!projectId) return;
|
||||
const memberDetails = getProjectMemberDetails(userId, projectId.toString());
|
||||
|
||||
return {
|
||||
value: `${memberDetails?.member?.id}`,
|
||||
query: `${memberDetails?.member?.display_name}`,
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={memberDetails?.member?.display_name}
|
||||
src={getFileURL(memberDetails?.member?.avatar_url ?? "")}
|
||||
showTooltip={false}
|
||||
/>
|
||||
{memberDetails?.member?.display_name}
|
||||
</div>
|
||||
{issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && (
|
||||
<div>
|
||||
<Check className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((o) => o !== undefined) ?? [];
|
||||
|
||||
const handleUpdateIssue = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueAssignees = (assignee: string) => {
|
||||
const updatedAssignees = issue.assignee_ids ?? [];
|
||||
|
||||
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||
else updatedAssignees.push(assignee);
|
||||
|
||||
handleUpdateIssue({ assignee_ids: updatedAssignees });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map(
|
||||
(option) =>
|
||||
option && (
|
||||
<Command.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleIssueAssignees(option.value)}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
{option.content}
|
||||
</Command.Item>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssuesStoreType, ISSUE_PRIORITIES } from "@plane/constants";
|
||||
// plane types
|
||||
import { TIssue, TIssuePriorities } from "@plane/types";
|
||||
// mobx store
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
import { useIssues } from "@/hooks/store";
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const {
|
||||
issues: { updateIssue },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const submitChanges = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueState = (priority: TIssuePriorities) => {
|
||||
submitChanges({ priority });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ISSUE_PRIORITIES.map((priority) => (
|
||||
<Command.Item key={priority.key} onSelect={() => handleIssueState(priority.key)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<PriorityIcon priority={priority.key} />
|
||||
<span className="capitalize">{priority.title ?? "None"}</span>
|
||||
</div>
|
||||
<div>{priority.key === issue.priority && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import { Check } from "lucide-react";
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
import { useProjectState, useIssues } from "@/hooks/store";
|
||||
// ui
|
||||
// icons
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issues: { updateIssue },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { projectStates } = useProjectState();
|
||||
|
||||
const submitChanges = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { ...formData };
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, payload).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const handleIssueState = (stateId: string) => {
|
||||
submitChanges({ state_id: stateId });
|
||||
closePalette();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectStates ? (
|
||||
projectStates.length > 0 ? (
|
||||
projectStates.map((state) => (
|
||||
<Command.Item key={state.id} onSelect={() => handleIssueState(state.id)} className="focus:outline-none">
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div>{state.id === issue.state_id && <Check className="h-3 w-3" />}</div>
|
||||
</Command.Item>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No states found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./actions-list";
|
||||
export * from "./change-state";
|
||||
export * from "./change-priority";
|
||||
export * from "./change-assignee";
|
||||
@@ -1,89 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { ContrastIcon, FileText, Layers } from "lucide-react";
|
||||
// hooks
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
import { useCommandPalette, useEventTracker } from "@/hooks/store";
|
||||
// ui
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
// store hooks
|
||||
const { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal } =
|
||||
useCommandPalette();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command.Group heading="Cycle">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command palette");
|
||||
toggleCreateCycleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<ContrastIcon className="h-3.5 w-3.5" />
|
||||
Create new cycle
|
||||
</div>
|
||||
<kbd>Q</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Module">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command palette");
|
||||
toggleCreateModuleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<DiceIcon className="h-3.5 w-3.5" />
|
||||
Create new module
|
||||
</div>
|
||||
<kbd>M</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="View">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command palette");
|
||||
toggleCreateViewModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
Create new view
|
||||
</div>
|
||||
<kbd>V</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Page">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command palette");
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
Create new page
|
||||
</div>
|
||||
<kbd>D</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,423 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
ChangeIssueAssignee,
|
||||
ChangeIssuePriority,
|
||||
ChangeIssueState,
|
||||
CommandPaletteHelpActions,
|
||||
CommandPaletteIssueActions,
|
||||
CommandPaletteProjectActions,
|
||||
CommandPaletteSearchResults,
|
||||
CommandPaletteThemeActions,
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
} from "@/components/command-palette";
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
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";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const CommandModal: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId, issueId } = useParams();
|
||||
// states
|
||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { workspaceProjectIds } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { canPerformAnyCreateAction } = useUser();
|
||||
const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } =
|
||||
useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
// derived values
|
||||
const page = pages[pages.length - 1];
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
const { baseTabIndex } = getTabIndex(undefined, isMobile);
|
||||
const canPerformWorkspaceActions = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
|
||||
// TODO: update this to mobx store
|
||||
const { data: issueDetails } = useSWR(
|
||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||
workspaceSlug && projectId && issueId
|
||||
? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const closePalette = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
};
|
||||
|
||||
const createNewWorkspace = () => {
|
||||
closePalette();
|
||||
router.push("/create-workspace");
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
if (debouncedSearchTerm) {
|
||||
setIsSearching(true);
|
||||
workspaceService
|
||||
.searchWorkspace(workspaceSlug.toString(), {
|
||||
...(projectId ? { project_id: projectId.toString() } : {}),
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: !projectId ? true : isWorkspaceLevel,
|
||||
})
|
||||
.then((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 {
|
||||
setResults({
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isCommandPaletteOpen} afterLeave={() => setSearchTerm("")} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-30 overflow-y-auto">
|
||||
<div className="flex items-center justify-center p-4 sm:p-6 md:p-20">
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full max-w-2xl transform items-center justify-center divide-y divide-custom-border-200 divide-opacity-10 rounded-lg bg-custom-background-100 shadow-custom-shadow-md transition-all">
|
||||
<div className="w-full max-w-2xl">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
return 0;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// when search term is not empty, esc should clear the search term
|
||||
if (e.key === "Escape" && searchTerm) setSearchTerm("");
|
||||
|
||||
// when user tries to close the modal with esc
|
||||
if (e.key === "Escape" && !page && !searchTerm) closePalette();
|
||||
|
||||
// Escape goes to previous page
|
||||
// Backspace goes to previous page when search is empty
|
||||
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
|
||||
e.preventDefault();
|
||||
setPages((pages) => pages.slice(0, -1));
|
||||
setPlaceholder("Type a command or search...");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex gap-4 p-3 pb-0 sm:items-center ${
|
||||
issueDetails ? "flex-col justify-between sm:flex-row" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
{issueDetails && (
|
||||
<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">
|
||||
{issueDetails.project_id && (
|
||||
<IssueIdentifier
|
||||
issueId={issueDetails.id}
|
||||
projectId={issueDetails.project_id}
|
||||
textContainerClassName="text-xs font-medium text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
{issueDetails.name}
|
||||
</div>
|
||||
)}
|
||||
{projectId && (
|
||||
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
|
||||
<div className="flex flex-shrink-0 cursor-pointer items-center gap-1 self-end text-xs sm:self-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
Workspace Level
|
||||
</button>
|
||||
<ToggleSwitch
|
||||
value={isWorkspaceLevel}
|
||||
onChange={() => setIsWorkspaceLevel((prevData) => !prevData)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
<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"
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => setSearchTerm(e)}
|
||||
autoFocus
|
||||
tabIndex={baseTabIndex}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Command.List className="vertical-scrollbar scrollbar-sm max-h-96 overflow-scroll p-2">
|
||||
{searchTerm !== "" && (
|
||||
<h5 className="mx-[3px] my-4 text-xs text-custom-text-100">
|
||||
Search results for{" "}
|
||||
<span className="font-medium">
|
||||
{'"'}
|
||||
{searchTerm}
|
||||
{'"'}
|
||||
</span>{" "}
|
||||
in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
|
||||
</h5>
|
||||
)}
|
||||
|
||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<SimpleEmptyState title={t("command_k.empty_state.search.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isLoading || isSearching) && (
|
||||
<Command.Loading>
|
||||
<Loader className="space-y-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
</Command.Loading>
|
||||
)}
|
||||
|
||||
{debouncedSearchTerm !== "" && (
|
||||
<CommandPaletteSearchResults closePalette={closePalette} results={results} />
|
||||
)}
|
||||
|
||||
{!page && (
|
||||
<>
|
||||
{/* issue actions */}
|
||||
{issueId && (
|
||||
<CommandPaletteIssueActions
|
||||
closePalette={closePalette}
|
||||
issueDetails={issueDetails}
|
||||
pages={pages}
|
||||
setPages={(newPages) => setPages(newPages)}
|
||||
setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)}
|
||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||
/>
|
||||
)}
|
||||
{workspaceSlug &&
|
||||
workspaceProjectIds &&
|
||||
workspaceProjectIds.length > 0 &&
|
||||
canPerformAnyCreateAction && (
|
||||
<Command.Group heading="Work item">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
setTrackElement("Command Palette");
|
||||
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();
|
||||
setTrackElement("Command palette");
|
||||
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} />
|
||||
)}
|
||||
{canPerformWorkspaceActions && (
|
||||
<Command.Group heading="Workspace Settings">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
setPlaceholder("Search workspace settings...");
|
||||
setSearchTerm("");
|
||||
setPages([...pages, "settings"]);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
Search settings...
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
<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 */}
|
||||
<CommandPaletteHelpActions closePalette={closePalette} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* workspace settings actions */}
|
||||
{page === "settings" && workspaceSlug && (
|
||||
<CommandPaletteWorkspaceSettingsActions closePalette={closePalette} />
|
||||
)}
|
||||
|
||||
{/* issue details page actions */}
|
||||
{page === "change-issue-state" && issueDetails && (
|
||||
<ChangeIssueState closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
{page === "change-issue-priority" && issueDetails && (
|
||||
<ChangeIssuePriority closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
{page === "change-issue-assignee" && issueDetails && (
|
||||
<ChangeIssueAssignee closePalette={closePalette} issue={issueDetails} />
|
||||
)}
|
||||
|
||||
{/* theme actions */}
|
||||
{page === "change-interface-theme" && (
|
||||
<CommandPaletteThemeActions
|
||||
closePalette={() => {
|
||||
closePalette();
|
||||
setPages((pages) => pages.slice(0, -1));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
@@ -6,8 +6,6 @@ import { useParams } from "next/navigation";
|
||||
// ui
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
@@ -27,6 +25,8 @@ import {
|
||||
getWorkspaceShortcutsList,
|
||||
handleAdditionalKeyDownEvents,
|
||||
} from "@/plane-web/helpers/command-palette";
|
||||
// local components
|
||||
import { PowerKModal, ShortcutsModal } from "./";
|
||||
|
||||
export const CommandPalette: FC = observer(() => {
|
||||
// router params
|
||||
@@ -237,7 +237,7 @@ export const CommandPalette: FC = observer(() => {
|
||||
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
<IssueLevelModals />
|
||||
<CommandModal />
|
||||
<PowerKModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./actions";
|
||||
export * from "./shortcuts-modal";
|
||||
export * from "./command-modal";
|
||||
export * from "./power-k";
|
||||
export * from "./command-palette";
|
||||
export * from "./helpers";
|
||||
export * from "./shortcuts-modal";
|
||||
|
||||
36
web/core/components/command-palette/power-k/breadcrumbs.tsx
Normal file
36
web/core/components/command-palette/power-k/breadcrumbs.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
export const PowerKBreadcrumbs = observer(() => {
|
||||
// navigation
|
||||
const { issueId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
// derived values
|
||||
const issueDetails = issueId ? getIssueById(issueId.toString()) : undefined;
|
||||
|
||||
if (issueId && issueDetails) {
|
||||
return (
|
||||
<div className="flex gap-4 p-3 pb-0 sm:items-center">
|
||||
<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">
|
||||
{issueDetails.project_id && (
|
||||
<IssueIdentifier
|
||||
issueId={issueDetails.id}
|
||||
projectId={issueDetails.project_id}
|
||||
textContainerClassName="text-xs font-medium text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
{issueDetails.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
40
web/core/components/command-palette/power-k/command-item.tsx
Normal file
40
web/core/components/command-palette/power-k/command-item.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import type { ISvgIcons } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
icon?: LucideIcon | React.FC<ISvgIcons> | JSX.Element;
|
||||
label: string | React.ReactNode;
|
||||
onSelect: () => void;
|
||||
shortcut?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export const PowerKCommandItem: React.FC<Props> = (props) => {
|
||||
const { label, onSelect, shortcut, value } = props;
|
||||
|
||||
const renderIcon = () => {
|
||||
if (!props.icon) return null;
|
||||
|
||||
if (React.isValidElement(props.icon)) return props.icon;
|
||||
|
||||
// @ts-expect-error hardcoded types
|
||||
if (typeof props.icon === "function" || (props.icon as LucideIcon).$$typeof) {
|
||||
// @ts-expect-error hardcoded types
|
||||
return <props.icon className="flex-shrink-0 size-3.5" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Command.Item value={value} onSelect={onSelect} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
{renderIcon()}
|
||||
{label}
|
||||
</div>
|
||||
{shortcut && <kbd>{shortcut}</kbd>}
|
||||
</Command.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./module";
|
||||
export * from "./work-item";
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
handleUpdateMember: (assigneeId: string) => void;
|
||||
value: string[];
|
||||
};
|
||||
|
||||
export const PowerKMembersMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleUpdateMember, value } = props;
|
||||
// store hooks
|
||||
const {
|
||||
getUserDetails,
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectMemberIds?.map((memberId) => {
|
||||
const memberDetails = getUserDetails(memberId);
|
||||
if (!memberDetails) return;
|
||||
|
||||
return (
|
||||
<Command.Item key={memberId} onSelect={() => handleUpdateMember(memberId)} className="focus:outline-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={memberDetails?.display_name}
|
||||
src={getFileURL(memberDetails?.avatar_url ?? "")}
|
||||
showTooltip={false}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
{memberDetails?.display_name}
|
||||
</div>
|
||||
{value.includes(memberId ?? "") && (
|
||||
<div className="flex-shrink-0">
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
)}
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { LinkIcon, Users } from "lucide-react";
|
||||
// plane types
|
||||
import { IModule, TPowerKPageKeys } from "@plane/types";
|
||||
// hooks
|
||||
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "../../command-item";
|
||||
import { PowerKMembersMenu } from "../members-menu";
|
||||
import { PowerKModuleStatusMenu } from "./status-menu";
|
||||
|
||||
type Props = {
|
||||
activePage: TPowerKPageKeys | undefined;
|
||||
handleClose: () => void;
|
||||
handleUpdateSearchTerm: (searchTerm: string) => void;
|
||||
handleUpdatePage: (page: TPowerKPageKeys) => void;
|
||||
moduleId: string;
|
||||
};
|
||||
|
||||
export const PowerKModuleActionsMenu: React.FC<Props> = observer((props) => {
|
||||
const { activePage, handleClose, handleUpdateSearchTerm, handleUpdatePage, moduleId } = props;
|
||||
// navigation
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { getModuleById, updateModuleDetails } = useModule();
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
|
||||
const handleUpdateModule = async (formData: Partial<IModule>) => {
|
||||
if (!workspaceSlug || !projectId || !moduleDetails) return;
|
||||
await updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleDetails.id, formData).catch(
|
||||
(error) => {
|
||||
console.error("Error in updating issue from Power K:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdateMember = (memberId: string) => {
|
||||
if (!moduleDetails) return;
|
||||
|
||||
const updatedMembers = moduleDetails.member_ids ?? [];
|
||||
if (updatedMembers.includes(memberId)) updatedMembers.splice(updatedMembers.indexOf(memberId), 1);
|
||||
else updatedMembers.push(memberId);
|
||||
|
||||
handleUpdateModule({ member_ids: updatedMembers });
|
||||
};
|
||||
|
||||
const copyModuleUrlToClipboard = () => {
|
||||
const url = new URL(window.location.href);
|
||||
copyTextToClipboard(url.href)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Some error occurred",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!activePage && (
|
||||
<Command.Group heading="Module actions">
|
||||
<PowerKCommandItem
|
||||
icon={Users}
|
||||
label="Add/remove members"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-module-member");
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem
|
||||
icon={DoubleCircleIcon}
|
||||
label="Change status"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-module-status");
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem
|
||||
icon={LinkIcon}
|
||||
label="Copy module URL"
|
||||
onSelect={() => {
|
||||
handleClose();
|
||||
copyModuleUrlToClipboard();
|
||||
}}
|
||||
/>
|
||||
</Command.Group>
|
||||
)}
|
||||
{/* members menu */}
|
||||
{activePage === "change-module-member" && moduleDetails && (
|
||||
<PowerKMembersMenu handleUpdateMember={handleUpdateMember} value={moduleDetails.member_ids} />
|
||||
)}
|
||||
{/* status menu */}
|
||||
{activePage === "change-module-status" && moduleDetails?.status && (
|
||||
<PowerKModuleStatusMenu
|
||||
handleClose={handleClose}
|
||||
handleUpdateModule={handleUpdateModule}
|
||||
value={moduleDetails.status}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane imports
|
||||
import { MODULE_STATUS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IModule } from "@plane/types";
|
||||
import { ModuleStatusIcon, TModuleStatus } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleUpdateModule: (data: Partial<IModule>) => void;
|
||||
value: TModuleStatus;
|
||||
};
|
||||
|
||||
export const PowerKModuleStatusMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, handleUpdateModule, value } = props;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{MODULE_STATUS.map((status) => (
|
||||
<Command.Item
|
||||
key={status.value}
|
||||
onSelect={() => {
|
||||
handleUpdateModule({
|
||||
status: status.value,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<ModuleStatusIcon status={status.value} height="14px" width="14px" />
|
||||
<p>{t(status.i18n_label)}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">{status.value === value && <Check className="size-3" />}</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane imports
|
||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleUpdateIssue: (data: Partial<TIssue>) => void;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const PowerKPrioritiesMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, handleUpdateIssue, issue } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ISSUE_PRIORITIES.map((priority) => (
|
||||
<Command.Item
|
||||
key={priority.key}
|
||||
onSelect={() => {
|
||||
handleUpdateIssue({
|
||||
priority: priority.key,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<PriorityIcon priority={priority.key} />
|
||||
<span className="capitalize">{priority.title ?? "None"}</span>
|
||||
</div>
|
||||
<div className="flex-shrink-0">{priority.key === issue.priority && <Check className="size-3" />}</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
|
||||
// plane constants
|
||||
import { EIssuesStoreType } from "@plane/constants";
|
||||
// plane types
|
||||
import { TIssue, TPowerKPageKeys } from "@plane/types";
|
||||
// hooks
|
||||
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useCommandPalette, useIssueDetail, useIssues, useUser } from "@/hooks/store";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "../../command-item";
|
||||
import { PowerKMembersMenu } from "../members-menu";
|
||||
import { PowerKPrioritiesMenu } from "./priorities-menu";
|
||||
import { PowerKProjectStatesMenu } from "./states-menu";
|
||||
|
||||
type Props = {
|
||||
activePage: TPowerKPageKeys | undefined;
|
||||
handleClose: () => void;
|
||||
handleUpdateSearchTerm: (searchTerm: string) => void;
|
||||
handleUpdatePage: (page: TPowerKPageKeys) => void;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const PowerKIssueActionsMenu: React.FC<Props> = observer((props) => {
|
||||
const { activePage, handleClose, handleUpdateSearchTerm, handleUpdatePage, issueId } = props;
|
||||
// navigation
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issues: { updateIssue },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { toggleDeleteIssueModal } = useCommandPalette();
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId);
|
||||
const isCurrentUserAssigned = issueDetails?.assignee_ids.includes(currentUser?.id ?? "");
|
||||
|
||||
const handleUpdateIssue = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !issueDetails) return;
|
||||
await updateIssue(workspaceSlug.toString(), projectId.toString(), issueDetails.id, formData).catch((error) => {
|
||||
console.error("Error in updating issue from Power K:", error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be updated. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateAssignee = (assigneeId: string) => {
|
||||
if (!issueDetails) return;
|
||||
|
||||
const updatedAssignees = issueDetails.assignee_ids ?? [];
|
||||
if (updatedAssignees.includes(assigneeId)) updatedAssignees.splice(updatedAssignees.indexOf(assigneeId), 1);
|
||||
else updatedAssignees.push(assigneeId);
|
||||
|
||||
handleUpdateIssue({ assignee_ids: updatedAssignees });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDeleteIssue = () => {
|
||||
toggleDeleteIssueModal(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const copyIssueUrlToClipboard = () => {
|
||||
if (!issueId) return;
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
copyTextToClipboard(url.href)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Copied to clipboard",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Some error occurred",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!activePage && (
|
||||
<Command.Group heading="Work item actions">
|
||||
<PowerKCommandItem
|
||||
icon={DoubleCircleIcon}
|
||||
label="Change state"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-work-item-state");
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem
|
||||
icon={Signal}
|
||||
label="Change priority"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-work-item-priority");
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem
|
||||
icon={Users}
|
||||
label="Assign to"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-work-item-assignee");
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem
|
||||
icon={isCurrentUserAssigned ? UserMinus2 : UserPlus2}
|
||||
label={isCurrentUserAssigned ? "Un-assign from me" : "Assign to me"}
|
||||
onSelect={() => {
|
||||
if (!currentUser) return;
|
||||
handleUpdateAssignee(currentUser?.id);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
<PowerKCommandItem icon={Trash2} label="Delete" onSelect={handleDeleteIssue} />
|
||||
<PowerKCommandItem
|
||||
icon={LinkIcon}
|
||||
label="Copy URL"
|
||||
onSelect={() => {
|
||||
copyIssueUrlToClipboard();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</Command.Group>
|
||||
)}
|
||||
{/* states menu */}
|
||||
{activePage === "change-work-item-state" && issueDetails && (
|
||||
<PowerKProjectStatesMenu handleClose={handleClose} handleUpdateIssue={handleUpdateIssue} issue={issueDetails} />
|
||||
)}
|
||||
{/* priority menu */}
|
||||
{activePage === "change-work-item-priority" && issueDetails && (
|
||||
<PowerKPrioritiesMenu handleClose={handleClose} handleUpdateIssue={handleUpdateIssue} issue={issueDetails} />
|
||||
)}
|
||||
{/* members menu */}
|
||||
{activePage === "change-work-item-assignee" && issueDetails && (
|
||||
<PowerKMembersMenu handleUpdateMember={handleUpdateAssignee} value={issueDetails.assignee_ids} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// plane types
|
||||
import { TIssue } from "@plane/types";
|
||||
// plane ui
|
||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
handleUpdateIssue: (data: Partial<TIssue>) => void;
|
||||
issue: TIssue;
|
||||
};
|
||||
|
||||
export const PowerKProjectStatesMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose, handleUpdateIssue, issue } = props;
|
||||
// store hooks
|
||||
const { projectStates } = useProjectState();
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectStates ? (
|
||||
projectStates.length > 0 ? (
|
||||
projectStates.map((state) => (
|
||||
<Command.Item
|
||||
key={state.id}
|
||||
onSelect={() => {
|
||||
handleUpdateIssue({
|
||||
state_id: state.id,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} height="14px" width="14px" />
|
||||
<p>{state.name}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">{state.id === issue.state_id && <Check className="size-3" />}</div>
|
||||
</Command.Item>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center">No states found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
51
web/core/components/command-palette/power-k/create-menu.tsx
Normal file
51
web/core/components/command-palette/power-k/create-menu.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
// plane ui
|
||||
import { useEventTracker, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web helpers
|
||||
import { getCreateActionsList } from "@/plane-web/components/command-palette/power-k";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "./command-item";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PowerKCreateActionsMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { canPerformAnyCreateAction } = useUser();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
// derived values
|
||||
const CREATE_OPTIONS_LIST = useMemo(() => getCreateActionsList(router), [router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canPerformAnyCreateAction && (
|
||||
<Command.Group heading="Create">
|
||||
{CREATE_OPTIONS_LIST.map((option) => {
|
||||
if (option.shouldRender !== undefined && option.shouldRender === false) return null;
|
||||
|
||||
return (
|
||||
<PowerKCommandItem
|
||||
key={option.key}
|
||||
icon={option.icon}
|
||||
label={option.label}
|
||||
onSelect={() => {
|
||||
setTrackElement("Power K");
|
||||
option.onClick();
|
||||
handleClose();
|
||||
}}
|
||||
shortcut={option.shortcut}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
75
web/core/components/command-palette/power-k/help-menu.tsx
Normal file
75
web/core/components/command-palette/power-k/help-menu.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
|
||||
// plane ui
|
||||
import { DiscordIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useCommandPalette, useTransient } from "@/hooks/store";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "./command-item";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PowerKHelpMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// store hooks
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { toggleIntercom } = useTransient();
|
||||
|
||||
const HELP_MENU_OPTIONS = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "keyboard-shortcuts",
|
||||
label: "Open keyboard shortcuts",
|
||||
icon: Rocket,
|
||||
onClick: () => toggleShortcutModal(true),
|
||||
},
|
||||
{
|
||||
key: "documentation",
|
||||
label: "Open Plane documentation",
|
||||
icon: FileText,
|
||||
onClick: () => window.open("https://docs.plane.so/", "_blank"),
|
||||
},
|
||||
{
|
||||
key: "discord",
|
||||
label: "Join our Discord",
|
||||
icon: DiscordIcon,
|
||||
onClick: () => window.open("https://discord.com/invite/A92xrEGCge", "_blank"),
|
||||
},
|
||||
{
|
||||
key: "report-bug",
|
||||
label: "Report a bug",
|
||||
icon: GithubIcon,
|
||||
onClick: () => window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank"),
|
||||
},
|
||||
{
|
||||
key: "chat",
|
||||
label: "Chat with us",
|
||||
icon: MessageSquare,
|
||||
onClick: () => toggleIntercom(true),
|
||||
},
|
||||
],
|
||||
[toggleIntercom, toggleShortcutModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<Command.Group heading="Help">
|
||||
{HELP_MENU_OPTIONS.map((option) => (
|
||||
<PowerKCommandItem
|
||||
key={option.key}
|
||||
icon={option.icon}
|
||||
label={option.label}
|
||||
onSelect={() => {
|
||||
option.onClick();
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
||||
1
web/core/components/command-palette/power-k/index.ts
Normal file
1
web/core/components/command-palette/power-k/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
14
web/core/components/command-palette/power-k/loader.tsx
Normal file
14
web/core/components/command-palette/power-k/loader.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Command } from "cmdk";
|
||||
// plane ui
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
export const PowerKLoader = () => (
|
||||
<Command.Loading>
|
||||
<Loader className="space-y-3">
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
<Loader.Item height="40px" />
|
||||
</Loader>
|
||||
</Command.Loading>
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
// plane i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SIDEBAR_WORKSPACE_MENU_ITEMS, sidebarUserMenuItems } from "@/components/workspace";
|
||||
// hooks
|
||||
import { useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "./command-item";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PowerKNavigationMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const SIDEBAR_USER_MENU_ITEMS = sidebarUserMenuItems(currentUser?.id ?? "");
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Command.Group heading="Navigation">
|
||||
{[...SIDEBAR_USER_MENU_ITEMS, ...SIDEBAR_WORKSPACE_MENU_ITEMS].map((item) => {
|
||||
if (!allowPermissions(item.access as any, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PowerKCommandItem
|
||||
key={item.key}
|
||||
icon={item.Icon}
|
||||
label={t(item.labelTranslationKey)}
|
||||
onSelect={() => {
|
||||
router.push(`/${workspaceSlug.toString()}${item.href}`);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import React, { FC, useEffect, useState } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Settings } from "lucide-react";
|
||||
// plane imports
|
||||
import { THEME_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
@@ -13,11 +12,11 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { useUserProfile } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
const { closePalette } = props;
|
||||
export const PowerKChangeThemeMenu: FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
const { setTheme } = useTheme();
|
||||
// hooks
|
||||
const { updateUserTheme } = useUserProfile();
|
||||
@@ -49,12 +48,31 @@ export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
updateTheme(theme.value);
|
||||
closePalette();
|
||||
handleClose();
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<Settings className="h-4 w-4 text-custom-text-200" />
|
||||
<div
|
||||
className="border border-1 relative size-4 flex items-center justify-center rotate-45 transform rounded-full"
|
||||
style={{
|
||||
borderColor: theme.icon.border,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-l-full"
|
||||
style={{
|
||||
background: theme.icon.color1,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="h-full w-1/2 rounded-r-full border-l"
|
||||
style={{
|
||||
borderLeftColor: theme.icon.border,
|
||||
background: theme.icon.color2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{t(theme.i18n_label)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Command } from "cmdk";
|
||||
import { Settings } from "lucide-react";
|
||||
// plane types
|
||||
import { TPowerKPageKeys } from "@plane/types";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "../command-item";
|
||||
import { PowerKChangeThemeMenu } from "./change-theme";
|
||||
|
||||
type Props = {
|
||||
activePage: TPowerKPageKeys | undefined;
|
||||
handleClose: () => void;
|
||||
handleUpdateSearchTerm: (value: string) => void;
|
||||
handleUpdatePage: (page: TPowerKPageKeys) => void;
|
||||
};
|
||||
|
||||
export const PowerKPersonalizationMenu: React.FC<Props> = (props) => {
|
||||
const { activePage, handleClose, handleUpdateSearchTerm, handleUpdatePage } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!activePage && (
|
||||
<Command.Group heading="Personalization">
|
||||
<PowerKCommandItem
|
||||
icon={Settings}
|
||||
label="Change theme"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("change-theme");
|
||||
}}
|
||||
/>
|
||||
</Command.Group>
|
||||
)}
|
||||
{/* theme change menu */}
|
||||
{activePage === "change-theme" && <PowerKChangeThemeMenu handleClose={handleClose} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
204
web/core/components/command-palette/power-k/root.tsx
Normal file
204
web/core/components/command-palette/power-k/root.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceSearchResults, TPowerKPageKeys } from "@plane/types";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// plane web constants
|
||||
import { POWER_K_PLACEHOLDER_TEXT, PowerKContextBasedActions } from "@/plane-web/components/command-palette/power-k";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// local components
|
||||
import { PowerKBreadcrumbs } from "./breadcrumbs";
|
||||
import { PowerKCreateActionsMenu } from "./create-menu";
|
||||
import { PowerKHelpMenu } from "./help-menu";
|
||||
import { PowerKLoader } from "./loader";
|
||||
import { PowerKNavigationMenu } from "./navigation-menu";
|
||||
import { PowerKPersonalizationMenu } from "./personalization";
|
||||
import { PowerKSearchInput } from "./search-input";
|
||||
import { PowerKSearchResults } from "./search-results";
|
||||
import { PowerKSettingsMenu } from "./settings";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
export const PowerKModal: React.FC = observer(() => {
|
||||
// states
|
||||
const [resultsCount, setResultsCount] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
const [pages, setPages] = useState<TPowerKPageKeys[]>([]);
|
||||
const { isCommandPaletteOpen, toggleCommandPaletteModal } = useCommandPalette();
|
||||
// router params
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// debounce
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const activePage = pages.length > 0 ? pages[pages.length - 1] : undefined;
|
||||
const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" });
|
||||
|
||||
const handleClose = () => {
|
||||
toggleCommandPaletteModal(false);
|
||||
setTimeout(() => {
|
||||
setSearchTerm("");
|
||||
setPages([]);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleUpdatePage = useCallback((page: TPowerKPageKeys) => setPages((prev) => [...prev, page]), []);
|
||||
const handleUpdateSearchTerm = useCallback((searchTerm: string) => setSearchTerm(searchTerm), []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
// when search term is not empty, esc should clear the search term
|
||||
if (e.key === "Escape" && searchTerm) setSearchTerm("");
|
||||
|
||||
// when user tries to close the modal with esc
|
||||
if (e.key === "Escape" && !activePage && !searchTerm) handleClose();
|
||||
|
||||
// Escape goes to previous page
|
||||
// Backspace goes to previous page when search is empty
|
||||
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
|
||||
e.preventDefault();
|
||||
setPages((pages) => pages.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
// search handler
|
||||
useEffect(
|
||||
() => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
if (debouncedSearchTerm) {
|
||||
setIsSearching(true);
|
||||
workspaceService
|
||||
.searchWorkspace(workspaceSlug.toString(), {
|
||||
...(projectId ? { project_id: projectId.toString() } : {}),
|
||||
search: debouncedSearchTerm,
|
||||
workspace_search: !projectId ? true : isWorkspaceLevel,
|
||||
})
|
||||
.then((results) => {
|
||||
setResults(results);
|
||||
const count = Object.keys(results.results).reduce(
|
||||
(accumulator, key) => results.results[key as keyof typeof results.results].length + accumulator,
|
||||
0
|
||||
);
|
||||
setResultsCount(count);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
});
|
||||
} else {
|
||||
setResults({
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalCore
|
||||
isOpen={isCommandPaletteOpen}
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.TOP}
|
||||
width={EModalWidth.XXL}
|
||||
>
|
||||
<div className="w-full max-w-2xl">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
||||
return 0;
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<PowerKBreadcrumbs />
|
||||
<PowerKSearchInput
|
||||
handleUpdateSearchTerm={setSearchTerm}
|
||||
isWorkspaceLevel={isWorkspaceLevel}
|
||||
placeholder={POWER_K_PLACEHOLDER_TEXT[activePage ?? "default"]}
|
||||
searchTerm={searchTerm}
|
||||
toggleWorkspaceLevel={() => setIsWorkspaceLevel((prev) => !prev)}
|
||||
/>
|
||||
<Command.List className="vertical-scrollbar scrollbar-sm max-h-96 overflow-scroll p-2">
|
||||
{searchTerm !== "" && (
|
||||
<h5 className="mx-[3px] my-4 text-xs text-custom-text-100">
|
||||
Search results for{" "}
|
||||
<span className="font-medium">
|
||||
{'"'}
|
||||
{searchTerm}
|
||||
{'"'}
|
||||
</span>{" "}
|
||||
in {!projectId || isWorkspaceLevel ? "workspace" : "project"}:
|
||||
</h5>
|
||||
)}
|
||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<SimpleEmptyState title={t("command_k.empty_state.search.title")} assetPath={resolvedPath} />
|
||||
</div>
|
||||
)}
|
||||
{(isLoading || isSearching) && <PowerKLoader />}
|
||||
{debouncedSearchTerm !== "" && <PowerKSearchResults handleClose={handleClose} results={results} />}
|
||||
<PowerKContextBasedActions
|
||||
handleClose={handleClose}
|
||||
activePage={activePage}
|
||||
handleUpdatePage={handleUpdatePage}
|
||||
handleUpdateSearchTerm={handleUpdateSearchTerm}
|
||||
/>
|
||||
{!activePage && <PowerKNavigationMenu handleClose={handleClose} />}
|
||||
{!activePage && <PowerKCreateActionsMenu handleClose={handleClose} />}
|
||||
<PowerKSettingsMenu
|
||||
activePage={activePage}
|
||||
handleClose={handleClose}
|
||||
handleUpdateSearchTerm={handleUpdateSearchTerm}
|
||||
handleUpdatePage={handleUpdatePage}
|
||||
/>
|
||||
<PowerKPersonalizationMenu
|
||||
activePage={activePage}
|
||||
handleClose={handleClose}
|
||||
handleUpdateSearchTerm={handleUpdateSearchTerm}
|
||||
handleUpdatePage={handleUpdatePage}
|
||||
/>
|
||||
{!activePage && <PowerKHelpMenu handleClose={handleClose} />}
|
||||
</Command.List>
|
||||
</Command>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
51
web/core/components/command-palette/power-k/search-input.tsx
Normal file
51
web/core/components/command-palette/power-k/search-input.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Command } from "cmdk";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
// plane hooks
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
// plane ui
|
||||
import { ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { getTabIndex } from "@/helpers/tab-indices.helper";
|
||||
|
||||
type Props = {
|
||||
handleUpdateSearchTerm: (value: string) => void;
|
||||
isWorkspaceLevel: boolean;
|
||||
placeholder: string;
|
||||
searchTerm: string;
|
||||
toggleWorkspaceLevel: () => void;
|
||||
};
|
||||
|
||||
export const PowerKSearchInput: React.FC<Props> = (props) => {
|
||||
const { handleUpdateSearchTerm, isWorkspaceLevel, placeholder, searchTerm, toggleWorkspaceLevel } = props;
|
||||
// navigation
|
||||
const { projectId } = useParams();
|
||||
// platform os
|
||||
const { isMobile } = usePlatformOS();
|
||||
// tab index
|
||||
const { baseTabIndex } = getTabIndex(undefined, isMobile);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-4 border-b border-custom-border-200">
|
||||
<Search className="flex-shrink-0 size-4 stroke-2 text-custom-text-200" aria-hidden="true" />
|
||||
<Command.Input
|
||||
className="w-full border-0 bg-transparent text-sm text-custom-text-100 outline-none placeholder:text-custom-text-400 focus:ring-0"
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
onValueChange={handleUpdateSearchTerm}
|
||||
autoFocus
|
||||
tabIndex={baseTabIndex}
|
||||
/>
|
||||
{projectId && (
|
||||
<Tooltip tooltipContent="Toggle workspace level search" isMobile={isMobile}>
|
||||
<div className="flex-shrink-0 flex items-center gap-1 text-xs">
|
||||
<button type="button" onClick={toggleWorkspaceLevel} className="flex-shrink-0">
|
||||
Workspace level
|
||||
</button>
|
||||
<ToggleSwitch value={isWorkspaceLevel} onChange={toggleWorkspaceLevel} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,20 +2,22 @@
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
// plane types
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
// helpers
|
||||
import { commandGroups } from "@/components/command-palette";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "./command-item";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
handleClose: () => void;
|
||||
results: IWorkspaceSearchResults;
|
||||
};
|
||||
|
||||
export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
|
||||
const { closePalette, results } = props;
|
||||
export const PowerKSearchResults: React.FC<Props> = (props) => {
|
||||
const { handleClose, results } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { projectId: routerProjectId } = useParams();
|
||||
@@ -25,27 +27,23 @@ export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(results.results).map((key) => {
|
||||
const section = (results.results as any)[key];
|
||||
const section = results.results[key as keyof typeof results.results];
|
||||
const currentSection = commandGroups[key];
|
||||
|
||||
if (section.length > 0) {
|
||||
return (
|
||||
<Command.Group key={key} heading={`${currentSection.title} search`}>
|
||||
<Command.Group key={key} heading={currentSection.title}>
|
||||
{section.map((item: any) => (
|
||||
<Command.Item
|
||||
<PowerKCommandItem
|
||||
key={item.id}
|
||||
icon={currentSection.icon ?? undefined}
|
||||
value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`}
|
||||
label={currentSection.itemName(item)}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
handleClose();
|
||||
router.push(currentSection.path(item, projectId));
|
||||
}}
|
||||
value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
|
||||
{currentSection.icon}
|
||||
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
|
||||
</div>
|
||||
</Command.Item>
|
||||
/>
|
||||
))}
|
||||
</Command.Group>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { ProjectActionIcons } from "app/profile/sidebar";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
// plane imports
|
||||
import { PROFILE_ACTION_LINKS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PowerKProfileSettingsMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const router = useRouter();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{PROFILE_ACTION_LINKS.map((setting) => (
|
||||
<Command.Item
|
||||
key={setting.key}
|
||||
onSelect={() => {
|
||||
handleClose();
|
||||
router.push(`/profile${setting.href}`);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<Link href={`/profile${setting.href}`} className="flex items-center gap-2 text-custom-text-200">
|
||||
<ProjectActionIcons type={setting.key} size={16} />
|
||||
{t(setting.i18n_label)}
|
||||
</Link>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane web constants
|
||||
import { EUserPermissionsLevel, PROJECT_SETTINGS_LINKS } from "@/plane-web/constants";
|
||||
|
||||
type Props = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PowerKProjectSettingsMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
return (
|
||||
<>
|
||||
{PROJECT_SETTINGS_LINKS.map(
|
||||
(setting) =>
|
||||
allowPermissions(
|
||||
setting.access,
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString()
|
||||
) && (
|
||||
<Command.Item
|
||||
key={setting.key}
|
||||
onSelect={() => {
|
||||
handleClose();
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}${setting.href}`);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}${setting.href}`}
|
||||
className="flex items-center gap-2 text-custom-text-200"
|
||||
>
|
||||
<setting.Icon className="flex-shrink-0 size-4 text-custom-text-200" />
|
||||
{setting.label}
|
||||
</Link>
|
||||
</Command.Item>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Settings } from "lucide-react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { TPowerKPageKeys } from "@plane/types";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// local components
|
||||
import { PowerKCommandItem } from "../command-item";
|
||||
import { PowerKProfileSettingsMenu } from "./profile-settings";
|
||||
import { PowerKProjectSettingsMenu } from "./project-settings";
|
||||
import { PowerKWorkspaceSettingsMenu } from "./workspace-settings";
|
||||
|
||||
type Props = {
|
||||
activePage: TPowerKPageKeys | undefined;
|
||||
handleClose: () => void;
|
||||
handleUpdateSearchTerm: (value: string) => void;
|
||||
handleUpdatePage: (page: TPowerKPageKeys) => void;
|
||||
};
|
||||
|
||||
export const PowerKSettingsMenu: React.FC<Props> = observer((props) => {
|
||||
const { activePage, handleClose, handleUpdateSearchTerm, handleUpdatePage } = props;
|
||||
// navigation
|
||||
const { projectId } = useParams();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const canAccessWorkspaceSettings = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
const canAccessProjectSettings = !!projectId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!activePage && (
|
||||
<Command.Group heading="Configs">
|
||||
{canAccessWorkspaceSettings && (
|
||||
<PowerKCommandItem
|
||||
icon={Settings}
|
||||
label="Workspace settings"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("workspace-settings");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canAccessProjectSettings && (
|
||||
<PowerKCommandItem
|
||||
icon={Settings}
|
||||
label="Project settings"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("project-settings");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<PowerKCommandItem
|
||||
icon={Settings}
|
||||
label="Profile settings"
|
||||
onSelect={() => {
|
||||
handleUpdateSearchTerm("");
|
||||
handleUpdatePage("profile-settings");
|
||||
}}
|
||||
/>
|
||||
</Command.Group>
|
||||
)}
|
||||
{/* workspace settings menu */}
|
||||
{activePage === "workspace-settings" && canAccessWorkspaceSettings && (
|
||||
<PowerKWorkspaceSettingsMenu handleClose={handleClose} />
|
||||
)}
|
||||
{/* project settings menu */}
|
||||
{activePage === "project-settings" && canAccessProjectSettings && (
|
||||
<PowerKProjectSettingsMenu handleClose={handleClose} />
|
||||
)}
|
||||
{/* profile settings menu */}
|
||||
{activePage === "profile-settings" && <PowerKProfileSettingsMenu handleClose={handleClose} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,39 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Command } from "cmdk";
|
||||
// hooks
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, WORKSPACE_SETTINGS_LINKS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SettingIcon } from "@/components/icons";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane wev constants
|
||||
// plane web helpers
|
||||
import { shouldRenderSettingLink } from "@/plane-web/helpers/workspace.helper";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// router params
|
||||
export const PowerKWorkspaceSettingsMenu: React.FC<Props> = observer((props) => {
|
||||
const { handleClose } = props;
|
||||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// mobx store
|
||||
const router = useRouter();
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
|
||||
const redirect = (path: string) => {
|
||||
closePalette();
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -43,18 +35,19 @@ export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) =
|
||||
shouldRenderSettingLink(setting.key) && (
|
||||
<Command.Item
|
||||
key={setting.key}
|
||||
onSelect={() => redirect(`/${workspaceSlug}${setting.href}`)}
|
||||
onSelect={() => {
|
||||
handleClose();
|
||||
router.push(`/${workspaceSlug}${setting.href}`);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<Link href={`/${workspaceSlug}${setting.href}`}>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||
{t(setting.i18n_label)}
|
||||
</div>
|
||||
<Link href={`/${workspaceSlug}${setting.href}`} className="flex items-center gap-2 text-custom-text-200">
|
||||
<SettingIcon className="flex-shrink-0 size-4 text-custom-text-200" />
|
||||
{t(setting.i18n_label)}
|
||||
</Link>
|
||||
</Command.Item>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
@@ -62,7 +62,7 @@ export const SidebarUserMenuItem: FC<SidebarUserMenuItemProps> = observer((props
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={item.href} onClick={() => handleLinkClick(item.key)}>
|
||||
<Link href={`/${workspaceSlug.toString()}${item.href}`} onClick={() => handleLinkClick(item.key)}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={isActive}
|
||||
|
||||
@@ -14,43 +14,46 @@ import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useUserPermissions, useUser } from "@/hooks/store";
|
||||
|
||||
export const sidebarUserMenuItems = (currentUserId: string) => [
|
||||
{
|
||||
key: "home",
|
||||
labelTranslationKey: "sidebar.home",
|
||||
href: "/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Home,
|
||||
},
|
||||
{
|
||||
key: "your-work",
|
||||
labelTranslationKey: "sidebar.your_work",
|
||||
href: `/profile/${currentUserId}/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: UserActivityIcon,
|
||||
},
|
||||
{
|
||||
key: "notifications",
|
||||
labelTranslationKey: "sidebar.inbox",
|
||||
href: "/notifications/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Inbox,
|
||||
},
|
||||
{
|
||||
key: "drafts",
|
||||
labelTranslationKey: "sidebar.drafts",
|
||||
href: "/drafts/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: PenSquare,
|
||||
},
|
||||
];
|
||||
|
||||
export const SidebarUserMenu = observer(() => {
|
||||
// navigation
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
const { workspaceUserInfo } = useUserPermissions();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const SIDEBAR_USER_MENU_ITEMS = [
|
||||
{
|
||||
key: "home",
|
||||
labelTranslationKey: "sidebar.home",
|
||||
href: `/${workspaceSlug.toString()}/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Home,
|
||||
},
|
||||
{
|
||||
key: "your-work",
|
||||
labelTranslationKey: "sidebar.your_work",
|
||||
href: `/${workspaceSlug.toString()}/profile/${currentUser?.id}/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: UserActivityIcon,
|
||||
},
|
||||
{
|
||||
key: "notifications",
|
||||
labelTranslationKey: "sidebar.inbox",
|
||||
href: `/${workspaceSlug.toString()}/notifications/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Inbox,
|
||||
},
|
||||
{
|
||||
key: "drafts",
|
||||
labelTranslationKey: "sidebar.drafts",
|
||||
href: `/${workspaceSlug.toString()}/drafts/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: PenSquare,
|
||||
},
|
||||
];
|
||||
|
||||
// derived values
|
||||
const SIDEBAR_USER_MENU_ITEMS = sidebarUserMenuItems(currentUser?.id ?? "");
|
||||
const draftIssueCount = workspaceUserInfo[workspaceSlug.toString()]?.draft_issue_count;
|
||||
|
||||
return (
|
||||
|
||||
@@ -57,7 +57,7 @@ export const SidebarWorkspaceMenuItem: FC<SidebarWorkspaceMenuItemProps> = obser
|
||||
disabled={!sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Link href={item.href} onClick={() => handleLinkClick()}>
|
||||
<Link href={`/${workspaceSlug.toString()}${item.href}`} onClick={() => handleLinkClick()}>
|
||||
<SidebarNavItem
|
||||
className={`${sidebarCollapsed ? "p-0 size-8 aspect-square justify-center mx-auto" : ""}`}
|
||||
isActive={isActive}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { BarChart2, Briefcase, Layers } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
@@ -16,9 +15,38 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useAppTheme } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export const SIDEBAR_WORKSPACE_MENU_ITEMS = [
|
||||
{
|
||||
key: "projects",
|
||||
labelTranslationKey: "sidebar.projects",
|
||||
href: "/projects/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Briefcase,
|
||||
},
|
||||
{
|
||||
key: "views",
|
||||
labelTranslationKey: "sidebar.views",
|
||||
href: "/workspace-views/all-issues/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Layers,
|
||||
},
|
||||
{
|
||||
key: "active-cycles",
|
||||
labelTranslationKey: "sidebar.cycles",
|
||||
href: "/active-cycles/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: ContrastIcon,
|
||||
},
|
||||
{
|
||||
key: "analytics",
|
||||
labelTranslationKey: "sidebar.analytics",
|
||||
href: "/analytics/",
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: BarChart2,
|
||||
},
|
||||
];
|
||||
|
||||
export const SidebarWorkspaceMenu = observer(() => {
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { sidebarCollapsed } = useAppTheme();
|
||||
// local storage
|
||||
@@ -30,37 +58,6 @@ export const SidebarWorkspaceMenu = observer(() => {
|
||||
if (sidebarCollapsed) toggleWorkspaceMenu(true);
|
||||
}, [sidebarCollapsed, toggleWorkspaceMenu]);
|
||||
|
||||
const SIDEBAR_WORKSPACE_MENU_ITEMS = [
|
||||
{
|
||||
key: "projects",
|
||||
labelTranslationKey: "sidebar.projects",
|
||||
href: `/${workspaceSlug}/projects/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Briefcase,
|
||||
},
|
||||
{
|
||||
key: "views",
|
||||
labelTranslationKey: "sidebar.views",
|
||||
href: `/${workspaceSlug}/workspace-views/all-issues/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
Icon: Layers,
|
||||
},
|
||||
{
|
||||
key: "active-cycles",
|
||||
labelTranslationKey: "sidebar.cycles",
|
||||
href: `/${workspaceSlug}/active-cycles/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: ContrastIcon,
|
||||
},
|
||||
{
|
||||
key: "analytics",
|
||||
labelTranslationKey: "sidebar.analytics",
|
||||
href: `/${workspaceSlug}/analytics/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
Icon: BarChart2,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Disclosure as="div" defaultOpen>
|
||||
<SidebarWorkspaceMenuHeader isWorkspaceMenuOpen={isWorkspaceMenuOpen} toggleWorkspaceMenu={toggleWorkspaceMenu} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// router from n-progress-bar
|
||||
import { useRouter } from "@/lib/n-progress";
|
||||
import { TAppRouterInstance } from "@/lib/n-progress/AppProgressBar";
|
||||
|
||||
export const useAppRouter = () => useRouter();
|
||||
export const useAppRouter = (): TAppRouterInstance => useRouter();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { NavigateOptions } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
import { NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
import { usePathname, useSearchParams, useRouter as useNextRouter } from "next/navigation";
|
||||
import NProgress from "nprogress";
|
||||
import { getAnchorProperty, hasPreventProgressAttribute } from "./utils/getAnchorProperty";
|
||||
@@ -242,6 +242,15 @@ export const AppProgressBar = React.memo(
|
||||
|
||||
AppProgressBar.displayName = "AppProgressBar";
|
||||
|
||||
export type TAppRouterInstance = {
|
||||
push: (href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => void;
|
||||
replace: (href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => void;
|
||||
back: (NProgressOptions?: RouterNProgressOptions) => void;
|
||||
forward(): void;
|
||||
refresh(): void;
|
||||
prefetch(href: string, options?: PrefetchOptions): void;
|
||||
};
|
||||
|
||||
export function useRouter() {
|
||||
const router = useNextRouter();
|
||||
|
||||
@@ -270,24 +279,24 @@ export function useRouter() {
|
||||
[router]
|
||||
);
|
||||
|
||||
const push = useCallback(
|
||||
(href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => {
|
||||
const push: TAppRouterInstance["push"] = useCallback(
|
||||
(href, options, NProgressOptions) => {
|
||||
progress(href, options, NProgressOptions);
|
||||
return router.push(href, options);
|
||||
},
|
||||
[router, startProgress]
|
||||
);
|
||||
|
||||
const replace = useCallback(
|
||||
(href: string, options?: NavigateOptions, NProgressOptions?: RouterNProgressOptions) => {
|
||||
const replace: TAppRouterInstance["replace"] = useCallback(
|
||||
(href, options, NProgressOptions) => {
|
||||
progress(href, options, NProgressOptions);
|
||||
return router.replace(href, options);
|
||||
},
|
||||
[router, startProgress]
|
||||
);
|
||||
|
||||
const back = useCallback(
|
||||
(NProgressOptions?: RouterNProgressOptions) => {
|
||||
const back: TAppRouterInstance["back"] = useCallback(
|
||||
(NProgressOptions) => {
|
||||
if (NProgressOptions?.showProgressBar === false) return router.back();
|
||||
|
||||
startProgress(NProgressOptions?.startPosition);
|
||||
|
||||
1
web/ee/components/command-palette/power-k/index.ts
Normal file
1
web/ee/components/command-palette/power-k/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "ce/components/command-palette/power-k";
|
||||
@@ -1,39 +0,0 @@
|
||||
[cmdk-group]:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
[cmdk-group-heading] {
|
||||
color: rgb(var(--color-text-secondary));
|
||||
font-size: 0.75rem;
|
||||
margin: 0 0 0.25rem 0.25rem;
|
||||
}
|
||||
|
||||
[cmdk-item] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.825rem;
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[cmdk-item] kbd {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(var(--color-background-100));
|
||||
}
|
||||
|
||||
[cmdk-item]:hover {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
}
|
||||
|
||||
[cmdk-item][aria-selected="true"] {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
}
|
||||
37
web/styles/power-k.css
Normal file
37
web/styles/power-k.css
Normal file
@@ -0,0 +1,37 @@
|
||||
[cmdk-group]:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
[cmdk-group-heading] {
|
||||
color: rgba(var(--color-text-secondary));
|
||||
font-size: 0.75rem;
|
||||
margin: 0 0 0.25rem 0.25rem;
|
||||
}
|
||||
|
||||
[cmdk-item] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.825rem;
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&[aria-selected="true"] {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
}
|
||||
|
||||
kbd {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(var(--color-background-100));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user