From 0cda15408d9bb28826d43c7be4f39f24d8fab019 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 15 Dec 2022 09:47:56 +0530 Subject: [PATCH 01/10] fix: bug fixes in cycles --- .../project/cycles/BoardView/single-board.tsx | 662 ---------------- .../project/cycles/ListView/index.tsx | 714 ------------------ .../{BoardView => board-view}/index.tsx | 8 +- .../cycles/board-view/single-board.tsx | 377 +++++++++ .../project/cycles/list-view/index.tsx | 352 +++++++++ .../project/cycles/stats-view/index.tsx | 23 + .../project/cycles/stats-view/single-stat.tsx | 102 +++ .../project/issues/IssuesListModal.tsx | 5 +- .../index.tsx} | 407 ++++------ .../issue-detail-sidebar/select-assignee.tsx | 181 +++++ .../issue-detail-sidebar/select-cycle.tsx | 98 +++ .../issue-detail-sidebar/select-parent.tsx | 74 ++ .../issue-detail-sidebar/select-priority.tsx | 84 +++ .../issue-detail-sidebar/select-state.tsx | 116 +++ .../components/sidebar/workspace-options.tsx | 189 +++-- apps/app/layouts/app-layout.tsx | 4 +- apps/app/layouts/types.d.ts | 2 + apps/app/pages/me/my-issues.tsx | 643 +++++++++++++--- .../projects/[projectId]/cycles/[cycleId].tsx | 386 +++++----- .../projects/[projectId]/cycles/index.tsx | 9 +- .../projects/[projectId]/issues/[issueId].tsx | 8 +- .../pages/projects/[projectId]/settings.tsx | 7 +- apps/app/pages/workspace/index.tsx | 151 ++-- 23 files changed, 2550 insertions(+), 2052 deletions(-) delete mode 100644 apps/app/components/project/cycles/BoardView/single-board.tsx delete mode 100644 apps/app/components/project/cycles/ListView/index.tsx rename apps/app/components/project/cycles/{BoardView => board-view}/index.tsx (91%) create mode 100644 apps/app/components/project/cycles/board-view/single-board.tsx create mode 100644 apps/app/components/project/cycles/list-view/index.tsx create mode 100644 apps/app/components/project/cycles/stats-view/index.tsx create mode 100644 apps/app/components/project/cycles/stats-view/single-stat.tsx rename apps/app/components/project/issues/issue-detail/{IssueDetailSidebar.tsx => issue-detail-sidebar/index.tsx} (55%) create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx diff --git a/apps/app/components/project/cycles/BoardView/single-board.tsx b/apps/app/components/project/cycles/BoardView/single-board.tsx deleted file mode 100644 index 92ff37110..000000000 --- a/apps/app/components/project/cycles/BoardView/single-board.tsx +++ /dev/null @@ -1,662 +0,0 @@ -// react -import React, { useState } from "react"; -// next -import Link from "next/link"; -import Image from "next/image"; -// swr -import useSWR from "swr"; -// services -import cycleServices from "lib/services/cycles.service"; -// hooks -import useUser from "lib/hooks/useUser"; -// ui -import { Spinner } from "ui"; -// icons -import { - ArrowsPointingInIcon, - ArrowsPointingOutIcon, - CalendarDaysIcon, - PlusIcon, - EllipsisHorizontalIcon, - TrashIcon, -} from "@heroicons/react/24/outline"; -import User from "public/user.png"; -// types -import { - CycleIssueResponse, - ICycle, - IIssue, - IWorkspaceMember, - NestedKeyOf, - Properties, -} from "types"; -// constants -import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys"; -import { - addSpaceIfCamelCase, - findHowManyDaysLeft, - renderShortNumericDateFormat, -} from "constants/common"; -import { Menu, Transition } from "@headlessui/react"; -import workspaceService from "lib/services/workspace.service"; - -type Props = { - properties: Properties; - groupedByIssues: { - [key: string]: IIssue[]; - }; - selectedGroup: NestedKeyOf | null; - groupTitle: string; - createdBy: string | null; - bgColor?: string; - openCreateIssueModal: ( - sprintId: string, - issue?: IIssue, - actionType?: "create" | "edit" | "delete" - ) => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; -}; - -const SingleCycleBoard: React.FC = ({ - properties, - groupedByIssues, - selectedGroup, - groupTitle, - createdBy, - bgColor, - openCreateIssueModal, - openIssuesListModal, - removeIssueFromCycle, -}) => { - // Collapse/Expand - const [show, setState] = useState(true); - - const { activeWorkspace, activeProject } = useUser(); - - if (selectedGroup === "priority") - groupTitle === "high" - ? (bgColor = "#dc2626") - : groupTitle === "medium" - ? (bgColor = "#f97316") - : groupTitle === "low" - ? (bgColor = "#22c55e") - : (bgColor = "#ff0000"); - - const { data: people } = useSWR( - activeWorkspace ? WORKSPACE_MEMBERS : null, - activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null - ); - - return ( -
-
-
-
-
- -

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

- - {groupedByIssues[groupTitle].length} - -
-
-
-
- {groupedByIssues[groupTitle].map((childIssue, index: number) => { - const assignees = [ - ...(childIssue?.assignees_list ?? []), - ...(childIssue?.assignees ?? []), - ]?.map((assignee) => { - const tempPerson = people?.find((p) => p.member.id === assignee)?.member; - - return { - avatar: tempPerson?.avatar, - first_name: tempPerson?.first_name, - email: tempPerson?.email, - }; - }); - - return ( -
-
-
- -
- - - {properties.key && ( -
- {activeProject?.identifier}-{childIssue.sequence_id} -
- )} -
- {childIssue.name} -
-
- -
- {properties.priority && ( -
- {/* {getPriorityIcon(childIssue.priority ?? "")} */} - {childIssue.priority ?? "None"} -
-
Priority
-
- {childIssue.priority ?? "None"} -
-
-
- )} - {properties.state && ( -
- - {addSpaceIfCamelCase(childIssue.state_detail.name)} -
-
State
-
{childIssue.state_detail.name}
-
-
- )} - {properties.start_date && ( -
- - {childIssue.start_date - ? renderShortNumericDateFormat(childIssue.start_date) - : "N/A"} -
-
Started at
-
{renderShortNumericDateFormat(childIssue.start_date ?? "")}
-
-
- )} - {properties.target_date && ( -
- - {childIssue.target_date - ? renderShortNumericDateFormat(childIssue.target_date) - : "N/A"} -
-
Target date
-
{renderShortNumericDateFormat(childIssue.target_date ?? "")}
-
- {childIssue.target_date && - (childIssue.target_date < new Date().toISOString() - ? `Target date has passed by ${findHowManyDaysLeft( - childIssue.target_date - )} days` - : findHowManyDaysLeft(childIssue.target_date) <= 3 - ? `Target date is in ${findHowManyDaysLeft( - childIssue.target_date - )} days` - : "Target date")} -
-
-
- )} - {properties.assignee && ( -
- {childIssue.assignee_details?.length > 0 ? ( - childIssue.assignee_details?.map((assignee, index: number) => ( -
- {assignee.avatar && assignee.avatar !== "" ? ( -
- {assignee.name} -
- ) : ( -
- {assignee.first_name.charAt(0)} -
- )} -
- )) - ) : ( -
- No user -
- )} -
-
Assigned to
-
- {childIssue.assignee_details?.length > 0 - ? childIssue.assignee_details - .map((assignee) => assignee.first_name) - .join(", ") - : "No one"} -
-
-
- )} -
-
-
- ); - })} - -
-
-
- ); - - // return ( - //
- //
- //
- //
- //
- //

- // {cycle.name} - //

- // {cycleIssues?.length} - //
- //
- - //
- // - // - // - // - // - - // - // - //
- // - // {(active) => ( - // - // )} - // - // - // {(active) => ( - // - // )} - // - //
- //
- //
- //
- //
- //
- //
- // {cycleIssues ? ( - // cycleIssues.map((issue, index: number) => ( - //
- //
- //
- // - // - // - // - // - // - //
- // - //
- //
- // - //
- // - //
- //
- //
- //
- //
- // - // - // {properties.key && ( - //
- // {activeProject?.identifier}-{childIssue.sequence_id} - //
- // )} - //
- // {childIssue.name} - //
- //
- // - //
- // {properties.priority && ( - //
- // {/* {getPriorityIcon(childIssue.priority ?? "")} */} - // {childIssue.priority ?? "None"} - //
- //
Priority
- //
- // {childIssue.priority ?? "None"} - //
- //
- //
- // )} - // {properties.state && ( - //
- // - // {addSpaceIfCamelCase(childIssue.state_detail.name)} - //
- //
State
- //
{childIssue.state_detail.name}
- //
- //
- // )} - // {properties.start_date && ( - //
- // - // {childIssue.start_date - // ? renderShortNumericDateFormat(childIssue.start_date) - // : "N/A"} - //
- //
Started at
- //
- // {renderShortNumericDateFormat(childIssue.start_date ?? "")} - //
- //
- //
- // )} - // {properties.target_date && ( - //
- // - // {childIssue.target_date - // ? renderShortNumericDateFormat(childIssue.target_date) - // : "N/A"} - //
- //
Target date
- //
- // {renderShortNumericDateFormat(childIssue.target_date ?? "")} - //
- //
- // {childIssue.target_date && - // (childIssue.target_date < new Date().toISOString() - // ? `Target date has passed by ${findHowManyDaysLeft( - // childIssue.target_date - // )} days` - // : findHowManyDaysLeft(childIssue.target_date) <= 3 - // ? `Target date is in ${findHowManyDaysLeft( - // childIssue.target_date - // )} days` - // : "Target date")} - //
- //
- //
- // )} - // {properties.assignee && ( - //
- // {childIssue.assignee_details?.length > 0 ? ( - // childIssue.assignee_details?.map((assignee, index: number) => ( - //
- // {assignee.avatar && assignee.avatar !== "" ? ( - //
- // {assignee.name} - //
- // ) : ( - //
- // {assignee.first_name.charAt(0)} - //
- // )} - //
- // )) - // ) : ( - //
- // No user - //
- // )} - //
- //
Assigned to
- //
- // {childIssue.assignee_details?.length > 0 - // ? childIssue.assignee_details - // .map((assignee) => assignee.first_name) - // .join(", ") - // : "No one"} - //
- //
- //
- // )} - //
- //
- //
- // )) - // ) : ( - //
- // - //
- // )} - // - //
- //
- //
- // ); -}; - -export default SingleCycleBoard; diff --git a/apps/app/components/project/cycles/ListView/index.tsx b/apps/app/components/project/cycles/ListView/index.tsx deleted file mode 100644 index 5add9687a..000000000 --- a/apps/app/components/project/cycles/ListView/index.tsx +++ /dev/null @@ -1,714 +0,0 @@ -// react -import React from "react"; -// next -import Link from "next/link"; -// swr -import useSWR from "swr"; -// headless ui -import { Disclosure, Transition, Menu } from "@headlessui/react"; -// services -import cycleServices from "lib/services/cycles.service"; -// hooks -import useUser from "lib/hooks/useUser"; -// ui -import { Spinner } from "ui"; -// icons -import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; -import { CalendarDaysIcon } from "@heroicons/react/24/outline"; -// types -import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types"; -// fetch keys -import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys"; -// constants -import { - addSpaceIfCamelCase, - findHowManyDaysLeft, - renderShortNumericDateFormat, -} from "constants/common"; -import workspaceService from "lib/services/workspace.service"; - -type Props = { - groupedByIssues: { - [key: string]: IIssue[]; - }; - properties: Properties; - selectedGroup: NestedKeyOf | null; - openCreateIssueModal: ( - sprintId: string, - issue?: IIssue, - actionType?: "create" | "edit" | "delete" - ) => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; -}; - -const CyclesListView: React.FC = ({ - groupedByIssues, - selectedGroup, - openCreateIssueModal, - openIssuesListModal, - properties, - removeIssueFromCycle, -}) => { - const { activeWorkspace, activeProject } = useUser(); - - const { data: people } = useSWR( - activeWorkspace ? WORKSPACE_MEMBERS : null, - activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null - ); - - return ( -
- {Object.keys(groupedByIssues).map((singleGroup) => ( - - {({ open }) => ( -
-
- -
- - - - {selectedGroup !== null ? ( -

- {singleGroup === null || singleGroup === "null" - ? selectedGroup === "priority" && "No priority" - : addSpaceIfCamelCase(singleGroup)} -

- ) : ( -

All Issues

- )} -

- {groupedByIssues[singleGroup as keyof IIssue].length} -

-
-
-
- - -
- {groupedByIssues[singleGroup] ? ( - groupedByIssues[singleGroup].length > 0 ? ( - groupedByIssues[singleGroup].map((issue: IIssue) => { - const assignees = [ - ...(issue?.assignees_list ?? []), - ...(issue?.assignees ?? []), - ]?.map((assignee) => { - const tempPerson = people?.find( - (p) => p.member.id === assignee - )?.member; - - return { - avatar: tempPerson?.avatar, - first_name: tempPerson?.first_name, - email: tempPerson?.email, - }; - }); - - return ( -
- -
- {properties.priority && ( -
- {/* {getPriorityIcon(issue.priority ?? "")} */} - {issue.priority ?? "None"} -
-
Priority
-
- {issue.priority ?? "None"} -
-
-
- )} - {properties.state && ( -
- - {addSpaceIfCamelCase(issue?.state_detail.name)} -
-
State
-
{issue?.state_detail.name}
-
-
- )} - {properties.start_date && ( -
- - {issue.start_date - ? renderShortNumericDateFormat(issue.start_date) - : "N/A"} -
-
Started at
-
- {renderShortNumericDateFormat(issue.start_date ?? "")} -
-
-
- )} - {properties.target_date && ( -
- - {issue.target_date - ? renderShortNumericDateFormat(issue.target_date) - : "N/A"} -
-
- Target date -
-
- {renderShortNumericDateFormat(issue.target_date ?? "")} -
-
- {issue.target_date && - (issue.target_date < new Date().toISOString() - ? `Target date has passed by ${findHowManyDaysLeft( - issue.target_date - )} days` - : findHowManyDaysLeft(issue.target_date) <= 3 - ? `Target date is in ${findHowManyDaysLeft( - issue.target_date - )} days` - : "Target date")} -
-
-
- )} - - - - - - - - - -
- -
-
- -
- -
-
-
-
-
-
- ); - }) - ) : ( -

No issues.

- ) - ) : ( -
- -
- )} -
-
-
-
- -
-
- )} -
- ))} -
- ); - - // return ( - // <> - // - // {({ open }) => ( - //
- //
- // - - // - // - // - // - // - // - // - // - // - // - // - // - // - //
- // - // - // - // {(provided) => ( - //
- // {cycleIssues ? ( - // cycleIssues.length > 0 ? ( - // cycleIssues.map((issue, index) => ( - // - // {(provided, snapshot) => ( - //
- // - //
- // {properties.priority && ( - //
- // {/* {getPriorityIcon(issue.priority ?? "")} */} - // {issue.priority ?? "None"} - //
- //
- // Priority - //
- //
- // {issue.priority ?? "None"} - //
- //
- //
- // )} - // {properties.state && ( - //
- // - // {addSpaceIfCamelCase( - // issue?.state_detail.name - // )} - //
- //
State
- //
{issue?.state_detail.name}
- //
- //
- // )} - // {properties.start_date && ( - //
- // - // {issue.start_date - // ? renderShortNumericDateFormat( - // issue.start_date - // ) - // : "N/A"} - //
- //
Started at
- //
- // {renderShortNumericDateFormat( - // issue.start_date ?? "" - // )} - //
- //
- //
- // )} - // {properties.target_date && ( - //
- // - // {issue.target_date - // ? renderShortNumericDateFormat( - // issue.target_date - // ) - // : "N/A"} - //
- //
- // Target date - //
- //
- // {renderShortNumericDateFormat( - // issue.target_date ?? "" - // )} - //
- //
- // {issue.target_date && - // (issue.target_date < - // new Date().toISOString() - // ? `Target date has passed by ${findHowManyDaysLeft( - // issue.target_date - // )} days` - // : findHowManyDaysLeft( - // issue.target_date - // ) <= 3 - // ? `Target date is in ${findHowManyDaysLeft( - // issue.target_date - // )} days` - // : "Target date")} - //
- //
- //
- // )} - // - // - // - // - // - // - // - // - // - //
- // - //
- //
- // - //
- // - //
- //
- //
- //
- //
- //
- // )} - //
- // )) - // ) : ( - //

