diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 03f9ea822..93298b4e0 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -35,7 +35,6 @@ export const EmailCodeForm = ({ onSuccess }: any) => { }); const onSubmit = ({ email }: EmailCodeFormValues) => { - console.log(email); authenticationService .emailCode({ email }) .then((res) => { diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 441fb31fa..e6138da94 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -1,37 +1,38 @@ // TODO: Refactor this component: into a different file, use this file to export the components import React, { useState, useCallback, useEffect } from "react"; -// next + import { useRouter } from "next/router"; -// swr + import useSWR from "swr"; -// hooks + +// headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; +// services +import userService from "services/user.service"; +// hooks +import useTheme from "hooks/use-theme"; +import useToast from "hooks/use-toast"; +import useUser from "hooks/use-user"; +// components +import ShortcutsModal from "components/command-palette/shortcuts"; +import { BulkDeleteIssuesModal } from "components/core"; +import { CreateProjectModal } from "components/project"; +import { CreateUpdateIssueModal } from "components/issues"; +import { CreateUpdateModuleModal } from "components/modules"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; +// ui +import { Button } from "components/ui"; +// icons import { FolderIcon, RectangleStackIcon, ClipboardDocumentListIcon, MagnifyingGlassIcon, } from "@heroicons/react/24/outline"; -import useTheme from "hooks/use-theme"; -import useToast from "hooks/use-toast"; -import useUser from "hooks/use-user"; -// services -import userService from "services/user.service"; -// components -import ShortcutsModal from "components/command-palette/shortcuts"; -import { CreateProjectModal } from "components/project"; -import { CreateUpdateIssueModal } from "components/issues/modal"; -import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; -import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal"; -import BulkDeleteIssuesModal from "components/common/bulk-delete-issues-modal"; -// headless ui // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types import { IIssue } from "types"; -// ui -import { Button } from "components/ui"; -// icons // fetch-keys import { USER_ISSUE } from "constants/fetch-keys"; @@ -74,7 +75,7 @@ const CommandPalette: React.FC = () => { name: "Add new issue...", icon: RectangleStackIcon, hide: !projectId, - shortcut: "I", + shortcut: "C", onClick: () => { setIsIssueModalOpen(true); }, @@ -111,7 +112,6 @@ const CommandPalette: React.FC = () => { if (!router.query.issueId) return; const url = new URL(window.location.href); - console.log(url); copyTextToClipboard(url.href) .then(() => { setToastAlert({ @@ -179,7 +179,6 @@ const CommandPalette: React.FC = () => { )} @@ -330,7 +329,6 @@ const CommandPalette: React.FC = () => { /> {action.name} - {action.shortcut} diff --git a/apps/app/components/common/board-view/single-board.tsx b/apps/app/components/common/board-view/single-board.tsx deleted file mode 100644 index aedc969b5..000000000 --- a/apps/app/components/common/board-view/single-board.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const SingleBoard = () => <>; - -export default SingleBoard; diff --git a/apps/app/components/common/board-view/single-issue.tsx b/apps/app/components/common/board-view/single-issue.tsx deleted file mode 100644 index cd84697e9..000000000 --- a/apps/app/components/common/board-view/single-issue.tsx +++ /dev/null @@ -1,464 +0,0 @@ -import React from "react"; - -import Link from "next/link"; -import Image from "next/image"; -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// react-beautiful-dnd -import { DraggableStateSnapshot } from "react-beautiful-dnd"; -// react-datepicker -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -// headless ui -import { Listbox, Transition } from "@headlessui/react"; -// constants -import { TrashIcon } from "@heroicons/react/24/outline"; -// services -import issuesService from "services/issues.service"; -import stateService from "services/state.service"; -import projectService from "services/project.service"; -// components -import { AssigneesList, CustomDatePicker } from "components/ui"; -// helpers -import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; -import { addSpaceIfCamelCase } from "helpers/string.helper"; -// types -import { - CycleIssueResponse, - IIssue, - IssueResponse, - IUserLite, - IWorkspaceMember, - ModuleIssueResponse, - Properties, - UserAuth, -} from "types"; -// common -import { PRIORITIES } from "constants/"; -import { - STATE_LIST, - PROJECT_DETAILS, - CYCLE_ISSUES, - MODULE_ISSUES, - PROJECT_ISSUES_LIST, -} from "constants/fetch-keys"; -import { getPriorityIcon } from "constants/global"; - -type Props = { - type?: string; - typeId?: string; - issue: IIssue; - properties: Properties; - snapshot?: DraggableStateSnapshot; - assignees: Partial[] | (Partial | undefined)[]; - people: IWorkspaceMember[] | undefined; - handleDeleteIssue?: React.Dispatch>; - userAuth: UserAuth; -}; - -const SingleBoardIssue: React.FC = ({ - type, - typeId, - issue, - properties, - snapshot, - assignees, - people, - handleDeleteIssue, - userAuth, -}) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: states } = useSWR( - workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - - const { data: projectDetails } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.getProject(workspaceSlug as string, projectId as string) - : null - ); - - const partialUpdateIssue = (formData: Partial) => { - if (!workspaceSlug || !projectId) return; - - if (typeId) { - mutate( - CYCLE_ISSUES(typeId ?? ""), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, - false - ); - - mutate( - MODULE_ISSUES(typeId ?? ""), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, - false - ); - } - - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), - (prevData) => ({ - ...(prevData as IssueResponse), - results: (prevData?.results ?? []).map((p) => { - if (p.id === issue.id) return { ...p, ...formData }; - return p; - }), - }), - false - ); - - issuesService - .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) - .then((res) => { - if (typeId) { - mutate(CYCLE_ISSUES(typeId ?? "")); - mutate(MODULE_ISSUES(typeId ?? "")); - } - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); - }) - .catch((error) => { - console.log(error); - }); - }; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - - return ( -
-
- {handleDeleteIssue && !isNotAllowed && ( -
- -
- )} - - - {properties.key && ( -
- {projectDetails?.identifier}-{issue.sequence_id} -
- )} -
- {issue.name} -
-
- -
- {properties.priority && ( - { - partialUpdateIssue({ priority: data }); - }} - className="group relative flex-shrink-0" - disabled={isNotAllowed} - > - {({ open }) => ( - <> -
- - {getPriorityIcon(issue?.priority ?? "None")} - - - - - {PRIORITIES?.map((priority) => ( - - `flex cursor-pointer select-none items-center gap-2 px-3 py-2 capitalize ${ - active ? "bg-indigo-50" : "bg-white" - }` - } - value={priority} - > - {getPriorityIcon(priority)} - {priority} - - ))} - - -
- - )} -
- )} - {properties.state && ( - { - partialUpdateIssue({ state: data }); - }} - className="group relative flex-shrink-0" - disabled={isNotAllowed} - > - {({ open }) => ( - <> -
- - - {addSpaceIfCamelCase(issue.state_detail.name)} - - - - - {states?.map((state) => ( - - `flex cursor-pointer select-none items-center gap-2 px-3 py-2 ${ - active ? "bg-indigo-50" : "bg-white" - }` - } - value={state.id} - > - - {addSpaceIfCamelCase(state.name)} - - ))} - - -
- - )} -
- )} - {/* {properties.cycle && !typeId && ( -
- {issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"} -
- )} */} - {properties.due_date && ( -
- - partialUpdateIssue({ - target_date: val, - }) - } - className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} - /> - {/* { - partialUpdateIssue({ - target_date: val - ? `${val.getFullYear()}-${val.getMonth() + 1}-${val.getDate()}` - : null, - }); - }} - dateFormat="dd-MM-yyyy" - className={`cursor-pointer rounded-md border px-2 py-[3px] text-xs shadow-sm duration-300 hover:bg-gray-100 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 ${ - issue?.target_date ? "w-[4.5rem]" : "w-[3rem] text-center" - }`} - isClearable - /> */} -
- )} - {properties.sub_issue_count && ( -
- {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- )} - {properties.assignee && ( - { - const newData = issue.assignees ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ assignees_list: newData }); - }} - className="group relative flex-shrink-0" - disabled={isNotAllowed} - > - {({ open }) => ( -
- -
- -
-
- - - - {people?.map((person) => ( - - `cursor-pointer select-none p-2 ${active ? "bg-indigo-50" : "bg-white"}` - } - value={person.member.id} - > -
- {person.member.avatar && person.member.avatar !== "" ? ( -
- avatar -
- ) : ( -
- {person.member.first_name && person.member.first_name !== "" - ? person.member.first_name.charAt(0) - : person.member.email.charAt(0)} -
- )} -

- {person.member.first_name && person.member.first_name !== "" - ? person.member.first_name - : person.member.email} -

-
-
- ))} -
-
-
- )} -
- )} -
-
-
- ); -}; - -export default SingleBoardIssue; diff --git a/apps/app/components/common/list-view/single-issue.tsx b/apps/app/components/common/list-view/single-issue.tsx deleted file mode 100644 index 262f332b5..000000000 --- a/apps/app/components/common/list-view/single-issue.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import React, { useState } from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -import useSWR, { mutate } from "swr"; - -// services -import issuesService from "services/issues.service"; -import workspaceService from "services/workspace.service"; -import stateService from "services/state.service"; -// headless ui -import { Listbox, Transition } from "@headlessui/react"; -// ui -import { CustomMenu, CustomSelect, AssigneesList, Avatar, CustomDatePicker } from "components/ui"; -// components -import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; -// helpers -import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; -import { addSpaceIfCamelCase } from "helpers/string.helper"; -// types -import { - CycleIssueResponse, - IIssue, - IssueResponse, - IWorkspaceMember, - ModuleIssueResponse, - Properties, - UserAuth, -} from "types"; -// fetch-keys -import { - CYCLE_ISSUES, - MODULE_ISSUES, - PROJECT_ISSUES_LIST, - STATE_LIST, - WORKSPACE_MEMBERS, -} from "constants/fetch-keys"; -// constants -import { getPriorityIcon } from "constants/global"; -import { PRIORITIES } from "constants/"; - -type Props = { - type?: string; - typeId?: string; - issue: IIssue; - properties: Properties; - editIssue: () => void; - removeIssue?: () => void; - userAuth: UserAuth; -}; - -const SingleListIssue: React.FC = ({ - type, - typeId, - issue, - properties, - editIssue, - removeIssue, - userAuth, -}) => { - const [deleteIssue, setDeleteIssue] = useState(); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: states } = useSWR( - workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, - workspaceSlug && projectId - ? () => stateService.getStates(workspaceSlug as string, projectId as string) - : null - ); - - const { data: people } = useSWR( - workspaceSlug ? WORKSPACE_MEMBERS : null, - workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null - ); - - const partialUpdateIssue = (formData: Partial) => { - if (!workspaceSlug || !projectId) return; - - if (typeId) { - mutate( - CYCLE_ISSUES(typeId ?? ""), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, - false - ); - - mutate( - MODULE_ISSUES(typeId ?? ""), - (prevData) => { - const updatedIssues = (prevData ?? []).map((p) => { - if (p.issue_detail.id === issue.id) { - return { - ...p, - issue_detail: { - ...p.issue_detail, - ...formData, - }, - }; - } - return p; - }); - return [...updatedIssues]; - }, - false - ); - } - - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), - (prevData) => ({ - ...(prevData as IssueResponse), - results: (prevData?.results ?? []).map((p) => { - if (p.id === issue.id) return { ...p, ...formData }; - return p; - }), - }), - false - ); - - issuesService - .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) - .then((res) => { - if (typeId) { - mutate(CYCLE_ISSUES(typeId ?? "")); - mutate(MODULE_ISSUES(typeId ?? "")); - } - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); - }) - .catch((error) => { - console.log(error); - }); - }; - - const isNotAllowed = userAuth.isGuest || userAuth.isViewer; - - return ( - <> - setDeleteIssue(undefined)} - isOpen={!!deleteIssue} - data={deleteIssue} - /> -
- -
- {properties.priority && ( - { - partialUpdateIssue({ priority: data }); - }} - className="group relative flex-shrink-0" - disabled={isNotAllowed} - > - {({ open }) => ( - <> -
- - {getPriorityIcon( - issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", - "text-sm" - )} - - - - - {PRIORITIES?.map((priority) => ( - - `flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${ - active ? "bg-indigo-50" : "bg-white" - }` - } - value={priority} - > - {getPriorityIcon(priority, "text-sm")} - {priority ?? "None"} - - ))} - - -
-
-
Priority
-
- {issue.priority ?? "None"} -
-
- - )} -
- )} - {properties.state && ( - - - {addSpaceIfCamelCase(issue.state_detail.name)} - - } - value={issue.state} - onChange={(data: string) => { - partialUpdateIssue({ state: data }); - }} - maxHeight="md" - noChevron - disabled={isNotAllowed} - > - {states?.map((state) => ( - - <> - - {addSpaceIfCamelCase(state.name)} - - - ))} - - )} - {/* {properties.cycle && !typeId && ( -
- {issue.issue_cycle ? issue.issue_cycle.cycle_detail.name : "None"} -
- )} */} - {properties.due_date && ( -
- - partialUpdateIssue({ - target_date: val, - }) - } - className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} - /> -
-
Due date
-
{renderShortNumericDateFormat(issue.target_date ?? "")}
-
- {issue.target_date - ? issue.target_date < new Date().toISOString() - ? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days` - : findHowManyDaysLeft(issue.target_date) <= 3 - ? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days` - : "Due date" - : "N/A"} -
-
-
- )} - {properties.sub_issue_count && ( -
- {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- )} - {properties.assignee && ( - { - const newData = issue.assignees ?? []; - - if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); - else newData.push(data); - - partialUpdateIssue({ assignees_list: newData }); - }} - className="group relative flex-shrink-0" - disabled={isNotAllowed} - > - {({ open }) => ( - <> -
- -
- -
-
- - - - {people?.map((person) => ( - - `flex items-center gap-x-1 cursor-pointer select-none p-2 ${ - active ? "bg-indigo-50" : "" - } ${ - selected || issue.assignees?.includes(person.member.id) - ? "bg-indigo-50 font-medium" - : "font-normal" - }` - } - value={person.member.id} - > - -

- {person.member.first_name && person.member.first_name !== "" - ? person.member.first_name - : person.member.email} -

-
- ))} -
-
-
-
-
Assigned to
-
- {issue.assignee_details?.length > 0 - ? issue.assignee_details.map((assignee) => assignee.first_name).join(", ") - : "No one"} -
-
- - )} -
- )} - {type && !isNotAllowed && ( - - Edit - {type !== "issue" && ( - - <>Remove from {type} - - )} - setDeleteIssue(issue)}> - Delete permanently - - - )} -
-
- - ); -}; - -export default SingleListIssue; diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx new file mode 100644 index 000000000..a6d99fa0f --- /dev/null +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -0,0 +1,92 @@ +// react-beautiful-dnd +import { DragDropContext, DropResult } from "react-beautiful-dnd"; +// hooks +import useIssueView from "hooks/use-issue-view"; +// components +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { SingleBoard } from "components/core/board-view/single-board"; +// types +import { IIssue, IProjectMember, IState, UserAuth } from "types"; + +type Props = { + type: "issue" | "cycle" | "module"; + issues: IIssue[]; + states: IState[] | undefined; + members: IProjectMember[] | undefined; + addIssueToState: (groupTitle: string, stateId: string | null) => void; + openIssuesListModal?: (() => void) | null; + handleDeleteIssue: (issue: IIssue) => void; + handleOnDragEnd: (result: DropResult) => void; + userAuth: UserAuth; +}; + +export const AllBoards: React.FC = ({ + type, + issues, + states, + members, + addIssueToState, + openIssuesListModal, + handleDeleteIssue, + handleOnDragEnd, + userAuth, +}) => { + const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues); + + return ( + <> + {groupedByIssues ? ( +
+ +
+ + {(provided) => ( +
+
+ {Object.keys(groupedByIssues).map((singleGroup, index) => { + const stateId = + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.id ?? null + : null; + + const bgColor = + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.color + : "#000000"; + + return ( + addIssueToState(singleGroup, stateId)} + handleDeleteIssue={handleDeleteIssue} + openIssuesListModal={openIssuesListModal ?? null} + orderBy={orderBy} + userAuth={userAuth} + /> + ); + })} +
+ {provided.placeholder} +
+ )} +
+
+
+
+ ) : ( +
Loading...
+ )} + + ); +}; diff --git a/apps/app/components/common/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx similarity index 80% rename from apps/app/components/common/board-view/board-header.tsx rename to apps/app/components/core/board-view/board-header.tsx index c04bc95d5..ba4d2c02a 100644 --- a/apps/app/components/common/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -14,6 +14,7 @@ import { addSpaceIfCamelCase } from "helpers/string.helper"; // types import { IIssue, NestedKeyOf } from "types"; type Props = { + provided: DraggableProvided; isCollapsed: boolean; setIsCollapsed: React.Dispatch>; groupedByIssues: { @@ -22,12 +23,11 @@ type Props = { selectedGroup: NestedKeyOf | null; groupTitle: string; createdBy: string | null; - bgColor: string; + bgColor?: string; addIssueToState: () => void; - provided?: DraggableProvided; }; -const BoardHeader: React.FC = ({ +export const BoardHeader: React.FC = ({ isCollapsed, setIsCollapsed, provided, @@ -44,18 +44,16 @@ const BoardHeader: React.FC = ({ }`} >
- {provided && ( - - )} +
= ({
); - -export default BoardHeader; diff --git a/apps/app/components/core/board-view/index.ts b/apps/app/components/core/board-view/index.ts new file mode 100644 index 000000000..6e5cdf8bf --- /dev/null +++ b/apps/app/components/core/board-view/index.ts @@ -0,0 +1,4 @@ +export * from "./all-boards"; +export * from "./board-header"; +export * from "./single-board"; +export * from "./single-issue"; diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx new file mode 100644 index 000000000..5aa999f87 --- /dev/null +++ b/apps/app/components/core/board-view/single-board.tsx @@ -0,0 +1,164 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +// react-beautiful-dnd +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { Draggable } from "react-beautiful-dnd"; +// hooks +import useIssuesProperties from "hooks/use-issue-properties"; +// components +import { BoardHeader, SingleBoardIssue } from "components/core"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { PlusIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types"; + +type Props = { + index: number; + type?: "issue" | "cycle" | "module"; + bgColor?: string; + groupTitle: string; + groupedByIssues: { + [key: string]: IIssue[]; + }; + selectedGroup: NestedKeyOf | null; + members: IProjectMember[] | undefined; + addIssueToState: () => void; + handleDeleteIssue: (issue: IIssue) => void; + openIssuesListModal?: (() => void) | null; + orderBy: NestedKeyOf | "manual" | null; + userAuth: UserAuth; +}; + +export const SingleBoard: React.FC = ({ + index, + type, + bgColor, + groupTitle, + groupedByIssues, + selectedGroup, + members, + addIssueToState, + handleDeleteIssue, + openIssuesListModal, + orderBy, + userAuth, +}) => { + // collapse/expand + const [isCollapsed, setIsCollapsed] = useState(true); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + + const createdBy = + selectedGroup === "created_by" + ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." + : null; + + if (selectedGroup === "priority") + groupTitle === "high" + ? (bgColor = "#dc2626") + : groupTitle === "medium" + ? (bgColor = "#f97316") + : groupTitle === "low" + ? (bgColor = "#22c55e") + : (bgColor = "#ff0000"); + + return ( + + {(provided, snapshot) => ( +
+
+ + + {(provided, snapshot) => ( +
+ {groupedByIssues[groupTitle].map((issue, index: number) => ( + + ))} + + {provided.placeholder} + + {type === "issue" ? ( + + ) : ( + + + Add issue + + } + className="mt-1" + optionsPosition="left" + noBorder + > + + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + )} +
+ )} +
+
+
+ )} +
+ ); +}; diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx new file mode 100644 index 000000000..4f7ae0591 --- /dev/null +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -0,0 +1,248 @@ +import React, { useCallback } from "react"; + +import Link from "next/link"; +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// react-beautiful-dnd +import { + Draggable, + DraggableStateSnapshot, + DraggingStyle, + NotDraggingStyle, +} from "react-beautiful-dnd"; +// constants +import { TrashIcon } from "@heroicons/react/24/outline"; +// services +import issuesService from "services/issues.service"; +import stateService from "services/state.service"; +// components +import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select"; +// types +import { + CycleIssueResponse, + IIssue, + IProjectMember, + IssueResponse, + ModuleIssueResponse, + NestedKeyOf, + Properties, + UserAuth, +} from "types"; +// fetch-keys +import { STATE_LIST, CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST } from "constants/fetch-keys"; + +type Props = { + index: number; + type?: string; + issue: IIssue; + properties: Properties; + members: IProjectMember[] | undefined; + handleDeleteIssue: (issue: IIssue) => void; + orderBy: NestedKeyOf | "manual" | null; + userAuth: UserAuth; +}; + +export const SingleBoardIssue: React.FC = ({ + index, + type, + issue, + properties, + members, + handleDeleteIssue, + orderBy, + userAuth, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const { data: states } = useSWR( + workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + + const partialUpdateIssue = useCallback( + (formData: Partial) => { + if (!workspaceSlug || !projectId) return; + + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + const updatedIssues = (prevData ?? []).map((p) => { + if (p.issue_detail.id === issue.id) { + return { + ...p, + issue_detail: { + ...p.issue_detail, + ...formData, + }, + }; + } + return p; + }); + return [...updatedIssues]; + }, + false + ); + + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + const updatedIssues = (prevData ?? []).map((p) => { + if (p.issue_detail.id === issue.id) { + return { + ...p, + issue_detail: { + ...p.issue_detail, + ...formData, + }, + }; + } + return p; + }); + return [...updatedIssues]; + }, + false + ); + + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + (prevData) => ({ + ...(prevData as IssueResponse), + results: (prevData?.results ?? []).map((p) => { + if (p.id === issue.id) return { ...p, ...formData }; + return p; + }), + }), + false + ); + + issuesService + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) + .then((res) => { + mutate( + cycleId + ? CYCLE_ISSUES(cycleId as string) + : CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "") + ); + mutate( + moduleId + ? MODULE_ISSUES(moduleId as string) + : MODULE_ISSUES(issue?.issue_module?.module ?? "") + ); + + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }) + .catch((error) => { + console.log(error); + }); + }, + [workspaceSlug, projectId, cycleId, moduleId, issue] + ); + + function getStyle( + style: DraggingStyle | NotDraggingStyle | undefined, + snapshot: DraggableStateSnapshot + ) { + if (orderBy === "manual") return style; + if (!snapshot.isDragging) return {}; + if (!snapshot.isDropAnimating) { + return style; + } + + return { + ...style, + transitionDuration: `0.001s`, + }; + } + + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + + return ( + + {(provided, snapshot) => ( +
+
+ {!isNotAllowed && ( +
+ +
+ )} + + + {properties.key && ( +
+ {issue.project_detail.identifier}-{issue.sequence_id} +
+ )} +
+ {issue.name} +
+
+ +
+ {properties.priority && ( + + )} + {properties.state && ( + + )} + {properties.due_date && ( + + )} + {properties.sub_issue_count && ( +
+ {issue.sub_issues_count}{" "} + {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} +
+ )} + {properties.assignee && ( + + )} +
+
+
+ )} +
+ ); +}; diff --git a/apps/app/components/common/bulk-delete-issues-modal.tsx b/apps/app/components/core/bulk-delete-issues-modal.tsx similarity index 92% rename from apps/app/components/common/bulk-delete-issues-modal.tsx rename to apps/app/components/core/bulk-delete-issues-modal.tsx index 64a65c22a..4a5ab575d 100644 --- a/apps/app/components/common/bulk-delete-issues-modal.tsx +++ b/apps/app/components/core/bulk-delete-issues-modal.tsx @@ -1,27 +1,26 @@ -// react import React, { useState } from "react"; -// next + import { useRouter } from "next/router"; -// swr + import useSWR, { mutate } from "swr"; + // react hook form import { SubmitHandler, useForm } from "react-hook-form"; -// services +// headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; -import { FolderIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +// services import issuesServices from "services/issues.service"; -import projectService from "services/project.service"; // hooks import useToast from "hooks/use-toast"; -// headless ui // ui import { Button } from "components/ui"; // icons +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { LayerDiagonalIcon } from "components/icons"; // types import { IIssue, IssueResponse } from "types"; // fetch keys -import { PROJECT_ISSUES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; type FormInput = { delete_issue_ids: string[]; @@ -32,7 +31,7 @@ type Props = { setIsOpen: React.Dispatch>; }; -const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { +export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -50,13 +49,6 @@ const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { : null ); - const { data: projectDetails } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.getProject(workspaceSlug as string, projectId as string) - : null - ); - const { setToastAlert } = useToast(); const { @@ -213,7 +205,7 @@ const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { }} /> - {projectDetails?.identifier}-{issue.sequence_id} + {issue.project_detail.identifier}-{issue.sequence_id} {issue.name} @@ -256,5 +248,3 @@ const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen }) => { ); }; - -export default BulkDeleteIssuesModal; diff --git a/apps/app/components/common/existing-issues-list-modal.tsx b/apps/app/components/core/existing-issues-list-modal.tsx similarity index 92% rename from apps/app/components/common/existing-issues-list-modal.tsx rename to apps/app/components/core/existing-issues-list-modal.tsx index 5179facd4..f07025808 100644 --- a/apps/app/components/common/existing-issues-list-modal.tsx +++ b/apps/app/components/core/existing-issues-list-modal.tsx @@ -1,24 +1,17 @@ import React, { useState } from "react"; -import { useRouter } from "next/router"; - -import useSWR from "swr"; // react-hook-form import { Controller, SubmitHandler, useForm } from "react-hook-form"; // hooks import { Combobox, Dialog, Transition } from "@headlessui/react"; import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; import useToast from "hooks/use-toast"; -// services -import projectService from "services/project.service"; // headless ui // ui import { Button } from "components/ui"; import { LayerDiagonalIcon } from "components/icons"; // types import { IIssue } from "types"; -// fetch-keys -import { PROJECT_DETAILS } from "constants/fetch-keys"; type FormInput = { issues: string[]; @@ -32,7 +25,7 @@ type Props = { handleOnSubmit: any; }; -const ExistingIssuesListModal: React.FC = ({ +export const ExistingIssuesListModal: React.FC = ({ isOpen, handleClose: onClose, issues, @@ -41,16 +34,6 @@ const ExistingIssuesListModal: React.FC = ({ }) => { const [query, setQuery] = useState(""); - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { data: projectDetails } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId - ? () => projectService.getProject(workspaceSlug as string, projectId as string) - : null - ); - const { setToastAlert } = useToast(); const handleClose = () => { @@ -175,7 +158,7 @@ const ExistingIssuesListModal: React.FC = ({ }} /> - {projectDetails?.identifier}-{issue.sequence_id} + {issue.project_detail.identifier}-{issue.sequence_id} {issue.name} @@ -233,5 +216,3 @@ const ExistingIssuesListModal: React.FC = ({ ); }; - -export default ExistingIssuesListModal; diff --git a/apps/app/components/common/image-upload-modal.tsx b/apps/app/components/core/image-upload-modal.tsx similarity index 100% rename from apps/app/components/common/image-upload-modal.tsx rename to apps/app/components/core/image-upload-modal.tsx diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 8266a5111..0865ea441 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1 +1,8 @@ +export * from "./board-view"; +export * from "./list-view"; +export * from "./bulk-delete-issues-modal"; +export * from "./existing-issues-list-modal"; +export * from "./image-upload-modal"; +export * from "./issues-view-filter"; +export * from "./issues-view"; export * from "./not-authorized-view"; diff --git a/apps/app/components/core/view.tsx b/apps/app/components/core/issues-view-filter.tsx similarity index 92% rename from apps/app/components/core/view.tsx rename to apps/app/components/core/issues-view-filter.tsx index 1fe147f22..3920d0e91 100644 --- a/apps/app/components/core/view.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -23,7 +23,7 @@ type Props = { issues?: IIssue[]; }; -const View: React.FC = ({ issues }) => { +export const IssuesFilterView: React.FC = ({ issues }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -104,14 +104,16 @@ const View: React.FC = ({ issues }) => { } width="lg" > - {groupByOptions.map((option) => ( - setGroupByProperty(option.key)} - > - {option.name} - - ))} + {groupByOptions.map((option) => + issueView === "kanban" && option.key === null ? null : ( + setGroupByProperty(option.key)} + > + {option.name} + + ) + )}
@@ -203,5 +205,3 @@ const View: React.FC = ({ issues }) => { ); }; - -export default View; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx new file mode 100644 index 000000000..3339bfdae --- /dev/null +++ b/apps/app/components/core/issues-view.tsx @@ -0,0 +1,465 @@ +import { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// react-beautiful-dnd +import { DropResult } from "react-beautiful-dnd"; +// services +import issuesService from "services/issues.service"; +import stateService from "services/state.service"; +import projectService from "services/project.service"; +import modulesService from "services/modules.service"; +// hooks +import useIssueView from "hooks/use-issue-view"; +// components +import { AllLists, AllBoards, ExistingIssuesListModal } from "components/core"; +import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +// types +import { + CycleIssueResponse, + IIssue, + IssueResponse, + IState, + ModuleIssueResponse, + UserAuth, +} from "types"; +// fetch-keys +import { + CYCLE_ISSUES, + MODULE_ISSUES, + PROJECT_ISSUES_LIST, + PROJECT_MEMBERS, + STATE_LIST, +} from "constants/fetch-keys"; + +type Props = { + type?: "issue" | "cycle" | "module"; + issues: IIssue[]; + openIssuesListModal?: () => void; + userAuth: UserAuth; +}; + +export const IssuesView: React.FC = ({ + type = "issue", + issues, + openIssuesListModal, + userAuth, +}) => { + // create issue modal + const [createIssueModal, setCreateIssueModal] = useState(false); + const [preloadedData, setPreloadedData] = useState< + (Partial & { actionType: "createIssue" | "edit" | "delete" }) | undefined + >(undefined); + + // updates issue modal + const [editIssueModal, setEditIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState< + (IIssue & { actionType: "edit" | "delete" }) | undefined + >(undefined); + + // delete issue modal + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [issueToDelete, setIssueToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const { issueView, groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues); + + const { data: states, mutate: mutateState } = useSWR( + workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, + workspaceSlug + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + + const { data: members } = useSWR( + projectId ? PROJECT_MEMBERS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) + : null + ); + + const handleOnDragEnd = useCallback( + (result: DropResult) => { + if (!result.destination || !workspaceSlug || !projectId) return; + + const { source, destination, type } = result; + + if (type === "state") { + const newStates = Array.from(states ?? []); + const [reorderedState] = newStates.splice(source.index, 1); + newStates.splice(destination.index, 0, reorderedState); + const prevSequenceNumber = newStates[destination.index - 1]?.sequence; + const nextSequenceNumber = newStates[destination.index + 1]?.sequence; + + const sequenceNumber = + prevSequenceNumber && nextSequenceNumber + ? (prevSequenceNumber + nextSequenceNumber) / 2 + : nextSequenceNumber + ? nextSequenceNumber - 15000 / 2 + : prevSequenceNumber + ? prevSequenceNumber + 15000 / 2 + : 15000; + + newStates[destination.index].sequence = sequenceNumber; + + mutateState(newStates, false); + + stateService + .patchState( + workspaceSlug as string, + projectId as string, + newStates[destination.index].id, + { + sequence: sequenceNumber, + } + ) + .then((response) => { + console.log(response); + }) + .catch((err) => { + console.error(err); + }); + } else { + const draggedItem = groupedByIssues[source.droppableId][source.index]; + if (source.droppableId !== destination.droppableId) { + const sourceGroup = source.droppableId; // source group id + const destinationGroup = destination.droppableId; // destination group id + + if (!sourceGroup || !destinationGroup) return; + + if (selectedGroup === "priority") { + // update the removed item for mutation + draggedItem.priority = destinationGroup; + + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + priority: destinationGroup, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + priority: destinationGroup, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + (prevData) => { + if (!prevData) return prevData; + + const updatedIssues = prevData.results.map((issue) => { + if (issue.id === draggedItem.id) + return { + ...draggedItem, + priority: destinationGroup, + }; + + return issue; + }); + + return { + ...prevData, + results: updatedIssues, + }; + }, + false + ); + + // patch request + issuesService + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + priority: destinationGroup, + }) + .then((res) => { + mutate( + cycleId + ? CYCLE_ISSUES(cycleId as string) + : CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "") + ); + mutate( + moduleId + ? MODULE_ISSUES(moduleId as string) + : MODULE_ISSUES(draggedItem.issue_module?.module ?? "") + ); + + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }); + } else if (selectedGroup === "state_detail.name") { + const destinationState = states?.find((s) => s.name === destinationGroup); + const destinationStateId = destinationState?.id; + + // update the removed item for mutation + if (!destinationStateId || !destinationState) return; + draggedItem.state = destinationStateId; + draggedItem.state_detail = destinationState; + + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + (prevData) => { + if (!prevData) return prevData; + + const updatedIssues = prevData.results.map((issue) => { + if (issue.id === draggedItem.id) + return { + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, + }; + + return issue; + }); + + return { + ...prevData, + results: updatedIssues, + }; + }, + false + ); + + // patch request + issuesService + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + state: destinationStateId, + }) + .then((res) => { + mutate( + cycleId + ? CYCLE_ISSUES(cycleId as string) + : CYCLE_ISSUES(draggedItem.issue_cycle?.cycle ?? "") + ); + mutate( + moduleId + ? MODULE_ISSUES(moduleId as string) + : MODULE_ISSUES(draggedItem.issue_module?.module ?? "") + ); + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }); + } + } + } + }, + [ + workspaceSlug, + cycleId, + moduleId, + mutateState, + groupedByIssues, + projectId, + selectedGroup, + states, + ] + ); + + const addIssueToState = (groupTitle: string, stateId: string | null) => { + setCreateIssueModal(true); + if (selectedGroup) + setPreloadedData({ + state: stateId ?? undefined, + [selectedGroup]: groupTitle, + actionType: "createIssue", + }); + else setPreloadedData({ actionType: "createIssue" }); + }; + + const handleEditIssue = (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, + }); + }; + + const handleDeleteIssue = (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }; + + const removeIssueFromCycle = (bridgeId: string) => { + if (!workspaceSlug || !projectId) return; + + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => prevData?.filter((p) => p.id !== bridgeId), + false + ); + + issuesService + .removeIssueFromCycle( + workspaceSlug as string, + projectId as string, + cycleId as string, + bridgeId + ) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + }; + + const removeIssueFromModule = (bridgeId: string) => { + if (!workspaceSlug || !projectId) return; + + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => prevData?.filter((p) => p.id !== bridgeId), + false + ); + + modulesService + .removeIssueFromModule( + workspaceSlug as string, + projectId as string, + moduleId as string, + bridgeId + ) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + }; + + return ( + <> + setCreateIssueModal(false)} + prePopulateData={{ + ...preloadedData, + }} + /> + setEditIssueModal(false)} + data={issueToEdit} + /> + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issueToDelete} + /> + {issueView === "list" ? ( + + ) : ( + + )} + + ); +}; diff --git a/apps/app/components/core/list-view/all-lists.tsx b/apps/app/components/core/list-view/all-lists.tsx new file mode 100644 index 000000000..c2b6c498a --- /dev/null +++ b/apps/app/components/core/list-view/all-lists.tsx @@ -0,0 +1,63 @@ +// hooks +import useIssueView from "hooks/use-issue-view"; +// components +import { SingleList } from "components/core/list-view/single-list"; +// types +import { IIssue, IProjectMember, IState, UserAuth } from "types"; + +// types +type Props = { + type: "issue" | "cycle" | "module"; + issues: IIssue[]; + states: IState[] | undefined; + members: IProjectMember[] | undefined; + addIssueToState: (groupTitle: string, stateId: string | null) => void; + handleEditIssue: (issue: IIssue) => void; + handleDeleteIssue: (issue: IIssue) => void; + openIssuesListModal?: (() => void) | null; + removeIssue: ((bridgeId: string) => void) | null; + userAuth: UserAuth; +}; + +export const AllLists: React.FC = ({ + type, + issues, + states, + members, + addIssueToState, + openIssuesListModal, + handleEditIssue, + handleDeleteIssue, + removeIssue, + userAuth, +}) => { + const { groupedByIssues, groupByProperty: selectedGroup } = useIssueView(issues); + + return ( +
+ {Object.keys(groupedByIssues).map((singleGroup) => { + const stateId = + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup)?.id ?? null + : null; + + return ( + addIssueToState(singleGroup, stateId)} + handleEditIssue={handleEditIssue} + handleDeleteIssue={handleDeleteIssue} + openIssuesListModal={type !== "issue" ? openIssuesListModal : null} + removeIssue={removeIssue} + userAuth={userAuth} + /> + ); + })} +
+ ); +}; diff --git a/apps/app/components/core/list-view/index.ts b/apps/app/components/core/list-view/index.ts new file mode 100644 index 000000000..c515ed1c2 --- /dev/null +++ b/apps/app/components/core/list-view/index.ts @@ -0,0 +1,3 @@ +export * from "./all-lists"; +export * from "./single-issue"; +export * from "./single-list"; diff --git a/apps/app/components/core/list-view/single-issue.tsx b/apps/app/components/core/list-view/single-issue.tsx new file mode 100644 index 000000000..9ed4fa156 --- /dev/null +++ b/apps/app/components/core/list-view/single-issue.tsx @@ -0,0 +1,214 @@ +import React, { useCallback } from "react"; + +import Link from "next/link"; +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// services +import issuesService from "services/issues.service"; +import stateService from "services/state.service"; +// components +import { AssigneeSelect, DueDateSelect, PrioritySelect, StateSelect } from "components/core/select"; +// ui +import { CustomMenu } from "components/ui"; +// types +import { + CycleIssueResponse, + IIssue, + IProjectMember, + IssueResponse, + ModuleIssueResponse, + Properties, + UserAuth, +} from "types"; +// fetch-keys +import { CYCLE_ISSUES, MODULE_ISSUES, PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys"; + +type Props = { + type?: string; + issue: IIssue; + properties: Properties; + members: IProjectMember[] | undefined; + editIssue: () => void; + removeIssue?: (() => void) | null; + handleDeleteIssue: (issue: IIssue) => void; + userAuth: UserAuth; +}; + +export const SingleListIssue: React.FC = ({ + type, + issue, + properties, + members, + editIssue, + removeIssue, + handleDeleteIssue, + userAuth, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + const { data: states } = useSWR( + workspaceSlug && projectId ? STATE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => stateService.getStates(workspaceSlug as string, projectId as string) + : null + ); + + const partialUpdateIssue = useCallback( + (formData: Partial) => { + if (!workspaceSlug || !projectId) return; + + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + const updatedIssues = (prevData ?? []).map((p) => { + if (p.issue_detail.id === issue.id) { + return { + ...p, + issue_detail: { + ...p.issue_detail, + ...formData, + }, + }; + } + return p; + }); + return [...updatedIssues]; + }, + false + ); + + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + const updatedIssues = (prevData ?? []).map((p) => { + if (p.issue_detail.id === issue.id) { + return { + ...p, + issue_detail: { + ...p.issue_detail, + ...formData, + }, + }; + } + return p; + }); + return [...updatedIssues]; + }, + false + ); + + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), + (prevData) => ({ + ...(prevData as IssueResponse), + results: (prevData?.results ?? []).map((p) => { + if (p.id === issue.id) return { ...p, ...formData }; + return p; + }), + }), + false + ); + + issuesService + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) + .then((res) => { + mutate( + cycleId + ? CYCLE_ISSUES(cycleId as string) + : CYCLE_ISSUES(issue?.issue_cycle?.cycle ?? "") + ); + mutate( + moduleId + ? MODULE_ISSUES(moduleId as string) + : MODULE_ISSUES(issue?.issue_module?.module ?? "") + ); + + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); + }) + .catch((error) => { + console.log(error); + }); + }, + [workspaceSlug, projectId, cycleId, moduleId, issue] + ); + + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + + return ( +
+ +
+ {properties.priority && ( + + )} + {properties.state && ( + + )} + {properties.due_date && ( + + )} + {properties.sub_issue_count && ( +
+ {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} +
+ )} + {properties.assignee && ( + + )} + {type && !isNotAllowed && ( + + Edit + {type !== "issue" && removeIssue && ( + + <>Remove from {type} + + )} + handleDeleteIssue(issue)}> + Delete permanently + + + )} +
+
+ ); +}; diff --git a/apps/app/components/core/list-view/single-list.tsx b/apps/app/components/core/list-view/single-list.tsx new file mode 100644 index 000000000..b3d478593 --- /dev/null +++ b/apps/app/components/core/list-view/single-list.tsx @@ -0,0 +1,156 @@ +import { useRouter } from "next/router"; + +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// hooks +import useIssuesProperties from "hooks/use-issue-properties"; +// components +import { SingleListIssue } from "components/core"; +// icons +import { ChevronDownIcon, PlusIcon } from "@heroicons/react/24/outline"; +// helpers +import { addSpaceIfCamelCase } from "helpers/string.helper"; +// types +import { IIssue, IProjectMember, NestedKeyOf, UserAuth } from "types"; +import { CustomMenu } from "components/ui"; + +type Props = { + type?: "issue" | "cycle" | "module"; + groupTitle: string; + groupedByIssues: { + [key: string]: IIssue[]; + }; + selectedGroup: NestedKeyOf | null; + members: IProjectMember[] | undefined; + addIssueToState: () => void; + handleEditIssue: (issue: IIssue) => void; + handleDeleteIssue: (issue: IIssue) => void; + openIssuesListModal?: (() => void) | null; + removeIssue: ((bridgeId: string) => void) | null; + userAuth: UserAuth; +}; + +export const SingleList: React.FC = ({ + type, + groupTitle, + groupedByIssues, + selectedGroup, + members, + addIssueToState, + handleEditIssue, + handleDeleteIssue, + openIssuesListModal, + removeIssue, + userAuth, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); + + const createdBy = + selectedGroup === "created_by" + ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." + : null; + + return ( + + {({ open }) => ( +
+
+ +
+ + + + {selectedGroup !== null ? ( +

+ {groupTitle === null || groupTitle === "null" + ? "None" + : createdBy + ? createdBy + : addSpaceIfCamelCase(groupTitle)} +

+ ) : ( +

All Issues

+ )} +

