fix: command palette changes (#2465)

This commit is contained in:
sriram veeraghanta 2023-10-17 20:34:16 +05:30 committed by GitHub
parent d689c63368
commit 90776237f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 469 additions and 228 deletions

View File

@ -1,12 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import { WorkspaceService } from "services/workspace.service";
@ -60,16 +55,20 @@ import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
type Props = {
deleteIssue: () => void;
isPaletteOpen: boolean;
setIsPaletteOpen: React.Dispatch<React.SetStateAction<boolean>>;
closePalette: () => void;
};
// services
const workspaceService = new WorkspaceService();
const issueService = new IssueService();
export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPaletteOpen }) => {
export const CommandModal: React.FC<Props> = (props) => {
const { deleteIssue, isPaletteOpen, closePalette } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
// states
const [placeholder, setPlaceholder] = useState("Type a command or search...");
const [resultsCount, setResultsCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [isSearching, setIsSearching] = useState(false);
@ -86,15 +85,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
},
});
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const [pages, setPages] = useState<string[]>([]);
const page = pages[pages.length - 1];
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
const { setToastAlert } = useToast();
const { user } = useUser();
@ -141,7 +137,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
const handleIssueAssignees = (assignee: string) => {
if (!issueDetails) return;
setIsPaletteOpen(false);
closePalette();
const updatedAssignees = issueDetails.assignees ?? [];
if (updatedAssignees.includes(assignee)) {
@ -153,12 +149,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
};
const redirect = (path: string) => {
setIsPaletteOpen(false);
closePalette();
router.push(path);
};
const createNewWorkspace = () => {
setIsPaletteOpen(false);
closePalette();
router.push("/create-workspace");
};
@ -236,7 +232,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
}}
as={React.Fragment}
>
<Dialog as="div" className="relative z-30" onClose={() => setIsPaletteOpen(false)}>
<Dialog as="div" className="relative z-30" onClose={() => closePalette()}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
@ -269,7 +265,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
// when search is empty and page is undefined
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) {
setIsPaletteOpen(false);
closePalette();
}
// Escape goes to previous page
// Backspace goes to previous page when search is empty
@ -365,7 +361,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Item
key={item.id}
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
router.push(currentSection.path(item));
}}
value={`${key}-${item?.name}`}
@ -388,7 +384,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Issue actions">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
setPlaceholder("Change state...");
setSearchTerm("");
setPages([...pages, "change-issue-state"]);
@ -455,7 +451,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
copyIssueUrlToClipboard();
}}
className="focus:outline-none"
@ -470,7 +466,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Issue">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "c",
});
@ -490,7 +486,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Project">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "p",
});
@ -512,7 +508,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Cycle">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "q",
});
@ -530,7 +526,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Module">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "m",
});
@ -548,7 +544,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="View">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "v",
});
@ -566,7 +562,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Page">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "d",
});
@ -623,7 +619,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Group heading="Help">
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
const e = new KeyboardEvent("keydown", {
key: "h",
});
@ -638,7 +634,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
window.open("https://docs.plane.so/", "_blank");
}}
className="focus:outline-none"
@ -650,7 +646,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
window.open("https://discord.com/invite/A92xrEGCge", "_blank");
}}
className="focus:outline-none"
@ -662,7 +658,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
}}
className="focus:outline-none"
@ -674,7 +670,7 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
closePalette();
(window as any).$crisp.push(["do", "chat:open"]);
}}
className="focus:outline-none"
@ -747,15 +743,15 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</>
)}
{page === "change-issue-state" && issueDetails && (
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-issue-assignee" && issueDetails && (
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={closePalette} user={user} />
)}
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />}
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={closePalette} />}
</Command.List>
</Command>
</Dialog.Panel>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, FC } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// components
import { CommandK, ShortcutsModal } from "components/command-palette";
import { CommandModal, ShortcutsModal } from "components/command-palette";
import { BulkDeleteIssuesModal } from "components/core";
import { CreateUpdateCycleModal } from "components/cycles";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
@ -26,22 +26,34 @@ import { useMobxStore } from "lib/mobx/store-provider";
// services
const issueService = new IssueService();
export const CommandPalette: React.FC = observer(() => {
const store: any = useMobxStore();
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
const [isIssueModalOpen, setIsIssueModalOpen] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false);
const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false);
const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false);
const [isCreateModuleModalOpen, setIsCreateModuleModalOpen] = useState(false);
const [isBulkDeleteIssuesModalOpen, setIsBulkDeleteIssuesModalOpen] = useState(false);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
export const CommandPalette: FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query;
// store
const { commandPalette, theme: themeStore } = useMobxStore();
const {
isCommandPaletteOpen,
toggleCommandPaletteModal,
isCreateIssueModalOpen,
toggleCreateIssueModal,
isCreateCycleModalOpen,
toggleCreateCycleModal,
isCreatePageModalOpen,
toggleCreatePageModal,
isCreateProjectModalOpen,
toggleCreateProjectModal,
isCreateModuleModalOpen,
toggleCreateModuleModal,
isCreateViewModalOpen,
toggleCreateViewModal,
isShortcutModalOpen,
toggleShortcutModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
isDeleteIssueModalOpen,
toggleDeleteIssueModal,
} = commandPalette;
const { setSidebarCollapsed } = themeStore;
const { user } = useUser();
@ -55,7 +67,7 @@ export const CommandPalette: React.FC = observer(() => {
);
const copyIssueUrlToClipboard = useCallback(() => {
if (!router.query.issueId) return;
if (!issueId) return;
const url = new URL(window.location.href);
copyTextToClipboard(url.href)
@ -71,7 +83,7 @@ export const CommandPalette: React.FC = observer(() => {
title: "Some error occurred",
});
});
}, [router, setToastAlert]);
}, [setToastAlert, issueId]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@ -91,101 +103,142 @@ export const CommandPalette: React.FC = observer(() => {
if (cmdClicked) {
if (keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
toggleCommandPaletteModal(true);
} else if (keyPressed === "c" && altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
} else if (keyPressed === "b") {
e.preventDefault();
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
setSidebarCollapsed();
}
} else {
if (keyPressed === "c") {
setIsIssueModalOpen(true);
toggleCreateIssueModal(true);
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
toggleCreateProjectModal(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
toggleCreateViewModal(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
toggleCreatePageModal(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
toggleShortcutModal(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
toggleCreateCycleModal(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
toggleCreateModuleModal(true);
} else if (keyPressed === "backspace" || keyPressed === "delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
toggleBulkDeleteIssueModal(true);
}
}
},
[copyIssueUrlToClipboard, store.theme]
[
copyIssueUrlToClipboard,
toggleCreateProjectModal,
toggleCreateViewModal,
toggleCreatePageModal,
toggleShortcutModal,
toggleCreateCycleModal,
toggleCreateModuleModal,
toggleBulkDeleteIssueModal,
toggleCommandPaletteModal,
setSidebarCollapsed,
toggleCreateIssueModal,
]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
if (!user) return null;
const deleteIssue = () => {
setIsPaletteOpen(false);
setDeleteIssueModal(true);
toggleCommandPaletteModal(false);
toggleDeleteIssueModal(true);
};
return (
<>
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
<ShortcutsModal
isOpen={isShortcutModalOpen}
onClose={() => {
toggleShortcutModal(false);
}}
/>
{workspaceSlug && (
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} user={user} />
<CreateProjectModal
isOpen={isCreateProjectModalOpen}
onClose={() => {
toggleCreateProjectModal(false);
}}
workspaceSlug={workspaceSlug.toString()}
/>
)}
{projectId && (
{workspaceSlug && projectId && (
<>
<CreateUpdateCycleModal
isOpen={isCreateCycleModalOpen}
handleClose={() => setIsCreateCycleModalOpen(false)}
user={user}
handleClose={() => toggleCreateCycleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
setIsOpen={setIsCreateModuleModalOpen}
user={user}
onClose={() => {
toggleCreateModuleModal(false);
}}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateProjectViewModal
isOpen={isCreateViewModalOpen}
onClose={() => setIsCreateViewModalOpen(false)}
onClose={() => toggleCreateViewModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdatePageModal
isOpen={isCreateUpdatePageModalOpen}
handleClose={() => setIsCreateUpdatePageModalOpen(false)}
isOpen={isCreatePageModalOpen}
handleClose={() => toggleCreatePageModal(false)}
user={user}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
</>
)}
{issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
isOpen={deleteIssueModal}
data={issueDetails}
user={user}
/>
)}
<CreateUpdateIssueModal
isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)}
isOpen={isCreateIssueModalOpen}
handleClose={() => toggleCreateIssueModal(false)}
prePopulateData={
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}
setIsOpen={setIsBulkDeleteIssuesModalOpen}
{issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => toggleDeleteIssueModal(false)}
isOpen={isDeleteIssueModalOpen}
data={issueDetails}
user={user}
/>
<CommandK deleteIssue={deleteIssue} isPaletteOpen={isPaletteOpen} setIsPaletteOpen={setIsPaletteOpen} />
)}
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssueModalOpen}
onClose={() => {
toggleBulkDeleteIssueModal(false);
}}
user={user}
/>
<CommandModal
deleteIssue={deleteIssue}
isPaletteOpen={isCommandPaletteOpen}
closePalette={() => {
toggleCommandPaletteModal(false);
}}
/>
</>
);
});

View File

@ -1,6 +1,6 @@
export * from "./issue";
export * from "./change-interface-theme";
export * from "./command-k";
export * from "./command-modal";
export * from "./command-pallette";
export * from "./helpers";
export * from "./shortcuts-modal";

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { FC, useEffect, useState, Dispatch, SetStateAction, Fragment } from "react";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// icons
@ -8,7 +8,7 @@ import { Input } from "@plane/ui";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
};
const shortcuts = [
@ -43,8 +43,11 @@ const shortcuts = [
const allShortcuts = shortcuts.map((i) => i.shortcuts).flat(1);
export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
export const ShortcutsModal: FC<Props> = (props) => {
const { isOpen, onClose } = props;
// states
const [query, setQuery] = useState("");
// computed
const filteredShortcuts = allShortcuts.filter((shortcut) =>
shortcut.description.toLowerCase().includes(query.trim().toLowerCase()) || query === "" ? true : false
);
@ -54,10 +57,10 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
}, [isOpen]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={setIsOpen}>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -71,7 +74,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
@ -89,7 +92,7 @@ export const ShortcutsModal: React.FC<Props> = ({ isOpen, setIsOpen }) => {
>
<span>Keyboard Shortcuts</span>
<span>
<button type="button" onClick={() => setIsOpen(false)}>
<button type="button" onClick={onClose}>
<X className="h-6 w-6 text-custom-text-200 hover:text-custom-text-100" aria-hidden="true" />
</button>
</span>

View File

@ -33,18 +33,20 @@ type FormInput = {
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
user: IUser | undefined;
};
const issueService = new IssueService();
export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
const [query, setQuery] = useState("");
export const BulkDeleteIssuesModal: React.FC<Props> = (props) => {
const { isOpen, onClose, user } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
// states
const [query, setQuery] = useState("");
// fetching project issues.
const { data: issues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
@ -68,9 +70,9 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
});
const handleClose = () => {
setIsOpen(false);
setQuery("");
reset();
onClose();
};
const handleDelete: SubmitHandler<FormInput> = async (data) => {

View File

@ -1,5 +1,4 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { Dialog, Transition } from "@headlessui/react";
// services
@ -11,7 +10,7 @@ import { CycleForm } from "components/cycles";
// helper
import { getDateRangeStatus } from "helpers/date-time.helper";
// types
import type { CycleDateCheckData, IUser, ICycle, IProject } from "types";
import type { CycleDateCheckData, ICycle, IProject, IUser } from "types";
// fetch keys
import {
COMPLETED_CYCLES_LIST,
@ -27,23 +26,21 @@ type CycleModalProps = {
isOpen: boolean;
handleClose: () => void;
data?: ICycle | null;
user: IUser | undefined;
workspaceSlug: string;
projectId: string;
};
// services
const cycleService = new CycleService();
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, handleClose, data, user }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
export const CreateUpdateCycleModal: React.FC<CycleModalProps> = (props) => {
const { isOpen, handleClose, data, workspaceSlug, projectId } = props;
const { setToastAlert } = useToast();
const createCycle = async (payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
await cycleService
.createCycle(workspaceSlug.toString(), projectId.toString(), payload, user)
.createCycle(workspaceSlug.toString(), projectId.toString(), payload, {} as IUser)
.then((res) => {
switch (getDateRangeStatus(res.start_date, res.end_date)) {
case "completed":
@ -91,10 +88,8 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
};
const updateCycle = async (cycleId: string, payload: Partial<ICycle>) => {
if (!workspaceSlug || !projectId) return;
await cycleService
.updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload, user)
.updateCycle(workspaceSlug.toString(), projectId.toString(), cycleId, payload, {} as IUser)
.then((res) => {
switch (getDateRangeStatus(data?.start_date, data?.end_date)) {
case "completed":
@ -177,7 +172,6 @@ export const CreateUpdateCycleModal: React.FC<CycleModalProps> = ({ isOpen, hand
if (isDateValid) {
if (data) await updateCycle(data.id, payload);
else await createCycle(payload);
handleClose();
} else
setToastAlert({

View File

@ -50,7 +50,14 @@ export const CycleSelect: React.FC<IssueCycleSelectProps> = ({ projectId, value,
return (
<>
<CreateUpdateCycleModal isOpen={isCycleModalActive} handleClose={closeCycleModal} user={user} />
{workspaceSlug && projectId && (
<CreateUpdateCycleModal
isOpen={isCycleModalActive}
handleClose={closeCycleModal}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
<Listbox as="div" className="relative" value={value} onChange={onChange} multiple={multiple}>
{({ open }) => (
<>

View File

@ -19,13 +19,20 @@ export const ProjectViewsHeader: FC<IProjectViewsHeader> = (props) => {
const { title } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
// states
const [createViewModal, setCreateViewModal] = useState(false);
return (
<>
<CreateUpdateProjectViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
{workspaceSlug && projectId && (
<CreateUpdateProjectViewModal
isOpen={createViewModal}
onClose={() => setCreateViewModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4`}
>

View File

@ -1,5 +1,4 @@
import { Fragment } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
import { useForm } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
@ -16,9 +15,10 @@ import { MODULE_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
data?: IModule;
user: IUser | undefined;
workspaceSlug: string;
projectId: string;
};
const defaultValues: Partial<IModule> = {
@ -31,15 +31,14 @@ const defaultValues: Partial<IModule> = {
const moduleService = new ModuleService();
export const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, data, user }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
export const CreateUpdateModuleModal: React.FC<Props> = (props) => {
const { isOpen, onClose, data, workspaceSlug, projectId } = props;
const { setToastAlert } = useToast();
const handleClose = () => {
setIsOpen(false);
reset(defaultValues);
onClose();
};
const { reset } = useForm<IModule>({
@ -48,7 +47,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, da
const createModule = async (payload: Partial<IModule>) => {
await moduleService
.createModule(workspaceSlug as string, projectId as string, payload, user)
.createModule(workspaceSlug as string, projectId as string, payload, {} as IUser)
.then(() => {
mutate(MODULE_LIST(projectId as string));
handleClose();
@ -70,7 +69,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = ({ isOpen, setIsOpen, da
const updateModule = async (payload: Partial<IModule>) => {
await moduleService
.updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, user)
.updateModule(workspaceSlug as string, projectId as string, data?.id ?? "", payload, {} as IUser)
.then((res) => {
mutate<IModule[]>(
MODULE_LIST(projectId as string),

View File

@ -22,14 +22,17 @@ type Props = {
handleClose: () => void;
data?: IPage | null;
user: IUser | undefined;
workspaceSlug: string;
projectId: string;
};
// services
const pageService = new PageService();
export const CreateUpdatePageModal: React.FC<Props> = ({ isOpen, handleClose, data, user }) => {
export const CreateUpdatePageModal: React.FC<Props> = (props) => {
const { isOpen, handleClose, data, user, workspaceSlug, projectId } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();

View File

@ -38,14 +38,14 @@ const pageService = new PageService();
const projectService = new ProjectService();
export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [selectedPageToUpdate, setSelectedPageToUpdate] = useState<IPage | null>(null);
const [deletePageModal, setDeletePageModal] = useState(false);
const [selectedPageToDelete, setSelectedPageToDelete] = useState<IPage | null>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [selectedPageToUpdate, setSelectedPageToUpdate] = useState<IPage | null>(null);
const [deletePageModal, setDeletePageModal] = useState(false);
const [selectedPageToDelete, setSelectedPageToDelete] = useState<IPage | null>(null);
const { user } = useUserAuth();
@ -187,12 +187,16 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
};
return (
<>
{workspaceSlug && projectId && (
<>
<CreateUpdatePageModal
isOpen={createUpdatePageModal}
handleClose={() => setCreateUpdatePageModal(false)}
data={selectedPageToUpdate}
user={user}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<DeletePageModal
isOpen={deletePageModal}
@ -200,6 +204,9 @@ export const PagesView: React.FC<Props> = ({ pages, viewType }) => {
data={selectedPageToDelete}
user={user}
/>
</>
)}
{pages ? (
<div className="space-y-4 h-full overflow-y-auto">
{pages.length > 0 ? (

View File

@ -1,10 +1,11 @@
import { useState, useEffect, Fragment } from "react";
import { useState, useEffect, Fragment, FC } from "react";
import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form";
import { Dialog, Transition } from "@headlessui/react";
// icons
import { Users2, X } from "lucide-react";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
import { useWorkspaceMyMembership } from "contexts/workspace-member.context";
import useWorkspaceMembers from "hooks/use-workspace-members";
@ -17,16 +18,15 @@ import EmojiIconPicker from "components/emoji-icon-picker";
// helpers
import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper";
// types
import { IUser, IProject } from "types";
import { IProject } from "types";
// constants
import { NETWORK_CHOICES } from "constants/project";
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
onClose: () => void;
setToFavorite?: boolean;
user: IUser | undefined;
workspaceSlug: string;
};
const defaultValues: Partial<IProject> = {
@ -40,26 +40,27 @@ const defaultValues: Partial<IProject> = {
project_lead: null,
};
const IsGuestCondition: React.FC<{
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
}> = ({ setIsOpen }) => {
interface IIsGuestCondition {
onClose: () => void;
}
const IsGuestCondition: FC<IIsGuestCondition> = ({ onClose }) => {
const { setToastAlert } = useToast();
useEffect(() => {
setIsOpen(false);
onClose();
setToastAlert({
title: "Error",
type: "error",
message: "You don't have permission to create project.",
});
}, [setIsOpen, setToastAlert]);
}, [onClose, setToastAlert]);
return null;
};
export const CreateProjectModal: React.FC<Props> = (props) => {
const { isOpen, setIsOpen, setToFavorite = false } = props;
const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props;
// store
const { project: projectStore } = useMobxStore();
// states
@ -67,9 +68,6 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
const { setToastAlert } = useToast();
const router = useRouter();
const { workspaceSlug } = router.query;
const { memberDetails } = useWorkspaceMyMembership();
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
@ -86,7 +84,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
});
const handleClose = () => {
setIsOpen(false);
onClose();
setIsChangeInIdentifierRequired(true);
reset(defaultValues);
};
@ -172,7 +170,7 @@ export const CreateProjectModal: React.FC<Props> = (props) => {
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === watch("network"));
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition setIsOpen={setIsOpen} />;
if (memberDetails && isOpen) if (memberDetails.role <= 10) return <IsGuestCondition onClose={onClose} />;
return (
<Transition.Root show={isOpen} as={Fragment}>

View File

@ -108,12 +108,16 @@ export const ProjectSidebarList: FC = observer(() => {
return (
<>
{workspaceSlug && (
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
onClose={() => {
setIsProjectModalOpen(false);
}}
setToFavorite={isFavoriteProjectCreate}
user={user}
workspaceSlug={workspaceSlug.toString()}
/>
)}
<div
ref={containerRef}
className={`h-full overflow-y-auto px-4 space-y-2 ${

View File

@ -1,11 +1,8 @@
import React from "react";
import { useRouter } from "next/router";
import { FC, Fragment } from "react";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// components
import { ProjectViewForm } from "components/views";
@ -17,31 +14,24 @@ type Props = {
isOpen: boolean;
onClose: () => void;
preLoadedData?: Partial<IProjectView> | null;
workspaceSlug: string;
projectId: string;
};
export const CreateUpdateProjectViewModal: React.FC<Props> = observer((props) => {
const { data, isOpen, onClose, preLoadedData } = props;
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
const { data, isOpen, onClose, preLoadedData, workspaceSlug, projectId } = props;
// store
const { projectViews: projectViewsStore } = useMobxStore();
// hooks
const { setToastAlert } = useToast();
const handleClose = () => {
onClose();
};
const createView = async (formData: IProjectView) => {
if (!workspaceSlug || !projectId) return;
const payload = {
...formData,
};
const createView = async (payload: IProjectView) => {
await projectViewsStore
.createView(workspaceSlug.toString(), projectId.toString(), payload)
.createView(workspaceSlug, projectId, payload)
.then(() => handleClose())
.catch(() =>
setToastAlert({
@ -52,15 +42,9 @@ export const CreateUpdateProjectViewModal: React.FC<Props> = observer((props) =>
);
};
const updateView = async (formData: IProjectView) => {
if (!workspaceSlug || !projectId) return;
const payload = {
...formData,
};
const updateView = async (payload: IProjectView) => {
await projectViewsStore
.updateView(workspaceSlug.toString(), projectId.toString(), data?.id as string, payload)
.updateView(workspaceSlug, projectId, data?.id as string, payload)
.then(() => handleClose())
.catch(() =>
setToastAlert({
@ -77,10 +61,10 @@ export const CreateUpdateProjectViewModal: React.FC<Props> = observer((props) =>
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@ -94,7 +78,7 @@ export const CreateUpdateProjectViewModal: React.FC<Props> = observer((props) =>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"

View File

@ -16,7 +16,7 @@ export const AppLayout: FC<IAppLayout> = (props) => {
return (
<>
{/* <CommandPalette /> */}
<CommandPalette />
<UserAuthWrapper>
<WorkspaceAuthWrapper>
<div className="relative flex h-screen w-full overflow-hidden">

View File

@ -66,12 +66,18 @@ const ProjectModules: NextPage = () => {
<AppLayout
header={<ModulesHeader name={activeProject?.name} modulesView={modulesView} setModulesView={setModulesView} />}
>
{workspaceSlug && projectId && (
<CreateUpdateModuleModal
isOpen={createUpdateModule}
setIsOpen={setCreateUpdateModule}
onClose={() => {
setCreateUpdateModule(false);
}}
data={selectedModule}
user={user}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
{modules ? (
modules.length > 0 ? (
<>

View File

@ -51,12 +51,11 @@ const tabsList = ["Recent", "All", "Favorites", "Created by me", "Created by oth
const projectService = new ProjectService();
const ProjectPages: NextPage = () => {
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [viewType, setViewType] = useState<TPageViewProps>("list");
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [viewType, setViewType] = useState<TPageViewProps>("list");
const { user } = useUserAuth();
@ -87,11 +86,16 @@ const ProjectPages: NextPage = () => {
return (
<>
{workspaceSlug && projectId && (
<CreateUpdatePageModal
isOpen={createUpdatePageModal}
handleClose={() => setCreateUpdatePageModal(false)}
user={user}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
)}
<ProjectAuthorizationWrapper
breadcrumbs={
<Breadcrumbs onBack={() => router.back()}>

View File

@ -0,0 +1,161 @@
import { observable, action, makeObservable } from "mobx";
// types
import { RootStore } from "./root";
// services
import { ProjectService } from "services/project";
import { PageService } from "services/page.service";
export interface ICommandPaletteStore {
isCommandPaletteOpen: boolean;
isShortcutModalOpen: boolean;
isCreateProjectModalOpen: boolean;
isCreateCycleModalOpen: boolean;
isCreateModuleModalOpen: boolean;
isCreateViewModalOpen: boolean;
isCreatePageModalOpen: boolean;
isCreateIssueModalOpen: boolean;
isDeleteIssueModalOpen: boolean;
isBulkDeleteIssueModalOpen: boolean;
toggleCommandPaletteModal: (value?: boolean) => void;
toggleShortcutModal: (value?: boolean) => void;
toggleCreateProjectModal: (value?: boolean) => void;
toggleCreateCycleModal: (value?: boolean) => void;
toggleCreateViewModal: (value?: boolean) => void;
toggleCreatePageModal: (value?: boolean) => void;
toggleCreateIssueModal: (value?: boolean) => void;
toggleCreateModuleModal: (value?: boolean) => void;
toggleDeleteIssueModal: (value?: boolean) => void;
toggleBulkDeleteIssueModal: (value?: boolean) => void;
}
class CommandPaletteStore implements ICommandPaletteStore {
isCommandPaletteOpen: boolean = false;
isShortcutModalOpen: boolean = false;
isCreateProjectModalOpen: boolean = false;
isCreateCycleModalOpen: boolean = false;
isCreateModuleModalOpen: boolean = false;
isCreateViewModalOpen: boolean = false;
isCreatePageModalOpen: boolean = false;
isCreateIssueModalOpen: boolean = false;
isDeleteIssueModalOpen: boolean = false;
isBulkDeleteIssueModalOpen: boolean = false;
// root store
rootStore;
// service
projectService;
pageService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observable
isCommandPaletteOpen: observable.ref,
isShortcutModalOpen: observable.ref,
isCreateProjectModalOpen: observable.ref,
isCreateCycleModalOpen: observable.ref,
isCreateModuleModalOpen: observable.ref,
isCreateViewModalOpen: observable.ref,
isCreatePageModalOpen: observable.ref,
isDeleteIssueModalOpen: observable.ref,
isCreateIssueModalOpen: observable.ref,
// computed
// projectPages: computed,
// action
toggleCommandPaletteModal: action,
toggleShortcutModal: action,
toggleCreateProjectModal: action,
toggleCreateCycleModal: action,
toggleCreateViewModal: action,
toggleCreatePageModal: action,
toggleCreateIssueModal: action,
toggleCreateModuleModal: action,
toggleBulkDeleteIssueModal: action,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.pageService = new PageService();
}
toggleCommandPaletteModal = (value?: boolean) => {
if (value) {
this.isCommandPaletteOpen = value;
} else {
this.isCommandPaletteOpen = !this.isCommandPaletteOpen;
}
};
toggleShortcutModal = (value?: boolean) => {
if (value) {
this.isShortcutModalOpen = value;
} else {
this.isShortcutModalOpen = !this.isShortcutModalOpen;
}
};
toggleCreateProjectModal = (value?: boolean) => {
if (value) {
this.isCreateProjectModalOpen = value;
} else {
this.isCreateProjectModalOpen = !this.isCreateProjectModalOpen;
}
};
toggleCreateCycleModal = (value?: boolean) => {
if (value) {
this.isCreateCycleModalOpen = value;
} else {
this.isCreateCycleModalOpen = !this.isCreateCycleModalOpen;
}
};
toggleCreateViewModal = (value?: boolean) => {
if (value) {
this.isCreateViewModalOpen = value;
} else {
this.isCreateViewModalOpen = !this.isCreateViewModalOpen;
}
};
toggleCreatePageModal = (value?: boolean) => {
if (value) {
this.isCreatePageModalOpen = value;
} else {
this.isCreatePageModalOpen = !this.isCreatePageModalOpen;
}
};
toggleCreateIssueModal = (value?: boolean) => {
if (value) {
this.isCreateIssueModalOpen = value;
} else {
this.isCreateIssueModalOpen = !this.isCreateIssueModalOpen;
}
};
toggleDeleteIssueModal = (value?: boolean) => {
if (value) {
this.isDeleteIssueModalOpen = value;
} else {
this.isDeleteIssueModalOpen = !this.isDeleteIssueModalOpen;
}
};
toggleCreateModuleModal = (value?: boolean) => {
if (value) {
this.isCreateModuleModalOpen = value;
} else {
this.isCreateModuleModalOpen = !this.isCreateModuleModalOpen;
}
};
toggleBulkDeleteIssueModal = (value?: boolean) => {
if (value) {
this.isBulkDeleteIssueModalOpen = value;
} else {
this.isBulkDeleteIssueModalOpen = !this.isBulkDeleteIssueModalOpen;
}
};
}
export default CommandPaletteStore;

View File

@ -1,7 +1,8 @@
import { enableStaticRendering } from "mobx-react-lite";
// store imports
import UserStore from "store/user.store";
import ThemeStore from "store/theme.store";
import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.store";
import UserStore, { IUserStore } from "store/user.store";
import ThemeStore, { IThemeStore } from "store/theme.store";
import {
DraftIssuesStore,
IIssueDetailStore,
@ -79,9 +80,10 @@ import {
enableStaticRendering(typeof window === "undefined");
export class RootStore {
user;
theme;
user: IUserStore;
theme: IThemeStore;
commandPalette: ICommandPaletteStore;
workspace: IWorkspaceStore;
workspaceFilter: IWorkspaceFilterStore;
@ -129,6 +131,7 @@ export class RootStore {
inboxFilters: IInboxFiltersStore;
constructor() {
this.commandPalette = new CommandPaletteStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);

View File

@ -3,7 +3,15 @@ import { action, observable, makeObservable } from "mobx";
// helper
import { applyTheme, unsetCustomCssVariables } from "helpers/theme.helper";
class ThemeStore {
export interface IThemeStore {
theme: string | null;
sidebarCollapsed: boolean | null;
setSidebarCollapsed: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
}
class ThemeStore implements IThemeStore {
sidebarCollapsed: boolean | null = null;
theme: string | null = null;
// root store
@ -24,8 +32,8 @@ class ThemeStore {
this.initialLoad();
}
setSidebarCollapsed(collapsed: boolean | null = null) {
if (collapsed === null) {
setSidebarCollapsed(collapsed?: boolean) {
if (!collapsed) {
let _sidebarCollapsed: string | boolean | null = localStorage.getItem("app_sidebar_collapsed");
_sidebarCollapsed = _sidebarCollapsed ? (_sidebarCollapsed === "true" ? true : false) : false;
this.sidebarCollapsed = _sidebarCollapsed;

View File

@ -8,7 +8,7 @@ import { WorkspaceService } from "services/workspace.service";
import { IUser, IUserSettings } from "types/users";
import { IWorkspaceMember, IProjectMember } from "types";
interface IUserStore {
export interface IUserStore {
loader: boolean;
currentUser: IUser | null;
@ -28,9 +28,11 @@ interface IUserStore {
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMember>;
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<IProjectMember>;
fetchUserDashboardInfo: (workspaceSlug: string, month: number) => Promise<any>;
updateTourCompleted: () => Promise<void>;
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser>;
updateCurrentUserTheme: (theme: string) => Promise<IUser>;
}
class UserStore implements IUserStore {