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 diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index ef53b4519..d4a9faa6f 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-members/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, + ) diff --git a/app.json b/app.json index 34941e9ab..017911920 100644 --- a/app.json +++ b/app.json @@ -5,16 +5,24 @@ "logo": "https://avatars.githubusercontent.com/u/115727700?s=200&v=4", "website": "https://plane.so/", "success_url": "/", - "stack": "container", + "stack": "heroku-22", "keywords": ["plane", "project management", "django", "next"], "addons": ["heroku-postgresql:mini", "heroku-redis:mini"], + "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-python.git" + }, + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs#v176" + } + ], "env": { "EMAIL_HOST": { "description": "Email host to send emails from", "value": "" }, "EMAIL_HOST_USER": { - "description" : "Email host to send emails from", + "description": "Email host to send emails from", "value": "" }, "EMAIL_HOST_PASSWORD": { @@ -22,7 +30,7 @@ "value": "" }, "AWS_REGION": { - "description" : "AWS Region to use for S3", + "description": "AWS Region to use for S3", "value": "false" }, "AWS_ACCESS_KEY_ID": { @@ -66,4 +74,4 @@ "value": "" } } -} \ No newline at end of file +} 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 && ( - + <> + + + )} | 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 87% rename from apps/app/components/project/cycles/BoardView/index.tsx rename to apps/app/components/project/cycles/board-view/index.tsx index 8026786c4..97ecd8290 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,13 +13,9 @@ type Props = { properties: Properties; selectedGroup: NestedKeyOf | null; members: IProjectMember[] | undefined; - openCreateIssueModal: ( - sprintId: string, - issue?: IIssue, - actionType?: "create" | "edit" | "delete" - ) => void; - openIssuesListModal: (cycleId: string) => void; - removeIssueFromCycle: (cycleId: string, bridgeId: string) => void; + openCreateIssueModal: (issue?: IIssue, actionType?: "create" | "edit" | "delete") => 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 new file mode 100644 index 000000000..16b9cd0b4 --- /dev/null +++ b/apps/app/components/project/cycles/board-view/single-board.tsx @@ -0,0 +1,371 @@ +// 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: () => void; + removeIssueFromCycle: (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/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 new file mode 100644 index 000000000..aa658f2a6 --- /dev/null +++ b/apps/app/components/project/cycles/list-view/index.tsx @@ -0,0 +1,320 @@ +// 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 { CustomMenu, 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: (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")} +
+
+
+ )} + + openCreateIssueModal(issue, "edit")} + > + Edit + + removeIssueFromCycle(issue.bridge ?? "")} + > + Remove from cycle + + Delete permanently + +
+
+ ); + }) + ) : ( +

No issues.

+ ) + ) : ( +
+ +
+ )} +
+
+
+
+ +
+
+ )} +
+ ))} +
+ // + // + // + // + // >; + setSelectedCycle: React.Dispatch>; +}; + +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)} + /> + ))} + + ); +}; + +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..f2382e279 --- /dev/null +++ b/apps/app/components/project/cycles/stats-view/single-stat.tsx @@ -0,0 +1,177 @@ +// 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 { ArrowPathIcon, CheckIcon, UserIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; + +type Props = { + cycle: ICycle; + handleEditCycle: () => void; + handleDeleteCycle: () => void; +}; + +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 + ? () => + 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"), + }; + + const startDate = new Date(cycle.start_date ?? ""); + const endDate = new Date(cycle.end_date ?? ""); + const today = new Date(); + + return ( + <> +
+
+
+ + +

{cycle.name}

+
+ + {today.getDate() < startDate.getDate() + ? "Not started" + : today.getDate() > endDate.getDate() + ? "Over" + : "Active"} + + + Edit cycle + + Delete cycle permanently + + +
+
+ + +
+
+ + 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 +
+
+
+
+ + +
+
+
+

PROGRESS

