diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/command-palette/addAsSubIssue.tsx new file mode 100644 index 000000000..41c3e3d28 --- /dev/null +++ b/apps/app/components/command-palette/addAsSubIssue.tsx @@ -0,0 +1,196 @@ +import React, { useState } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { useForm } from "react-hook-form"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// hooks +import useUser from "lib/hooks/useUser"; +// icons +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { FolderIcon } from "@heroicons/react/24/outline"; +// commons +import { classNames } from "constants/common"; +// types +import { IIssue, IssueResponse } from "types"; +import { Button } from "ui"; +import { PROJECT_ISSUES_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import issuesServices from "lib/services/issues.services"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + parentId: string; +}; + +type FormInput = { + issue_ids: string[]; + cycleId: string; +}; + +const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parentId }) => { + const [query, setQuery] = useState(""); + + const { activeWorkspace, activeProject, issues } = useUser(); + + const filteredIssues: IIssue[] = + query === "" + ? issues?.results ?? [] + : issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? + []; + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + control, + reset, + setError, + } = useForm(); + + const handleCommandPaletteClose = () => { + setIsOpen(false); + setQuery(""); + reset(); + }; + + const addAsSubIssue = (issueId: string) => { + if (activeWorkspace && activeProject) { + issuesServices + .patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: parentId }) + .then((res) => { + mutate( + PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), + (prevData) => ({ + ...(prevData as IssueResponse), + results: (prevData?.results ?? []).map((p) => + p.id === issueId ? { ...p, ...res } : p + ), + }), + false + ); + }) + .catch((e) => { + console.log(e); + }); + } + }; + + return ( + <> + setQuery("")} appear> + + +
+ + +
+ + + { + // const { url, onClick } = item; + // if (url) router.push(url); + // else if (onClick) onClick(); + // handleCommandPaletteClose(); + // }} + > +
+
+ + + {filteredIssues.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Issues +

    + )} +
      + {filteredIssues.map((issue) => { + if (issue.parent === "" || issue.parent === null) + return ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + onClick={() => { + addAsSubIssue(issue.id); + setIsOpen(false); + }} + > + + {issue.name} + + ); + })} +
    +
  • + + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    +
    +
    +
    +
    +
    + + ); +}; + +export default AddAsSubIssue; diff --git a/apps/app/components/lexical/editor.tsx b/apps/app/components/lexical/editor.tsx index 3685de22e..e7cd68033 100644 --- a/apps/app/components/lexical/editor.tsx +++ b/apps/app/components/lexical/editor.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { EditorState, LexicalEditor, $getRoot, $getSelection } from "lexical"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { ContentEditable } from "@lexical/react/LexicalContentEditable"; @@ -25,12 +24,15 @@ export interface RichTextEditorProps { onChange: (state: string) => void; id: string; value: string; + placeholder?: string; } -const RichTextEditor: FC = (props) => { - // props - const { onChange, value, id } = props; - +const RichTextEditor: React.FC = ({ + onChange, + id, + value, + placeholder = "Enter some text...", +}) => { function handleChange(state: EditorState, editor: LexicalEditor) { state.read(() => { onChange(JSON.stringify(state.toJSON())); @@ -54,8 +56,8 @@ const RichTextEditor: FC = (props) => { } ErrorBoundary={LexicalErrorBoundary} placeholder={ -
    - Enter some text... +
    + {placeholder}
    } /> diff --git a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx index e038ac7e7..68f2f91eb 100644 --- a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx @@ -262,7 +262,7 @@ const IssueDetailSidebar: React.FC = ({ control, submitChanges, issueDeta render={({ field: { value, onChange } }) => ( { submitChanges({ target_date: e.target.value }); onChange(e.target.value); diff --git a/apps/app/layouts/AdminLayout.tsx b/apps/app/layouts/AdminLayout.tsx index 7a1b29982..e297590ce 100644 --- a/apps/app/layouts/AdminLayout.tsx +++ b/apps/app/layouts/AdminLayout.tsx @@ -1,5 +1,9 @@ // react -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +// next +import { useRouter } from "next/router"; +// hooks +import useUser from "lib/hooks/useUser"; // layouts import Container from "layouts/Container"; import Sidebar from "layouts/Navbar/Sidebar"; @@ -11,6 +15,14 @@ import type { Props } from "./types"; const AdminLayout: React.FC = ({ meta, children }) => { const [isOpen, setIsOpen] = useState(false); + const router = useRouter(); + + const { user, isUserLoading } = useUser(); + + useEffect(() => { + if (!isUserLoading && (!user || user === null)) router.push("/signin"); + }, [isUserLoading, user, router]); + return ( diff --git a/apps/app/lib/services/issues.services.ts b/apps/app/lib/services/issues.services.ts index 0e285602e..4fe9be16b 100644 --- a/apps/app/lib/services/issues.services.ts +++ b/apps/app/lib/services/issues.services.ts @@ -22,8 +22,8 @@ class ProjectIssuesServices extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async createIssues(workspace_slug: string, projectId: string, data: any): Promise { - return this.post(ISSUES_ENDPOINT(workspace_slug, projectId), data) + async createIssues(workspaceSlug: string, projectId: string, data: any): Promise { + return this.post(ISSUES_ENDPOINT(workspaceSlug, projectId), data) .then((response) => { return response?.data; }) @@ -32,8 +32,8 @@ class ProjectIssuesServices extends APIService { }); } - async getIssues(workspace_slug: string, projectId: string): Promise { - return this.get(ISSUES_ENDPOINT(workspace_slug, projectId)) + async getIssues(workspaceSlug: string, projectId: string): Promise { + return this.get(ISSUES_ENDPOINT(workspaceSlug, projectId)) .then((response) => { return response?.data; }) @@ -42,8 +42,8 @@ class ProjectIssuesServices extends APIService { }); } - async getIssue(workspace_slug: string, projectId: string, issueId: string): Promise { - return this.get(ISSUE_DETAIL(workspace_slug, projectId, issueId)) + async getIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(ISSUE_DETAIL(workspaceSlug, projectId, issueId)) .then((response) => { return response?.data; }) @@ -53,11 +53,11 @@ class ProjectIssuesServices extends APIService { } async getIssueActivities( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string ): Promise { - return this.get(ISSUE_ACTIVITIES(workspace_slug, projectId, issueId)) + return this.get(ISSUE_ACTIVITIES(workspaceSlug, projectId, issueId)) .then((response) => { return response?.data; }) @@ -66,8 +66,8 @@ class ProjectIssuesServices extends APIService { }); } - async getIssueComments(workspace_slug: string, projectId: string, issueId: string): Promise { - return this.get(ISSUE_COMMENTS(workspace_slug, projectId, issueId)) + async getIssueComments(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.get(ISSUE_COMMENTS(workspaceSlug, projectId, issueId)) .then((response) => { return response?.data; }) @@ -76,8 +76,8 @@ class ProjectIssuesServices extends APIService { }); } - async getIssueProperties(workspace_slug: string, projectId: string): Promise { - return this.get(ISSUE_PROPERTIES_ENDPOINT(workspace_slug, projectId)) + async getIssueProperties(workspaceSlug: string, projectId: string): Promise { + return this.get(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId)) .then((response) => { return response?.data; }) @@ -87,14 +87,14 @@ class ProjectIssuesServices extends APIService { } async addIssueToSprint( - workspace_slug: string, + workspaceSlug: string, projectId: string, cycleId: string, data: { issue: string; } ) { - return this.post(CYCLE_DETAIL(workspace_slug, projectId, cycleId), data) + return this.post(CYCLE_DETAIL(workspaceSlug, projectId, cycleId), data) .then((response) => { return response?.data; }) @@ -103,8 +103,8 @@ class ProjectIssuesServices extends APIService { }); } - async createIssueProperties(workspace_slug: string, projectId: string, data: any): Promise { - return this.post(ISSUE_PROPERTIES_ENDPOINT(workspace_slug, projectId), data) + async createIssueProperties(workspaceSlug: string, projectId: string, data: any): Promise { + return this.post(ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId), data) .then((response) => { return response?.data; }) @@ -114,13 +114,13 @@ class ProjectIssuesServices extends APIService { } async patchIssueProperties( - workspace_slug: string, + workspaceSlug: string, projectId: string, issuePropertyId: string, data: any ): Promise { return this.patch( - ISSUE_PROPERTIES_ENDPOINT(workspace_slug, projectId) + `${issuePropertyId}/`, + ISSUE_PROPERTIES_ENDPOINT(workspaceSlug, projectId) + `${issuePropertyId}/`, data ) @@ -133,12 +133,12 @@ class ProjectIssuesServices extends APIService { } async createIssueComment( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string, data: any ): Promise { - return this.post(ISSUE_COMMENTS(workspace_slug, projectId, issueId), data) + return this.post(ISSUE_COMMENTS(workspaceSlug, projectId, issueId), data) .then((response) => { return response?.data; }) @@ -148,13 +148,13 @@ class ProjectIssuesServices extends APIService { } async patchIssueComment( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string, commentId: string, data: IIssueComment ): Promise { - return this.patch(ISSUE_COMMENT_DETAIL(workspace_slug, projectId, issueId, commentId), data) + return this.patch(ISSUE_COMMENT_DETAIL(workspaceSlug, projectId, issueId, commentId), data) .then((response) => { return response?.data; }) @@ -164,12 +164,12 @@ class ProjectIssuesServices extends APIService { } async deleteIssueComment( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string, commentId: string ): Promise { - return this.delete(ISSUE_COMMENT_DETAIL(workspace_slug, projectId, issueId, commentId)) + return this.delete(ISSUE_COMMENT_DETAIL(workspaceSlug, projectId, issueId, commentId)) .then((response) => { return response?.data; }) @@ -178,8 +178,8 @@ class ProjectIssuesServices extends APIService { }); } - async getIssueLabels(workspace_slug: string, projectId: string): Promise { - return this.get(ISSUE_LABELS(workspace_slug, projectId)) + async getIssueLabels(workspaceSlug: string, projectId: string): Promise { + return this.get(ISSUE_LABELS(workspaceSlug, projectId)) .then((response) => { return response?.data; }) @@ -188,8 +188,8 @@ class ProjectIssuesServices extends APIService { }); } - async createIssueLabel(workspace_slug: string, projectId: string, data: any): Promise { - return this.post(ISSUE_LABELS(workspace_slug, projectId), data) + async createIssueLabel(workspaceSlug: string, projectId: string, data: any): Promise { + return this.post(ISSUE_LABELS(workspaceSlug, projectId), data) .then((response) => { return response?.data; }) @@ -199,12 +199,12 @@ class ProjectIssuesServices extends APIService { } async updateIssue( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string, data: any ): Promise { - return this.put(ISSUE_DETAIL(workspace_slug, projectId, issueId), data) + return this.put(ISSUE_DETAIL(workspaceSlug, projectId, issueId), data) .then((response) => { return response?.data; }) @@ -214,12 +214,12 @@ class ProjectIssuesServices extends APIService { } async patchIssue( - workspace_slug: string, + workspaceSlug: string, projectId: string, issueId: string, data: Partial ): Promise { - return this.patch(ISSUE_DETAIL(workspace_slug, projectId, issueId), data) + return this.patch(ISSUE_DETAIL(workspaceSlug, projectId, issueId), data) .then((response) => { return response?.data; }) @@ -228,8 +228,8 @@ class ProjectIssuesServices extends APIService { }); } - async deleteIssue(workspace_slug: string, projectId: string, issuesId: string): Promise { - return this.delete(ISSUE_DETAIL(workspace_slug, projectId, issuesId)) + async deleteIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { + return this.delete(ISSUE_DETAIL(workspaceSlug, projectId, issuesId)) .then((response) => { return response?.data; }) @@ -238,8 +238,8 @@ class ProjectIssuesServices extends APIService { }); } - async bulkDeleteIssues(workspace_slug: string, projectId: string, data: any): Promise { - return this.delete(BULK_DELETE_ISSUES(workspace_slug, projectId), data) + async bulkDeleteIssues(workspaceSlug: string, projectId: string, data: any): Promise { + return this.delete(BULK_DELETE_ISSUES(workspaceSlug, projectId), data) .then((response) => { return response?.data; }) @@ -249,12 +249,12 @@ class ProjectIssuesServices extends APIService { } async bulkAddIssuesToCycle( - workspace_slug: string, + workspaceSlug: string, projectId: string, cycleId: string, data: any ): Promise { - return this.post(BULK_ADD_ISSUES_TO_CYCLE(workspace_slug, projectId, cycleId), data) + return this.post(BULK_ADD_ISSUES_TO_CYCLE(workspaceSlug, projectId, cycleId), data) .then((response) => { return response?.data; }) diff --git a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx index f7f412d90..082496906 100644 --- a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx @@ -1,19 +1,26 @@ // next import type { NextPage } from "next"; import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; // react import React, { useCallback, useEffect, useState } from "react"; // swr -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; // react hook form -import { useForm } from "react-hook-form"; +import { useForm, Controller } from "react-hook-form"; // headless ui -import { Tab } from "@headlessui/react"; +import { Disclosure, Menu, Tab, Transition } from "@headlessui/react"; // services import issuesServices from "lib/services/issues.services"; import stateServices from "lib/services/state.services"; // fetch keys -import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUES_COMMENTS, STATE_LIST } from "constants/fetch-keys"; +import { + PROJECT_ISSUES_ACTIVITY, + PROJECT_ISSUES_COMMENTS, + PROJECT_ISSUES_DETAILS, + PROJECT_ISSUES_LIST, + STATE_LIST, +} from "constants/fetch-keys"; // hooks import useUser from "lib/hooks/useUser"; // layouts @@ -34,7 +41,14 @@ import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; // types import { IIssue, IIssueComment, IssueResponse, IState } from "types"; // icons -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { + ChevronLeftIcon, + ChevronRightIcon, + EllipsisHorizontalIcon, + PlusIcon, +} from "@heroicons/react/24/outline"; +import Link from "next/link"; +import AddAsSubIssue from "components/command-palette/addAsSubIssue"; const IssueDetail: NextPage = () => { const router = useRouter(); @@ -44,9 +58,24 @@ const IssueDetail: NextPage = () => { const { activeWorkspace, activeProject, issues, mutateIssues } = useUser(); const [isOpen, setIsOpen] = useState(false); + const [isAddAsSubIssueOpen, setIsAddAsSubIssueOpen] = useState(false); const [issueDetail, setIssueDetail] = useState(undefined); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + const [issueDescriptionValue, setIssueDescriptionValue] = useState(""); + const handleDescriptionChange: any = (value: any) => { + console.log(value); + setIssueDescriptionValue(value); + }; + + const RichTextEditor = dynamic(() => import("components/lexical/editor"), { + ssr: false, + }); + const { register, formState: { errors }, @@ -151,14 +180,44 @@ const IssueDetail: NextPage = () => { const prevIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) - 1]; const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1]; + const subIssues = (issues && issues.results.filter((i) => i.parent === issueDetail?.id)) ?? []; + + const handleRemove = (issueId: string) => { + if (activeWorkspace && activeProject) { + issuesServices + .patchIssue(activeWorkspace.slug, activeProject.id, issueId, { parent: null }) + .then((res) => { + mutate( + PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id), + (prevData) => ({ + ...(prevData as IssueResponse), + results: (prevData?.results ?? []).map((p) => + p.id === issueId ? { ...p, ...res } : p + ), + }), + false + ); + }) + .catch((e) => { + console.log(e); + }); + } + }; + return ( +
    @@ -200,33 +259,235 @@ const IssueDetail: NextPage = () => {
    -