- // This cycle has no issue. - //

- // ) - // ) : ( - //
- // - //
- // )} - // {provided.placeholder} - //
- // )} - //
- //
- //
- //
- // - // - // - // Add issue - // - - // - // - //
- // - // {(active) => ( - // - // )} - // - // - // {(active) => ( - // - // )} - // - //
- //
- //
- //
- //
- //
- // )} - //
- // - // ); -}; - -export default CyclesListView; diff --git a/apps/app/components/project/cycles/BoardView/index.tsx b/apps/app/components/project/cycles/board-view/index.tsx similarity index 91% rename from apps/app/components/project/cycles/BoardView/index.tsx rename to apps/app/components/project/cycles/board-view/index.tsx index 8026786c4..07d68f20d 100644 --- a/apps/app/components/project/cycles/BoardView/index.tsx +++ b/apps/app/components/project/cycles/board-view/index.tsx @@ -1,5 +1,5 @@ // components -import SingleBoard from "components/project/cycles/BoardView/single-board"; +import SingleBoard from "components/project/cycles/board-view/single-board"; // ui import { Spinner } from "ui"; // types @@ -13,11 +13,7 @@ type Props = { properties: Properties; selectedGroup: NestedKeyOf | null; members: IProjectMember[] | undefined; - openCreateIssueModal: ( - sprintId: string, - issue?: IIssue, - actionType?: "create" | "edit" | "delete" - ) => void; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: (cycleId: string) => void; removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; }; diff --git a/apps/app/components/project/cycles/board-view/single-board.tsx b/apps/app/components/project/cycles/board-view/single-board.tsx new file mode 100644 index 000000000..c437824e4 --- /dev/null +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -0,0 +1,377 @@ +// react +import React, { useState } from "react"; +// next +import Link from "next/link"; +import Image from "next/image"; +// swr +import useSWR from "swr"; +// services +import cycleServices from "lib/services/cycles.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { Spinner } from "ui"; +// icons +import { + ArrowsPointingInIcon, + ArrowsPointingOutIcon, + CalendarDaysIcon, + PlusIcon, + EllipsisHorizontalIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// types +import { + CycleIssueResponse, + ICycle, + IIssue, + IWorkspaceMember, + NestedKeyOf, + Properties, +} from "types"; +// constants +import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys"; +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; +import { Menu, Transition } from "@headlessui/react"; +import workspaceService from "lib/services/workspace.service"; + +type Props = { + properties: Properties; + groupedByIssues: { + [key: string]: IIssue[]; + }; + selectedGroup: NestedKeyOf | null; + groupTitle: string; + createdBy: string | null; + bgColor?: string; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; + openIssuesListModal: (cycleId: string) => void; + removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; +}; + +const SingleCycleBoard: React.FC = ({ + properties, + groupedByIssues, + selectedGroup, + groupTitle, + createdBy, + bgColor, + openCreateIssueModal, + openIssuesListModal, + removeIssueFromCycle, +}) => { + // Collapse/Expand + const [show, setState] = useState(true); + + const { activeWorkspace, activeProject } = useUser(); + + if (selectedGroup === "priority") + groupTitle === "high" + ? (bgColor = "#dc2626") + : groupTitle === "medium" + ? (bgColor = "#f97316") + : groupTitle === "low" + ? (bgColor = "#22c55e") + : (bgColor = "#ff0000"); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
+
+
+
+
+ +

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