+
+ {Object.keys(groupedIssues).map((group) => { + return ( +
+
+ +
{group}
+
+
+ + {groupedIssues[group].length}{" "} + + -{" "} + {cycleIssues && cycleIssues.length > 0 + ? `${(groupedIssues[group].length / cycleIssues.length) * 100}%` + : "0%"} + + +
+
+ ); + })} +
+
+
+
+
+ + ); +}; + +export default SingleStat; 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 41abbd348..c7cc69c3c 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/create-update-state-modal"; -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 aba9a0c34..000000000 --- a/apps/app/components/project/issues/IssuesListModal.tsx +++ /dev/null @@ -1,177 +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; -}; - -const IssuesListModal: React.FC = ({ - isOpen, - handleClose: onClose, - value, - onChange, - issues, - title = "Issues", - multiple = false, -}) => { - 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} - > -
-
- - - {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/project/issues/issue-detail/IssueDetailSidebar.tsx b/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx deleted file mode 100644 index c6e1eb573..000000000 --- a/apps/app/components/project/issues/issue-detail/IssueDetailSidebar.tsx +++ /dev/null @@ -1,641 +0,0 @@ -import React, { useState } from "react"; -// swr -import useSWR from "swr"; -// headless ui -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 { - PROJECT_ISSUES_LIST, - STATE_LIST, - WORKSPACE_MEMBERS, - PROJECT_ISSUE_LABELS, -} from "constants/fetch-keys"; -// commons -import { classNames, copyTextToClipboard } from "constants/common"; -import { PRIORITIES } from "constants/"; -// 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, - XMarkIcon, -} from "@heroicons/react/24/outline"; -// types -import type { Control } from "react-hook-form"; -import type { IIssue, IIssueLabels, IssueResponse, IState, NestedKeyOf } from "types"; -import { TwitterPicker } from "react-color"; -import { positionEditorElement } from "components/lexical/helpers/editor"; - -type Props = { - control: Control; - submitChanges: (formData: Partial) => void; - issueDetail: IIssue | undefined; - watch: UseFormWatch; - setDeleteIssueModal: React.Dispatch>; -}; - -const defaultValues: Partial = { - name: "", - colour: "#ff0000", -}; - -const IssueDetailSidebar: React.FC = ({ - control, - submitChanges, - issueDetail, - watch: watchIssue, - setDeleteIssueModal, -}) => { - 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 - ? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id) - : null - ); - - const { - register, - handleSubmit, - formState: { isSubmitting }, - reset, - watch, - control: controlLabel, - } = useForm({ - defaultValues, - }); - - const onSubmit = (formData: any) => { - if (!activeWorkspace || !activeProject || isSubmitting) return; - issuesServices - .createIssueLabel(activeWorkspace.slug, activeProject.id, formData) - .then((res) => { - console.log(res); - reset(defaultValues); - issueLabelMutate((prevData) => [...(prevData ?? []), res], false); - }); - }; - - 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: "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", - // 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, - // }, - { - 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, - }, - ], - ]; - - const handleCycleChange = (cycleId: string) => { - if (activeWorkspace && activeProject && issueDetail) - issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { - issue: issueDetail.id, - }); - }; - - return ( - <> -
    -
    -

    - {activeProject?.identifier}-{issueDetail?.sequence_id} -

    -
    - - - -
    -
    -
    - {sidebarSections.map((section, index) => ( -
    - {section.map((item) => ( -
    -
    - -

    {item.label}

    -
    -
    - {item.name === "target_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" - /> - )} - /> - ) : 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
    - ) - ) : ( - - )} -
    -
    -
    -
    - )} -
    - )} - /> - )} -
    -
    - ))} -
    - ))} -
    -
    -
    -
    - -

    Label

    -
    -
    -
    - {issueDetail?.label_details.map((label) => ( - - // submitChanges({ - // labels_list: issueDetail?.labels_list.filter((l) => l !== label.id), - // }) - // } - > - - {label.name} - - ))} - ( - submitChanges({ labels_list: value })} - className="flex-shrink-0" - > - {({ open }) => ( - <> - Label -
    - - - Select Label - - - - - -
    - {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => ( - - `${ - 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={label.id} - > - - {label.name} - - )) - ) : ( -
    No labels found
    - ) - ) : ( - - )} -
    -
    -
    -
    - - )} -
    - )} - /> -
    -
    -
    -
    - -
    - {createLabelForm && ( -
    -
    - - {({ open }) => ( - <> - - {watch("colour") && watch("colour") !== "" && ( - - )} - - - - - - ( - onChange(value.hex)} - /> - )} - /> - - - - )} - -
    - - -
    - )} -
    -
    - - ); -}; - -export default IssueDetailSidebar; 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 new file mode 100644 index 000000000..b1b6bc15e --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/index.tsx @@ -0,0 +1,417 @@ +import React, { useState } from "react"; +// swr +import useSWR from "swr"; +// headless ui +import { Listbox, Transition } from "@headlessui/react"; +// react hook form +import { useForm, Controller, UseFormWatch } from "react-hook-form"; +// services +import issuesServices from "lib/services/issues.service"; +// hooks +import useUser from "lib/hooks/useUser"; +import useToast from "lib/hooks/useToast"; +// fetching keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; +// commons +import { copyTextToClipboard } from "constants/common"; +// ui +import { Input, Button, Spinner } from "ui"; +import { Popover } from "@headlessui/react"; +// icons +import { + TagIcon, + ChevronDownIcon, + ClipboardDocumentIcon, + LinkIcon, + CalendarDaysIcon, + TrashIcon, + PlusIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +// types +import type { Control } from "react-hook-form"; +import type { IIssue, IIssueLabels, NestedKeyOf } from "types"; +import { TwitterPicker } from "react-color"; +import { positionEditorElement } from "components/lexical/helpers/editor"; +import SelectState from "./select-state"; +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; + submitChanges: (formData: Partial) => void; + issueDetail: IIssue | undefined; + watch: UseFormWatch; + setDeleteIssueModal: React.Dispatch>; +}; + +const defaultValues: Partial = { + name: "", + colour: "#ff0000", +}; + +const IssueDetailSidebar: React.FC = ({ + control, + submitChanges, + issueDetail, + watch: watchIssue, + setDeleteIssueModal, +}) => { + const [createLabelForm, setCreateLabelForm] = useState(false); + + const { activeWorkspace, activeProject, issues } = useUser(); + + const { setToastAlert } = useToast(); + + const { data: issueLabels, mutate: issueLabelMutate } = useSWR( + activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null, + activeProject && activeWorkspace + ? () => issuesServices.getIssueLabels(activeWorkspace.slug, activeProject.id) + : null + ); + + const { + register, + handleSubmit, + formState: { isSubmitting }, + reset, + watch, + control: controlLabel, + } = useForm({ + defaultValues, + }); + + const handleNewLabel = (formData: any) => { + if (!activeWorkspace || !activeProject || isSubmitting) return; + issuesServices + .createIssueLabel(activeWorkspace.slug, activeProject.id, formData) + .then((res) => { + console.log(res); + reset(defaultValues); + issueLabelMutate((prevData) => [...(prevData ?? []), res], false); + }); + }; + + const handleCycleChange = (cycleId: string) => { + if (activeWorkspace && activeProject && issueDetail) + issuesServices.addIssueToCycle(activeWorkspace.slug, activeProject.id, cycleId, { + issue: issueDetail.id, + }); + }; + + console.log(issueDetail); + + return ( + <> +
    +
    +

    + {activeProject?.identifier}-{issueDetail?.sequence_id} +

    +
    + + + +
    +
    +
    +
    + + + +
    +
    + + i.id !== issueDetail?.id && + i.id !== issueDetail?.parent && + i.parent !== issueDetail?.id + ) ?? [] + } + customDisplay={ + issueDetail?.parent_detail ? ( + + ) : ( +
    + No parent selected +
    + ) + } + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={watchIssue} + /> + i.id !== issueDetail?.id) ?? []} + watch={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" + /> + )} + /> +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +

    Label

    +
    +
    +
    + {issueDetail?.label_details.map((label) => ( + { + const updatedLabels = issueDetail?.labels.filter((l) => l !== label.id); + submitChanges({ + labels_list: updatedLabels, + }); + }} + > + + {label.name} + + + ))} + ( + submitChanges({ labels_list: val })} + className="flex-shrink-0" + > + {({ open }) => ( + <> + Label +
    + + Select Label + + + + +
    + {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => ( + + `${ + active || selected ? "bg-indigo-50" : "" + } flex items-center gap-2 text-gray-900 cursor-pointer select-none relative p-2 truncate` + } + value={label.id} + > + + {label.name} + + )) + ) : ( +
    No labels found
    + ) + ) : ( + + )} +
    +
    +
    +
    + + )} +
    + )} + /> + +
    +
    +
    + {createLabelForm && ( +
    +
    + + {({ open }) => ( + <> + + {watch("colour") && watch("colour") !== "" && ( + + )} + + + + + + ( + onChange(value.hex)} + /> + )} + /> + + + + )} + +
    + + +
    + )} +
    +
    + + ); +}; + +export default IssueDetailSidebar; 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..c5451c548 --- /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 ? "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} + > + {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-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 new file mode 100644 index 000000000..80dd4a899 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-cycle.tsx @@ -0,0 +1,73 @@ +// 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"; +import CustomSelect from "ui/custom-select"; + +type Props = { + control: Control; + handleCycleChange: (cycleId: string) => void; +}; + +const SelectCycle: React.FC = ({ control, handleCycleChange }) => { + const { cycles } = useUser(); + + return ( +
    +
    + +

    Cycle

    +
    +
    + ( + <> + + {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
    + ) + ) : ( + + )} +
    + + )} + /> +
    +
    + ); +}; + +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..3ec5922fe --- /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/issues-list-modal"; +// 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; + watch: UseFormWatch; +}; + +const SelectParent: React.FC = ({ + control, + submitChanges, + issuesList, + customDisplay, + watch, +}) => { + 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..7c098f957 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-priority.tsx @@ -0,0 +1,58 @@ +// react +import React from "react"; +// react-hook-form +import { Control, Controller, UseFormWatch } 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/"; +import CustomSelect from "ui/custom-select"; + +type Props = { + control: Control; + submitChanges: (formData: Partial) => void; + watch: UseFormWatch; +}; + +const SelectPriority: React.FC = ({ control, submitChanges, watch }) => { + return ( +
    +
    + +

    Priority

    +
    +
    + ( + + {watch("priority") && watch("priority") !== "" ? watch("priority") : "None"} + + } + value={value} + onChange={(value: any) => { + submitChanges({ priority: value }); + }} + > + {PRIORITIES.map((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..e55bd9b14 --- /dev/null +++ b/apps/app/components/project/issues/issue-detail/issue-detail-sidebar/select-state.tsx @@ -0,0 +1,91 @@ +// 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 { 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; + submitChanges: (formData: Partial) => void; +}; + +const SelectState: React.FC = ({ control, submitChanges }) => { + const { states } = useUser(); + + return ( +
    +
    + +

    State

    +
    +
    + ( + + {value ? ( + <> + option.id === value)?.color, + }} + > + {states?.find((option) => option.id === value)?.name} + + ) : ( + "None" + )} + + } + value={value} + onChange={(value: any) => { + submitChanges({ state: value }); + }} + > + {states ? ( + states.length > 0 ? ( + states.map((option) => ( + + <> + {option.color && ( + + )} + {option.name} + + + )) + ) : ( +
    No states found
    + ) + ) : ( + + )} +
    + )} + /> +
    +
    + ); +}; + +export default SelectState; 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 + +
    +
    + +
    +
    +