forked from github/plane
refactor: command k modal (#2803)
* refactor: command palette file structure * fix: identifier search
This commit is contained in:
parent
15927c9cae
commit
7aaf840fb1
83
web/components/command-palette/actions/help-actions.tsx
Normal file
83
web/components/command-palette/actions/help-actions.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Command } from "cmdk";
|
||||||
|
import { FileText, GithubIcon, MessageSquare, Rocket } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { DiscordIcon } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommandPaletteHelpActions: React.FC<Props> = (props) => {
|
||||||
|
const { closePalette } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
commandPalette: { toggleShortcutModal },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
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();
|
||||||
|
(window as any)?.$crisp.push(["do", "chat:open"]);
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
6
web/components/command-palette/actions/index.ts
Normal file
6
web/components/command-palette/actions/index.ts
Normal file
@ -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,166 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||||
|
// helpers
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
issueDetails: IIssue | 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;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
commandPalette: { toggleCommandPaletteModal, toggleDeleteIssueModal },
|
||||||
|
issueDetail: { updateIssue },
|
||||||
|
user: { currentUser },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
|
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.assignees ?? [];
|
||||||
|
|
||||||
|
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||||
|
else updatedAssignees.push(assignee);
|
||||||
|
|
||||||
|
handleUpdateIssue({ assignees: updatedAssignees });
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteIssue = () => {
|
||||||
|
toggleCommandPaletteModal(false);
|
||||||
|
toggleDeleteIssueModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyIssueUrlToClipboard = () => {
|
||||||
|
if (!router.query.issueId) return;
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
copyTextToClipboard(url.href)
|
||||||
|
.then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Some error occurred",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Command.Group heading="Issue 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">
|
||||||
|
<UserGroupIcon 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?.assignees.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 issue
|
||||||
|
</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 issue URL
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
</Command.Group>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,79 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
issue: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||||
|
const { closePalette, issue } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// store
|
||||||
|
const {
|
||||||
|
issueDetail: { updateIssue },
|
||||||
|
projectMember: { projectMembers },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const options =
|
||||||
|
projectMembers?.map(({ member }) => ({
|
||||||
|
value: member.id,
|
||||||
|
query: member.display_name,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar name={member.display_name} src={member.avatar} showTooltip={false} />
|
||||||
|
{member.display_name}
|
||||||
|
</div>
|
||||||
|
{issue.assignees.includes(member.id) && (
|
||||||
|
<div>
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const handleUpdateIssue = async (formData: Partial<IIssue>) => {
|
||||||
|
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.assignees ?? [];
|
||||||
|
|
||||||
|
if (updatedAssignees.includes(assignee)) updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
||||||
|
else updatedAssignees.push(assignee);
|
||||||
|
|
||||||
|
handleUpdateIssue({ assignees: updatedAssignees });
|
||||||
|
closePalette();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{options.map((option: any) => (
|
||||||
|
<Command.Item
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => handleIssueAssignees(option.value)}
|
||||||
|
className="focus:outline-none"
|
||||||
|
>
|
||||||
|
{option.content}
|
||||||
|
</Command.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,56 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { PriorityIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { IIssue, TIssuePriorities } from "types";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
issue: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeIssuePriority: React.FC<Props> = observer((props) => {
|
||||||
|
const { closePalette, issue } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueDetail: { updateIssue },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
{PRIORITIES.map((priority) => (
|
||||||
|
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<PriorityIcon priority={priority} />
|
||||||
|
<span className="capitalize">{priority ?? "None"}</span>
|
||||||
|
</div>
|
||||||
|
<div>{priority === issue.priority && <Check className="h-3 w-3" />}</div>
|
||||||
|
</Command.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,65 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// cmdk
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
// ui
|
||||||
|
import { Spinner, StateGroupIcon } from "@plane/ui";
|
||||||
|
// icons
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
issue: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeIssueState: React.FC<Props> = observer((props) => {
|
||||||
|
const { closePalette, issue } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
projectState: { projectStates },
|
||||||
|
issueDetail: { updateIssue },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const submitChanges = async (formData: Partial<IIssue>) => {
|
||||||
|
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: 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 && <Check className="h-3 w-3" />}</div>
|
||||||
|
</Command.Item>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center">No states found</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./actions-list";
|
||||||
|
export * from "./change-state";
|
||||||
|
export * from "./change-priority";
|
||||||
|
export * from "./change-assignee";
|
83
web/components/command-palette/actions/project-actions.tsx
Normal file
83
web/components/command-palette/actions/project-actions.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Command } from "cmdk";
|
||||||
|
import { ContrastIcon, FileText } from "lucide-react";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// ui
|
||||||
|
import { DiceIcon, PhotoFilterIcon } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommandPaletteProjectActions: React.FC<Props> = (props) => {
|
||||||
|
const { closePalette } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
commandPalette: { toggleCreateCycleModal, toggleCreateModuleModal, toggleCreatePageModal, toggleCreateViewModal },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Command.Group heading="Cycle">
|
||||||
|
<Command.Item
|
||||||
|
onSelect={() => {
|
||||||
|
closePalette();
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
toggleCreateViewModal(true);
|
||||||
|
}}
|
||||||
|
className="focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<PhotoFilterIcon 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();
|
||||||
|
toggleCreatePageModal(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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
49
web/components/command-palette/actions/search-results.tsx
Normal file
49
web/components/command-palette/actions/search-results.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
// helpers
|
||||||
|
import { commandGroups } from "components/command-palette";
|
||||||
|
// types
|
||||||
|
import { IWorkspaceSearchResults } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
results: IWorkspaceSearchResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
|
||||||
|
const { closePalette, results } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.keys(results.results).map((key) => {
|
||||||
|
const section = (results.results as any)[key];
|
||||||
|
const currentSection = commandGroups[key];
|
||||||
|
|
||||||
|
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));
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, Dispatch, SetStateAction, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
@ -10,22 +10,25 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
import { THEME_OPTIONS } from "constants/themes";
|
import { THEME_OPTIONS } from "constants/themes";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
closePalette: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChangeInterfaceTheme: FC<Props> = observer((props) => {
|
export const CommandPaletteThemeActions: FC<Props> = observer((props) => {
|
||||||
const { setIsPaletteOpen } = props;
|
const { closePalette } = props;
|
||||||
// store
|
|
||||||
const { user: userStore } = useMobxStore();
|
|
||||||
// states
|
// states
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
// store
|
||||||
|
const {
|
||||||
|
user: { updateCurrentUserTheme },
|
||||||
|
} = useMobxStore();
|
||||||
// hooks
|
// hooks
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const updateUserTheme = (newTheme: string) => {
|
const updateUserTheme = async (newTheme: string) => {
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
return userStore.updateCurrentUserTheme(newTheme).catch(() => {
|
|
||||||
|
return updateCurrentUserTheme(newTheme).catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Failed to save user theme settings!",
|
title: "Failed to save user theme settings!",
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -47,7 +50,7 @@ export const ChangeInterfaceTheme: FC<Props> = observer((props) => {
|
|||||||
key={theme.value}
|
key={theme.value}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateUserTheme(theme.value);
|
updateUserTheme(theme.value);
|
||||||
setIsPaletteOpen(false);
|
closePalette();
|
||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
@ -0,0 +1,61 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
// icons
|
||||||
|
import { SettingIcon } from "components/icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closePalette: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommandPaletteWorkspaceSettingsActions: React.FC<Props> = (props) => {
|
||||||
|
const { closePalette } = props;
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const redirect = (path: string) => {
|
||||||
|
closePalette();
|
||||||
|
router.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
General
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/members`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Members
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Billing and Plans
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Integrations
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Import
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
<Command.Item onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)} className="focus:outline-none">
|
||||||
|
<div className="flex items-center gap-2 text-custom-text-200">
|
||||||
|
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Export
|
||||||
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,22 +1,10 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
import { Command } from "cmdk";
|
import { Command } from "cmdk";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import {
|
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||||
FileText,
|
|
||||||
FolderPlus,
|
|
||||||
LinkIcon,
|
|
||||||
MessageSquare,
|
|
||||||
Rocket,
|
|
||||||
Search,
|
|
||||||
Settings,
|
|
||||||
Signal,
|
|
||||||
Trash2,
|
|
||||||
UserMinus2,
|
|
||||||
UserPlus2,
|
|
||||||
} from "lucide-react";
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
@ -24,47 +12,29 @@ import { WorkspaceService } from "services/workspace.service";
|
|||||||
import { IssueService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
// hooks
|
// hooks
|
||||||
import useDebounce from "hooks/use-debounce";
|
import useDebounce from "hooks/use-debounce";
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
ChangeInterfaceTheme,
|
CommandPaletteThemeActions,
|
||||||
ChangeIssueAssignee,
|
ChangeIssueAssignee,
|
||||||
ChangeIssuePriority,
|
ChangeIssuePriority,
|
||||||
ChangeIssueState,
|
ChangeIssueState,
|
||||||
commandGroups,
|
CommandPaletteHelpActions,
|
||||||
|
CommandPaletteIssueActions,
|
||||||
|
CommandPaletteProjectActions,
|
||||||
|
CommandPaletteWorkspaceSettingsActions,
|
||||||
|
CommandPaletteSearchResults,
|
||||||
} from "components/command-palette";
|
} from "components/command-palette";
|
||||||
import {
|
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||||
ContrastIcon,
|
|
||||||
DiceIcon,
|
|
||||||
DoubleCircleIcon,
|
|
||||||
LayersIcon,
|
|
||||||
Loader,
|
|
||||||
PhotoFilterIcon,
|
|
||||||
ToggleSwitch,
|
|
||||||
Tooltip,
|
|
||||||
UserGroupIcon,
|
|
||||||
} from "@plane/ui";
|
|
||||||
// icons
|
|
||||||
import { DiscordIcon, GithubIcon, SettingIcon } from "components/icons";
|
|
||||||
// helpers
|
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue, IWorkspaceSearchResults } from "types";
|
import { IWorkspaceSearchResults } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
|
||||||
deleteIssue: () => void;
|
|
||||||
isPaletteOpen: boolean;
|
|
||||||
closePalette: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
// services
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
|
|
||||||
export const CommandModal: React.FC<Props> = observer((props) => {
|
export const CommandModal: React.FC = observer(() => {
|
||||||
const { deleteIssue, isPaletteOpen, closePalette } = props;
|
|
||||||
// states
|
// states
|
||||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||||
const [resultsCount, setResultsCount] = useState(0);
|
const [resultsCount, setResultsCount] = useState(0);
|
||||||
@ -85,8 +55,14 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore();
|
const {
|
||||||
const user = userStore.currentUser ?? undefined;
|
commandPalette: {
|
||||||
|
isCommandPaletteOpen,
|
||||||
|
toggleCommandPaletteModal,
|
||||||
|
toggleCreateIssueModal,
|
||||||
|
toggleCreateProjectModal,
|
||||||
|
},
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -96,64 +72,16 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
// TODO: update this to mobx store
|
||||||
|
|
||||||
const { data: issueDetails } = useSWR(
|
const { data: issueDetails } = useSWR(
|
||||||
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
|
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
|
||||||
workspaceSlug && projectId && issueId
|
workspaceSlug && projectId && issueId
|
||||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
|
? () => issueService.retrieve(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateIssue = useCallback(
|
const closePalette = () => {
|
||||||
async (formData: Partial<IIssue>) => {
|
toggleCommandPaletteModal(false);
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutate<IIssue>(
|
|
||||||
ISSUE_DETAILS(issueId as string),
|
|
||||||
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = { ...formData };
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
|
||||||
mutate(ISSUE_DETAILS(issueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, issueId, projectId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIssueAssignees = (assignee: string) => {
|
|
||||||
if (!issueDetails) return;
|
|
||||||
|
|
||||||
closePalette();
|
|
||||||
const updatedAssignees = issueDetails.assignees ?? [];
|
|
||||||
|
|
||||||
if (updatedAssignees.includes(assignee)) {
|
|
||||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
|
||||||
} else {
|
|
||||||
updatedAssignees.push(assignee);
|
|
||||||
}
|
|
||||||
updateIssue({ assignees: updatedAssignees });
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirect = (path: string) => {
|
|
||||||
closePalette();
|
|
||||||
router.push(path);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createNewWorkspace = () => {
|
const createNewWorkspace = () => {
|
||||||
@ -161,25 +89,6 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
router.push("/create-workspace");
|
router.push("/create-workspace");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyIssueUrlToClipboard = useCallback(() => {
|
|
||||||
if (!router.query.issueId) return;
|
|
||||||
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
copyTextToClipboard(url.href)
|
|
||||||
.then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Copied to clipboard",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Some error occurred",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [router, setToastAlert]);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
@ -189,7 +98,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
if (debouncedSearchTerm) {
|
if (debouncedSearchTerm) {
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
workspaceService
|
workspaceService
|
||||||
.searchWorkspace(workspaceSlug as string, {
|
.searchWorkspace(workspaceSlug.toString(), {
|
||||||
...(projectId ? { project_id: projectId.toString() } : {}),
|
...(projectId ? { project_id: projectId.toString() } : {}),
|
||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
workspace_search: !projectId ? true : isWorkspaceLevel,
|
workspace_search: !projectId ? true : isWorkspaceLevel,
|
||||||
@ -225,16 +134,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
[debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes
|
[debouncedSearchTerm, isWorkspaceLevel, projectId, workspaceSlug] // Only call effect if debounced search term changes
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root show={isCommandPaletteOpen} afterLeave={() => setSearchTerm("")} as={React.Fragment}>
|
||||||
show={isPaletteOpen}
|
|
||||||
afterLeave={() => {
|
|
||||||
setSearchTerm("");
|
|
||||||
}}
|
|
||||||
as={React.Fragment}
|
|
||||||
>
|
|
||||||
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}>
|
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={React.Fragment}
|
||||||
@ -268,9 +169,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// when search is empty and page is undefined
|
// when search is empty and page is undefined
|
||||||
// when user tries to close the modal with esc
|
// when user tries to close the modal with esc
|
||||||
if (e.key === "Escape" && !page && !searchTerm) {
|
if (e.key === "Escape" && !page && !searchTerm) closePalette();
|
||||||
closePalette();
|
|
||||||
}
|
|
||||||
// Escape goes to previous page
|
// Escape goes to previous page
|
||||||
// Backspace goes to previous page when search is empty
|
// Backspace goes to previous page when search is empty
|
||||||
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
|
if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) {
|
||||||
@ -318,9 +218,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm"
|
className="w-full border-0 border-b border-custom-border-200 bg-transparent p-4 pl-11 text-custom-text-100 placeholder:text-custom-text-400 outline-none focus:ring-0 text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => setSearchTerm(e)}
|
||||||
setSearchTerm(e);
|
|
||||||
}}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
/>
|
/>
|
||||||
@ -340,7 +238,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
|
||||||
<div className="my-4 text-center text-custom-text-200">No results found.</div>
|
<div className="my-4 text-center text-custom-text-200 text-sm">No results found.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isLoading || isSearching) && (
|
{(isLoading || isSearching) && (
|
||||||
@ -354,125 +252,28 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
</Command.Loading>
|
</Command.Loading>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{debouncedSearchTerm !== "" &&
|
{debouncedSearchTerm !== "" && (
|
||||||
Object.keys(results.results).map((key) => {
|
<CommandPaletteSearchResults closePalette={closePalette} results={results} />
|
||||||
const section = (results.results as any)[key];
|
)}
|
||||||
const currentSection = commandGroups[key];
|
|
||||||
|
|
||||||
if (section.length > 0) {
|
|
||||||
return (
|
|
||||||
<Command.Group key={key} heading={currentSection.title}>
|
|
||||||
{section.map((item: any) => (
|
|
||||||
<Command.Item
|
|
||||||
key={item.id}
|
|
||||||
onSelect={() => {
|
|
||||||
closePalette();
|
|
||||||
router.push(currentSection.path(item));
|
|
||||||
}}
|
|
||||||
value={`${key}-${item?.name}`}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
{!page && (
|
{!page && (
|
||||||
<>
|
<>
|
||||||
|
{/* issue actions */}
|
||||||
{issueId && (
|
{issueId && (
|
||||||
<Command.Group heading="Issue actions">
|
<CommandPaletteIssueActions
|
||||||
<Command.Item
|
closePalette={closePalette}
|
||||||
onSelect={() => {
|
issueDetails={issueDetails}
|
||||||
closePalette();
|
pages={pages}
|
||||||
setPlaceholder("Change state...");
|
setPages={(newPages) => setPages(newPages)}
|
||||||
setSearchTerm("");
|
setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)}
|
||||||
setPages([...pages, "change-issue-state"]);
|
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||||
}}
|
/>
|
||||||
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">
|
|
||||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
|
||||||
Assign to...
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => {
|
|
||||||
handleIssueAssignees(user.id);
|
|
||||||
setSearchTerm("");
|
|
||||||
}}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
{issueDetails?.assignees.includes(user.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 issue
|
|
||||||
</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 issue URL
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
</Command.Group>
|
|
||||||
)}
|
)}
|
||||||
<Command.Group heading="Issue">
|
<Command.Group heading="Issue">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
closePalette();
|
closePalette();
|
||||||
commandPaletteStore.toggleCreateIssueModal(true);
|
toggleCreateIssueModal(true);
|
||||||
}}
|
}}
|
||||||
className="focus:bg-custom-background-80"
|
className="focus:bg-custom-background-80"
|
||||||
>
|
>
|
||||||
@ -489,7 +290,7 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
<Command.Item
|
<Command.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
closePalette();
|
closePalette();
|
||||||
commandPaletteStore.toggleCreateProjectModal(true);
|
toggleCreateProjectModal(true);
|
||||||
}}
|
}}
|
||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
>
|
>
|
||||||
@ -502,70 +303,8 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
</Command.Group>
|
</Command.Group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projectId && (
|
{/* project actions */}
|
||||||
<>
|
{projectId && <CommandPaletteProjectActions closePalette={closePalette} />}
|
||||||
<Command.Group heading="Cycle">
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => {
|
|
||||||
closePalette();
|
|
||||||
commandPaletteStore.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();
|
|
||||||
commandPaletteStore.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();
|
|
||||||
commandPaletteStore.toggleCreateViewModal(true);
|
|
||||||
}}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<PhotoFilterIcon 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();
|
|
||||||
commandPaletteStore.toggleCreatePageModal(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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Command.Group heading="Workspace Settings">
|
<Command.Group heading="Workspace Settings">
|
||||||
<Command.Item
|
<Command.Item
|
||||||
@ -603,139 +342,37 @@ export const CommandModal: React.FC<Props> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
<Command.Group heading="Help">
|
|
||||||
<Command.Item
|
{/* help options */}
|
||||||
onSelect={() => {
|
<CommandPaletteHelpActions closePalette={closePalette} />
|
||||||
closePalette();
|
|
||||||
commandPaletteStore.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();
|
|
||||||
(window as any).$crisp.push(["do", "chat:open"]);
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* workspace settings actions */}
|
||||||
{page === "settings" && workspaceSlug && (
|
{page === "settings" && workspaceSlug && (
|
||||||
<>
|
<CommandPaletteWorkspaceSettingsActions closePalette={closePalette} />
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
General
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/members`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
Members
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/billing`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
Billing and Plans
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/integrations`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
Integrations
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/imports`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
Import
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item
|
|
||||||
onSelect={() => redirect(`/${workspaceSlug}/settings/exports`)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
<SettingIcon className="h-4 w-4 text-custom-text-200" />
|
|
||||||
Export
|
|
||||||
</div>
|
|
||||||
</Command.Item>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* issue details page actions */}
|
||||||
{page === "change-issue-state" && issueDetails && (
|
{page === "change-issue-state" && issueDetails && (
|
||||||
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
<ChangeIssueState closePalette={closePalette} issue={issueDetails} />
|
||||||
)}
|
)}
|
||||||
{page === "change-issue-priority" && issueDetails && (
|
{page === "change-issue-priority" && issueDetails && (
|
||||||
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
<ChangeIssuePriority closePalette={closePalette} issue={issueDetails} />
|
||||||
)}
|
)}
|
||||||
{page === "change-issue-assignee" && issueDetails && (
|
{page === "change-issue-assignee" && issueDetails && (
|
||||||
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
|
<ChangeIssueAssignee closePalette={closePalette} issue={issueDetails} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* theme actions */}
|
||||||
|
{page === "change-interface-theme" && (
|
||||||
|
<CommandPaletteThemeActions
|
||||||
|
closePalette={() => {
|
||||||
|
closePalette();
|
||||||
|
setPages((pages) => pages.slice(0, -1));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
|
|
||||||
</Command.List>
|
</Command.List>
|
||||||
</Command>
|
</Command>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,6 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
// store
|
// store
|
||||||
const { commandPalette, theme: themeStore } = useMobxStore();
|
const { commandPalette, theme: themeStore } = useMobxStore();
|
||||||
const {
|
const {
|
||||||
isCommandPaletteOpen,
|
|
||||||
toggleCommandPaletteModal,
|
toggleCommandPaletteModal,
|
||||||
isCreateIssueModalOpen,
|
isCreateIssueModalOpen,
|
||||||
toggleCreateIssueModal,
|
toggleCreateIssueModal,
|
||||||
@ -156,11 +155,6 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const deleteIssue = () => {
|
|
||||||
toggleCommandPaletteModal(false);
|
|
||||||
toggleDeleteIssueModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShortcutsModal
|
<ShortcutsModal
|
||||||
@ -231,13 +225,7 @@ export const CommandPalette: FC = observer(() => {
|
|||||||
}}
|
}}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<CommandModal
|
<CommandModal />
|
||||||
deleteIssue={deleteIssue}
|
|
||||||
isPaletteOpen={isCommandPaletteOpen}
|
|
||||||
closePalette={() => {
|
|
||||||
toggleCommandPaletteModal(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -20,9 +20,7 @@ export const commandGroups: {
|
|||||||
icon: <ContrastIcon className="h-3 w-3" />,
|
icon: <ContrastIcon className="h-3 w-3" />,
|
||||||
itemName: (cycle: IWorkspaceDefaultSearchResult) => (
|
itemName: (cycle: IWorkspaceDefaultSearchResult) => (
|
||||||
<h6>
|
<h6>
|
||||||
<span className="text-custom-text-200 text-xs">{cycle.project__identifier}</span>
|
<span className="text-custom-text-300 text-xs">{cycle.project__identifier}</span> {cycle.name}
|
||||||
{"- "}
|
|
||||||
{cycle.name}
|
|
||||||
</h6>
|
</h6>
|
||||||
),
|
),
|
||||||
path: (cycle: IWorkspaceDefaultSearchResult) =>
|
path: (cycle: IWorkspaceDefaultSearchResult) =>
|
||||||
@ -33,8 +31,9 @@ export const commandGroups: {
|
|||||||
icon: <LayersIcon className="h-3 w-3" />,
|
icon: <LayersIcon className="h-3 w-3" />,
|
||||||
itemName: (issue: IWorkspaceIssueSearchResult) => (
|
itemName: (issue: IWorkspaceIssueSearchResult) => (
|
||||||
<h6>
|
<h6>
|
||||||
<span className="text-custom-text-200 text-xs">{issue.project__identifier}</span>
|
<span className="text-custom-text-300 text-xs">
|
||||||
{"- "}
|
{issue.project__identifier}-{issue.sequence_id}
|
||||||
|
</span>{" "}
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</h6>
|
</h6>
|
||||||
),
|
),
|
||||||
@ -46,9 +45,7 @@ export const commandGroups: {
|
|||||||
icon: <PhotoFilterIcon className="h-3 w-3" />,
|
icon: <PhotoFilterIcon className="h-3 w-3" />,
|
||||||
itemName: (view: IWorkspaceDefaultSearchResult) => (
|
itemName: (view: IWorkspaceDefaultSearchResult) => (
|
||||||
<h6>
|
<h6>
|
||||||
<span className="text-custom-text-200 text-xs">{view.project__identifier}</span>
|
<span className="text-custom-text-300 text-xs">{view.project__identifier}</span> {view.name}
|
||||||
{"- "}
|
|
||||||
{view.name}
|
|
||||||
</h6>
|
</h6>
|
||||||
),
|
),
|
||||||
path: (view: IWorkspaceDefaultSearchResult) =>
|
path: (view: IWorkspaceDefaultSearchResult) =>
|
||||||
@ -59,9 +56,7 @@ export const commandGroups: {
|
|||||||
icon: <DiceIcon className="h-3 w-3" />,
|
icon: <DiceIcon className="h-3 w-3" />,
|
||||||
itemName: (module: IWorkspaceDefaultSearchResult) => (
|
itemName: (module: IWorkspaceDefaultSearchResult) => (
|
||||||
<h6>
|
<h6>
|
||||||
<span className="text-custom-text-200 text-xs">{module.project__identifier}</span>
|
<span className="text-custom-text-300 text-xs">{module.project__identifier}</span> {module.name}
|
||||||
{"- "}
|
|
||||||
{module.name}
|
|
||||||
</h6>
|
</h6>
|
||||||
),
|
),
|
||||||
path: (module: IWorkspaceDefaultSearchResult) =>
|
path: (module: IWorkspaceDefaultSearchResult) =>
|
||||||
@ -72,9 +67,7 @@ export const commandGroups: {
|
|||||||
icon: <FileText className="h-3 w-3" />,
|
icon: <FileText className="h-3 w-3" />,
|
||||||
itemName: (page: IWorkspaceDefaultSearchResult) => (
|
itemName: (page: IWorkspaceDefaultSearchResult) => (
|
||||||
<h6>
|
<h6>
|
||||||
<span className="text-custom-text-200 text-xs">{page.project__identifier}</span>
|
<span className="text-custom-text-300 text-xs">{page.project__identifier}</span> {page.name}
|
||||||
{"- "}
|
|
||||||
{page.name}
|
|
||||||
</h6>
|
</h6>
|
||||||
),
|
),
|
||||||
path: (page: IWorkspaceDefaultSearchResult) =>
|
path: (page: IWorkspaceDefaultSearchResult) =>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./issue";
|
export * from "./actions";
|
||||||
export * from "./change-interface-theme";
|
|
||||||
export * from "./command-modal";
|
export * from "./command-modal";
|
||||||
export * from "./command-pallette";
|
export * from "./command-pallette";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, useCallback, FC } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
import { Command } from "cmdk";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
// mobx store
|
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
|
||||||
// services
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
// ui
|
|
||||||
import { Avatar } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { IUser, IIssue } from "types";
|
|
||||||
// constants
|
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
issue: IIssue;
|
|
||||||
user: IUser | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
|
|
||||||
export const ChangeIssueAssignee: FC<Props> = observer((props) => {
|
|
||||||
const { setIsPaletteOpen, issue } = props;
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
|
||||||
// store
|
|
||||||
const {
|
|
||||||
projectMember: { projectMembers },
|
|
||||||
} = useMobxStore();
|
|
||||||
|
|
||||||
const options =
|
|
||||||
projectMembers?.map(({ member }) => ({
|
|
||||||
value: member.id,
|
|
||||||
query: member.display_name,
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar name={member.display_name} src={member.avatar} showTooltip={false} />
|
|
||||||
{member.display_name}
|
|
||||||
</div>
|
|
||||||
{issue.assignees.includes(member.id) && (
|
|
||||||
<div>
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
})) ?? [];
|
|
||||||
|
|
||||||
const updateIssue = useCallback(
|
|
||||||
async (formData: Partial<IIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutate<IIssue>(
|
|
||||||
ISSUE_DETAILS(issueId as string),
|
|
||||||
async (prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = { ...formData };
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, issueId, projectId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIssueAssignees = (assignee: string) => {
|
|
||||||
const updatedAssignees = issue.assignees ?? [];
|
|
||||||
|
|
||||||
if (updatedAssignees.includes(assignee)) {
|
|
||||||
updatedAssignees.splice(updatedAssignees.indexOf(assignee), 1);
|
|
||||||
} else {
|
|
||||||
updatedAssignees.push(assignee);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIssue({ assignees: updatedAssignees });
|
|
||||||
setIsPaletteOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{options.map((option: any) => (
|
|
||||||
<Command.Item
|
|
||||||
key={option.value}
|
|
||||||
onSelect={() => handleIssueAssignees(option.value)}
|
|
||||||
className="focus:outline-none"
|
|
||||||
>
|
|
||||||
{option.content}
|
|
||||||
</Command.Item>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,78 +0,0 @@
|
|||||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// cmdk
|
|
||||||
import { Command } from "cmdk";
|
|
||||||
// services
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
// types
|
|
||||||
import { IIssue, IUser, TIssuePriorities } from "types";
|
|
||||||
// constants
|
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
import { PRIORITIES } from "constants/project";
|
|
||||||
// icons
|
|
||||||
import { PriorityIcon } from "@plane/ui";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
issue: IIssue;
|
|
||||||
user: IUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
|
|
||||||
export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
|
||||||
async (formData: Partial<IIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutate<IIssue>(
|
|
||||||
ISSUE_DETAILS(issueId as string),
|
|
||||||
async (prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = { ...formData };
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, issueId, projectId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIssueState = (priority: TIssuePriorities) => {
|
|
||||||
submitChanges({ priority });
|
|
||||||
setIsPaletteOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{PRIORITIES.map((priority) => (
|
|
||||||
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<PriorityIcon priority={priority} />
|
|
||||||
<span className="capitalize">{priority ?? "None"}</span>
|
|
||||||
</div>
|
|
||||||
<div>{priority === issue.priority && <Check className="h-3 w-3" />}</div>
|
|
||||||
</Command.Item>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,93 +0,0 @@
|
|||||||
import React, { Dispatch, SetStateAction, useCallback } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
// cmdk
|
|
||||||
import { Command } from "cmdk";
|
|
||||||
// services
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
import { ProjectStateService } from "services/project";
|
|
||||||
// ui
|
|
||||||
import { Spinner, StateGroupIcon } from "@plane/ui";
|
|
||||||
// icons
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { IUser, IIssue } from "types";
|
|
||||||
// fetch keys
|
|
||||||
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
issue: IIssue;
|
|
||||||
user: IUser | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
const stateService = new ProjectStateService();
|
|
||||||
|
|
||||||
export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
|
||||||
|
|
||||||
const { data: states, mutate: mutateStates } = useSWR(
|
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId ? () => stateService.getStates(workspaceSlug as string, projectId as string) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
|
||||||
async (formData: Partial<IIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutate<IIssue>(
|
|
||||||
ISSUE_DETAILS(issueId as string),
|
|
||||||
async (prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload = { ...formData };
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutateStates();
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, issueId, projectId, mutateStates]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleIssueState = (stateId: string) => {
|
|
||||||
submitChanges({ state: stateId });
|
|
||||||
setIsPaletteOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{states ? (
|
|
||||||
states.length > 0 ? (
|
|
||||||
states.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 && <Check className="h-3 w-3" />}</div>
|
|
||||||
</Command.Item>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center">No states found</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,3 +0,0 @@
|
|||||||
export * from "./change-issue-state";
|
|
||||||
export * from "./change-issue-priority";
|
|
||||||
export * from "./change-issue-assignee";
|
|
Loading…
Reference in New Issue
Block a user