+ + {groupedByIssues[groupTitle].length} + +
+ + + + + + + + +
+ + {(active) => ( + + )} + + + {(active) => ( + + )} + +
+
+
+
+
+
+
+ {groupedByIssues[groupTitle].map((childIssue, index: number) => { + const assignees = [ + ...(childIssue?.assignees_list ?? []), + ...(childIssue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find((p) => p.member.id === assignee)?.member; + + return { + avatar: tempPerson?.avatar, + first_name: tempPerson?.first_name, + email: tempPerson?.email, + }; + }); + + return ( +
+
+ + + {properties.key && ( +
+ {activeProject?.identifier}-{childIssue.sequence_id} +
+ )} +
+ {childIssue.name} +
+
+ +
+ {properties.priority && ( +
+ {/* {getPriorityIcon(childIssue.priority ?? "")} */} + {childIssue.priority ?? "None"} +
+
Priority
+
+ {childIssue.priority ?? "None"} +
+
+
+ )} + {properties.state && ( +
+ + {addSpaceIfCamelCase(childIssue.state_detail.name)} +
+
State
+
{childIssue.state_detail.name}
+
+
+ )} + {properties.start_date && ( +
+ + {childIssue.start_date + ? renderShortNumericDateFormat(childIssue.start_date) + : "N/A"} +
+
Started at
+
{renderShortNumericDateFormat(childIssue.start_date ?? "")}
+
+
+ )} + {properties.target_date && ( +
+ + {childIssue.target_date + ? renderShortNumericDateFormat(childIssue.target_date) + : "N/A"} +
+
Target date
+
{renderShortNumericDateFormat(childIssue.target_date ?? "")}
+
+ {childIssue.target_date && + (childIssue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : findHowManyDaysLeft(childIssue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + childIssue.target_date + )} days` + : "Target date")} +
+
+
+ )} + {properties.assignee && ( +
+ {childIssue.assignee_details?.length > 0 ? ( + childIssue.assignee_details?.map((assignee, index: number) => ( +
+ {assignee.avatar && assignee.avatar !== "" ? ( +
+ {assignee.name} +
+ ) : ( +
+ {assignee.first_name.charAt(0)} +
+ )} +
+ )) + ) : ( +
+ No user +
+ )} +
+
Assigned to
+
+ {childIssue.assignee_details?.length > 0 + ? childIssue.assignee_details + .map((assignee) => assignee.first_name) + .join(", ") + : "No one"} +
+
+
+ )} +
+
+
+ ); + })} + +
+
+
+ ); +}; + +export default SingleCycleBoard; diff --git a/apps/app/components/project/cycles/list-view/index.tsx b/apps/app/components/project/cycles/list-view/index.tsx new file mode 100644 index 000000000..2ec0c6731 --- /dev/null +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -0,0 +1,352 @@ +// react +import React from "react"; +// next +import Link from "next/link"; +// swr +import useSWR from "swr"; +// headless ui +import { Disclosure, Transition, Menu } from "@headlessui/react"; +// services +import cycleServices from "lib/services/cycles.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// ui +import { Spinner } from "ui"; +// icons +import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue, IWorkspaceMember, NestedKeyOf, Properties, SelectSprintType } from "types"; +// fetch keys +import { CYCLE_ISSUES, WORKSPACE_MEMBERS } from "constants/fetch-keys"; +// constants +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; +import workspaceService from "lib/services/workspace.service"; + +type Props = { + groupedByIssues: { + [key: string]: IIssue[]; + }; + properties: Properties; + selectedGroup: NestedKeyOf | null; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; + openIssuesListModal: (cycleId: string) => void; + removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; +}; + +const CyclesListView: React.FC = ({ + groupedByIssues, + selectedGroup, + openCreateIssueModal, + openIssuesListModal, + properties, + removeIssueFromCycle, +}) => { + const { activeWorkspace, activeProject } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
+ {Object.keys(groupedByIssues).map((singleGroup) => ( + + {({ open }) => ( +
+
+ +
+ + + + {selectedGroup !== null ? ( +

+ {singleGroup === null || singleGroup === "null" + ? selectedGroup === "priority" && "No priority" + : addSpaceIfCamelCase(singleGroup)} +

+ ) : ( +

All Issues

+ )} +

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

+
+
+
+ + +
+ {groupedByIssues[singleGroup] ? ( + groupedByIssues[singleGroup].length > 0 ? ( + groupedByIssues[singleGroup].map((issue: IIssue) => { + const assignees = [ + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find( + (p) => p.member.id === assignee + )?.member; + + return { + avatar: tempPerson?.avatar, + first_name: tempPerson?.first_name, + email: tempPerson?.email, + }; + }); + + return ( +
+ +
+ {properties.priority && ( +
+ {/* {getPriorityIcon(issue.priority ?? "")} */} + {issue.priority ?? "None"} +
+
Priority
+
+ {issue.priority ?? "None"} +
+
+
+ )} + {properties.state && ( +
+ + {addSpaceIfCamelCase(issue?.state_detail.name)} +
+
State
+
{issue?.state_detail.name}
+
+
+ )} + {properties.start_date && ( +
+ + {issue.start_date + ? renderShortNumericDateFormat(issue.start_date) + : "N/A"} +
+
Started at
+
+ {renderShortNumericDateFormat(issue.start_date ?? "")} +
+
+
+ )} + {properties.target_date && ( +
+ + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
+
+ Target date +
+
+ {renderShortNumericDateFormat(issue.target_date ?? "")} +
+
+ {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Target date")} +
+
+
+ )} + + + + + + + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ ); + }) + ) : ( +

No issues.

+ ) + ) : ( +
+ +
+ )} +
+
+
+
+ +
+
+ )} +
+ ))} +
+ // + // + // + // + // = ({ cycles }) => { + return ( + <> + {cycles.map((cycle) => ( + + ))} + + ); +}; + +export default CycleStatsView; diff --git a/apps/app/components/project/cycles/stats-view/single-stat.tsx b/apps/app/components/project/cycles/stats-view/single-stat.tsx new file mode 100644 index 000000000..ae5301fe9 --- /dev/null +++ b/apps/app/components/project/cycles/stats-view/single-stat.tsx @@ -0,0 +1,102 @@ +// next +import Link from "next/link"; +// swr +import useSWR from "swr"; +// services +import cyclesService from "lib/services/cycles.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// types +import { CycleIssueResponse, ICycle } from "types"; +// fetch-keys +import { CYCLE_ISSUES } from "constants/fetch-keys"; +import { groupBy, renderShortNumericDateFormat } from "constants/common"; +import { + CheckIcon, + ExclamationCircleIcon, + ExclamationTriangleIcon, +} from "@heroicons/react/24/outline"; + +type Props = { cycle: ICycle }; + +const stateGroupIcons: { + [key: string]: JSX.Element; +} = { + backlog: , + unstarted: , + started: , + cancelled: , + completed: , +}; + +const SingleStat: React.FC = ({ cycle }) => { + const { activeWorkspace, activeProject } = useUser(); + + const { data: cycleIssues } = useSWR( + activeWorkspace && activeProject && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null, + activeWorkspace && activeProject && cycle.id + ? () => + cyclesService.getCycleIssues(activeWorkspace?.slug, activeProject?.id, cycle.id as string) + : null + ); + const groupedIssues = { + backlog: [], + unstarted: [], + started: [], + cancelled: [], + completed: [], + ...groupBy(cycleIssues ?? [], "issue_details.state_detail.group"), + }; + + // status calculator + const startDate = new Date(cycle.start_date ?? ""); + const endDate = new Date(cycle.end_date ?? ""); + const today = new Date(); + + return ( + <> +
+
+
+ + {cycle.name} + +
+ {renderShortNumericDateFormat(startDate)} + {" - "} + {renderShortNumericDateFormat(endDate)} +
+
+
+ {today.getDate() < startDate.getDate() + ? "Not started" + : today.getDate() > endDate.getDate() + ? "Over" + : "Active"} +
+
+
+
+ {Object.keys(groupedIssues).map((group) => { + return ( +
+
+
+ {stateGroupIcons[group]} +
+
+
+
{group}
+ {groupedIssues[group].length} +
+
+ ); + })} +
+
+
+ + ); +}; + +export default SingleStat; diff --git a/apps/app/components/project/issues/IssuesListModal.tsx b/apps/app/components/project/issues/IssuesListModal.tsx index aba9a0c34..b635a16b6 100644 --- a/apps/app/components/project/issues/IssuesListModal.tsx +++ b/apps/app/components/project/issues/IssuesListModal.tsx @@ -19,6 +19,7 @@ type Props = { issues: IIssue[]; title?: string; multiple?: boolean; + customDisplay?: JSX.Element; }; const IssuesListModal: React.FC = ({ @@ -29,6 +30,7 @@ const IssuesListModal: React.FC = ({ issues, title = "Issues", multiple = false, + customDisplay, }) => { const [query, setQuery] = useState(""); const [values, setValues] = useState([]); @@ -90,9 +92,10 @@ const IssuesListModal: React.FC = ({ className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm outline-none" placeholder="Search..." onChange={(e) => setQuery(e.target.value)} + displayValue={() => ""} /> - +
{customDisplay}
; @@ -71,25 +71,12 @@ const IssueDetailSidebar: React.FC = ({ }) => { const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); - const [isParentModalOpen, setIsParentModalOpen] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false); const { activeWorkspace, activeProject, cycles, issues } = useUser(); const { setToastAlert } = useToast(); - const { data: states } = useSWR( - activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null, - activeWorkspace && activeProject - ? () => stateServices.getStates(activeWorkspace.slug, activeProject.id) - : null - ); - - const { data: people } = useSWR( - activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, - activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null - ); - const { data: issueLabels, mutate: issueLabelMutate } = useSWR( activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null, activeProject && activeWorkspace @@ -108,7 +95,7 @@ const IssueDetailSidebar: React.FC = ({ defaultValues, }); - const onSubmit = (formData: any) => { + const handleNewLabel = (formData: any) => { if (!activeWorkspace || !activeProject || isSubmitting) return; issuesServices .createIssueLabel(activeWorkspace.slug, activeProject.id, formData) @@ -133,58 +120,6 @@ const IssueDetailSidebar: React.FC = ({ }> > = [ [ - { - label: "Status", - name: "state", - canSelectMultipleOptions: false, - icon: Squares2X2Icon, - options: states?.map((state) => ({ - label: state.name, - value: state.id, - color: state.color, - })), - modal: false, - }, - { - label: "Assignees", - name: "assignees_list", - canSelectMultipleOptions: true, - icon: UserGroupIcon, - options: people?.map((person) => ({ - label: person.member.first_name, - value: person.member.id, - })), - modal: false, - }, - { - label: "Priority", - name: "priority", - canSelectMultipleOptions: false, - icon: ChartBarIcon, - options: PRIORITIES.map((property) => ({ - label: property, - value: property, - })), - modal: false, - }, - ], - [ - { - label: "Parent", - name: "parent", - canSelectMultipleOptions: false, - icon: UserIcon, - issuesList: - issues?.results.filter( - (i) => - i.id !== issueDetail?.id && - i.id !== issueDetail?.parent && - i.parent !== issueDetail?.id - ) ?? [], - modal: true, - isOpen: isParentModalOpen, - setIsOpen: setIsParentModalOpen, - }, // { // label: "Blocker", // name: "blockers_list", @@ -205,26 +140,6 @@ const IssueDetailSidebar: React.FC = ({ // isOpen: isBlockedModalOpen, // setIsOpen: setIsBlockedModalOpen, // }, - { - label: "Target Date", - name: "target_date", - canSelectMultipleOptions: true, - icon: CalendarDaysIcon, - modal: false, - }, - ], - [ - { - label: "Cycle", - name: "cycle", - canSelectMultipleOptions: false, - icon: ArrowPathIcon, - options: cycles?.map((cycle) => ({ - label: cycle.name, - value: cycle.id, - })), - modal: false, - }, ], ]; @@ -297,7 +212,69 @@ const IssueDetailSidebar: React.FC = ({
- {sidebarSections.map((section, index) => ( +
+ + + +
+
+ + i.id !== issueDetail?.id && + i.id !== issueDetail?.parent && + i.parent !== issueDetail?.id + ) ?? [] + } + customDisplay={ + issueDetail?.parent_detail ? ( + + ) : ( +
+ No parent selected +
+ ) + } + watchIssue={watchIssue} + /> +
+
+ +

Due date

+
+
+ ( + { + submitChanges({ target_date: e.target.value }); + onChange(e.target.value); + }} + className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full" + /> + )} + /> +
+
+
+
+ +
+ {/* {sidebarSections.map((section, index) => (
{section.map((item) => (
@@ -306,154 +283,63 @@ const IssueDetailSidebar: React.FC = ({

{item.label}

- {item.name === "target_date" ? ( - ( - { - submitChanges({ target_date: e.target.value }); - onChange(e.target.value); + ( + <> + item.setIsOpen && item.setIsOpen(false)} + onChange={(val) => { + console.log(val); + submitChanges({ [item.name]: val }); + onChange(val); }} - className="hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full" + issues={item?.issuesList ?? []} + title={`Select ${item.label}`} + multiple={item.canSelectMultipleOptions} + value={value} + customDisplay={ + issueDetail?.parent_detail ? ( + + ) : ( +
+ No parent selected +
+ ) + } /> - )} - /> - ) : item.modal ? ( - ( - <> - item.setIsOpen && item.setIsOpen(false)} - onChange={(val) => { - console.log(val); - // submitChanges({ [item.name]: val }); - onChange(val); - }} - issues={item?.issuesList ?? []} - title={`Select ${item.label}`} - multiple={item.canSelectMultipleOptions} - value={value} - /> - - - )} - /> - ) : ( - ( - { - if (item.name === "cycle") handleCycleChange(value); - else submitChanges({ [item.name]: value }); - }} - className="flex-shrink-0" - > - {({ open }) => ( -
- - - {value - ? Array.isArray(value) - ? value - .map( - (i: any) => - item.options?.find((option) => option.value === i) - ?.label - ) - .join(", ") || item.label - : item.options?.find((option) => option.value === value) - ?.label - : "None"} - - - - - - -
- {item.options ? ( - item.options.length > 0 ? ( - item.options.map((option) => ( - - `${ - active || selected - ? "text-white bg-theme" - : "text-gray-900" - } ${ - item.label === "Priority" && "capitalize" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.value} - > - {option.color && ( - - )} - {option.label} - - )) - ) : ( -
No {item.label}s found
- ) - ) : ( - - )} -
-
-
-
- )} -
- )} - /> - )} + : `Select ${item.label}`} + + + )} + />
))}
- ))} + ))} */}
@@ -466,18 +352,20 @@ const IssueDetailSidebar: React.FC = ({ {issueDetail?.label_details.map((label) => ( - // submitChanges({ - // labels_list: issueDetail?.labels_list.filter((l) => l !== label.id), - // }) - // } + className="group flex items-center gap-1 border rounded-2xl text-xs px-1 py-0.5 hover:bg-red-50 hover:border-red-500 cursor-pointer" + onClick={() => { + const updatedLabels = issueDetail?.labels.filter((l) => l !== label.id); + submitChanges({ + labels_list: updatedLabels, + }); + }} > {label.name} + ))} = ({ as="div" value={value} multiple - onChange={(value: any) => submitChanges({ labels_list: value })} + onChange={(val) => submitChanges({ labels_list: val })} className="flex-shrink-0" > {({ open }) => ( <> Label
- - - Select Label - + + Select Label = ({ )} /> +
-
- -
{createLabelForm && ( -
+
{({ open }) => ( diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx new file mode 100644 index 000000000..0fff8006b --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx @@ -0,0 +1,181 @@ +// react +import React from "react"; +// next +import Image from "next/image"; +// swr +import useSWR from "swr"; +// react-hook-form +import { Control, Controller } from "react-hook-form"; +// services +import workspaceService from "lib/services/workspace.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// ui +import { Spinner } from "ui"; +// icons +import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import User from "public/user.png"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; +import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; + +type Props = { + control: Control; + submitChanges: (formData: Partial) => void; +}; + +const SelectAssignee: React.FC = ({ control, submitChanges }) => { + const { activeWorkspace } = useUser(); + + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + return ( +
+
+ +

Assignees

+
+
+ ( + { + submitChanges({ assignees_list: value }); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
+ + +
+ {value && Array.isArray(value) ? ( + <> + {value.length > 0 ? ( + value.map((assignee, index: number) => { + const person = people?.find( + (p) => p.member.id === assignee + )?.member; + + return ( +
+ {person && person.avatar && person.avatar !== "" ? ( +
+ {person.first_name} +
+ ) : ( +
+ {person?.first_name.charAt(0)} +
+ )} +
+ ); + }) + ) : ( +
+ No user +
+ )} + + ) : null} +
+
+
+ + + +
+ {people ? ( + people.length > 0 ? ( + people.map((option) => ( + + `${ + active || selected ? "text-white bg-theme" : "text-gray-900" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={option.member.id} + > + {option.member.avatar && option.member.avatar !== "" ? ( +
+ avatar +
+ ) : ( +
+ {option.member.first_name && option.member.first_name !== "" + ? option.member.first_name.charAt(0) + : option.member.email.charAt(0)} +
+ )} + {option.member.first_name} +
+ )) + ) : ( +
No assignees found
+ ) + ) : ( + + )} +
+
+
+
+ )} +
+ )} + /> +
+
+ ); +}; + +export default SelectAssignee; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx new file mode 100644 index 000000000..0e36c54af --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx @@ -0,0 +1,98 @@ +// react-hook-form +import { Control, Controller } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// types +import { IIssue } from "types"; +import { classNames } from "constants/common"; +import { Spinner } from "ui"; +import React from "react"; +import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; + +type Props = { + control: Control; + handleCycleChange: (cycleId: string) => void; +}; + +const SelectCycle: React.FC = ({ control, handleCycleChange }) => { + const { cycles } = useUser(); + + return ( +
+
+ +

Cycle

+
+
+ ( + { + handleCycleChange(value); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
+ + + {value ? cycles?.find((c) => c.id === value)?.name : "None"} + + + + + + +
+ {cycles ? ( + cycles.length > 0 ? ( + cycles.map((option) => ( + + `${ + active || selected ? "text-white bg-theme" : "text-gray-900" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={option.id} + > + {option.name} + + )) + ) : ( +
No cycles found
+ ) + ) : ( + + )} +
+
+
+
+ )} +
+ )} + /> +
+
+ ); +}; + +export default SelectCycle; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx new file mode 100644 index 000000000..03cf0be4e --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx @@ -0,0 +1,74 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { Control, Controller, UseFormWatch } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +// components +import IssuesListModal from "components/project/issues/IssuesListModal"; +// icons +import { UserIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; + +type Props = { + control: Control; + submitChanges: (formData: Partial) => void; + issuesList: IIssue[]; + customDisplay: JSX.Element; + watchIssue: UseFormWatch; +}; + +const SelectParent: React.FC = ({ + control, + submitChanges, + issuesList, + customDisplay, + watchIssue, +}) => { + const [isParentModalOpen, setIsParentModalOpen] = useState(false); + + const { activeProject, issues } = useUser(); + + return ( +
+
+ +

Parent

+
+
+ ( + setIsParentModalOpen(false)} + onChange={(val) => { + submitChanges({ parent: val }); + onChange(val); + }} + issues={issuesList} + title="Select Parent" + value={value} + customDisplay={customDisplay} + /> + )} + /> + +
+
+ ); +}; + +export default SelectParent; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx new file mode 100644 index 000000000..e76beeb25 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx @@ -0,0 +1,84 @@ +// react +import React from "react"; +// react-hook-form +import { Control, Controller } from "react-hook-form"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// icons +import { ChevronDownIcon, ChartBarIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; +import { PRIORITIES } from "constants/"; + +type Props = { + control: Control; + submitChanges: (formData: Partial) => void; +}; + +const SelectPriority: React.FC = ({ control, submitChanges }) => { + return ( +
+
+ +

Priority

+
+
+ ( + { + submitChanges({ priority: value }); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
+ + + {value} + + + + + + +
+ {PRIORITIES.map((option) => ( + + `${ + active || selected ? "text-white bg-theme" : "text-gray-900" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate capitalize` + } + value={option} + > + {option} + + ))} +
+
+
+
+ )} +
+ )} + /> +
+
+ ); +}; + +export default SelectPriority; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx new file mode 100644 index 000000000..cc129c536 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx @@ -0,0 +1,116 @@ +// react-hook-form +import { Control, Controller } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// types +import { IIssue } from "types"; +import { classNames } from "constants/common"; +import { Spinner } from "ui"; +import React from "react"; +import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; + +type Props = { + control: Control; + submitChanges: (formData: Partial) => void; +}; + +const SelectState: React.FC = ({ control, submitChanges }) => { + const { states } = useUser(); + + return ( +
+
+ +

