From d480325829001c9ac2162e99cfd87c24d1ec0657 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 28 Feb 2023 10:31:52 +0530 Subject: [PATCH 01/32] chore: cycle validation services and constants added --- apps/app/constants/fetch-keys.ts | 2 ++ apps/app/services/cycles.service.ts | 31 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index a2831d818..99dcff81f 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -35,6 +35,8 @@ export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`; +export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`; +export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`; export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`; export const STATE_DETAIL = "STATE_DETAIL"; diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index 109b62108..fa9769dc8 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -87,6 +87,37 @@ class ProjectCycleServices extends APIService { throw error?.response?.data; }); } + + async cycleDateCheck(workspaceSlug: string, projectId: string, data: { + start_date: string, + end_date: string + }): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCompletedCycles(workspaceSlug: string, projectId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/past-cycles/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new ProjectCycleServices(); From 0cd3bb5956c4aaedd2b2e57e93bb2ee64212564b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 28 Feb 2023 14:42:46 +0530 Subject: [PATCH 02/32] style: kanban board --- .../components/core/board-view/all-boards.tsx | 8 ++- .../core/board-view/board-header.tsx | 31 +++++------- .../core/board-view/single-board.tsx | 7 ++- .../core/board-view/single-issue.tsx | 10 ++-- .../components/icons/backlog-state-icon.tsx | 21 ++++++++ .../components/icons/completed-state-icon.tsx | 33 ++++++++++++ apps/app/components/icons/index.ts | 5 ++ .../components/icons/started-state-icon.tsx | 36 +++++++++++++ .../app/components/icons/state-group-icon.tsx | 25 ++++++++++ .../issues/view-select/priority.tsx | 50 ++++++++++--------- apps/app/components/ui/custom-select.tsx | 42 +++++++++------- 11 files changed, 200 insertions(+), 68 deletions(-) create mode 100644 apps/app/components/icons/backlog-state-icon.tsx create mode 100644 apps/app/components/icons/completed-state-icon.tsx create mode 100644 apps/app/components/icons/started-state-icon.tsx create mode 100644 apps/app/components/icons/state-group-icon.tsx diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index f5b03267c..8e63d04e2 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -40,8 +40,13 @@ export const AllBoards: React.FC = ({
-
+
{Object.keys(groupedByIssues).map((singleGroup, index) => { + const currentState = + selectedGroup === "state_detail.name" + ? states?.find((s) => s.name === singleGroup) + : null; + const stateId = selectedGroup === "state_detail.name" ? states?.find((s) => s.name === singleGroup)?.id ?? null @@ -56,6 +61,7 @@ export const AllBoards: React.FC = ({ | null; groupTitle: string; bgColor?: string; @@ -28,6 +23,7 @@ type Props = { export const BoardHeader: React.FC = ({ groupedByIssues, + currentState, selectedGroup, groupTitle, bgColor, @@ -60,16 +56,13 @@ export const BoardHeader: React.FC = ({ >
+ {currentState && getStateGroupIcon(currentState.group)}

= ({ ? assignees : addSpaceIfCamelCase(groupTitle)}

- {groupedByIssues[groupTitle].length} + + {groupedByIssues[groupTitle].length} +
+ } noChevron disabled={isNotAllowed} selfPositioned={selfPositioned} diff --git a/apps/app/components/ui/custom-select.tsx b/apps/app/components/ui/custom-select.tsx index 677d7dab5..5890261a4 100644 --- a/apps/app/components/ui/custom-select.tsx +++ b/apps/app/components/ui/custom-select.tsx @@ -8,13 +8,13 @@ type CustomSelectProps = { value: any; onChange: any; children: React.ReactNode; - label: string | JSX.Element; + label?: string | JSX.Element; textAlignment?: "left" | "center" | "right"; maxHeight?: "sm" | "rg" | "md" | "lg" | "none"; width?: "auto" | string; input?: boolean; noChevron?: boolean; - buttonClassName?: string; + customButton?: JSX.Element; optionsClassName?: string; disabled?: boolean; selfPositioned?: boolean; @@ -30,7 +30,7 @@ const CustomSelect = ({ width = "auto", input = false, noChevron = false, - buttonClassName = "", + customButton, optionsClassName = "", disabled = false, selfPositioned = false, @@ -43,22 +43,26 @@ const CustomSelect = ({ disabled={disabled} >
- - {label} - {!noChevron && !disabled && + {customButton ? ( + customButton + ) : ( + + {label} + {!noChevron && !disabled && + )}
Date: Tue, 28 Feb 2023 14:47:32 +0530 Subject: [PATCH 03/32] chore: cycle type and services updated --- apps/app/services/cycles.service.ts | 8 ++++---- apps/app/types/cycles.d.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index fa9769dc8..42f2f21ae 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // types -import type { ICycle } from "types"; +import type { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, ICycle } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -99,7 +99,7 @@ class ProjectCycleServices extends APIService { }); } - async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise { + async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise { return this.get( `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` ) @@ -109,9 +109,9 @@ class ProjectCycleServices extends APIService { }); } - async getCompletedCycles(workspaceSlug: string, projectId: string): Promise { + async getCompletedCycles(workspaceSlug: string, projectId: string): Promise { return this.get( - `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/past-cycles/` + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/` ) .then((response) => response?.data) .catch((error) => { diff --git a/apps/app/types/cycles.d.ts b/apps/app/types/cycles.d.ts index dbd7573bf..060fe53f3 100644 --- a/apps/app/types/cycles.d.ts +++ b/apps/app/types/cycles.d.ts @@ -15,8 +15,20 @@ export interface ICycle { project: string; workspace: string; issue: string; + current_cycle: []; + upcoming_cycle: []; + past_cycles: []; } +export interface CurrentAndUpcomingCyclesResponse { + current_cycle : ICycle[]; + upcoming_cycle : ICycle[]; +} + +export interface CompletedCyclesResponse { + completed_cycles : ICycle[]; + } + export interface CycleIssueResponse { id: string; issue_detail: IIssue; From 17e09d70e2e3895145c0f8f77bde41a51591facf Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 28 Feb 2023 14:53:10 +0530 Subject: [PATCH 04/32] chore: completed cycle dynamic importing and refactor --- .../cycles/completed-cycles-list.tsx | 82 ++++++++ .../{cycles-list-view.tsx => cycles-list.tsx} | 2 +- apps/app/components/cycles/index.ts | 3 +- .../projects/[projectId]/cycles/index.tsx | 192 +++++++----------- 4 files changed, 163 insertions(+), 116 deletions(-) create mode 100644 apps/app/components/cycles/completed-cycles-list.tsx rename apps/app/components/cycles/{cycles-list-view.tsx => cycles-list.tsx} (97%) diff --git a/apps/app/components/cycles/completed-cycles-list.tsx b/apps/app/components/cycles/completed-cycles-list.tsx new file mode 100644 index 000000000..b283e3da6 --- /dev/null +++ b/apps/app/components/cycles/completed-cycles-list.tsx @@ -0,0 +1,82 @@ +// react +import { useState } from "react"; +// next +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// components +import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; +// types +import { ICycle, SelectCycleType } from "types"; +import { CompletedCycleIcon } from "components/icons"; +import cyclesService from "services/cycles.service"; +import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; + +type Props = { + setCreateUpdateCycleModal: React.Dispatch>; + setSelectedCycle: React.Dispatch>; +}; + +export const CompletedCyclesList: React.FC = ({ + setCreateUpdateCycleModal, + setSelectedCycle, +}) => { + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); + const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: completedCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string) + : null + ); + + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); + setCycleDeleteModal(true); + }; + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycle({ ...cycle, actionType: "edit" }); + setCreateUpdateCycleModal(true); + }; + + return ( + <> + {completedCycles && ( + <> + + {completedCycles?.completed_cycles.length > 0 ? ( + completedCycles.completed_cycles.map((cycle) => ( + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + /> + )) + ) : ( +
+ +

+ No completed cycles yet. Create with{" "} +
Q
. +

+
+ )} + + )} + + ); +}; diff --git a/apps/app/components/cycles/cycles-list-view.tsx b/apps/app/components/cycles/cycles-list.tsx similarity index 97% rename from apps/app/components/cycles/cycles-list-view.tsx rename to apps/app/components/cycles/cycles-list.tsx index 8491190e8..e55f0e6f1 100644 --- a/apps/app/components/cycles/cycles-list-view.tsx +++ b/apps/app/components/cycles/cycles-list.tsx @@ -13,7 +13,7 @@ type TCycleStatsViewProps = { type: "current" | "upcoming" | "completed"; }; -export const CyclesListView: React.FC = ({ +export const CyclesList: React.FC = ({ cycles, setCreateUpdateCycleModal, setSelectedCycle, diff --git a/apps/app/components/cycles/index.ts b/apps/app/components/cycles/index.ts index 77dcb06a9..d1fd0d6b6 100644 --- a/apps/app/components/cycles/index.ts +++ b/apps/app/components/cycles/index.ts @@ -1,4 +1,5 @@ -export * from "./cycles-list-view"; +export * from "./completed-cycles-list"; +export * from "./cycles-list"; export * from "./delete-cycle-modal"; export * from "./form"; export * from "./modal"; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index df18ba620..78229fc54 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,30 +1,47 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; import useSWR from "swr"; import { PlusIcon } from "@heroicons/react/24/outline"; import { Tab } from "@headlessui/react"; // lib import { requiredAuth } from "lib/auth"; -import { CyclesIcon } from "components/icons"; + // services import cycleService from "services/cycles.service"; import projectService from "services/project.service"; -import workspaceService from "services/workspace.service"; + // layouts import AppLayout from "layouts/app-layout"; // components -import { CreateUpdateCycleModal, CyclesListView } from "components/cycles"; +import { CreateUpdateCycleModal, CyclesList } from "components/cycles"; // ui -import { HeaderButton, EmptySpace, EmptySpaceItem, Loader } from "components/ui"; +import { HeaderButton, Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons // types -import { ICycle, SelectCycleType } from "types"; +import { SelectCycleType } from "types"; import type { NextPage, GetServerSidePropsContext } from "next"; // fetching keys -import { CYCLE_LIST, PROJECT_DETAILS, WORKSPACE_DETAILS } from "constants/fetch-keys"; +import { + CYCLE_COMPLETE_LIST, + CYCLE_CURRENT_AND_UPCOMING_LIST, + PROJECT_DETAILS, +} from "constants/fetch-keys"; + +const CompletedCyclesList = dynamic( + () => import("components/cycles").then((a) => a.CompletedCyclesList), + { + ssr: false, + loading: () => ( + + + + ), + } +); const ProjectCycles: NextPage = () => { const [selectedCycle, setSelectedCycle] = useState(); @@ -34,22 +51,17 @@ const ProjectCycles: NextPage = () => { query: { workspaceSlug, projectId }, } = useRouter(); - const { data: activeWorkspace } = useSWR( - workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, - () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) - ); - const { data: activeProject } = useSWR( - activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null, - activeWorkspace && projectId - ? () => projectService.getProject(activeWorkspace.slug, projectId as string) + workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null ); - const { data: cycles } = useSWR( - activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null, - activeWorkspace && projectId - ? () => cycleService.getCycles(activeWorkspace.slug, projectId as string) + const { data: currentAndUpcomingCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string) : null ); @@ -61,18 +73,6 @@ const ProjectCycles: NextPage = () => { else return "current"; }; - const currentCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "current" - ); - - const upcomingCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "upcoming" - ); - - const completedCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "completed" - ); - useEffect(() => { if (createUpdateCycleModal) return; const timer = setTimeout(() => { @@ -110,92 +110,56 @@ const ProjectCycles: NextPage = () => { handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycle} /> - {cycles ? ( - cycles.length > 0 ? ( -
-

Current Cycle

-
- -
-
- - - - `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` - } - > - Upcoming - - - `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` - } - > - Completed - - - - - - - - - - - -
-
- ) : ( -
- +

Current Cycle

+
+ +
+
+ + - - Use
Q
shortcut to - create a new cycle - + + `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` } - Icon={PlusIcon} - action={() => { - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); - }} - /> - -
- ) - ) : ( - - - - - )} + > + Upcoming + + + `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + } + > + Completed + + + + + + + + + + + +
+
); }; From 19e9f510bc7d62cec3ffdbcf579c2f6eb80795cc Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 28 Feb 2023 14:55:19 +0530 Subject: [PATCH 05/32] feat: cycle modal date validation --- apps/app/components/cycles/form.tsx | 100 +++++++++++++++++++-------- apps/app/components/cycles/modal.tsx | 2 +- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index 58f57ba14..d32fd047d 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -1,11 +1,16 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +// toast +import useToast from "hooks/use-toast"; // react-hook-form import { Controller, useForm } from "react-hook-form"; // ui import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui"; // types import { ICycle } from "types"; +// services +import cyclesService from "services/cycles.service"; type Props = { handleFormSubmit: (values: Partial) => Promise; @@ -23,11 +28,19 @@ const defaultValues: Partial = { }; export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const [isDateValid, setIsDateValid] = useState(true); + const { register, formState: { errors, isSubmitting }, handleSubmit, control, + watch, reset, } = useForm({ defaultValues, @@ -41,6 +54,31 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat }); }; + const dateChecker = async (payload: any) => { + await cyclesService + .cycleDateCheck(workspaceSlug as string, projectId as string, payload) + .then((res) => { + if (res.status) { + setIsDateValid(true); + } else { + setIsDateValid(false); + setToastAlert({ + type: "error", + title: "Error!", + message: + "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", + }); + } + }) + .catch((err) => { + console.log(err); + }); + }; + + const checkEmptyDate = + (watch("start_date") === "" && watch("end_date") === "") || + (watch("start_date") === null && watch("end_date") === null); + useEffect(() => { reset({ ...defaultValues, @@ -84,30 +122,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat register={register} />
-
-
Status
- ( - {field.value ?? "Select Status"}} - input - > - {[ - { label: "Draft", value: "draft" }, - { label: "Started", value: "started" }, - { label: "Completed", value: "completed" }, - ].map((item) => ( - - {item.label} - - ))} - - )} - /> -
+
Start Date
@@ -115,12 +130,19 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat ( { + onChange(val); + watch("end_date") + ? dateChecker({ + start_date: val, + end_date: watch("end_date"), + }) + : ""; + }} error={errors.start_date ? true : false} /> )} @@ -136,12 +158,19 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat ( { + onChange(val); + watch("start_date") + ? dateChecker({ + start_date: watch("start_date"), + end_date: val, + }) + : ""; + }} error={errors.end_date ? true : false} /> )} @@ -158,7 +187,18 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat - - + + + +
= (props) => { ))}
-
); diff --git a/apps/app/components/workspace/sidebar-dropdown.tsx b/apps/app/components/workspace/sidebar-dropdown.tsx index 5a892e069..5faf85618 100644 --- a/apps/app/components/workspace/sidebar-dropdown.tsx +++ b/apps/app/components/workspace/sidebar-dropdown.tsx @@ -69,45 +69,43 @@ export const WorkspaceSidebarDropdown = () => { return (
-
- -
-
- {activeWorkspace?.logo && activeWorkspace.logo !== "" ? ( - Workspace Logo - ) : ( - activeWorkspace?.name?.charAt(0) ?? "..." - )} -
- {!sidebarCollapse && ( -

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

+ +
+
+ {activeWorkspace?.logo && activeWorkspace.logo !== "" ? ( + Workspace Logo + ) : ( + activeWorkspace?.name?.charAt(0) ?? "..." )}
{!sidebarCollapse && ( -
-
+

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

)} - -
+
+ {!sidebarCollapse && ( +
+
+ )} +
[ { - icon: HomeIcon, - name: "Home", + icon: GridViewIcon, + name: "Dashboard", href: `/${workspaceSlug}`, }, { - icon: ClipboardDocumentListIcon, + icon: AssignmentClipboardIcon, name: "Projects", href: `/${workspaceSlug}/projects`, }, { - icon: RectangleStackIcon, + icon: TickMarkIcon, name: "My Issues", href: `/${workspaceSlug}/me/my-issues`, }, { - icon: Cog6ToothIcon, + icon: SettingIcon, name: "Settings", href: `/${workspaceSlug}/settings`, }, ]; -export const WorkspaceSidebarMenu = () => { +export const WorkspaceSidebarMenu: React.FC = () => { // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -49,15 +44,15 @@ export const WorkspaceSidebarMenu = () => {