feat: init
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { GithubIcon, MessageSquare, Rocket } from "lucide-react";
|
||||
// ui
|
||||
import { DiscordIcon, PageIcon } from "@plane/propel/icons";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useTransient } from "@/hooks/store/use-transient";
|
||||
|
||||
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">
|
||||
<PageIcon 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>
|
||||
);
|
||||
});
|
||||
25
apps/web/core/components/command-palette/actions/helper.tsx
Normal file
25
apps/web/core/components/command-palette/actions/helper.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export const openProjectAndScrollToSidebar = (itemProjectId: string | undefined) => {
|
||||
if (!itemProjectId) {
|
||||
console.warn("No project id provided. Cannot open project and scroll to sidebar.");
|
||||
return;
|
||||
}
|
||||
// open the project list
|
||||
store.commandPalette.toggleProjectListOpen(itemProjectId, true);
|
||||
// scroll to the element
|
||||
const scrollElementId = `sidebar-${itemProjectId}-JOINED`;
|
||||
const scrollElement = document.getElementById(scrollElementId);
|
||||
// if the element exists, scroll to it
|
||||
if (scrollElement) {
|
||||
setTimeout(() => {
|
||||
scrollElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
// Restart the highlight animation every time
|
||||
scrollElement.style.animation = "none";
|
||||
// Trigger a reflow to ensure the animation is restarted
|
||||
void scrollElement.offsetWidth;
|
||||
// Restart the highlight animation
|
||||
scrollElement.style.animation = "highlight 2s ease-in-out";
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
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";
|
||||
@@ -0,0 +1,164 @@
|
||||
"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 { DoubleCircleIcon } from "@plane/propel/icons";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// hooks
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
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 } = useParams();
|
||||
// hooks
|
||||
const { updateIssue } = useIssueDetail(issueDetails?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
|
||||
const { toggleCommandPaletteModal, toggleDeleteIssueModal } = useCommandPalette();
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
const issueId = issueDetails?.id;
|
||||
const projectId = issueDetails?.project_id;
|
||||
|
||||
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" });
|
||||
});
|
||||
};
|
||||
|
||||
const actionHeading = issueDetails?.is_epic ? "Epic actions" : "Work item actions";
|
||||
const entityType = issueDetails?.is_epic ? "epic" : "work item";
|
||||
|
||||
return (
|
||||
<Command.Group heading={actionHeading}>
|
||||
<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 ${entityType}`}
|
||||
</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 ${entityType} URL`}
|
||||
</div>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
// plane types
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
|
||||
type Props = { closePalette: () => void; issue: TIssue };
|
||||
|
||||
export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store
|
||||
const { updateIssue } = useIssueDetail(issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
|
||||
const {
|
||||
project: { getProjectMemberIds, getProjectMemberDetails },
|
||||
} = useMember();
|
||||
// derived values
|
||||
const projectId = issue?.project_id ?? "";
|
||||
const projectMemberIds = getProjectMemberIds(projectId, false);
|
||||
|
||||
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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Check } from "lucide-react";
|
||||
// plane constants
|
||||
import { ISSUE_PRIORITIES } from "@plane/constants";
|
||||
// plane types
|
||||
import { PriorityIcon } from "@plane/propel/icons";
|
||||
import type { TIssue, TIssuePriorities } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// mobx store
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// 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 } = useParams();
|
||||
// store hooks
|
||||
const { updateIssue } = useIssueDetail(issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
|
||||
// derived values
|
||||
const projectId = issue?.project_id;
|
||||
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// store hooks
|
||||
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
|
||||
// plane web imports
|
||||
import { ChangeWorkItemStateList } from "@/plane-web/components/command-palette/actions/work-item-actions";
|
||||
|
||||
type Props = { closePalette: () => void; issue: TIssue };
|
||||
|
||||
export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, issue } = props;
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { updateIssue } = useIssueDetail(issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
|
||||
// derived values
|
||||
const projectId = issue?.project_id;
|
||||
const currentStateId = issue?.state_id;
|
||||
|
||||
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 (
|
||||
<ChangeWorkItemStateList
|
||||
projectId={projectId}
|
||||
currentStateId={currentStateId}
|
||||
handleStateChange={handleIssueState}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./actions-list";
|
||||
export * from "./change-state";
|
||||
export * from "./change-priority";
|
||||
export * from "./change-assignee";
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
// hooks
|
||||
import {
|
||||
CYCLE_TRACKER_ELEMENTS,
|
||||
MODULE_TRACKER_ELEMENTS,
|
||||
PROJECT_PAGE_TRACKER_ELEMENTS,
|
||||
PROJECT_VIEW_TRACKER_ELEMENTS,
|
||||
} from "@plane/constants";
|
||||
import { CycleIcon, ModuleIcon, PageIcon, ViewsIcon } from "@plane/propel/icons";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
// ui
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
// store hooks
|
||||
const { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal } =
|
||||
useCommandPalette();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Command.Group heading="Cycle">
|
||||
<Command.Item
|
||||
data-ph-element={CYCLE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateCycleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<CycleIcon className="h-3.5 w-3.5" />
|
||||
Create new cycle
|
||||
</div>
|
||||
<kbd>Q</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Module">
|
||||
<Command.Item
|
||||
data-ph-element={MODULE_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateModuleModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<ModuleIcon className="h-3.5 w-3.5" />
|
||||
Create new module
|
||||
</div>
|
||||
<kbd>M</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="View">
|
||||
<Command.Item
|
||||
data-ph-element={PROJECT_VIEW_TRACKER_ELEMENTS.COMMAND_PALETTE_ADD_ITEM}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreateViewModal(true);
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<ViewsIcon className="h-3.5 w-3.5" />
|
||||
Create new view
|
||||
</div>
|
||||
<kbd>V</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
<Command.Group heading="Page">
|
||||
<Command.Item
|
||||
data-ph-element={PROJECT_PAGE_TRACKER_ELEMENTS.COMMAND_PALETTE_CREATE_BUTTON}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<PageIcon className="h-3.5 w-3.5" />
|
||||
Create new page
|
||||
</div>
|
||||
<kbd>D</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import type { IWorkspaceSearchResults } from "@plane/types";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web imports
|
||||
import { commandGroups } from "@/plane-web/components/command-palette";
|
||||
// helpers
|
||||
import { openProjectAndScrollToSidebar } from "./helper";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
results: IWorkspaceSearchResults;
|
||||
};
|
||||
|
||||
export const CommandPaletteSearchResults: React.FC<Props> = observer((props) => {
|
||||
const { closePalette, results } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { projectId: routerProjectId } = useParams();
|
||||
// derived values
|
||||
const projectId = routerProjectId?.toString();
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.keys(results.results).map((key) => {
|
||||
// TODO: add type for results
|
||||
const section = (results.results as any)[key];
|
||||
const currentSection = commandGroups[key];
|
||||
if (!currentSection) return null;
|
||||
if (section.length > 0) {
|
||||
return (
|
||||
<Command.Group key={key} heading={`${currentSection.title} search`}>
|
||||
{section.map((item: any) => (
|
||||
<Command.Item
|
||||
key={item.id}
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
router.push(currentSection.path(item, projectId));
|
||||
const itemProjectId =
|
||||
item?.project_id ||
|
||||
(Array.isArray(item?.project_ids) && item?.project_ids?.length > 0
|
||||
? item?.project_ids[0]
|
||||
: undefined);
|
||||
if (itemProjectId) openProjectAndScrollToSidebar(itemProjectId);
|
||||
}}
|
||||
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,65 @@
|
||||
"use client";
|
||||
|
||||
import type { FC } from "react";
|
||||
import React, { 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";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// hooks
|
||||
import { useUserProfile } from "@/hooks/store/user";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
};
|
||||
|
||||
export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||
const { closePalette } = props;
|
||||
const { setTheme } = useTheme();
|
||||
// hooks
|
||||
const { updateUserTheme } = useUserProfile();
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const updateTheme = async (newTheme: string) => {
|
||||
setTheme(newTheme);
|
||||
return updateUserTheme({ theme: newTheme }).catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Failed to save user theme settings!",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{THEME_OPTIONS.map((theme) => (
|
||||
<Command.Item
|
||||
key={theme.value}
|
||||
onSelect={() => {
|
||||
updateTheme(theme.value);
|
||||
closePalette();
|
||||
}}
|
||||
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" />
|
||||
{t(theme.i18n_label)}
|
||||
</div>
|
||||
</Command.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Command } from "cmdk";
|
||||
// hooks
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { WORKSPACE_SETTINGS_LINKS, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SettingIcon } from "@/components/icons";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
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;
|
||||
};
|
||||
|
||||
export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) => {
|
||||
const { closePalette } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// mobx store
|
||||
const { t } = useTranslation();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
|
||||
const redirect = (path: string) => {
|
||||
closePalette();
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{WORKSPACE_SETTINGS_LINKS.map(
|
||||
(setting) =>
|
||||
allowPermissions(setting.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug.toString()) &&
|
||||
shouldRenderSettingLink(workspaceSlug.toString(), setting.key) && (
|
||||
<Command.Item
|
||||
key={setting.key}
|
||||
onSelect={() => redirect(`/${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>
|
||||
</Command.Item>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user