State

+
+
+ ( + { + submitChanges({ state: value }); + }} + className="flex-shrink-0" + > + {({ open }) => ( +
+ + + {value ? ( + <> + option.id === value)?.color, + }} + > + {states?.find((option) => option.id === value)?.name} + + ) : ( + "None" + )} + + + + + + +
+ {states ? ( + states.length > 0 ? ( + states.map((option) => ( + + `${ + active || selected ? "text-white bg-theme" : "text-gray-900" + } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + } + value={option.id} + > + {option.color && ( + + )} + {option.name} + + )) + ) : ( +
No states found
+ ) + ) : ( + + )} +
+
+
+
+ )} +
+ )} + /> +
+
+ ); +}; + +export default SelectState; diff --git a/apps/app/components/sidebar/workspace-options.tsx b/apps/app/components/sidebar/workspace-options.tsx index b7760a49e..749eba17d 100644 --- a/apps/app/components/sidebar/workspace-options.tsx +++ b/apps/app/components/sidebar/workspace-options.tsx @@ -82,9 +82,7 @@ const WorkspaceOptions: React.FC = ({ sidebarCollapse }) => { return (
-
+
= ({ sidebarCollapse }) => { alt="Workspace Logo" layout="fill" objectFit="cover" + className="rounded" /> ) : ( activeWorkspace?.name?.charAt(0) ?? "N" )}
{!sidebarCollapse && ( -

- {activeWorkspace?.name ?? "Loading..."} +

+ {activeWorkspace?.name + ? activeWorkspace.name.length > 17 + ? `${activeWorkspace.name.substring(0, 17)}...` + : activeWorkspace.name + : "Loading..."}

)}
@@ -131,74 +134,124 @@ const WorkspaceOptions: React.FC = ({ sidebarCollapse }) => { leaveTo="transform opacity-0 scale-95" > -
- {workspaces ? ( - <> - {workspaces.length > 0 ? ( - workspaces.map((workspace: any) => ( - - {({ active }) => ( - - )} - - )) - ) : ( -

No workspace found!

- )} - { - router.push("/create-workspace"); - }} - className="w-full" - > - {({ active }) => ( - - - Create Workspace - +
+
+ + {user?.email} + +
+
+ {workspaces ? ( + <> + {workspaces.length > 0 ? ( + workspaces.map((workspace: any) => ( + + {({ active }) => ( + + )} + + )) + ) : ( +

No workspace found!

)} + { + router.push("/create-workspace"); + }} + className="w-full text-xs flex items-center gap-2 px-2 py-1 text-left rounded hover:bg-gray-100" + > + + Create Workspace + + + ) : ( +
+ +
+ )} +
+
+ {userLinks.map((link, index) => ( + + + + {link.name} + + - - ) : ( -
- -
- )} + ))} + { + await authenticationService + .signOut({ + refresh_token: authenticationService.getRefreshToken(), + }) + .then((response) => { + console.log("user signed out", response); + }) + .catch((error) => { + console.log("Failed to sign out", error); + }) + .finally(() => { + mutateUser(); + router.push("/signin"); + }); + }} + > + Sign out + +
- {!sidebarCollapse && ( + {/* {!sidebarCollapse && (
@@ -261,7 +314,7 @@ const WorkspaceOptions: React.FC = ({ sidebarCollapse }) => {
- )} + )} */}
{workspaceLinks.map((link, index) => ( diff --git a/apps/app/layouts/app-layout.tsx b/apps/app/layouts/app-layout.tsx index fd0f0b134..9f1500a6d 100644 --- a/apps/app/layouts/app-layout.tsx +++ b/apps/app/layouts/app-layout.tsx @@ -18,7 +18,9 @@ const AppLayout: React.FC = ({ children, noPadding = false, bg = "primary", + noHeader = false, breadcrumbs, + left, right, }) => { const [isOpen, setIsOpen] = useState(false); @@ -37,7 +39,7 @@ const AppLayout: React.FC = ({
-
+ {noHeader ? null :
}
{ - const [selectedWorkspace, setSelectedWorkspace] = useState(null); + const { activeWorkspace, user, states } = useUser(); - const { user, workspaces, activeWorkspace } = useUser(); + console.log(states); const { data: myIssues, mutate: mutateMyIssues } = useSWR( user && activeWorkspace ? USER_ISSUE(activeWorkspace.slug) : null, user && activeWorkspace ? () => userService.userIssues(activeWorkspace.slug) : null ); + const { data: people } = useSWR( + activeWorkspace ? WORKSPACE_MEMBERS : null, + activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null + ); + + const [properties, setProperties] = useIssuesProperties( + activeWorkspace?.slug, + "21b5fab2-cb0c-4875-9496-619134bf1f32" + ); + const updateMyIssues = ( workspaceSlug: string, projectId: string, @@ -79,98 +105,515 @@ const MyIssues: NextPage = () => { } right={ - { - const e = new KeyboardEvent("keydown", { - key: "i", - ctrlKey: true, - }); - document.dispatchEvent(e); - }} - /> +
+ + {({ open }) => ( + <> + + View + + + + +
+
+

Properties

+
+ {Object.keys(properties).map((key) => ( + + ))} +
+
+
+
+
+ + )} +
+ { + const e = new KeyboardEvent("keydown", { + key: "i", + ctrlKey: true, + }); + document.dispatchEvent(e); + }} + /> +
} >
{myIssues ? ( <> {myIssues.length > 0 ? ( -
-
-
-
- - - - - - - - - - - - {myIssues.map((myIssue, index) => ( - - - - - - - - ))} - -
- NAME - - DESCRIPTION - - PROJECT - - PRIORITY - - STATUS -
- - {myIssue.name} - - - {/* {myIssue.description} */} - - {myIssue.project_detail?.name} -
- {`(${myIssue.project_detail?.identifier}-${myIssue.sequence_id})`} -
{myIssue.priority} - -
+
+ + {({ open }) => ( +
+
+ +
+ + + +

My Issues

+

{myIssues.length}

+
+
+
+ + +
+ {myIssues.map((issue: IIssue) => { + const assignees = [ + ...(issue?.assignees_list ?? []), + ...(issue?.assignees ?? []), + ]?.map((assignee) => { + const tempPerson = people?.find( + (p) => p.member.id === assignee + )?.member; + + return { + avatar: tempPerson?.avatar, + first_name: tempPerson?.first_name, + email: tempPerson?.email, + }; + }); + + return ( +
+ +
+ {properties.priority && ( + { + // partialUpdateIssue({ priority: data }, issue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + {issue.priority ?? "None"} + + + + + {PRIORITIES?.map((priority) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer capitalize select-none px-3 py-2" + ) + } + value={priority} + > + {priority} + + ))} + + +
+
+
+ Priority +
+
+ {issue.priority ?? "None"} +
+
+ + )} +
+ )} + {properties.state && ( + { + // partialUpdateIssue({ state: data }, issue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ + + {addSpaceIfCamelCase(issue.state_detail.name)} + + + + + {states?.map((state) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none px-3 py-2" + ) + } + value={state.id} + > + {addSpaceIfCamelCase(state.name)} + + ))} + + +
+
+
State
+
{issue.state_detail.name}
+
+ + )} +
+ )} + {properties.start_date && ( +
+ + {issue.start_date + ? renderShortNumericDateFormat(issue.start_date) + : "N/A"} +
+
Started at
+
+ {renderShortNumericDateFormat(issue.start_date ?? "")} +
+
+
+ )} + {properties.target_date && ( +
+ + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
+
+ Target date +
+
+ {renderShortNumericDateFormat(issue.target_date ?? "")} +
+
+ {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Target date")} +
+
+
+ )} + {properties.assignee && ( + { + const newData = issue.assignees ?? []; + if (newData.includes(data)) { + newData.splice(newData.indexOf(data), 1); + } else { + newData.push(data); + } + // partialUpdateIssue({ assignees_list: newData }, issue.id); + }} + className="group relative flex-shrink-0" + > + {({ open }) => ( + <> +
+ +
+ {assignees.length > 0 ? ( + assignees.map((assignee, index: number) => ( +
+ {assignee.avatar && + assignee.avatar !== "" ? ( +
+ {assignee?.first_name} +
+ ) : ( +
+ {assignee.first_name?.charAt(0)} +
+ )} +
+ )) + ) : ( +
+ No user +
+ )} +
+
+ + + + {people?.map((person) => ( + + classNames( + active ? "bg-indigo-50" : "bg-white", + "cursor-pointer select-none p-2" + ) + } + 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} +

+
+
+ ))} +
+
+
+
+
Assigned to
+
+ {issue.assignee_details?.length > 0 + ? issue.assignee_details + .map((assignee) => assignee.first_name) + .join(", ") + : "No one"} +
+
+ + )} +
+ )} + + + + + + + + + +
+ +
+
+
+
+
+
+ ); + })} +
+
+
-
-
+ )} +
) : (
diff --git a/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx index f1dc51c6b..1f339bec1 100644 --- a/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/projects/[projectId]/cycles/[cycleId].tsx @@ -1,6 +1,7 @@ // react -import React from "react"; +import React, { useState } from "react"; // next +import Link from "next/link"; import { useRouter } from "next/router"; // swr import useSWR, { mutate } from "swr"; @@ -9,8 +10,8 @@ import { DropResult } from "react-beautiful-dnd"; // layouots import AppLayout from "layouts/app-layout"; // components -import CyclesListView from "components/project/cycles/ListView"; -import CyclesBoardView from "components/project/cycles/BoardView"; +import CyclesListView from "components/project/cycles/list-view"; +import CyclesBoardView from "components/project/cycles/board-view"; // services import issuesServices from "lib/services/issues.service"; import cycleServices from "lib/services/cycles.service"; @@ -25,19 +26,21 @@ import { Menu, Popover, Transition } from "@headlessui/react"; import { BreadcrumbItem, Breadcrumbs, CustomMenu } from "ui"; // icons import { Squares2X2Icon } from "@heroicons/react/20/solid"; -import { - ArrowPathIcon, - ChevronDownIcon, - EllipsisHorizontalIcon, - ListBulletIcon, -} from "@heroicons/react/24/outline"; +import { ArrowPathIcon, ChevronDownIcon, ListBulletIcon } from "@heroicons/react/24/outline"; // types -import { CycleIssueResponse, IIssue, NestedKeyOf, Properties } from "types"; +import { + CycleIssueResponse, + IIssue, + NestedKeyOf, + Properties, + SelectIssue, + SelectSprintType, +} from "types"; // fetch-keys import { CYCLE_ISSUES, PROJECT_MEMBERS } from "constants/fetch-keys"; // constants import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common"; -import Link from "next/link"; +import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; const groupByOptions: Array<{ name: string; key: NestedKeyOf | null }> = [ { name: "State", key: "state_detail.name" }, @@ -73,6 +76,10 @@ const filterIssueOptions: Array<{ type Props = {}; const SingleCycle: React.FC = () => { + const [isIssueModalOpen, setIsIssueModalOpen] = useState(false); + const [selectedCycle, setSelectedCycle] = useState(); + const [selectedIssues, setSelectedIssues] = useState(); + const { activeWorkspace, activeProject, cycles } = useUser(); const router = useRouter(); @@ -121,6 +128,21 @@ const SingleCycle: React.FC = () => { filterIssue, } = useIssuesFilter(cycleIssuesArray ?? []); + const openCreateIssueModal = ( + issue?: IIssue, + actionType: "create" | "edit" | "delete" = "create" + ) => { + const cycle = cycles?.find((cycle) => cycle.id === cycleId); + if (cycle) { + setSelectedCycle({ + ...cycle, + actionType: "create-issue", + }); + if (issue) setSelectedIssues({ ...issue, actionType }); + setIsIssueModalOpen(true); + } + }; + const addIssueToCycle = (cycleId: string, issueId: string) => { if (!activeWorkspace || !activeProject?.id) return; @@ -200,18 +222,32 @@ const SingleCycle: React.FC = () => { }; return ( - - - {/* c.id === cycleId)?.name ?? "Cycle"} `} /> */} + <> + + + + + } + left={ - + - Cycle + {cycles?.find((c) => c.id === cycleId)?.name} = () => { - - } - right={ -
-
- - -
- - {({ open }) => ( - <> - - View - + } + right={ +
+
+ + +
+ + {({ open }) => ( + <> + + View + - - -
-
-

Group by

- option.key === groupByProperty)?.name ?? - "Select" - } - > - {groupByOptions.map((option) => ( - setGroupByProperty(option.key)} - > - {option.name} - - ))} - -
-
-

Order by

- option.key === orderBy)?.name ?? - "Select" - } - > - {orderByOptions.map((option) => - groupByProperty === "priority" && option.key === "priority" ? null : ( + + +
+
+

Group by

+ option.key === groupByProperty) + ?.name ?? "Select" + } + > + {groupByOptions.map((option) => ( setOrderBy(option.key)} + onClick={() => setGroupByProperty(option.key)} > {option.name} - ) - )} - -
-
-

Issue type

- option.key === filterIssue)?.name ?? - "Select" - } - > - {filterIssueOptions.map((option) => ( - setFilterIssue(option.key)} - > - {option.name} - - ))} - -
-
-
-