+ {groupedByIssues[groupTitle as keyof IIssue].length} +

+
+
+
+ + +
+ {groupedByIssues[groupTitle] ? ( + groupedByIssues[groupTitle].length > 0 ? ( + groupedByIssues[groupTitle].map((issue: IIssue) => ( + handleEditIssue(issue)} + handleDeleteIssue={handleDeleteIssue} + removeIssue={() => { + removeIssue && removeIssue(issue.bridge); + }} + userAuth={userAuth} + /> + )) + ) : ( +

No issues.

+ ) + ) : ( +
Loading...
+ )} +
+
+
+
+ {type === "issue" ? ( + + ) : ( + + + Add issue + + } + optionsPosition="left" + noBorder + > + Create new + {openIssuesListModal && ( + + Add an existing issue + + )} + + )} +
+
+ )} +
+ ); +}; diff --git a/apps/app/components/core/select/assignee.tsx b/apps/app/components/core/select/assignee.tsx new file mode 100644 index 000000000..ce5fe3774 --- /dev/null +++ b/apps/app/components/core/select/assignee.tsx @@ -0,0 +1,94 @@ +import React from "react"; + +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// ui +import { AssigneesList, Avatar } from "components/ui"; +// types +import { IIssue, IProjectMember } from "types"; + +type Props = { + issue: IIssue; + members: IProjectMember[] | undefined; + partialUpdateIssue: (formData: Partial) => void; + isNotAllowed: boolean; +}; + +export const AssigneeSelect: React.FC = ({ + issue, + members, + partialUpdateIssue, + isNotAllowed, +}) => ( + { + const newData = issue.assignees ?? []; + + if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); + else newData.push(data); + + partialUpdateIssue({ assignees_list: newData }); + }} + className="group relative flex-shrink-0" + disabled={isNotAllowed} + > + {({ open }) => ( + <> +
+ +
+ +
+
+ + + + {members?.map((member) => ( + + `flex items-center gap-x-1 cursor-pointer select-none p-2 ${ + active ? "bg-indigo-50" : "" + } ${ + selected || issue.assignees?.includes(member.member.id) + ? "bg-indigo-50 font-medium" + : "font-normal" + }` + } + value={member.member.id} + > + +

+ {member.member.first_name && member.member.first_name !== "" + ? member.member.first_name + : member.member.email} +

+
+ ))} +
+
+
+
+
Assigned to
+
+ {issue.assignee_details?.length > 0 + ? issue.assignee_details.map((assignee) => assignee.first_name).join(", ") + : "No one"} +
+
+ + )} +
+); diff --git a/apps/app/components/core/select/due-date.tsx b/apps/app/components/core/select/due-date.tsx new file mode 100644 index 000000000..dc71a8362 --- /dev/null +++ b/apps/app/components/core/select/due-date.tsx @@ -0,0 +1,48 @@ +// ui +import { CustomDatePicker } from "components/ui"; +// helpers +import { findHowManyDaysLeft, renderShortNumericDateFormat } from "helpers/date-time.helper"; +// types +import { IIssue } from "types"; + +type Props = { + issue: IIssue; + partialUpdateIssue: (formData: Partial) => void; + isNotAllowed: boolean; +}; + +export const DueDateSelect: React.FC = ({ issue, partialUpdateIssue, isNotAllowed }) => ( +
+ + partialUpdateIssue({ + target_date: val, + }) + } + className={issue?.target_date ? "w-[6.5rem]" : "w-[3rem] text-center"} + /> +
+
Due date
+
{renderShortNumericDateFormat(issue.target_date ?? "")}
+
+ {issue.target_date + ? issue.target_date < new Date().toISOString() + ? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days` + : "Due date" + : "N/A"} +
+
+
+); diff --git a/apps/app/components/core/select/index.ts b/apps/app/components/core/select/index.ts new file mode 100644 index 000000000..c5f427971 --- /dev/null +++ b/apps/app/components/core/select/index.ts @@ -0,0 +1,4 @@ +export * from "./assignee"; +export * from "./due-date"; +export * from "./priority"; +export * from "./state"; diff --git a/apps/app/components/core/select/priority.tsx b/apps/app/components/core/select/priority.tsx new file mode 100644 index 000000000..8c3110cbe --- /dev/null +++ b/apps/app/components/core/select/priority.tsx @@ -0,0 +1,97 @@ +import React from "react"; + +// ui +import { Listbox, Transition } from "@headlessui/react"; +// types +import { IIssue, IState } from "types"; +// constants +import { getPriorityIcon } from "constants/global"; +import { PRIORITIES } from "constants/"; + +type Props = { + issue: IIssue; + partialUpdateIssue: (formData: Partial) => void; + isNotAllowed: boolean; +}; + +export const PrioritySelect: React.FC = ({ issue, partialUpdateIssue, isNotAllowed }) => ( + { + partialUpdateIssue({ priority: data }); + }} + className="group relative flex-shrink-0" + disabled={isNotAllowed} + > + {({ open }) => ( + <> +
+ + {getPriorityIcon( + issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None", + "text-sm" + )} + + + + + {PRIORITIES?.map((priority) => ( + + `flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${ + active ? "bg-indigo-50" : "bg-white" + }` + } + value={priority} + > + {getPriorityIcon(priority, "text-sm")} + {priority ?? "None"} + + ))} + + +
+
+
Priority
+
+ {issue.priority ?? "None"} +
+
+ + )} +
+); diff --git a/apps/app/components/core/select/state.tsx b/apps/app/components/core/select/state.tsx new file mode 100644 index 000000000..d65d7e7d3 --- /dev/null +++ b/apps/app/components/core/select/state.tsx @@ -0,0 +1,55 @@ +// ui +import { CustomSelect } from "components/ui"; +// helpers +import { addSpaceIfCamelCase } from "helpers/string.helper"; +// types +import { IIssue, IState } from "types"; + +type Props = { + issue: IIssue; + states: IState[] | undefined; + partialUpdateIssue: (formData: Partial) => void; + isNotAllowed: boolean; +}; + +export const StateSelect: React.FC = ({ + issue, + states, + partialUpdateIssue, + isNotAllowed, +}) => ( + + s.id === issue.state)?.color, + }} + /> + {addSpaceIfCamelCase(states?.find((s) => s.id === issue.state)?.name ?? "")} + + } + value={issue.state} + onChange={(data: string) => { + partialUpdateIssue({ state: data }); + }} + maxHeight="md" + noChevron + disabled={isNotAllowed} + > + {states?.map((state) => ( + + <> + + {addSpaceIfCamelCase(state.name)} + + + ))} + +); diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index 9a53a2949..76e1c5ad1 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -61,9 +61,8 @@ export const CycleModal: React.FC = (props) => { if (workspaceSlug && projectId) { const payload = { ...formValues, - start_date: formValues.start_date ? renderDateFormat(formValues.start_date) : null, - end_date: formValues.end_date ? renderDateFormat(formValues.end_date) : null, }; + if (initialData) { updateCycle(initialData.id, payload); } else { diff --git a/apps/app/components/dnd/StrictModeDroppable.tsx b/apps/app/components/dnd/StrictModeDroppable.tsx index e63cab246..9ed01d3bf 100644 --- a/apps/app/components/dnd/StrictModeDroppable.tsx +++ b/apps/app/components/dnd/StrictModeDroppable.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; + // react beautiful dnd import { Droppable, DroppableProps } from "react-beautiful-dnd"; @@ -14,9 +15,7 @@ const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { }; }, []); - if (!enabled) { - return null; - } + if (!enabled) return null; return {children}; }; diff --git a/apps/app/components/project/issues/issue-detail/activity/index.tsx b/apps/app/components/issues/activity.tsx similarity index 97% rename from apps/app/components/project/issues/issue-detail/activity/index.tsx rename to apps/app/components/issues/activity.tsx index 3594bf7a8..351aff485 100644 --- a/apps/app/components/project/issues/issue-detail/activity/index.tsx +++ b/apps/app/components/issues/activity.tsx @@ -14,7 +14,7 @@ import { // services import issuesServices from "services/issues.service"; // components -import CommentCard from "components/project/issues/issue-detail/comment/issue-comment-card"; +import { CommentCard } from "components/issues/comment"; // ui import { Loader } from "components/ui"; // icons @@ -76,7 +76,7 @@ const activityDetails: { }, }; -const IssueActivitySection: React.FC<{ +export const IssueActivitySection: React.FC<{ issueActivities: IIssueActivity[]; mutate: KeyedMutator; }> = ({ issueActivities, mutate }) => { @@ -216,7 +216,7 @@ const IssueActivitySection: React.FC<{
); - } else if ("comment_json" in activity) { + } else if ("comment_json" in activity) return ( ); - } })} ) : ( @@ -247,5 +246,3 @@ const IssueActivitySection: React.FC<{ ); }; - -export default IssueActivitySection; diff --git a/apps/app/components/project/issues/issue-detail/comment/issue-comment-section.tsx b/apps/app/components/issues/comment/add-comment.tsx similarity index 97% rename from apps/app/components/project/issues/issue-detail/comment/issue-comment-section.tsx rename to apps/app/components/issues/comment/add-comment.tsx index 5ac7e061a..a904a6c4a 100644 --- a/apps/app/components/project/issues/issue-detail/comment/issue-comment-section.tsx +++ b/apps/app/components/issues/comment/add-comment.tsx @@ -28,7 +28,8 @@ const defaultValues: Partial = { comment_html: "", comment_json: "", }; -const AddIssueComment: React.FC<{ + +export const AddComment: React.FC<{ mutate: KeyedMutator; }> = ({ mutate }) => { const { @@ -111,5 +112,3 @@ const AddIssueComment: React.FC<{ ); }; - -export default AddIssueComment; diff --git a/apps/app/components/project/issues/issue-detail/comment/issue-comment-card.tsx b/apps/app/components/issues/comment/comment-card.tsx similarity index 97% rename from apps/app/components/project/issues/issue-detail/comment/issue-comment-card.tsx rename to apps/app/components/issues/comment/comment-card.tsx index ec270ff25..79582df3f 100644 --- a/apps/app/components/project/issues/issue-detail/comment/issue-comment-card.tsx +++ b/apps/app/components/issues/comment/comment-card.tsx @@ -24,7 +24,7 @@ type Props = { handleCommentDeletion: (comment: string) => void; }; -const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion }) => { +export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion }) => { const { user } = useUser(); const [isEditing, setIsEditing] = useState(false); @@ -130,5 +130,3 @@ const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion ); }; - -export default CommentCard; diff --git a/apps/app/components/issues/comment/index.ts b/apps/app/components/issues/comment/index.ts new file mode 100644 index 000000000..cf13ca91e --- /dev/null +++ b/apps/app/components/issues/comment/index.ts @@ -0,0 +1,2 @@ +export * from "./add-comment"; +export * from "./comment-card"; diff --git a/apps/app/components/project/issues/confirm-issue-deletion.tsx b/apps/app/components/issues/delete-issue-modal.tsx similarity index 92% rename from apps/app/components/project/issues/confirm-issue-deletion.tsx rename to apps/app/components/issues/delete-issue-modal.tsx index 12b8c63e9..bbc6552ba 100644 --- a/apps/app/components/project/issues/confirm-issue-deletion.tsx +++ b/apps/app/components/issues/delete-issue-modal.tsx @@ -17,20 +17,20 @@ import { Button } from "components/ui"; // types import type { CycleIssueResponse, IIssue, IssueResponse, ModuleIssueResponse } from "types"; // fetch-keys -import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES } from "constants/fetch-keys"; +import { CYCLE_ISSUES, PROJECT_ISSUES_LIST, MODULE_ISSUES, USER_ISSUE } from "constants/fetch-keys"; type Props = { isOpen: boolean; handleClose: () => void; - data?: IIssue; + data: IIssue | null; }; -const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => { +export const DeleteIssueModal: React.FC = ({ isOpen, handleClose, data }) => { const cancelButtonRef = useRef(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId: queryProjectId } = router.query; const { setToastAlert } = useToast(); @@ -43,15 +43,40 @@ const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => handleClose(); }; - console.log(data); - const handleDeletion = async () => { setIsDeleteLoading(true); if (!data || !workspaceSlug) return; + const projectId = data.project; await issueServices .deleteIssue(workspaceSlug as string, projectId, data.id) .then(() => { + const cycleId = data?.cycle; + const moduleId = data?.module; + + if (cycleId) { + mutate( + CYCLE_ISSUES(cycleId), + (prevData) => prevData?.filter((i) => i.issue !== data.id), + false + ); + } + + if (moduleId) { + mutate( + MODULE_ISSUES(moduleId), + (prevData) => prevData?.filter((i) => i.issue !== data.id), + false + ); + } + + if (!queryProjectId) + mutate( + USER_ISSUE(workspaceSlug as string), + (prevData) => prevData?.filter((i) => i.id !== data.id), + false + ); + mutate( PROJECT_ISSUES_LIST(workspaceSlug as string, projectId), (prevData) => ({ @@ -62,24 +87,6 @@ const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => false ); - const moduleId = data?.module; - const cycleId = data?.cycle; - - if (moduleId) { - mutate( - MODULE_ISSUES(moduleId), - (prevData) => prevData?.filter((i) => i.issue !== data.id), - false - ); - } - if (cycleId) { - mutate( - CYCLE_ISSUES(cycleId), - (prevData) => prevData?.filter((i) => i.issue !== data.id), - false - ); - } - handleClose(); setToastAlert({ title: "Success", @@ -173,5 +180,3 @@ const ConfirmIssueDeletion: React.FC = ({ isOpen, handleClose, data }) => ); }; - -export default ConfirmIssueDeletion; diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index a2233fc52..5e034822a 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -16,7 +16,7 @@ import { IssueStateSelect, } from "components/issues/select"; import { CycleSelect as IssueCycleSelect } from "components/cycles/select"; -import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal"; +import { CreateUpdateStateModal } from "components/states"; import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // ui import { Button, CustomDatePicker, CustomMenu, Input, Loader } from "components/ui"; diff --git a/apps/app/components/issues/index.ts b/apps/app/components/issues/index.ts index 5608866d4..ab62034b5 100644 --- a/apps/app/components/issues/index.ts +++ b/apps/app/components/issues/index.ts @@ -1,5 +1,12 @@ -export * from "./list-item"; +export * from "./comment"; +export * from "./sidebar-select"; +export * from "./activity"; +export * from "./delete-issue-modal"; export * from "./description-form"; -export * from "./sub-issue-list"; export * from "./form"; export * from "./modal"; +export * from "./my-issues-list-item"; +export * from "./parent-issues-list-modal"; +export * from "./sidebar"; +export * from "./sub-issues-list"; +export * from "./sub-issues-list-modal"; diff --git a/apps/app/components/issues/list-item.tsx b/apps/app/components/issues/list-item.tsx deleted file mode 100644 index 603d8d299..000000000 --- a/apps/app/components/issues/list-item.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -// components -import { AssigneesList } from "components/ui/avatar"; -// icons -import { CalendarDaysIcon } from "@heroicons/react/24/outline"; -// helpers -import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper"; -import { addSpaceIfCamelCase } from "helpers/string.helper"; -// types -import { IIssue, Properties } from "types"; -// constants -import { getPriorityIcon } from "constants/global"; - -type Props = { - type?: string; - issue: IIssue; - properties: Properties; - editIssue?: () => void; - handleDeleteIssue?: () => void; - removeIssue?: () => void; -}; - -export const IssueListItem: React.FC = (props) => { - // const { type, issue, properties, editIssue, handleDeleteIssue, removeIssue } = props; - const { issue, properties } = props; - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - - return ( -
- -
- {properties.priority && ( -
- {getPriorityIcon(issue.priority)} -
-
Priority
-
- {issue.priority ?? "None"} -
-
-
- )} - {properties.state && ( -
- - {addSpaceIfCamelCase(issue?.state_detail.name)} -
-
State
-
{issue?.state_detail.name}
-
-
- )} - {properties.due_date && ( -
- - {issue.target_date ? renderShortNumericDateFormat(issue.target_date) : "N/A"} -
-
Due date
-
{renderShortNumericDateFormat(issue.target_date ?? "")}
-
- {issue.target_date && - (issue.target_date < new Date().toISOString() - ? `Due date has passed by ${findHowManyDaysLeft(issue.target_date)} days` - : findHowManyDaysLeft(issue.target_date) <= 3 - ? `Due date is in ${findHowManyDaysLeft(issue.target_date)} days` - : "Due date")} -
-
-
- )} - {properties.sub_issue_count && ( -
- {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- )} - {properties.assignee && ( -
- -
- )} -
-
- ); -}; diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index 3f7555435..c7ff0c2d7 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -16,11 +16,7 @@ import issuesService from "services/issues.service"; import useUser from "hooks/use-user"; import useToast from "hooks/use-toast"; // components -import CreateUpdateStateModal from "components/project/issues/BoardView/state/create-update-state-modal"; -import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; import { IssueForm } from "components/issues"; -// common -import { renderDateFormat } from "helpers/date-time.helper"; // types import type { IIssue, IssueResponse } from "types"; // fetch keys @@ -54,7 +50,10 @@ export const CreateUpdateIssueModal: React.FC = ({ const [activeProject, setActiveProject] = useState(null); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + + if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; + if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; const { user } = useUser(); const { setToastAlert } = useToast(); @@ -176,7 +175,7 @@ export const CreateUpdateIssueModal: React.FC = ({ .then((res) => { if (isUpdatingSingleIssue) { mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); - } else + } else { mutate( PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? ""), (prevData) => ({ @@ -187,8 +186,10 @@ export const CreateUpdateIssueModal: React.FC = ({ }), }) ); + } if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle); + if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module); if (!createMore) handleClose(); @@ -206,15 +207,16 @@ export const CreateUpdateIssueModal: React.FC = ({ }; const handleFormSubmit = async (formData: Partial) => { - if (workspaceSlug && activeProject) { - const payload: Partial = { - ...formData, - target_date: formData.target_date ? renderDateFormat(formData.target_date ?? "") : null, - }; + if (!workspaceSlug || !activeProject) return; - if (!data) await createIssue(payload); - else await updateIssue(payload); - } + const payload: Partial = { + ...formData, + description: formData.description ? formData.description : "", + description_html: formData.description_html ? formData.description_html : "

", + }; + + if (!data) await createIssue(payload); + else await updateIssue(payload); }; return ( diff --git a/apps/app/components/issues/my-issues-list-item.tsx b/apps/app/components/issues/my-issues-list-item.tsx new file mode 100644 index 000000000..8e7391283 --- /dev/null +++ b/apps/app/components/issues/my-issues-list-item.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from "react"; + +import Link from "next/link"; +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// services +import stateService from "services/state.service"; +import issuesService from "services/issues.service"; +// components +import { DueDateSelect, PrioritySelect, StateSelect } from "components/core/select"; +// ui +import { AssigneesList } from "components/ui/avatar"; +import { CustomMenu } from "components/ui"; +// types +import { IIssue, Properties } from "types"; +// fetch-keys +import { STATE_LIST, USER_ISSUE } from "constants/fetch-keys"; + +type Props = { + issue: IIssue; + properties: Properties; + projectId: string; + handleDeleteIssue: () => void; +}; + +export const MyIssuesListItem: React.FC = ({ + issue, + properties, + projectId, + handleDeleteIssue, +}) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { data: states } = useSWR( + workspaceSlug && projectId ? STATE_LIST(projectId) : null, + workspaceSlug && projectId + ? () => stateService.getStates(workspaceSlug as string, projectId) + : null + ); + + const partialUpdateIssue = useCallback( + (formData: Partial) => { + if (!workspaceSlug) return; + + mutate( + USER_ISSUE(workspaceSlug as string), + (prevData) => + prevData?.map((p) => { + if (p.id === issue.id) return { ...p, ...formData }; + + return p; + }), + false + ); + + issuesService + .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData) + .then((res) => { + mutate(USER_ISSUE(workspaceSlug as string)); + }) + .catch((error) => { + console.log(error); + }); + }, + [workspaceSlug, projectId, issue] + ); + + const isNotAllowed = false; + + return ( +
+ +
+ {properties.priority && ( + + )} + {properties.state && ( + + )} + {properties.due_date && ( + + )} + {properties.sub_issue_count && ( +
+ {issue?.sub_issues_count} {issue?.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} +
+ )} + {properties.assignee && ( +
+ +
+ )} + + Delete permanently + +
+
+ ); +}; diff --git a/apps/app/components/project/issues/issues-list-modal.tsx b/apps/app/components/issues/parent-issues-list-modal.tsx similarity index 99% rename from apps/app/components/project/issues/issues-list-modal.tsx rename to apps/app/components/issues/parent-issues-list-modal.tsx index 9c56d6406..b805e7b14 100644 --- a/apps/app/components/project/issues/issues-list-modal.tsx +++ b/apps/app/components/issues/parent-issues-list-modal.tsx @@ -21,7 +21,7 @@ type Props = { customDisplay?: JSX.Element; }; -const IssuesListModal: React.FC = ({ +export const ParentIssuesListModal: React.FC = ({ isOpen, handleClose: onClose, value, @@ -227,5 +227,3 @@ const IssuesListModal: React.FC = ({ ); }; - -export default IssuesListModal; diff --git a/apps/app/components/issues/select/index.ts b/apps/app/components/issues/select/index.ts index de43d9b0e..4338b3162 100644 --- a/apps/app/components/issues/select/index.ts +++ b/apps/app/components/issues/select/index.ts @@ -1,6 +1,6 @@ export * from "./assignee"; export * from "./label"; -export * from "./parent-issue"; +export * from "./parent"; export * from "./priority"; export * from "./project"; export * from "./state"; diff --git a/apps/app/components/issues/select/parent-issue.tsx b/apps/app/components/issues/select/parent.tsx similarity index 86% rename from apps/app/components/issues/select/parent-issue.tsx rename to apps/app/components/issues/select/parent.tsx index d08020d20..c04e89b92 100644 --- a/apps/app/components/issues/select/parent-issue.tsx +++ b/apps/app/components/issues/select/parent.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Controller, Control } from "react-hook-form"; // components -import IssuesListModal from "components/project/issues/issues-list-modal"; +import { ParentIssuesListModal } from "components/issues"; // types import type { IIssue } from "types"; @@ -17,7 +17,7 @@ export const IssueParentSelect: React.FC = ({ control, isOpen, setIsOpen, control={control} name="parent" render={({ field: { onChange } }) => ( - setIsOpen(false)} onChange={onChange} diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx b/apps/app/components/issues/sidebar-select/assignee.tsx similarity index 98% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx rename to apps/app/components/issues/sidebar-select/assignee.tsx index ceefa5e7a..369d03368 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx +++ b/apps/app/components/issues/sidebar-select/assignee.tsx @@ -27,7 +27,7 @@ type Props = { userAuth: UserAuth; }; -const SelectAssignee: React.FC = ({ control, submitChanges, userAuth }) => { +export const SidebarAssigneeSelect: React.FC = ({ control, submitChanges, userAuth }) => { const router = useRouter(); const { workspaceSlug } = router.query; @@ -143,5 +143,3 @@ const SelectAssignee: React.FC = ({ control, submitChanges, userAuth }) = ); }; - -export default SelectAssignee; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx b/apps/app/components/issues/sidebar-select/blocked.tsx similarity index 97% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx rename to apps/app/components/issues/sidebar-select/blocked.tsx index 0e8ec0881..70de8d7e9 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx +++ b/apps/app/components/issues/sidebar-select/blocked.tsx @@ -16,7 +16,7 @@ import issuesService from "services/issues.service"; // ui import { Button } from "components/ui"; // icons -import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { BlockedIcon, LayerDiagonalIcon } from "components/icons"; // types import { IIssue, UserAuth } from "types"; @@ -34,7 +34,12 @@ type Props = { userAuth: UserAuth; }; -const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch, userAuth }) => { +export const SidebarBlockedSelect: React.FC = ({ + submitChanges, + issuesList, + watch, + userAuth, +}) => { const [query, setQuery] = useState(""); const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); @@ -172,7 +177,6 @@ const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch, user
{ - console.log("Triggered"); const selectedIssues = watchBlocked("blocked_issue_ids"); if (selectedIssues.includes(val)) setValue( @@ -301,5 +305,3 @@ const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch, user ); }; - -export default SelectBlocked; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx b/apps/app/components/issues/sidebar-select/blocker.tsx similarity index 98% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx rename to apps/app/components/issues/sidebar-select/blocker.tsx index 433f5c9fe..789c11d1c 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx +++ b/apps/app/components/issues/sidebar-select/blocker.tsx @@ -34,7 +34,12 @@ type Props = { userAuth: UserAuth; }; -const SelectBlocker: React.FC = ({ submitChanges, issuesList, watch, userAuth }) => { +export const SidebarBlockerSelect: React.FC = ({ + submitChanges, + issuesList, + watch, + userAuth, +}) => { const [query, setQuery] = useState(""); const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); @@ -299,5 +304,3 @@ const SelectBlocker: React.FC = ({ submitChanges, issuesList, watch, user ); }; - -export default SelectBlocker; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx similarity index 95% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx rename to apps/app/components/issues/sidebar-select/cycle.tsx index 159f96c68..353bc5121 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -22,7 +22,11 @@ type Props = { userAuth: UserAuth; }; -const SelectCycle: React.FC = ({ issueDetail, handleCycleChange, userAuth }) => { +export const SidebarCycleSelect: React.FC = ({ + issueDetail, + handleCycleChange, + userAuth, +}) => { const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; @@ -71,7 +75,7 @@ const SelectCycle: React.FC = ({ issueDetail, handleCycleChange, userAuth onChange={(value: any) => { value === null ? removeIssueFromCycle(issueCycle?.id ?? "", issueCycle?.cycle ?? "") - : handleCycleChange(cycles?.find((c) => c.id === value) as any); + : handleCycleChange(cycles?.find((c) => c.id === value) as ICycle); }} disabled={isNotAllowed} > @@ -98,5 +102,3 @@ const SelectCycle: React.FC = ({ issueDetail, handleCycleChange, userAuth ); }; - -export default SelectCycle; diff --git a/apps/app/components/issues/sidebar-select/index.ts b/apps/app/components/issues/sidebar-select/index.ts new file mode 100644 index 000000000..9070d2d2e --- /dev/null +++ b/apps/app/components/issues/sidebar-select/index.ts @@ -0,0 +1,8 @@ +export * from "./assignee"; +export * from "./blocked"; +export * from "./blocker"; +export * from "./cycle"; +export * from "./module"; +export * from "./parent"; +export * from "./priority"; +export * from "./state"; diff --git a/apps/app/components/issues/sidebar-select/module.tsx b/apps/app/components/issues/sidebar-select/module.tsx new file mode 100644 index 000000000..e57688887 --- /dev/null +++ b/apps/app/components/issues/sidebar-select/module.tsx @@ -0,0 +1,103 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +import useSWR, { mutate } from "swr"; + +// services +import modulesService from "services/modules.service"; +// ui +import { Spinner, CustomSelect } from "components/ui"; +// icons +import { RectangleGroupIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IModule, UserAuth } from "types"; +// fetch-keys +import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; + +type Props = { + issueDetail: IIssue | undefined; + handleModuleChange: (module: IModule) => void; + userAuth: UserAuth; +}; + +export const SidebarModuleSelect: React.FC = ({ + issueDetail, + handleModuleChange, + userAuth, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId, issueId } = router.query; + + const { data: modules } = useSWR( + workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => modulesService.getModules(workspaceSlug as string, projectId as string) + : null + ); + + const removeIssueFromModule = (bridgeId: string, moduleId: string) => { + if (!workspaceSlug || !projectId) return; + + modulesService + .removeIssueFromModule(workspaceSlug as string, projectId as string, moduleId, bridgeId) + .then((res) => { + mutate(ISSUE_DETAILS(issueId as string)); + + mutate(MODULE_ISSUES(moduleId)); + }) + .catch((e) => { + console.log(e); + }); + }; + + const issueModule = issueDetail?.issue_module; + + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + + return ( +
+
+ +

Module

+
+
+ + {modules?.find((m) => m.id === issueModule?.module)?.name ?? "None"} + + } + value={issueModule?.module_detail?.id} + onChange={(value: any) => { + value === null + ? removeIssueFromModule(issueModule?.id ?? "", issueModule?.module ?? "") + : handleModuleChange(modules?.find((m) => m.id === value) as IModule); + }} + disabled={isNotAllowed} + > + {modules ? ( + modules.length > 0 ? ( + <> + + None + + {modules.map((option) => ( + + {option.name} + + ))} + + ) : ( +
No modules found
+ ) + ) : ( + + )} +
+
+
+ ); +}; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx b/apps/app/components/issues/sidebar-select/parent.tsx similarity index 94% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx rename to apps/app/components/issues/sidebar-select/parent.tsx index 7cb298324..1af86c359 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx +++ b/apps/app/components/issues/sidebar-select/parent.tsx @@ -10,7 +10,7 @@ import { UserIcon } from "@heroicons/react/24/outline"; // services import issuesServices from "services/issues.service"; // components -import IssuesListModal from "components/project/issues/issues-list-modal"; +import { ParentIssuesListModal } from "components/issues"; // icons // types import { IIssue, UserAuth } from "types"; @@ -26,7 +26,7 @@ type Props = { userAuth: UserAuth; }; -const SelectParent: React.FC = ({ +export const SidebarParentSelect: React.FC = ({ control, submitChanges, issuesList, @@ -61,7 +61,7 @@ const SelectParent: React.FC = ({ control={control} name="parent" render={({ field: { value, onChange } }) => ( - setIsParentModalOpen(false)} onChange={(val) => { @@ -93,5 +93,3 @@ const SelectParent: React.FC = ({ ); }; - -export default SelectParent; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx b/apps/app/components/issues/sidebar-select/priority.tsx similarity index 94% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx rename to apps/app/components/issues/sidebar-select/priority.tsx index 8057b14ec..252400669 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx +++ b/apps/app/components/issues/sidebar-select/priority.tsx @@ -19,7 +19,7 @@ type Props = { userAuth: UserAuth; }; -const SelectPriority: React.FC = ({ control, submitChanges, userAuth }) => { +export const SidebarPrioritySelect: React.FC = ({ control, submitChanges, userAuth }) => { const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( @@ -65,5 +65,3 @@ const SelectPriority: React.FC = ({ control, submitChanges, userAuth }) = ); }; - -export default SelectPriority; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx b/apps/app/components/issues/sidebar-select/state.tsx similarity index 96% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx rename to apps/app/components/issues/sidebar-select/state.tsx index 8906de605..bbe57cc7a 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx +++ b/apps/app/components/issues/sidebar-select/state.tsx @@ -22,7 +22,7 @@ type Props = { userAuth: UserAuth; }; -const SelectState: React.FC = ({ control, submitChanges, userAuth }) => { +export const SidebarStateSelect: React.FC = ({ control, submitChanges, userAuth }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -100,5 +100,3 @@ const SelectState: React.FC = ({ control, submitChanges, userAuth }) => { ); }; - -export default SelectState; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx b/apps/app/components/issues/sidebar.tsx similarity index 91% rename from apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx rename to apps/app/components/issues/sidebar.tsx index fb18d4ebd..54f94f6ab 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -14,18 +14,21 @@ import { Popover, Listbox, Transition } from "@headlessui/react"; import useToast from "hooks/use-toast"; // services import issuesServices from "services/issues.service"; +import modulesService from "services/modules.service"; // components -import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; -import SelectState from "components/project/issues/issue-detail/issue-detail-sidebar/select-state"; -import SelectPriority from "components/project/issues/issue-detail/issue-detail-sidebar/select-priority"; -import SelectParent from "components/project/issues/issue-detail/issue-detail-sidebar/select-parent"; -import SelectCycle from "components/project/issues/issue-detail/issue-detail-sidebar/select-cycle"; -import SelectAssignee from "components/project/issues/issue-detail/issue-detail-sidebar/select-assignee"; -import SelectBlocker from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocker"; -import SelectBlocked from "components/project/issues/issue-detail/issue-detail-sidebar/select-blocked"; +import { + DeleteIssueModal, + SidebarAssigneeSelect, + SidebarBlockedSelect, + SidebarBlockerSelect, + SidebarCycleSelect, + SidebarModuleSelect, + SidebarParentSelect, + SidebarPrioritySelect, + SidebarStateSelect, +} from "components/issues"; // ui import { Input, Button, Spinner, CustomDatePicker } from "components/ui"; -import DatePicker from "react-datepicker"; // icons import { TagIcon, @@ -39,12 +42,10 @@ import { // helpers import { copyTextToClipboard } from "helpers/string.helper"; // types -import type { ICycle, IIssue, IIssueLabels, UserAuth } from "types"; +import type { ICycle, IIssue, IIssueLabels, IModule, UserAuth } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_ISSUES_LIST, ISSUE_DETAILS } from "constants/fetch-keys"; -import "react-datepicker/dist/react-datepicker.css"; - type Props = { control: Control; submitChanges: (formData: Partial) => void; @@ -58,7 +59,7 @@ const defaultValues: Partial = { colour: "#ff0000", }; -const IssueDetailSidebar: React.FC = ({ +export const IssueDetailsSidebar: React.FC = ({ control, submitChanges, issueDetail, @@ -124,14 +125,26 @@ const IssueDetailSidebar: React.FC = ({ }); }; + const handleModuleChange = (moduleDetail: IModule) => { + if (!workspaceSlug || !projectId || !issueDetail) return; + + modulesService + .addIssuesToModule(workspaceSlug as string, projectId as string, moduleDetail.id, { + issues: [issueDetail.id], + }) + .then((res) => { + mutate(ISSUE_DETAILS(issueId as string)); + }); + }; + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( <> - setDeleteIssueModal(false)} isOpen={deleteIssueModal} - data={issueDetail} + data={issueDetail ?? null} />
@@ -175,12 +188,24 @@ const IssueDetailSidebar: React.FC = ({
- - - + + +
- = ({ watch={watchIssue} userAuth={userAuth} /> - i.id !== issueDetail?.id) ?? []} watch={watchIssue} userAuth={userAuth} /> - i.id !== issueDetail?.id) ?? []} watch={watchIssue} @@ -247,11 +272,16 @@ const IssueDetailSidebar: React.FC = ({
- +
@@ -446,5 +476,3 @@ const IssueDetailSidebar: React.FC = ({ ); }; - -export default IssueDetailSidebar; diff --git a/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx b/apps/app/components/issues/sub-issues-list-modal.tsx similarity index 95% rename from apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx rename to apps/app/components/issues/sub-issues-list-modal.tsx index 95eae4555..f3ffe50ad 100644 --- a/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx +++ b/apps/app/components/issues/sub-issues-list-modal.tsx @@ -17,11 +17,11 @@ import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys"; type Props = { isOpen: boolean; - setIsOpen: React.Dispatch>; + handleClose: () => void; parent: IIssue | undefined; }; -const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { +export const SubIssuesListModal: React.FC = ({ isOpen, handleClose, parent }) => { const [query, setQuery] = useState(""); const router = useRouter(); @@ -43,7 +43,7 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { []; const handleCommandPaletteClose = () => { - setIsOpen(false); + handleClose(); setQuery(""); }; @@ -147,11 +147,11 @@ const AddAsSubIssue: React.FC = ({ isOpen, setIsOpen, parent }) => { } onClick={() => { addAsSubIssue(issue.id); - setIsOpen(false); + handleClose(); }} > = ({ isOpen, setIsOpen, parent }) => { ); }; - -export default AddAsSubIssue; diff --git a/apps/app/components/issues/sub-issue-list.tsx b/apps/app/components/issues/sub-issues-list.tsx similarity index 89% rename from apps/app/components/issues/sub-issue-list.tsx rename to apps/app/components/issues/sub-issues-list.tsx index f8944f379..a903f3b62 100644 --- a/apps/app/components/issues/sub-issue-list.tsx +++ b/apps/app/components/issues/sub-issues-list.tsx @@ -4,8 +4,7 @@ import { Disclosure, Transition } from "@headlessui/react"; import { ChevronRightIcon, PlusIcon } from "@heroicons/react/24/outline"; // components import { CustomMenu } from "components/ui"; -import { CreateUpdateIssueModal } from "components/issues"; -import AddAsSubIssue from "components/project/issues/issue-detail/add-as-sub-issue"; +import { CreateUpdateIssueModal, SubIssuesListModal } from "components/issues"; // types import { IIssue, UserAuth } from "types"; @@ -18,7 +17,7 @@ export interface SubIssueListProps { userAuth: UserAuth; } -export const SubIssueList: FC = ({ +export const SubIssuesList: FC = ({ issues = [], handleSubIssueRemove, parentIssue, @@ -28,7 +27,7 @@ export const SubIssueList: FC = ({ }) => { // states const [isIssueModalActive, setIssueModalActive] = useState(false); - const [isSubIssueModalActive, setSubIssueModalActive] = useState(false); + const [subIssuesListModal, setSubIssuesListModal] = useState(false); const [preloadedData, setPreloadedData] = useState | null>(null); const openIssueModal = () => { @@ -40,11 +39,11 @@ export const SubIssueList: FC = ({ }; const openSubIssueModal = () => { - setSubIssueModalActive(true); + setSubIssuesListModal(true); }; const closeSubIssueModal = () => { - setSubIssueModalActive(false); + setSubIssuesListModal(false); }; const isNotAllowed = userAuth.isGuest || userAuth.isViewer; @@ -56,9 +55,9 @@ export const SubIssueList: FC = ({ prePopulateData={{ ...preloadedData }} handleClose={closeIssueModal} /> - setSubIssuesListModal(false)} parent={parentIssue} /> @@ -88,7 +87,7 @@ export const SubIssueList: FC = ({ { - setSubIssueModalActive(true); + setSubIssuesListModal(true); }} > Add an existing issue @@ -114,7 +113,7 @@ export const SubIssueList: FC = ({ = ({ isOpen, setIsOpen, data }) => { +export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); @@ -152,5 +152,3 @@ const ConfirmModuleDeletion: React.FC = ({ isOpen, setIsOpen, data }) => ); }; - -export default ConfirmModuleDeletion; diff --git a/apps/app/components/modules/form.tsx b/apps/app/components/modules/form.tsx new file mode 100644 index 000000000..60fd93059 --- /dev/null +++ b/apps/app/components/modules/form.tsx @@ -0,0 +1,128 @@ +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// components +import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules"; +// ui +import { Button, CustomDatePicker, Input, TextArea } from "components/ui"; +// types +import { IModule } from "types"; + +type Props = { + handleFormSubmit: (values: Partial) => void; + handleClose: () => void; + status: boolean; +}; + +const defaultValues: Partial = { + name: "", + description: "", + status: null, + lead: null, + members_list: [], +}; + +export const ModuleForm: React.FC = ({ handleFormSubmit, handleClose, status }) => { + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + control, + reset, + } = useForm({ + defaultValues, + }); + + const handleCreateUpdateModule = async (formData: Partial) => { + await handleFormSubmit(formData); + + reset({ + ...defaultValues, + }); + }; + + return ( + +
+

+ {status ? "Update" : "Create"} Module +

+
+
+ +
+
+