Properties

-
- {Object.keys(properties).map((key) => ( - - ))} + ))} + +
+
+

Order by

+ option.key === orderBy)?.name ?? + "Select" + } + > + {orderByOptions.map((option) => + groupByProperty === "priority" && option.key === "priority" ? null : ( + setOrderBy(option.key)} + > + {option.name} + + ) + )} + +
+
+

Issue type

+ option.key === filterIssue) + ?.name ?? "Select" + } + > + {filterIssueOptions.map((option) => ( + setFilterIssue(option.key)} + > + {option.name} + + ))} + +
+
+
+

Properties

+
+ {Object.keys(properties).map((key) => ( + + ))} +
-
-
-
- - )} - -
- } - > - {issueView === "list" ? ( - { - return; - }} - openIssuesListModal={() => { - return; - }} - removeIssueFromCycle={removeIssueFromCycle} - /> - ) : ( -
- + + + )} + +
+ } + > + {issueView === "list" ? ( + { - return; - }} + properties={properties} + openCreateIssueModal={openCreateIssueModal} openIssuesListModal={() => { return; }} + removeIssueFromCycle={removeIssueFromCycle} /> -
- )} - + ) : ( +
+ { + return; + }} + /> +
+ )} + + ); }; diff --git a/apps/app/pages/projects/[projectId]/cycles/index.tsx b/apps/app/pages/projects/[projectId]/cycles/index.tsx index 9a2709500..16d3d2227 100644 --- a/apps/app/pages/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/projects/[projectId]/cycles/index.tsx @@ -3,11 +3,9 @@ import React, { useEffect, useState } from "react"; // next import { useRouter } from "next/router"; import type { NextPage } from "next"; -import Link from "next/link"; // swr import useSWR from "swr"; // services -import issuesServices from "lib/services/issues.service"; import sprintService from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; @@ -24,6 +22,7 @@ import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deleti import ConfirmSprintDeletion from "components/project/cycles/ConfirmCycleDeletion"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateSprintsModal from "components/project/cycles/CreateUpdateCyclesModal"; +import CycleStatsView from "components/project/cycles/stats-view"; // headless ui import { Popover, Transition } from "@headlessui/react"; // ui @@ -204,11 +203,7 @@ const ProjectSprints: NextPage = () => { {cycles ? ( cycles.length > 0 ? (
- {cycles.map((cycle) => ( - - {cycle.name} - - ))} +
) : (
diff --git a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx index e4eb8d2a2..3940e3a1e 100644 --- a/apps/app/pages/projects/[projectId]/issues/[issueId].tsx +++ b/apps/app/pages/projects/[projectId]/issues/[issueId].tsx @@ -1,4 +1,5 @@ // next +import Link from "next/link"; import type { NextPage } from "next"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; @@ -27,10 +28,12 @@ import AppLayout from "layouts/app-layout"; // components import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection"; +import AddAsSubIssue from "components/command-palette/addAsSubIssue"; +import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; // common import { debounce } from "constants/common"; // components -import IssueDetailSidebar from "components/project/issues/issue-detail/IssueDetailSidebar"; +import IssueDetailSidebar from "components/project/issues/issue-detail/issue-detail-sidebar"; // activites import IssueActivitySection from "components/project/issues/issue-detail/activity"; // ui @@ -46,9 +49,6 @@ import { EllipsisHorizontalIcon, PlusIcon, } from "@heroicons/react/24/outline"; -import Link from "next/link"; -import AddAsSubIssue from "components/command-palette/addAsSubIssue"; -import ConfirmIssueDeletion from "components/project/issues/confirm-issue-deletion"; const RichTextEditor = dynamic(() => import("components/lexical/editor"), { ssr: false, diff --git a/apps/app/pages/projects/[projectId]/settings.tsx b/apps/app/pages/projects/[projectId]/settings.tsx index 6e7800d0e..18d5ed567 100644 --- a/apps/app/pages/projects/[projectId]/settings.tsx +++ b/apps/app/pages/projects/[projectId]/settings.tsx @@ -13,6 +13,7 @@ import { Tab } from "@headlessui/react"; import withAuth from "lib/hoc/withAuthWrapper"; // layouts import SettingsLayout from "layouts/settings-layout"; +import AppLayout from "layouts/app-layout"; // service import projectServices from "lib/services/project.service"; // hooks @@ -155,14 +156,14 @@ const ProjectSettings: NextPage = () => { ]; return ( - } - links={sidebarLinks} + // links={sidebarLinks} > {projectDetails ? (
@@ -209,7 +210,7 @@ const ProjectSettings: NextPage = () => {
)} -
+ ); }; diff --git a/apps/app/pages/workspace/index.tsx b/apps/app/pages/workspace/index.tsx index 94921eaf2..3c671e5f7 100644 --- a/apps/app/pages/workspace/index.tsx +++ b/apps/app/pages/workspace/index.tsx @@ -18,9 +18,14 @@ import userService from "lib/services/user.service"; // ui import { Spinner } from "ui"; // icons -import { ArrowRightIcon } from "@heroicons/react/24/outline"; +import { ArrowRightIcon, CalendarDaysIcon } from "@heroicons/react/24/outline"; // types import type { IIssue } from "types"; +import { + addSpaceIfCamelCase, + findHowManyDaysLeft, + renderShortNumericDateFormat, +} from "constants/common"; const Workspace: NextPage = () => { const { user, activeWorkspace, projects } = useUser(); @@ -46,7 +51,7 @@ const Workspace: NextPage = () => { const hours = new Date().getHours(); return ( - +
{user ? (
@@ -78,49 +83,105 @@ const Workspace: NextPage = () => {
{myIssues ? ( myIssues.length > 0 ? ( - - - - - - - - - - {myIssues?.map((issue, index) => ( - - - - - - ))} - -
- ISSUE - - KEY - - STATUS -
- - {issue.name} - - - {issue.project_detail?.identifier}-{issue.sequence_id} - - - {issue.state_detail.name ?? "None"} - -
+
+
+
+
+

My Issues

+

{myIssues.length}

+
+
+
+ {myIssues.map((issue) => ( +
+ +
+
+ {issue.priority ?? "None"} +
+ +
+ + {addSpaceIfCamelCase(issue.state_detail.name)} +
+
+ + {issue.target_date + ? renderShortNumericDateFormat(issue.target_date) + : "N/A"} +
+
Target date
+
{renderShortNumericDateFormat(issue.target_date ?? "")}
+
+ {issue.target_date && + (issue.target_date < new Date().toISOString() + ? `Target date has passed by ${findHowManyDaysLeft( + issue.target_date + )} days` + : findHowManyDaysLeft(issue.target_date) <= 3 + ? `Target date is in ${findHowManyDaysLeft( + issue.target_date + )} days` + : "Target date")} +
+
+
+
+
+ ))} +
+
+
) : (

No Issues Found

From b189cae449772c81fd64e86ca7fe704b28f0b4a7 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 16 Dec 2022 11:45:38 +0530 Subject: [PATCH 02/10] build: create procfile for deployments to heroku --- Procfile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..8b63cf98c --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +web: node apps/app/server.js +backend_web: cd apiserver && gunicorn plane.wsgi -k gthread --workers 8 --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile - +worker: cd apiserver && python manage.py rqworker \ No newline at end of file From 2bacbedeb7a697ce28d6db25a3e0ed971490c88a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 16 Dec 2022 21:30:04 +0530 Subject: [PATCH 03/10] feat: create a dedicated endpoint to view logged in users project attributes --- apiserver/plane/api/urls.py | 7 +++++++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/project.py | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index ef53b4519..2b1dabbf2 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -58,6 +58,7 @@ from plane.api.views import ( ModuleIssueViewSet, UserLastProjectWithWorkspaceEndpoint, UserWorkSpaceIssues, + ProjectMemberUserEndpoint, ) from plane.api.views.project import AddTeamToProjectEndpoint @@ -320,6 +321,12 @@ urlpatterns = [ ProjectUserViewsEndpoint.as_view(), name="project-view", ), + path( + "workspaces//projects//project-member/me/", + ProjectMemberUserEndpoint.as_view(), + name="project-view", + ), + # End Projects # States path( "workspaces//projects//states/", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index b55342f87..5706b1994 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -10,6 +10,7 @@ from .project import ( AddMemberToProjectEndpoint, ProjectJoinEndpoint, ProjectUserViewsEndpoint, + ProjectMemberUserEndpoint, ) from .people import ( PeopleEndpoint, diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index dafc62743..3a18ef85d 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -625,3 +625,27 @@ class ProjectUserViewsEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + + +class ProjectMemberUserEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + try: + + project_member = ProjectMember.objects.get( + project=project_id, workpsace__slug=slug, member=request.user + ) + serializer = ProjectMemberSerializer(project_member) + + return Response(serializer.data, status=status.HTTP_200_OK) + + except ProjectMember.DoesNotExist: + return Response( + {"error": "User not a member of the project"}, + status=status.HTTP_404_NOT_FOUND, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) From 8aecc143c7f2626d60769b3707d1047076fe2972 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 16 Dec 2022 21:33:34 +0530 Subject: [PATCH 04/10] feat: update url --- apiserver/plane/api/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 2b1dabbf2..d4a9faa6f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -322,7 +322,7 @@ urlpatterns = [ name="project-view", ), path( - "workspaces//projects//project-member/me/", + "workspaces//projects//project-members/me/", ProjectMemberUserEndpoint.as_view(), name="project-view", ), From 278fd6cdd021024eb26d41394938e8f71aaf4097 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 16 Dec 2022 21:50:09 +0530 Subject: [PATCH 05/10] feat: modules, style: cycles, all menus --- apps/app/components/command-palette/index.tsx | 24 +- .../components/command-palette/shortcuts.tsx | 1 + .../project/cycles/board-view/index.tsx | 4 +- .../cycles/board-view/single-board.tsx | 20 +- ...eletion.tsx => confirm-cycle-deletion.tsx} | 0 ...odal.tsx => create-update-cycle-modal.tsx} | 0 ...tModal.tsx => cycle-issues-list-modal.tsx} | 62 +- .../project/cycles/list-view/index.tsx | 60 +- .../project/cycles/stats-view/index.tsx | 49 +- .../project/cycles/stats-view/single-stat.tsx | 169 +++-- .../project/issues/BoardView/single-board.tsx | 6 - .../CreateUpdateIssueModal/SelectCycles.tsx | 2 +- .../SelectParentIssue.tsx | 2 +- .../issues/CreateUpdateIssueModal/index.tsx | 2 +- .../project/issues/IssuesListModal.tsx | 180 ----- .../issues/issue-detail/add-as-sub-issue.tsx} | 0 .../issue-detail-sidebar/index.tsx | 151 +---- .../issue-detail-sidebar/select-assignee.tsx | 6 +- .../issue-detail-sidebar/select-blocked.tsx | 238 +++++++ .../issue-detail-sidebar/select-blocker.tsx | 238 +++++++ .../issue-detail-sidebar/select-cycle.tsx | 89 +-- .../issue-detail-sidebar/select-parent.tsx | 12 +- .../issue-detail-sidebar/select-priority.tsx | 58 +- .../issue-detail-sidebar/select-state.tsx | 109 ++- .../project/issues/issues-list-modal.tsx | 249 +++++++ .../modules/create-update-module-modal.tsx | 245 +++++++ .../components/sidebar/workspace-options.tsx | 4 +- apps/app/constants/api-routes.ts | 15 + apps/app/constants/fetch-keys.ts | 6 +- apps/app/layouts/DefaultLayout.tsx | 4 +- apps/app/layouts/Navbar/Sidebar.tsx | 629 ------------------ apps/app/layouts/Navbar/main-sidebar.tsx | 8 +- apps/app/layouts/app-layout.tsx | 6 +- apps/app/layouts/settings-layout.tsx | 8 +- apps/app/lib/services/modules.service.ts | 119 ++++ .../projects/[projectId]/cycles/[cycleId].tsx | 90 ++- .../projects/[projectId]/cycles/index.tsx | 183 +---- .../projects/[projectId]/issues/[issueId].tsx | 4 +- .../projects/[projectId]/issues/index.tsx | 3 + .../projects/[projectId]/modules/index.tsx | 104 +++ apps/app/pages/projects/index.tsx | 32 +- apps/app/types/{sprints.d.ts => cycles.d.ts} | 2 +- apps/app/types/index.d.ts | 3 +- apps/app/types/issues.d.ts | 15 +- apps/app/types/modules.d.ts | 28 + apps/app/ui/Button/index.tsx | 6 +- apps/app/ui/CustomMenu/index.tsx | 65 +- apps/app/ui/CustomMenu/types.d.ts | 5 +- apps/app/ui/custom-select/index.tsx | 78 +++ 49 files changed, 1863 insertions(+), 1530 deletions(-) rename apps/app/components/project/cycles/{ConfirmCycleDeletion.tsx => confirm-cycle-deletion.tsx} (100%) rename apps/app/components/project/cycles/{CreateUpdateCyclesModal.tsx => create-update-cycle-modal.tsx} (100%) rename apps/app/components/project/cycles/{CycleIssuesListModal.tsx => cycle-issues-list-modal.tsx} (77%) delete mode 100644 apps/app/components/project/issues/IssuesListModal.tsx rename apps/app/components/{command-palette/addAsSubIssue.tsx => project/issues/issue-detail/add-as-sub-issue.tsx} (100%) create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx create mode 100644 apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx create mode 100644 apps/app/components/project/issues/issues-list-modal.tsx create mode 100644 apps/app/components/project/modules/create-update-module-modal.tsx delete mode 100644 apps/app/layouts/Navbar/Sidebar.tsx create mode 100644 apps/app/lib/services/modules.service.ts create mode 100644 apps/app/pages/projects/[projectId]/modules/index.tsx rename apps/app/types/{sprints.d.ts => cycles.d.ts} (94%) create mode 100644 apps/app/types/modules.d.ts create mode 100644 apps/app/ui/custom-select/index.tsx diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/index.tsx index 32f71a3db..8aafbf6da 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/index.tsx @@ -24,7 +24,7 @@ import { import ShortcutsModal from "components/command-palette/shortcuts"; import CreateProjectModal from "components/project/create-project-modal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; -import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // ui import { Button } from "ui"; // types @@ -33,6 +33,7 @@ import { IIssue, IssueResponse } from "types"; import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; // constants import { classNames, copyTextToClipboard } from "constants/common"; +import CreateUpdateModuleModal from "components/project/modules/create-update-module-modal"; type FormInput = { issue_ids: string[]; @@ -47,6 +48,7 @@ const CommandPalette: React.FC = () => { const [isProjectModalOpen, setIsProjectModalOpen] = useState(false); const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const [isCreateCycleModalOpen, setIsCreateCycleModalOpen] = useState(false); + const [isCreateModuleModalOpen, setisCreateModuleModalOpen] = useState(false); const { activeWorkspace, activeProject, issues } = useUser(); @@ -109,6 +111,9 @@ const CommandPalette: React.FC = () => { } else if ((e.ctrlKey || e.metaKey) && e.key === "q") { e.preventDefault(); setIsCreateCycleModalOpen(true); + } else if ((e.ctrlKey || e.metaKey) && e.key === "m") { + e.preventDefault(); + setisCreateModuleModalOpen(true); } else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") { e.preventDefault(); @@ -184,11 +189,18 @@ const CommandPalette: React.FC = () => { {activeProject && ( - + <> + + + )} = ({ isOpen, setIsOpen }) => { { keys: "ctrl,p", description: "To create project" }, { keys: "ctrl,i", description: "To create issue" }, { keys: "ctrl,q", description: "To create cycle" }, + { keys: "ctrl,m", description: "To create module" }, { keys: "ctrl,h", description: "To open shortcuts guide" }, { keys: "ctrl,alt,c", diff --git a/apps/app/components/project/cycles/board-view/index.tsx b/apps/app/components/project/cycles/board-view/index.tsx index 07d68f20d..97ecd8290 100644 --- a/apps/app/components/project/cycles/board-view/index.tsx +++ b/apps/app/components/project/cycles/board-view/index.tsx @@ -14,8 +14,8 @@ type Props = { selectedGroup: NestedKeyOf | null; members: IProjectMember[] | undefined; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + openIssuesListModal: () => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const CyclesBoardView: React.FC = ({ diff --git a/apps/app/components/project/cycles/board-view/single-board.tsx b/apps/app/components/project/cycles/board-view/single-board.tsx index c437824e4..16b9cd0b4 100644 --- a/apps/app/components/project/cycles/board-view/single-board.tsx +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -50,8 +50,8 @@ type Props = { createdBy: string | null; bgColor?: string; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + openIssuesListModal: () => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const SingleCycleBoard: React.FC = ({ @@ -106,12 +106,6 @@ const SingleCycleBoard: React.FC = ({ backgroundColor: `${bgColor}20`, }} > -

= ({ leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - -
+ +
{(active) => ( diff --git a/apps/app/components/project/cycles/ConfirmCycleDeletion.tsx b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx similarity index 100% rename from apps/app/components/project/cycles/ConfirmCycleDeletion.tsx rename to apps/app/components/project/cycles/confirm-cycle-deletion.tsx diff --git a/apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx b/apps/app/components/project/cycles/create-update-cycle-modal.tsx similarity index 100% rename from apps/app/components/project/cycles/CreateUpdateCyclesModal.tsx rename to apps/app/components/project/cycles/create-update-cycle-modal.tsx diff --git a/apps/app/components/project/cycles/CycleIssuesListModal.tsx b/apps/app/components/project/cycles/cycle-issues-list-modal.tsx similarity index 77% rename from apps/app/components/project/cycles/CycleIssuesListModal.tsx rename to apps/app/components/project/cycles/cycle-issues-list-modal.tsx index d9f3226c0..8132ca19f 100644 --- a/apps/app/components/project/cycles/CycleIssuesListModal.tsx +++ b/apps/app/components/project/cycles/cycle-issues-list-modal.tsx @@ -145,37 +145,37 @@ const CycleIssuesListModal: React.FC = ({ )}
    {filteredIssues.map((issue) => { - // if (issue.cycle !== cycleId) - return ( - - classNames( - "flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", - active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" - ) - } - > - {({ selected }) => ( - <> - - - - {activeProject?.identifier}-{issue.sequence_id} - - {issue.name} - - )} - - ); + if (!issue.issue_cycle) + return ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none w-full rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ selected }) => ( + <> + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} + + )} + + ); })}
diff --git a/apps/app/components/project/cycles/list-view/index.tsx b/apps/app/components/project/cycles/list-view/index.tsx index 2ec0c6731..aa658f2a6 100644 --- a/apps/app/components/project/cycles/list-view/index.tsx +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -11,7 +11,7 @@ import cycleServices from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; // ui -import { Spinner } from "ui"; +import { CustomMenu, Spinner } from "ui"; // icons import { PlusIcon, EllipsisHorizontalIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; import { CalendarDaysIcon } from "@heroicons/react/24/outline"; @@ -35,7 +35,7 @@ type Props = { selectedGroup: NestedKeyOf | null; openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => void; openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + removeIssueFromCycle: (bridgeId: string) => void; }; const CyclesListView: React.FC = ({ @@ -241,51 +241,19 @@ const CyclesListView: React.FC = ({
)} - - + openCreateIssueModal(issue, "edit")} > - - - - - - - -
- -
-
- -
- -
-
-
-
+ Edit + + removeIssueFromCycle(issue.bridge ?? "")} + > + Remove from cycle + + Delete permanently +

); diff --git a/apps/app/components/project/cycles/stats-view/index.tsx b/apps/app/components/project/cycles/stats-view/index.tsx index 4eb14608b..a72d57b13 100644 --- a/apps/app/components/project/cycles/stats-view/index.tsx +++ b/apps/app/components/project/cycles/stats-view/index.tsx @@ -1,20 +1,53 @@ -// next -import Link from "next/link"; -// hooks -import useUser from "lib/hooks/useUser"; +// react +import { useState } from "react"; +// components +import SingleStat from "components/project/cycles/stats-view/single-stat"; +import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion"; // types -import { ICycle } from "types"; -import SingleStat from "./single-stat"; +import { ICycle, SelectSprintType } from "types"; type Props = { cycles: ICycle[]; + setCreateUpdateCycleModal: React.Dispatch>; + setSelectedCycle: React.Dispatch>; }; -const CycleStatsView: React.FC = ({ cycles }) => { +const CycleStatsView: React.FC = ({ + cycles, + setCreateUpdateCycleModal, + setSelectedCycle, +}) => { + const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); + + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); + setCycleDeleteModal(true); + }; + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycle({ ...cycle, actionType: "edit" }); + setCreateUpdateCycleModal(true); + }; + return ( <> + {cycles.map((cycle) => ( - + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + /> ))} ); diff --git a/apps/app/components/project/cycles/stats-view/single-stat.tsx b/apps/app/components/project/cycles/stats-view/single-stat.tsx index ae5301fe9..f2382e279 100644 --- a/apps/app/components/project/cycles/stats-view/single-stat.tsx +++ b/apps/app/components/project/cycles/stats-view/single-stat.tsx @@ -1,37 +1,46 @@ +// react +import React, { useState } from "react"; // next import Link from "next/link"; +import Image from "next/image"; // swr import useSWR from "swr"; // services import cyclesService from "lib/services/cycles.service"; // hooks import useUser from "lib/hooks/useUser"; +// ui +import { Button, CustomMenu } from "ui"; // types import { CycleIssueResponse, ICycle } from "types"; // fetch-keys import { CYCLE_ISSUES } from "constants/fetch-keys"; import { groupBy, renderShortNumericDateFormat } from "constants/common"; -import { - CheckIcon, - ExclamationCircleIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; +import { ArrowPathIcon, CheckIcon, UserIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; -type Props = { cycle: ICycle }; - -const stateGroupIcons: { - [key: string]: JSX.Element; -} = { - backlog: , - unstarted: , - started: , - cancelled: , - completed: , +type Props = { + cycle: ICycle; + handleEditCycle: () => void; + handleDeleteCycle: () => void; }; -const SingleStat: React.FC = ({ cycle }) => { +const stateGroupColours: { + [key: string]: string; +} = { + backlog: "#3f76ff", + unstarted: "#ff9e9e", + started: "#d687ff", + cancelled: "#ff5353", + completed: "#096e8d", +}; + +const SingleStat: React.FC = ({ cycle, handleEditCycle, handleDeleteCycle }) => { const { activeWorkspace, activeProject } = useUser(); + const router = useRouter(); + const { data: cycleIssues } = useSWR( activeWorkspace && activeProject && cycle.id ? CYCLE_ISSUES(cycle.id as string) : null, activeWorkspace && activeProject && cycle.id @@ -48,7 +57,6 @@ const SingleStat: React.FC = ({ cycle }) => { ...groupBy(cycleIssues ?? [], "issue_details.state_detail.group"), }; - // status calculator const startDate = new Date(cycle.start_date ?? ""); const endDate = new Date(cycle.end_date ?? ""); const today = new Date(); @@ -56,43 +64,110 @@ const SingleStat: React.FC = ({ cycle }) => { return ( <>
-
-
+
+
- {cycle.name} + +

{cycle.name}

+
+ + {today.getDate() < startDate.getDate() + ? "Not started" + : today.getDate() > endDate.getDate() + ? "Over" + : "Active"} + + + Edit cycle + + Delete cycle permanently + + +
+
-
- {renderShortNumericDateFormat(startDate)} - {" - "} - {renderShortNumericDateFormat(endDate)} + +
+
+ + Cycle dates +
+
+ {renderShortNumericDateFormat(startDate)} - {renderShortNumericDateFormat(endDate)} +
+
+ + Created by +
+
+ {cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( + {cycle.owned_by.first_name} + ) : ( + + {cycle.owned_by.first_name.charAt(0)} + + )} + {cycle.owned_by.first_name} +
+
+ + Active members +
+
+
+
+ +
-
- {today.getDate() < startDate.getDate() - ? "Not started" - : today.getDate() > endDate.getDate() - ? "Over" - : "Active"} -
-
-
-
- {Object.keys(groupedIssues).map((group) => { - return ( -
-
-
- {stateGroupIcons[group]} +
+

PROGRESS

+
+ {Object.keys(groupedIssues).map((group) => { + return ( +
+
+ +
{group}
+
+
+ + {groupedIssues[group].length}{" "} + + -{" "} + {cycleIssues && cycleIssues.length > 0 + ? `${(groupedIssues[group].length / cycleIssues.length) * 100}%` + : "0%"} + +
-
-
{group}
- {groupedIssues[group].length} -
-
- ); - })} + ); + })} +
+
diff --git a/apps/app/components/project/issues/BoardView/single-board.tsx b/apps/app/components/project/issues/BoardView/single-board.tsx index 7e843a8ca..18c16304b 100644 --- a/apps/app/components/project/issues/BoardView/single-board.tsx +++ b/apps/app/components/project/issues/BoardView/single-board.tsx @@ -130,12 +130,6 @@ const SingleBoard: React.FC = ({ backgroundColor: `${bgColor}20`, }} > -

; diff --git a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx index c7bcd1ad5..e872d4753 100644 --- a/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx +++ b/apps/app/components/project/issues/CreateUpdateIssueModal/index.tsx @@ -33,7 +33,7 @@ import SelectPriority from "./SelectPriority"; import SelectAssignee from "./SelectAssignee"; import SelectParent from "./SelectParentIssue"; import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal"; -import CreateUpdateCycleModal from "components/project/cycles/CreateUpdateCyclesModal"; +import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; // types import type { IIssue, IssueResponse, CycleIssueResponse } from "types"; import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline"; diff --git a/apps/app/components/project/issues/IssuesListModal.tsx b/apps/app/components/project/issues/IssuesListModal.tsx deleted file mode 100644 index b635a16b6..000000000 --- a/apps/app/components/project/issues/IssuesListModal.tsx +++ /dev/null @@ -1,180 +0,0 @@ -// react -import React, { useState } from "react"; -// headless ui -import { Combobox, Dialog, Transition } from "@headlessui/react"; -// ui -import { Button } from "ui"; -// icons -import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; -// types -import { IIssue } from "types"; -import { classNames } from "constants/common"; -import useUser from "lib/hooks/useUser"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - value?: any; - onChange: (...event: any[]) => void; - issues: IIssue[]; - title?: string; - multiple?: boolean; - customDisplay?: JSX.Element; -}; - -const IssuesListModal: React.FC = ({ - isOpen, - handleClose: onClose, - value, - onChange, - issues, - title = "Issues", - multiple = false, - customDisplay, -}) => { - const [query, setQuery] = useState(""); - const [values, setValues] = useState([]); - - const { activeProject } = useUser(); - - const handleClose = () => { - onClose(); - setQuery(""); - setValues([]); - }; - - const filteredIssues: IIssue[] = - query === "" - ? issues ?? [] - : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; - - return ( - <> - setQuery("")} appear> - - -
- - -
- - - { - if (multiple) setValues(val); - else onChange(val); - }} - // multiple={multiple} - > -
-
-
{customDisplay}
- - {filteredIssues.length > 0 && ( -
  • - {query === "" && ( -

    - {title} -

    - )} -
      - {filteredIssues.map((issue) => ( - - classNames( - "flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2", - active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" - ) - } - onClick={() => { - if (!multiple) handleClose(); - }} - > - {({ selected }) => ( - <> - {multiple ? ( - - ) : null} - - - {activeProject?.identifier}-{issue.sequence_id} - {" "} - {issue.name} - - )} - - ))} -
    -
  • - )} -
    - - {query !== "" && filteredIssues.length === 0 && ( -
    -
    - )} -
    - {multiple ? ( -
    - - -
    - ) : null} -
    -
    -
    -
    -
    - - ); -}; - -export default IssuesListModal; diff --git a/apps/app/components/command-palette/addAsSubIssue.tsx b/apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx similarity index 100% rename from apps/app/components/command-palette/addAsSubIssue.tsx rename to apps/app/components/project/issues/issue-detail/add-as-sub-issue.tsx diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx index 0e839ca94..b1b6bc15e 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx @@ -6,33 +6,23 @@ import { Listbox, Transition } from "@headlessui/react"; // react hook form import { useForm, Controller, UseFormWatch } from "react-hook-form"; // services -import stateServices from "lib/services/state.service"; import issuesServices from "lib/services/issues.service"; -import workspaceService from "lib/services/workspace.service"; // hooks import useUser from "lib/hooks/useUser"; import useToast from "lib/hooks/useToast"; -// components -import IssuesListModal from "components/project/issues/IssuesListModal"; // fetching keys -import { STATE_LIST, WORKSPACE_MEMBERS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; // commons -import { classNames, copyTextToClipboard } from "constants/common"; -import { PRIORITIES } from "constants/"; +import { copyTextToClipboard } from "constants/common"; // ui import { Input, Button, Spinner } from "ui"; import { Popover } from "@headlessui/react"; // icons import { - UserIcon, TagIcon, - UserGroupIcon, ChevronDownIcon, - Squares2X2Icon, - ChartBarIcon, ClipboardDocumentIcon, LinkIcon, - ArrowPathIcon, CalendarDaysIcon, TrashIcon, PlusIcon, @@ -40,7 +30,7 @@ import { } from "@heroicons/react/24/outline"; // types import type { Control } from "react-hook-form"; -import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; +import type { IIssue, IIssueLabels, NestedKeyOf } from "types"; import { TwitterPicker } from "react-color"; import { positionEditorElement } from "components/lexical/helpers/editor"; import SelectState from "./select-state"; @@ -48,6 +38,8 @@ import SelectPriority from "./select-priority"; import SelectParent from "./select-parent"; import SelectCycle from "./select-cycle"; import SelectAssignee from "./select-assignee"; +import SelectBlocker from "./select-blocker"; +import SelectBlocked from "./select-blocked"; type Props = { control: Control; @@ -69,11 +61,9 @@ const IssueDetailSidebar: React.FC = ({ watch: watchIssue, setDeleteIssueModal, }) => { - const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); - const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false); - const { activeWorkspace, activeProject, cycles, issues } = useUser(); + const { activeWorkspace, activeProject, issues } = useUser(); const { setToastAlert } = useToast(); @@ -106,43 +96,6 @@ const IssueDetailSidebar: React.FC = ({ }); }; - const sidebarSections: Array< - Array<{ - label: string; - name: NestedKeyOf; - canSelectMultipleOptions: boolean; - icon: (props: any) => JSX.Element; - options?: Array<{ label: string; value: any; color?: string }>; - modal: boolean; - issuesList?: Array; - isOpen?: boolean; - setIsOpen?: (arg: boolean) => void; - }> - > = [ - [ - // { - // label: "Blocker", - // name: "blockers_list", - // canSelectMultipleOptions: true, - // icon: UserIcon, - // issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [], - // modal: true, - // isOpen: isBlockerModalOpen, - // setIsOpen: setIsBlockerModalOpen, - // }, - // { - // label: "Blocked", - // name: "blocked_list", - // canSelectMultipleOptions: true, - // icon: UserIcon, - // issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [], - // modal: true, - // isOpen: isBlockedModalOpen, - // setIsOpen: setIsBlockedModalOpen, - // }, - ], - ]; - const handleCycleChange = (cycleId: string) => { if (activeWorkspace && activeProject && issueDetail) issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { @@ -150,6 +103,8 @@ const IssueDetailSidebar: React.FC = ({ }); }; + console.log(issueDetail); + return ( <>
    @@ -215,7 +170,7 @@ const IssueDetailSidebar: React.FC = ({
    - +
    = ({
    ) } - watchIssue={watchIssue} + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} />
    @@ -274,72 +239,6 @@ const IssueDetailSidebar: React.FC = ({
    - {/* {sidebarSections.map((section, index) => ( -
    - {section.map((item) => ( -
    -
    - -

    {item.label}

    -
    -
    - ( - <> - item.setIsOpen && item.setIsOpen(false)} - onChange={(val) => { - console.log(val); - submitChanges({ [item.name]: val }); - onChange(val); - }} - issues={item?.issuesList ?? []} - title={`Select ${item.label}`} - multiple={item.canSelectMultipleOptions} - value={value} - customDisplay={ - issueDetail?.parent_detail ? ( - - ) : ( -
    - No parent selected -
    - ) - } - /> - - - )} - /> -
    -
    - ))} -
    - ))} */}
    @@ -376,7 +275,7 @@ const IssueDetailSidebar: React.FC = ({ as="div" value={value} multiple - onChange={(val) => submitChanges({ labels_list: val })} + onChange={(val: any) => submitChanges({ labels_list: val })} className="flex-shrink-0" > {({ open }) => ( @@ -395,7 +294,7 @@ const IssueDetailSidebar: React.FC = ({ leaveTo="opacity-0" > -
    +
    {issueLabels ? ( issueLabels.length > 0 ? ( issueLabels.map((label: IIssueLabels) => ( @@ -403,10 +302,8 @@ const IssueDetailSidebar: React.FC = ({ key={label.id} className={({ active, selected }) => `${ - active || selected - ? "text-white bg-theme" - : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate` } value={label.id} > diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx index 0fff8006b..c5451c548 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-assignee.tsx @@ -126,7 +126,7 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { leaveTo="opacity-0" > -
    +
    {people ? ( people.length > 0 ? ( people.map((option) => ( @@ -134,8 +134,8 @@ const SelectAssignee: React.FC = ({ control, submitChanges }) => { key={option.member.id} className={({ active, selected }) => `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 rounded-md truncate` } value={option.member.id} > diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx new file mode 100644 index 000000000..9c3b10093 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocked.tsx @@ -0,0 +1,238 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { + FolderIcon, + MagnifyingGlassIcon, + UserGroupIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; + +type FormInput = { + issue_ids: string[]; +}; + +type Props = { + submitChanges: (formData: Partial) => void; + issuesList: IIssue[]; + watch: UseFormWatch; +}; + +const SelectBlocked: React.FC = ({ submitChanges, issuesList, watch }) => { + const [query, setQuery] = useState(""); + const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false); + + const { activeProject, issues } = useUser(); + const { setToastAlert } = useToast(); + + const { register, handleSubmit, reset, watch: watchIssues } = useForm(); + + const handleClose = () => { + setIsBlockedModalOpen(false); + reset(); + }; + + const onSubmit: SubmitHandler = (data) => { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + const newBlocked = [...watch("blocked_list"), ...data.issue_ids]; + submitChanges({ blocked_list: newBlocked }); + handleClose(); + }; + + return ( +
    +
    + +

    Blocked issues

    +
    +
    +
    + {watch("blocked_list") && watch("blocked_list").length > 0 + ? watch("blocked_list").map((issue) => ( + { + const updatedBlockers = watch("blocked_list").filter((i) => i !== issue); + submitChanges({ + blocked_list: updatedBlockers, + }); + }} + > + {`${activeProject?.identifier}-${ + issues?.results.find((i) => i.id === issue)?.sequence_id + }`} + + + )) + : null} +
    + setQuery("")} + appear + > + + +
    + + +
    + + + + +
    +
    + + + {issuesList.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Select blocked issues +

    + )} +
      + {issuesList.map((issue) => { + if (!watch("blocked_list").includes(issue.id)) { + return ( + + classNames( + "flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ active }) => ( + <> +
      + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} +
      + + )} +
      + ); + } + })} +
    +
  • + + )} +
    + + {query !== "" && issuesList.length === 0 && ( +
    +
    + )} +
    + +
    + +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + ); +}; + +export default SelectBlocked; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx new file mode 100644 index 000000000..ee2352620 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-blocker.tsx @@ -0,0 +1,238 @@ +// react +import React, { useState } from "react"; +// react-hook-form +import { SubmitHandler, useForm, UseFormWatch } from "react-hook-form"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { + FolderIcon, + MagnifyingGlassIcon, + UserGroupIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +// constants +import { classNames } from "constants/common"; + +type FormInput = { + issue_ids: string[]; +}; + +type Props = { + submitChanges: (formData: Partial) => void; + issuesList: IIssue[]; + watch: UseFormWatch; +}; + +const SelectBlocker: React.FC = ({ submitChanges, issuesList, watch }) => { + const [query, setQuery] = useState(""); + const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false); + + const { activeProject, issues } = useUser(); + const { setToastAlert } = useToast(); + + const { register, handleSubmit, reset } = useForm(); + + const handleClose = () => { + setIsBlockerModalOpen(false); + reset(); + }; + + const onSubmit: SubmitHandler = (data) => { + if (!data.issue_ids || data.issue_ids.length === 0) { + setToastAlert({ + title: "Error", + type: "error", + message: "Please select atleast one issue", + }); + return; + } + const newBlockers = [...watch("blockers_list"), ...data.issue_ids]; + submitChanges({ blockers_list: newBlockers }); + handleClose(); + }; + + return ( +
    +
    + +

    Blocker issues

    +
    +
    +
    + {watch("blockers_list") && watch("blockers_list").length > 0 + ? watch("blockers_list").map((issue) => ( + { + const updatedBlockers = watch("blockers_list").filter((i) => i !== issue); + submitChanges({ + blockers_list: updatedBlockers, + }); + }} + > + {`${activeProject?.identifier}-${ + issues?.results.find((i) => i.id === issue)?.sequence_id + }`} + + + )) + : null} +
    + setQuery("")} + appear + > + + +
    + + +
    + + +
    + +
    +
    + + + {issuesList.length > 0 && ( + <> +
  • + {query === "" && ( +

    + Select blocker issues +

    + )} +
      + {issuesList.map((issue) => { + if (!watch("blockers_list").includes(issue.id)) { + return ( + + classNames( + "flex items-center justify-between cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ active }) => ( + <> +
      + + + + {activeProject?.identifier}-{issue.sequence_id} + + {issue.name} +
      + + )} +
      + ); + } + })} +
    +
  • + + )} +
    + + {query !== "" && issuesList.length === 0 && ( +
    +
    + )} +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + ); +}; + +export default SelectBlocker; diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx index 0e36c54af..80dd4a899 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx @@ -10,6 +10,7 @@ import { classNames } from "constants/common"; import { Spinner } from "ui"; import React from "react"; import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; @@ -30,64 +31,38 @@ const SelectCycle: React.FC = ({ control, handleCycleChange }) => { control={control} name="cycle" render={({ field: { value } }) => ( - { - handleCycleChange(value); - }} - className="flex-shrink-0" - > - {({ open }) => ( -
    - - - {value ? cycles?.find((c) => c.id === value)?.name : "None"} - - - - - + - -
    - {cycles ? ( - cycles.length > 0 ? ( - cycles.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.id} - > - {option.name} - - )) - ) : ( -
    No cycles found
    - ) - ) : ( - - )} -
    -
    -
    -
    - )} -
    + {value ? cycles?.find((c) => c.id === value)?.name : "None"} + + } + value={value} + onChange={(value: any) => { + handleCycleChange(value); + }} + > + {cycles ? ( + cycles.length > 0 ? ( + cycles.map((option) => ( + + {option.name} + + )) + ) : ( +
    No cycles found
    + ) + ) : ( + + )} + + )} />
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx index 03cf0be4e..3ec5922fe 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-parent.tsx @@ -5,7 +5,7 @@ import { Control, Controller, UseFormWatch } from "react-hook-form"; // hooks import useUser from "lib/hooks/useUser"; // components -import IssuesListModal from "components/project/issues/IssuesListModal"; +import IssuesListModal from "components/project/issues/issues-list-modal"; // icons import { UserIcon } from "@heroicons/react/24/outline"; // types @@ -16,7 +16,7 @@ type Props = { submitChanges: (formData: Partial) => void; issuesList: IIssue[]; customDisplay: JSX.Element; - watchIssue: UseFormWatch; + watch: UseFormWatch; }; const SelectParent: React.FC = ({ @@ -24,7 +24,7 @@ const SelectParent: React.FC = ({ submitChanges, issuesList, customDisplay, - watchIssue, + watch, }) => { const [isParentModalOpen, setIsParentModalOpen] = useState(false); @@ -60,11 +60,11 @@ const SelectParent: React.FC = ({ className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 w-full" onClick={() => setIsParentModalOpen(true)} > - {watchIssue("parent") && watchIssue("parent") !== "" + {watch("parent") && watch("parent") !== "" ? `${activeProject?.identifier}-${ - issues?.results.find((i) => i.id === watchIssue("parent"))?.sequence_id + issues?.results.find((i) => i.id === watch("parent"))?.sequence_id }` - : "Select Parent"} + : "Select issue"}
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx index e76beeb25..7c098f957 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx @@ -1,7 +1,7 @@ // react import React from "react"; // react-hook-form -import { Control, Controller } from "react-hook-form"; +import { Control, Controller, UseFormWatch } from "react-hook-form"; // headless ui import { Listbox, Transition } from "@headlessui/react"; // icons @@ -11,13 +11,15 @@ import { IIssue } from "types"; // constants import { classNames } from "constants/common"; import { PRIORITIES } from "constants/"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; submitChanges: (formData: Partial) => void; + watch: UseFormWatch; }; -const SelectPriority: React.FC = ({ control, submitChanges }) => { +const SelectPriority: React.FC = ({ control, submitChanges, watch }) => { return (
    @@ -29,51 +31,23 @@ const SelectPriority: React.FC = ({ control, submitChanges }) => { control={control} name="state" render={({ field: { value } }) => ( - + {watch("priority") && watch("priority") !== "" ? watch("priority") : "None"} + + } value={value} onChange={(value: any) => { submitChanges({ priority: value }); }} - className="flex-shrink-0" > - {({ open }) => ( -
    - - - {value} - - - - - - -
    - {PRIORITIES.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate capitalize` - } - value={option} - > - {option} - - ))} -
    -
    -
    -
    - )} -
    + {PRIORITIES.map((option) => ( + + {option} + + ))} + )} />
    diff --git a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx index cc129c536..e55bd9b14 100644 --- a/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx @@ -7,9 +7,10 @@ import { Listbox, Transition } from "@headlessui/react"; // types import { IIssue } from "types"; import { classNames } from "constants/common"; -import { Spinner } from "ui"; +import { CustomMenu, Spinner } from "ui"; import React from "react"; import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; +import CustomSelect from "ui/custom-select"; type Props = { control: Control; @@ -30,82 +31,56 @@ const SelectState: React.FC = ({ control, submitChanges }) => { control={control} name="state" render={({ field: { value } }) => ( - + {value ? ( + <> + option.id === value)?.color, + }} + > + {states?.find((option) => option.id === value)?.name} + + ) : ( + "None" + )} + + } value={value} onChange={(value: any) => { submitChanges({ state: value }); }} - className="flex-shrink-0" > - {({ open }) => ( -
    - - - {value ? ( - <> + {states ? ( + states.length > 0 ? ( + states.map((option) => ( + + <> + {option.color && ( option.id === value)?.color, - }} + style={{ backgroundColor: option.color }} > - {states?.find((option) => option.id === value)?.name} - - ) : ( - "None" - )} - - - - - - -
    - {states ? ( - states.length > 0 ? ( - states.map((option) => ( - - `${ - active || selected ? "text-white bg-theme" : "text-gray-900" - } flex items-center gap-2 cursor-pointer select-none relative p-2 rounded-md truncate` - } - value={option.id} - > - {option.color && ( - - )} - {option.name} - - )) - ) : ( -
    No states found
    - ) - ) : ( - )} -
    -
    -
    -
    + {option.name} + + + )) + ) : ( +
    No states found
    + ) + ) : ( + )} -
    + )} />
    diff --git a/apps/app/components/project/issues/issues-list-modal.tsx b/apps/app/components/project/issues/issues-list-modal.tsx new file mode 100644 index 000000000..b158aab3b --- /dev/null +++ b/apps/app/components/project/issues/issues-list-modal.tsx @@ -0,0 +1,249 @@ +// react +import React, { useState } from "react"; +// headless ui +import { Combobox, Dialog, Transition } from "@headlessui/react"; +// ui +import { Button } from "ui"; +// icons +import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; +// types +import { IIssue } from "types"; +import { classNames } from "constants/common"; +import useUser from "lib/hooks/useUser"; + +type Props = { + isOpen: boolean; + handleClose: () => void; + value?: any; + onChange: (...event: any[]) => void; + issues: IIssue[]; + title?: string; + multiple?: boolean; + customDisplay?: JSX.Element; +}; + +const IssuesListModal: React.FC = ({ + isOpen, + handleClose: onClose, + value, + onChange, + issues, + title = "Issues", + multiple = false, + customDisplay, +}) => { + const [query, setQuery] = useState(""); + const [values, setValues] = useState([]); + + const { activeProject } = useUser(); + + const handleClose = () => { + onClose(); + setQuery(""); + setValues([]); + }; + + const filteredIssues: IIssue[] = + query === "" + ? issues ?? [] + : issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? []; + + return ( + <> + setQuery("")} appear> + + +
    + + +
    + + + {multiple ? ( + <> + { + // setValues(val); + console.log(val); + }} + multiple + > +
    +
    +
    {customDisplay}
    + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + {title} +

    + )} +
      + {filteredIssues.map((issue) => ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + > + {({ selected }) => ( + <> + + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.id} + + )} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    +
    + + +
    + + ) : ( + +
    +
    +
    {customDisplay}
    + + {filteredIssues.length > 0 && ( +
  • + {query === "" && ( +

    + {title} +

    + )} +
      + {filteredIssues.map((issue) => ( + + classNames( + "flex items-center gap-2 cursor-pointer select-none rounded-md px-3 py-2", + active ? "bg-gray-900 bg-opacity-5 text-gray-900" : "" + ) + } + onClick={() => handleClose()} + > + {({ selected }) => ( + <> + + + {activeProject?.identifier}-{issue.sequence_id} + {" "} + {issue.name} + + )} + + ))} +
    +
  • + )} +
    + + {query !== "" && filteredIssues.length === 0 && ( +
    +
    + )} +
    + )} +
    +
    +
    +
    +
    + + ); +}; + +export default IssuesListModal; diff --git a/apps/app/components/project/modules/create-update-module-modal.tsx b/apps/app/components/project/modules/create-update-module-modal.tsx new file mode 100644 index 000000000..d7059d345 --- /dev/null +++ b/apps/app/components/project/modules/create-update-module-modal.tsx @@ -0,0 +1,245 @@ +import React, { useEffect } from "react"; +// swr +import { mutate } from "swr"; +// react hook form +import { useForm } from "react-hook-form"; +// headless +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button, Input, TextArea, Select } from "ui"; +// services +import modulesService from "lib/services/modules.service"; +// hooks +import useUser from "lib/hooks/useUser"; +// types +import type { IModule } from "types"; +// common +import { renderDateFormat } from "constants/common"; +// fetch keys +import { MODULE_LIST } from "constants/fetch-keys"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; + projectId: string; + data?: IModule; +}; + +const defaultValues: Partial = { + name: "", + description: "", +}; + +const CreateUpdateModuleModal: React.FC = ({ isOpen, setIsOpen, data, projectId }) => { + const handleClose = () => { + setIsOpen(false); + const timeout = setTimeout(() => { + reset(defaultValues); + clearTimeout(timeout); + }, 500); + }; + + const { activeWorkspace } = useUser(); + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + reset, + setError, + } = useForm({ + defaultValues, + }); + + const onSubmit = async (formData: IModule) => { + if (!activeWorkspace) return; + const payload = { + ...formData, + start_date: formData.start_date ? renderDateFormat(formData.start_date) : null, + target_date: formData.target_date ? renderDateFormat(formData.target_date) : null, + }; + if (!data) { + await modulesService + .createModule(activeWorkspace.slug, projectId, payload) + .then((res) => { + mutate( + MODULE_LIST(projectId), + (prevData) => [res, ...(prevData ?? [])], + false + ); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof typeof defaultValues, { + message: err[key].join(", "), + }); + }); + }); + } else { + await modulesService + .updateModule(activeWorkspace.slug, projectId, data.id, payload) + .then((res) => { + mutate( + MODULE_LIST(projectId), + (prevData) => { + const newData = prevData?.map((item) => { + if (item.id === res.id) { + return res; + } + return item; + }); + return newData; + }, + false + ); + handleClose(); + }) + .catch((err) => { + Object.keys(err).map((key) => { + setError(key as keyof typeof defaultValues, { + message: err[key].join(", "), + }); + }); + }); + } + }; + + useEffect(() => { + if (data) { + setIsOpen(true); + reset(data); + } else { + reset(defaultValues); + } + }, [data, setIsOpen, reset]); + + return ( + + + +
    + + +
    +
    + + +
    +
    + + {data ? "Update" : "Create"} Module + +
    +
    + +
    +
    +