From c60e771e9c530d72d17265703d48055d5fc88429 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 1 Feb 2023 15:08:52 +0530 Subject: [PATCH 01/52] chore: update all backend dependencies to the latest version --- apiserver/requirements/base.txt | 34 +++++++++++++-------------- apiserver/requirements/local.txt | 2 +- apiserver/requirements/production.txt | 12 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 578235003..d46f96de0 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -2,27 +2,27 @@ Django==3.2.16 django-braces==1.15.0 -django-taggit==2.1.0 -psycopg2==2.9.3 -django-oauth-toolkit==2.0.0 -mistune==2.0.3 +django-taggit==3.1.0 +psycopg2==2.9.5 +django-oauth-toolkit==2.2.0 +mistune==2.0.4 djangorestframework==3.14.0 -redis==4.2.2 -django-nested-admin==3.4.0 -django-cors-headers==3.11.0 -whitenoise==6.0.0 -django-allauth==0.50.0 +redis==4.4.2 +django-nested-admin==4.0.2 +django-cors-headers==3.13.0 +whitenoise==6.3.0 +django-allauth==0.52.0 faker==13.4.0 -django-filter==21.1 -jsonmodels==2.5.0 -djangorestframework-simplejwt==5.1.0 -sentry-sdk==1.13.0 -django-s3-storage==0.13.6 +django-filter==22.1 +jsonmodels==2.6.0 +djangorestframework-simplejwt==5.2.2 +sentry-sdk==1.14.0 +django-s3-storage==0.13.11 django-crum==0.7.9 django-guardian==2.4.0 dj_rest_auth==2.2.5 -google-auth==2.9.1 -google-api-python-client==2.55.0 -django-rq==2.5.1 +google-auth==2.16.0 +google-api-python-client==2.75.0 +django-rq==2.6.0 django-redis==5.2.0 uvicorn==0.20.0 \ No newline at end of file diff --git a/apiserver/requirements/local.txt b/apiserver/requirements/local.txt index 238fe63f2..efd74a071 100644 --- a/apiserver/requirements/local.txt +++ b/apiserver/requirements/local.txt @@ -1,3 +1,3 @@ -r base.txt -django-debug-toolbar==3.2.4 \ No newline at end of file +django-debug-toolbar==3.8.1 \ No newline at end of file diff --git a/apiserver/requirements/production.txt b/apiserver/requirements/production.txt index 231d3c0a1..2547ce255 100644 --- a/apiserver/requirements/production.txt +++ b/apiserver/requirements/production.txt @@ -1,12 +1,12 @@ -r base.txt -dj-database-url==0.5.0 +dj-database-url==1.2.0 gunicorn==20.1.0 -whitenoise==6.0.0 -django-storages==1.12.3 +whitenoise==6.3.0 +django-storages==1.13.2 boto==2.49.0 -django-anymail==8.5 -twilio==7.8.2 -django-debug-toolbar==3.2.4 +django-anymail==9.0 +twilio==7.16.2 +django-debug-toolbar==3.8.1 gevent==22.10.2 psycogreen==1.0.2 \ No newline at end of file From 9e9a6f4cce5fd568faf42de5e4e31c3e6314c3db Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Thu, 9 Feb 2023 10:41:43 +0530 Subject: [PATCH 02/52] feat: record issue completed at date when the issues are moved to fompleted group (#262) --- apiserver/plane/db/models/issue.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 3331b0832..a870eb93f 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -4,6 +4,7 @@ from django.db import models from django.conf import settings from django.db.models.signals import post_save from django.dispatch import receiver +from django.utils import timezone # Module imports from . import ProjectBaseModel @@ -58,6 +59,7 @@ class Issue(ProjectBaseModel): "db.Label", blank=True, related_name="labels", through="IssueLabel" ) sort_order = models.FloatField(default=65535) + completed_at = models.DateTimeField(null=True) class Meta: verbose_name = "Issue" @@ -86,7 +88,22 @@ class Issue(ProjectBaseModel): ) except ImportError: pass + else: + try: + from plane.db.models import State + # Get the completed states of the project + completed_states = State.objects.filter( + group="completed", project=self.project + ).values_list("pk", flat=True) + # Check if the current issue state and completed state id are same + if self.state.id in completed_states: + self.completed_at = timezone.now() + else: + self.completed_at = None + + except ImportError: + pass # Strip the html tags using html parser self.description_stripped = ( None From 7c06be19fca89208f5c8f6a0e08d94798482cb4d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Thu, 9 Feb 2023 17:54:47 +0530 Subject: [PATCH 03/52] feat: cycle status (#265) * feat: cycle status and dates added in sidebar * feat: update status added --------- Co-authored-by: Anmol Singh Bhatia --- .../cycles/cycle-detail-sidebar/index.tsx | 45 +++++++++--- apps/app/components/project/cycles/index.ts | 3 + .../project/cycles/sidebar-select/index.ts | 1 + .../cycles/sidebar-select/select-status.tsx | 69 +++++++++++++++++++ apps/app/constants/cycle.ts | 5 ++ 5 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 apps/app/components/project/cycles/index.ts create mode 100644 apps/app/components/project/cycles/sidebar-select/index.ts create mode 100644 apps/app/components/project/cycles/sidebar-select/select-status.tsx create mode 100644 apps/app/constants/cycle.ts diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index 7ad179d26..1650607f0 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -10,6 +10,9 @@ import { Controller, useForm } from "react-hook-form"; // react-circular-progressbar import { CircularProgressbar } from "react-circular-progressbar"; import "react-circular-progressbar/dist/styles.css"; +// icons +import { CalendarDaysIcon, ChartPieIcon, LinkIcon, UserIcon } from "@heroicons/react/24/outline"; +import { CycleSidebarStatusSelect } from "components/project/cycles"; // ui import { Loader, CustomDatePicker } from "components/ui"; // hooks @@ -18,8 +21,6 @@ import useToast from "hooks/use-toast"; import cyclesService from "services/cycles.service"; // components import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats"; -// icons -import { CalendarDaysIcon, ChartPieIcon, LinkIcon, UserIcon } from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; @@ -28,6 +29,8 @@ import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; +import { renderShortNumericDateFormat } from "helpers/date-time.helper"; + type Props = { issues: IIssue[]; cycle: ICycle | undefined; @@ -35,20 +38,17 @@ type Props = { cycleIssues: CycleIssueResponse[]; }; -const defaultValues: Partial = { - start_date: new Date().toString(), - end_date: new Date().toString(), -}; - const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssues }) => { const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; const { setToastAlert } = useToast(); - const { reset, control } = useForm({ - defaultValues, - }); + const defaultValues: Partial = { + start_date: new Date().toString(), + end_date: new Date().toString(), + status: cycle?.status, + }; const groupedIssues = { backlog: [], @@ -59,6 +59,10 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue ...groupBy(cycleIssues ?? [], "issue_detail.state_detail.group"), }; + const { reset, watch, control } = useForm({ + defaultValues, + }); + const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -94,6 +98,22 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue > {cycle ? ( <> +
+
+ {cycle.status} +
+
+ + {renderShortNumericDateFormat(`${cycle.start_date}`) + ? renderShortNumericDateFormat(`${cycle.start_date}`) + : "N/A"}{" "} + -{" "} + {renderShortNumericDateFormat(`${cycle.end_date}`) + ? renderShortNumericDateFormat(`${cycle.end_date}`) + : "N/A"} + +
+

{cycle.name}

@@ -219,6 +239,11 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue />
+
diff --git a/apps/app/components/project/cycles/index.ts b/apps/app/components/project/cycles/index.ts new file mode 100644 index 000000000..9c6e55594 --- /dev/null +++ b/apps/app/components/project/cycles/index.ts @@ -0,0 +1,3 @@ +export * from "./sidebar-select"; +export * from "./stats-view"; +export * from "./cycle-detail-sidebar"; \ No newline at end of file diff --git a/apps/app/components/project/cycles/sidebar-select/index.ts b/apps/app/components/project/cycles/sidebar-select/index.ts new file mode 100644 index 000000000..efa8c553e --- /dev/null +++ b/apps/app/components/project/cycles/sidebar-select/index.ts @@ -0,0 +1 @@ +export * from "./select-status"; \ No newline at end of file diff --git a/apps/app/components/project/cycles/sidebar-select/select-status.tsx b/apps/app/components/project/cycles/sidebar-select/select-status.tsx new file mode 100644 index 000000000..0c53083bd --- /dev/null +++ b/apps/app/components/project/cycles/sidebar-select/select-status.tsx @@ -0,0 +1,69 @@ +// react +import React from "react"; +// react-hook-form +import { Control, Controller, UseFormWatch } from "react-hook-form"; +// icons +import { Squares2X2Icon } from "@heroicons/react/24/outline"; +// ui +import { CustomSelect } from "components/ui"; +// types +import { ICycle } from "types"; +// common +// constants +import { CYCLE_STATUS } from "constants/cycle"; + +type Props = { + control: Control, any>; + submitChanges: (formData: Partial) => void; + watch: UseFormWatch>; +}; + +export const CycleSidebarStatusSelect: React.FC = ({ control, submitChanges, watch }) => ( +
+
+ +

Status

+
+
+ ( + + option.value === value)?.color, + }} + /> + {watch("status")} + + } + value={value} + onChange={(value: any) => { + submitChanges({ status: value }); + }} + > + {CYCLE_STATUS.map((option) => ( + + <> + + {option.label} + + + ))} + + )} + /> +
+
+); diff --git a/apps/app/constants/cycle.ts b/apps/app/constants/cycle.ts new file mode 100644 index 000000000..93336fb8c --- /dev/null +++ b/apps/app/constants/cycle.ts @@ -0,0 +1,5 @@ +export const CYCLE_STATUS = [ + { label: "Started", value: "started", color: "#5e6ad2" }, + { label: "Completed", value: "completed", color: "#eb5757" }, + { label: "Draft", value: "draft", color: "#f2c94c" }, + ]; \ No newline at end of file From 37c28b251d332e3448d7f5a3b9c554721ae4f9e6 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 9 Feb 2023 19:07:08 +0530 Subject: [PATCH 04/52] chore: update python runtime --- apiserver/runtime.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/runtime.txt b/apiserver/runtime.txt index cd6f13073..2d4e05157 100644 --- a/apiserver/runtime.txt +++ b/apiserver/runtime.txt @@ -1 +1 @@ -python-3.11.1 \ No newline at end of file +python-3.11.2 \ No newline at end of file From a403c0c346d5a741930725daba6d0d2ca9e8ab44 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 10 Feb 2023 18:02:18 +0530 Subject: [PATCH 05/52] feat: label grouping in dropdowns, default state in project settings (#266) * feat: label grouping in dropdowns, default state in project settings * feat: label disclosure default open * refactor: label setting page * chore: tooltip component updated * chore: tooltip component updated * feat/state_sequence_change --- .../{index.tsx => command-pallette.tsx} | 7 +- apps/app/components/command-palette/index.ts | 2 + .../{shortcuts.tsx => shortcuts-modal.tsx} | 4 +- apps/app/components/core/index.ts | 1 + apps/app/components/core/sidebar/index.ts | 2 + .../core/sidebar/sidebar-progress-stats.tsx | 6 +- .../core/sidebar/single-progress-stats.tsx | 32 +- apps/app/components/cycles/modal.tsx | 39 ++- apps/app/components/issues/select/label.tsx | 100 +++--- apps/app/components/issues/sidebar.tsx | 183 ++++++----- .../issues/view-select/priority.tsx | 8 +- .../labels/create-update-label-inline.tsx | 189 +++++++++++ apps/app/components/labels/index.ts | 2 + .../components/labels/single-label-group.tsx | 136 ++++++++ apps/app/components/labels/single-label.tsx | 182 ++--------- apps/app/components/modules/sidebar.tsx | 3 +- .../cycles/cycle-detail-sidebar/index.tsx | 5 +- .../states/create-update-state-inline.tsx | 40 +-- apps/app/components/states/index.ts | 1 + apps/app/components/states/single-state.tsx | 217 +++++++++++++ apps/app/components/ui/tooltip.tsx | 66 ++++ apps/app/components/ui/tooltip/index.tsx | 73 ----- apps/app/hooks/use-issue-properties.tsx | 3 +- apps/app/layouts/app-layout/index.tsx | 2 +- .../projects/[projectId]/settings/labels.tsx | 298 ++++++------------ .../projects/[projectId]/settings/states.tsx | 60 ++-- apps/app/types/state.d.ts | 19 +- 27 files changed, 1021 insertions(+), 659 deletions(-) rename apps/app/components/command-palette/{index.tsx => command-pallette.tsx} (98%) create mode 100644 apps/app/components/command-palette/index.ts rename apps/app/components/command-palette/{shortcuts.tsx => shortcuts-modal.tsx} (98%) create mode 100644 apps/app/components/core/sidebar/index.ts create mode 100644 apps/app/components/labels/create-update-label-inline.tsx create mode 100644 apps/app/components/labels/single-label-group.tsx create mode 100644 apps/app/components/states/single-state.tsx create mode 100644 apps/app/components/ui/tooltip.tsx delete mode 100644 apps/app/components/ui/tooltip/index.tsx diff --git a/apps/app/components/command-palette/index.tsx b/apps/app/components/command-palette/command-pallette.tsx similarity index 98% rename from apps/app/components/command-palette/index.tsx rename to apps/app/components/command-palette/command-pallette.tsx index e6138da94..678e17d46 100644 --- a/apps/app/components/command-palette/index.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -1,4 +1,3 @@ -// TODO: Refactor this component: into a different file, use this file to export the components import React, { useState, useCallback, useEffect } from "react"; import { useRouter } from "next/router"; @@ -14,7 +13,7 @@ import useTheme from "hooks/use-theme"; import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; // components -import ShortcutsModal from "components/command-palette/shortcuts"; +import { ShortcutsModal } from "components/command-palette"; import { BulkDeleteIssuesModal } from "components/core"; import { CreateProjectModal } from "components/project"; import { CreateUpdateIssueModal } from "components/issues"; @@ -36,7 +35,7 @@ import { IIssue } from "types"; // fetch-keys import { USER_ISSUE } from "constants/fetch-keys"; -const CommandPalette: React.FC = () => { +export const CommandPalette: React.FC = () => { const [query, setQuery] = useState(""); const [isPaletteOpen, setIsPaletteOpen] = useState(false); @@ -369,5 +368,3 @@ const CommandPalette: React.FC = () => { ); }; - -export default CommandPalette; diff --git a/apps/app/components/command-palette/index.ts b/apps/app/components/command-palette/index.ts new file mode 100644 index 000000000..542d69214 --- /dev/null +++ b/apps/app/components/command-palette/index.ts @@ -0,0 +1,2 @@ +export * from "./command-pallette"; +export * from "./shortcuts-modal"; diff --git a/apps/app/components/command-palette/shortcuts.tsx b/apps/app/components/command-palette/shortcuts-modal.tsx similarity index 98% rename from apps/app/components/command-palette/shortcuts.tsx rename to apps/app/components/command-palette/shortcuts-modal.tsx index f5435055c..c1800ab17 100644 --- a/apps/app/components/command-palette/shortcuts.tsx +++ b/apps/app/components/command-palette/shortcuts-modal.tsx @@ -41,7 +41,7 @@ const shortcuts = [ }, ]; -const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { +export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { const [query, setQuery] = useState(""); const filteredShortcuts = shortcuts.filter((shortcut) => @@ -150,5 +150,3 @@ const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { ); }; - -export default ShortcutsModal; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 0865ea441..482258b4a 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1,5 +1,6 @@ export * from "./board-view"; export * from "./list-view"; +export * from "./sidebar"; export * from "./bulk-delete-issues-modal"; export * from "./existing-issues-list-modal"; export * from "./image-upload-modal"; diff --git a/apps/app/components/core/sidebar/index.ts b/apps/app/components/core/sidebar/index.ts new file mode 100644 index 000000000..20d186d1e --- /dev/null +++ b/apps/app/components/core/sidebar/index.ts @@ -0,0 +1,2 @@ +export * from "./sidebar-progress-stats"; +export * from "./single-progress-stats"; diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index 2b150a890..9a3e53723 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -11,7 +11,7 @@ import { Tab } from "@headlessui/react"; import issuesServices from "services/issues.service"; import projectService from "services/project.service"; // components -import SingleProgressStats from "components/core/sidebar/single-progress-stats"; +import { SingleProgressStats } from "components/core"; // ui import { Avatar } from "components/ui"; // icons @@ -36,7 +36,7 @@ const stateGroupColours: { completed: "#096e8d", }; -const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { +export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; const { data: issueLabels } = useSWR( @@ -180,5 +180,3 @@ const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { ); }; - -export default SidebarProgressStats; diff --git a/apps/app/components/core/sidebar/single-progress-stats.tsx b/apps/app/components/core/sidebar/single-progress-stats.tsx index 885aed23e..4b3de9c9f 100644 --- a/apps/app/components/core/sidebar/single-progress-stats.tsx +++ b/apps/app/components/core/sidebar/single-progress-stats.tsx @@ -8,22 +8,22 @@ type TSingleProgressStatsProps = { total: number; }; -const SingleProgressStats: React.FC = ({ title, completed, total }) => ( - <> -
-
{title}
-
-
- - - - {Math.floor((completed / total) * 100)}% -
- of - {total} +export const SingleProgressStats: React.FC = ({ + title, + completed, + total, +}) => ( +
+
{title}
+
+
+ + + + {Math.floor((completed / total) * 100)}%
+ of + {total}
- +
); - -export default SingleProgressStats; diff --git a/apps/app/components/cycles/modal.tsx b/apps/app/components/cycles/modal.tsx index 76e1c5ad1..878a629ca 100644 --- a/apps/app/components/cycles/modal.tsx +++ b/apps/app/components/cycles/modal.tsx @@ -1,12 +1,15 @@ import { Fragment } from "react"; + import { mutate } from "swr"; + +// headless ui import { Dialog, Transition } from "@headlessui/react"; // services import cycleService from "services/cycles.service"; +// hooks +import useToast from "hooks/use-toast"; // components import { CycleForm } from "components/cycles"; -// helpers -import { renderDateFormat } from "helpers/date-time.helper"; // types import type { ICycle } from "types"; // fetch keys @@ -20,8 +23,14 @@ export interface CycleModalProps { initialData?: ICycle; } -export const CycleModal: React.FC = (props) => { - const { isOpen, handleClose, initialData, projectId, workspaceSlug } = props; +export const CycleModal: React.FC = ({ + isOpen, + handleClose, + initialData, + projectId, + workspaceSlug, +}) => { + const { setToastAlert } = useToast(); const createCycle = (payload: Partial) => { cycleService @@ -31,12 +40,11 @@ export const CycleModal: React.FC = (props) => { handleClose(); }) .catch((err) => { - // TODO: Handle this ERROR. - // Object.keys(err).map((key) => { - // setError(key as keyof typeof defaultValues, { - // message: err[key].join(", "), - // }); - // }); + setToastAlert({ + type: "error", + title: "Error", + message: "Error in creating cycle. Please try again!", + }); }); }; @@ -48,12 +56,11 @@ export const CycleModal: React.FC = (props) => { handleClose(); }) .catch((err) => { - // TODO: Handle this ERROR. - // Object.keys(err).map((key) => { - // setError(key as keyof typeof defaultValues, { - // message: err[key].join(", "), - // }); - // }); + setToastAlert({ + type: "error", + title: "Error", + message: "Error in updating cycle. Please try again!", + }); }); }; diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index b1b1c4338..2d4e5a179 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -9,7 +9,7 @@ import { useForm } from "react-hook-form"; // headless ui import { Combobox, Transition } from "@headlessui/react"; // icons -import { TagIcon } from "@heroicons/react/24/outline"; +import { RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline"; // services import issuesServices from "services/issues.service"; // types @@ -58,8 +58,6 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } }; const { - register, - handleSubmit, formState: { isSubmitting }, setFocus, reset, @@ -69,16 +67,10 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } isOpen && setFocus("name"); }, [isOpen, setFocus]); - const options = issueLabels?.map((label) => ({ - value: label.id, - display: label.name, - color: label.color, - })); - const filteredOptions = query === "" - ? options - : options?.filter((option) => option.display.toLowerCase().includes(query.toLowerCase())); + ? issueLabels + : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); return ( <> @@ -98,10 +90,9 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } {Array.isArray(value) - ? value - .map((v) => options?.find((option) => option.value === v)?.display) - .join(", ") || "Labels" - : options?.find((option) => option.value === value)?.display || "Labels"} + ? value.map((v) => issueLabels?.find((l) => l.id === v)?.name).join(", ") || + "Labels" + : issueLabels?.find((l) => l.id === value)?.name || "Labels"} @@ -122,31 +113,62 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } displayValue={(assigned: any) => assigned?.name} />
- {filteredOptions ? ( + {issueLabels && filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `${active ? "bg-indigo-50" : ""} ${ - selected ? "bg-indigo-50 font-medium" : "" - } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` - } - value={option.value} - > - {issueLabels && ( - <> - - {option.display} - - )} - - )) + filteredOptions.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if (children.length === 0) { + if (!label.parent) + return ( + + `${active ? "bg-indigo-50" : ""} ${ + selected ? "bg-indigo-50 font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={label.id} + > + + {label.name} + + ); + } else + return ( +
+
+ {label.name} +
+
+ {children.map((child) => ( + + `${active ? "bg-indigo-50" : ""} ${ + selected ? "bg-indigo-50 font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={child.id} + > + + {child.name} + + ))} +
+
+ ); + }) ) : (

No labels found

) diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 62a99eac1..c321f0a31 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -38,6 +38,7 @@ import { TrashIcon, PlusIcon, XMarkIcon, + RectangleGroupIcon, } from "@heroicons/react/24/outline"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; @@ -298,30 +299,31 @@ export const IssueDetailsSidebar: React.FC = ({
- {watchIssue("labels_list")?.map((label) => { - const singleLabel = issueLabels?.find((l) => l.id === label); + {watchIssue("labels_list")?.map((labelId) => { + const label = issueLabels?.find((l) => l.id === labelId); - if (!singleLabel) return null; - - return ( - { - const updatedLabels = watchIssue("labels_list")?.filter((l) => l !== label); - submitChanges({ - labels_list: updatedLabels, - }); - }} - > + if (label) + return ( - {singleLabel.name} - - - ); + key={label.id} + className="group flex cursor-pointer items-center gap-1 rounded-2xl border px-1 py-0.5 text-xs hover:border-red-500 hover:bg-red-50" + onClick={() => { + const updatedLabels = watchIssue("labels_list")?.filter( + (l) => l !== labelId + ); + submitChanges({ + labels_list: updatedLabels, + }); + }} + > + + {label.name} + + + ); })} = ({ disabled={isNotAllowed} > {({ open }) => ( - <> - Label -
- - Select Label - +
+ + Select Label + - - -
- {issueLabels ? ( - issueLabels.length > 0 ? ( - issueLabels.map((label: IIssueLabels) => ( - - `${ - active || selected ? "bg-indigo-50" : "" - } relative flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` - } - value={label.id} - > - - {label.name} - - )) - ) : ( -
No labels found
- ) + + +
+ {issueLabels ? ( + issueLabels.length > 0 ? ( + issueLabels.map((label: IIssueLabels) => { + const children = issueLabels?.filter( + (l) => l.parent === label.id + ); + + if (children.length === 0) { + if (!label.parent) + return ( + + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={label.id} + > + + {label.name} + + ); + } else + return ( +
+
+ {" "} + {label.name} +
+
+ {children.map((child) => ( + + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-2 truncate p-2 text-gray-900` + } + value={child.id} + > + + {child.name} + + ))} +
+
+ ); + }) ) : ( - - )} -
-
-
-
- +
No labels found
+ ) + ) : ( + + )} +
+ + +
)} )} diff --git a/apps/app/components/issues/view-select/priority.tsx b/apps/app/components/issues/view-select/priority.tsx index 5517494b2..9f55937a2 100644 --- a/apps/app/components/issues/view-select/priority.tsx +++ b/apps/app/components/issues/view-select/priority.tsx @@ -69,10 +69,10 @@ export const ViewPrioritySelect: React.FC = ({ {PRIORITIES?.map((priority) => ( - `flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize ${ - active ? "bg-indigo-50" : "bg-white" - }` + className={({ active, selected }) => + `${active || selected ? "bg-indigo-50" : ""} ${ + selected ? "font-medium" : "" + } flex cursor-pointer select-none items-center gap-x-2 px-3 py-2 capitalize` } value={priority} > diff --git a/apps/app/components/labels/create-update-label-inline.tsx b/apps/app/components/labels/create-update-label-inline.tsx new file mode 100644 index 000000000..3b7fe614c --- /dev/null +++ b/apps/app/components/labels/create-update-label-inline.tsx @@ -0,0 +1,189 @@ +import React, { useEffect } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// react-hook-form +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +// react-color +import { TwitterPicker } from "react-color"; +// headless ui +import { Popover, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// ui +import { Button, Input } from "components/ui"; +// types +import { IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + labelForm: boolean; + setLabelForm: React.Dispatch>; + isUpdating: boolean; + labelToUpdate: IIssueLabels | null; +}; + +const defaultValues: Partial = { + name: "", + color: "#ff0000", +}; + +export const CreateUpdateLabelInline: React.FC = ({ + labelForm, + setLabelForm, + isUpdating, + labelToUpdate, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { + handleSubmit, + control, + register, + reset, + formState: { errors, isSubmitting }, + watch, + setValue, + } = useForm({ + defaultValues, + }); + + const handleLabelCreate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .then((res) => { + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => [res, ...(prevData ?? [])], + false + ); + reset(defaultValues); + setLabelForm(false); + }); + }; + + const handleLabelUpdate: SubmitHandler = async (formData) => { + if (!workspaceSlug || !projectId || isSubmitting) return; + + await issuesService + .patchIssueLabel( + workspaceSlug as string, + projectId as string, + labelToUpdate?.id ?? "", + formData + ) + .then((res) => { + console.log(res); + reset(defaultValues); + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => + prevData?.map((p) => (p.id === labelToUpdate?.id ? { ...p, ...formData } : p)), + false + ); + setLabelForm(false); + }); + }; + + useEffect(() => { + if (!labelForm && isUpdating) return; + + reset(); + }, [labelForm, isUpdating, reset]); + + useEffect(() => { + if (!labelToUpdate) return; + + setValue("color", labelToUpdate.color); + setValue("name", labelToUpdate.name); + }, [labelToUpdate, setValue]); + + return ( +
+
+ + {({ open }) => ( + <> + + {watch("color") && watch("color") !== "" && ( + + )} + + + + + ( + onChange(value.hex)} /> + )} + /> + + + + )} + +
+
+ +
+ + {isUpdating ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/apps/app/components/labels/index.ts b/apps/app/components/labels/index.ts index 2a155c8dc..d407cd074 100644 --- a/apps/app/components/labels/index.ts +++ b/apps/app/components/labels/index.ts @@ -1,2 +1,4 @@ +export * from "./create-update-label-inline"; export * from "./labels-list-modal"; +export * from "./single-label-group"; export * from "./single-label"; diff --git a/apps/app/components/labels/single-label-group.tsx b/apps/app/components/labels/single-label-group.tsx new file mode 100644 index 000000000..efdb26f38 --- /dev/null +++ b/apps/app/components/labels/single-label-group.tsx @@ -0,0 +1,136 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// headless ui +import { Disclosure, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// ui +import { CustomMenu } from "components/ui"; +// icons +import { ChevronDownIcon, RectangleGroupIcon } from "@heroicons/react/24/outline"; +// types +import { IIssueLabels } from "types"; +// fetch-keys +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +type Props = { + label: IIssueLabels; + labelChildren: IIssueLabels[]; + addLabelToGroup: (parentLabel: IIssueLabels) => void; + editLabel: (label: IIssueLabels) => void; + handleLabelDelete: (labelId: string) => void; +}; + +export const SingleLabelGroup: React.FC = ({ + label, + labelChildren, + addLabelToGroup, + editLabel, + handleLabelDelete, +}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const removeFromGroup = (label: IIssueLabels) => { + if (!workspaceSlug || !projectId) return; + + mutate( + PROJECT_ISSUE_LABELS(projectId as string), + (prevData) => + prevData?.map((l) => { + if (l.id === label.id) return { ...l, parent: null }; + + return l; + }), + false + ); + + issuesService + .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { + parent: null, + }) + .then((res) => { + mutate(PROJECT_ISSUE_LABELS(projectId as string)); + }); + }; + + return ( + + {({ open }) => ( + <> +
+ +
+ + + + + + +
{label.name}
+
+
+ + addLabelToGroup(label)}> + Add more labels + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
+ + +
+ {labelChildren.map((child) => ( +
+
+ + {child.name} +
+
+ + removeFromGroup(child)}> + Remove from group + + editLabel(child)}> + Edit + + handleLabelDelete(child.id)}> + Delete + + +
+
+ ))} +
+
+
+ + )} +
+ ); +}; diff --git a/apps/app/components/labels/single-label.tsx b/apps/app/components/labels/single-label.tsx index eb721a8ac..927a30d5a 100644 --- a/apps/app/components/labels/single-label.tsx +++ b/apps/app/components/labels/single-label.tsx @@ -1,171 +1,43 @@ -import React, { useState } from "react"; +import React from "react"; -import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui -import { Disclosure, Transition } from "@headlessui/react"; -// services -import issuesService from "services/issues.service"; -// components -import { LabelsListModal } from "components/labels"; // ui import { CustomMenu } from "components/ui"; -// icons -import { ChevronDownIcon } from "@heroicons/react/24/outline"; // types import { IIssueLabels } from "types"; -// fetch-keys -import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; type Props = { label: IIssueLabels; - issueLabels: IIssueLabels[]; + addLabelToGroup: (parentLabel: IIssueLabels) => void; editLabel: (label: IIssueLabels) => void; handleLabelDelete: (labelId: string) => void; }; export const SingleLabel: React.FC = ({ label, - issueLabels, + addLabelToGroup, editLabel, handleLabelDelete, -}) => { - const [labelsListModal, setLabelsListModal] = useState(false); - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const children = issueLabels?.filter((l) => l.parent === label.id); - - const removeFromGroup = (label: IIssueLabels) => { - if (!workspaceSlug || !projectId) return; - - mutate( - PROJECT_ISSUE_LABELS(projectId as string), - (prevData) => - prevData?.map((l) => { - if (l.id === label.id) return { ...l, parent: null }; - - return l; - }), - false - ); - - issuesService - .patchIssueLabel(workspaceSlug as string, projectId as string, label.id, { - parent: null, - }) - .then((res) => { - mutate(PROJECT_ISSUE_LABELS(projectId as string)); - }); - }; - - return ( - <> - setLabelsListModal(false)} - parent={label} - /> - {children && children.length === 0 ? ( - label.parent === "" || !label.parent ? ( -
-
-
- -
{label.name}
-
- - setLabelsListModal(true)}> - Convert to group - - editLabel(label)}>Edit - handleLabelDelete(label.id)}> - Delete - - -
-
- ) : null - ) : ( - - {({ open }) => ( - <> -
- -
- - - -
{label.name}
-
-
- - setLabelsListModal(true)}> - Add more labels - - editLabel(label)}>Edit - handleLabelDelete(label.id)}> - Delete - - -
- - -
- {children.map((child) => ( -
-
- - {child.name} -
-
- - removeFromGroup(child)}> - Remove from group - - editLabel(child)}> - Edit - - handleLabelDelete(child.id)}> - Delete - - -
-
- ))} -
-
-
- - )} -
- )} - - ); -}; +}) => ( +
+
+
+ +
{label.name}
+
+ + addLabelToGroup(label)}> + Convert to group + + editLabel(label)}>Edit + handleLabelDelete(label.id)}> + Delete + + +
+
+); diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 562593384..3ea95cec3 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -30,6 +30,8 @@ import { } from "components/modules"; import "react-circular-progressbar/dist/styles.css"; +// components +import { SidebarProgressStats } from "components/core"; // ui import { CustomDatePicker, Loader } from "components/ui"; // helpers @@ -40,7 +42,6 @@ import { groupBy } from "helpers/array.helper"; import { IIssue, IModule, ModuleIssueResponse } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; -import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats"; const defaultValues: Partial = { lead: "", diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index 1650607f0..c55f30591 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -20,17 +20,16 @@ import useToast from "hooks/use-toast"; // services import cyclesService from "services/cycles.service"; // components -import SidebarProgressStats from "components/core/sidebar/sidebar-progress-stats"; +import { SidebarProgressStats } from "components/core"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; +import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; - type Props = { issues: IIssue[]; cycle: ICycle | undefined; diff --git a/apps/app/components/states/create-update-state-inline.tsx b/apps/app/components/states/create-update-state-inline.tsx index 46043b0cd..7d67f8fcd 100644 --- a/apps/app/components/states/create-update-state-inline.tsx +++ b/apps/app/components/states/create-update-state-inline.tsx @@ -1,5 +1,7 @@ import React, { useEffect } from "react"; +import { useRouter } from "next/router"; + import { mutate } from "swr"; // react-hook-form @@ -15,15 +17,13 @@ import useToast from "hooks/use-toast"; // ui import { Button, CustomSelect, Input } from "components/ui"; // types -import type { IState, StateResponse } from "types"; +import type { IState } from "types"; // fetch-keys import { STATE_LIST } from "constants/fetch-keys"; // constants import { GROUP_CHOICES } from "constants/project"; type Props = { - workspaceSlug?: string; - projectId?: string; data: IState | null; onClose: () => void; selectedGroup: StateGroup | null; @@ -37,13 +37,10 @@ const defaultValues: Partial = { group: "backlog", }; -export const CreateUpdateStateInline: React.FC = ({ - workspaceSlug, - projectId, - data, - onClose, - selectedGroup, -}) => { +export const CreateUpdateStateInline: React.FC = ({ data, onClose, selectedGroup }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + const { setToastAlert } = useToast(); const { @@ -59,16 +56,18 @@ export const CreateUpdateStateInline: React.FC = ({ }); useEffect(() => { - if (data === null) return; + if (!data) return; + reset(data); }, [data, reset]); useEffect(() => { - if (!data) - reset({ - ...defaultValues, - group: selectedGroup ?? "backlog", - }); + if (data) return; + + reset({ + ...defaultValues, + group: selectedGroup ?? "backlog", + }); }, [selectedGroup, data, reset]); const handleClose = () => { @@ -78,14 +77,15 @@ export const CreateUpdateStateInline: React.FC = ({ const onSubmit = async (formData: IState) => { if (!workspaceSlug || !projectId || isSubmitting) return; + const payload: IState = { ...formData, }; if (!data) { await stateService - .createState(workspaceSlug, projectId, { ...payload }) + .createState(workspaceSlug as string, projectId as string, { ...payload }) .then((res) => { - mutate(STATE_LIST(projectId)); + mutate(STATE_LIST(projectId as string)); handleClose(); setToastAlert({ @@ -103,11 +103,11 @@ export const CreateUpdateStateInline: React.FC = ({ }); } else { await stateService - .updateState(workspaceSlug, projectId, data.id, { + .updateState(workspaceSlug as string, projectId as string, data.id, { ...payload, }) .then((res) => { - mutate(STATE_LIST(projectId)); + mutate(STATE_LIST(projectId as string)); handleClose(); setToastAlert({ diff --git a/apps/app/components/states/index.ts b/apps/app/components/states/index.ts index 63bb55a9c..167e12d8b 100644 --- a/apps/app/components/states/index.ts +++ b/apps/app/components/states/index.ts @@ -1,3 +1,4 @@ export * from "./create-update-state-inline"; export * from "./create-update-state-modal"; export * from "./delete-state-modal"; +export * from "./single-state"; diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx new file mode 100644 index 000000000..5e9aff80c --- /dev/null +++ b/apps/app/components/states/single-state.tsx @@ -0,0 +1,217 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// services +import stateService from "services/state.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { Tooltip } from "components/ui"; +// icons +import { + ArrowDownIcon, + ArrowUpIcon, + PencilSquareIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +// helpers +import { addSpaceIfCamelCase } from "helpers/string.helper"; +import { groupBy, orderArrayBy } from "helpers/array.helper"; +import { orderStateGroups } from "helpers/state.helper"; +// types +import { IState } from "types"; +import { StateGroup } from "components/states"; +// fetch-keys +import { STATE_LIST } from "constants/fetch-keys"; + +type Props = { + index: number; + currentGroup: string; + state: IState; + statesList: IState[]; + activeGroup: StateGroup; + handleEditState: () => void; + handleDeleteState: () => void; +}; + +export const SingleState: React.FC = ({ + index, + currentGroup, + state, + statesList, + activeGroup, + handleEditState, + handleDeleteState, +}) => { + const [isSubmitting, setIsSubmitting] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const groupLength = statesList.filter((s) => s.group === currentGroup).length; + + const handleMakeDefault = (stateId: string) => { + setIsSubmitting(true); + + const currentDefaultState = statesList.find((s) => s.default); + + if (currentDefaultState) + stateService + .patchState(workspaceSlug as string, projectId as string, currentDefaultState?.id ?? "", { + default: false, + }) + .then(() => { + stateService + .patchState(workspaceSlug as string, projectId as string, stateId, { + default: true, + }) + .then((res) => { + mutate(STATE_LIST(projectId as string)); + setToastAlert({ + type: "success", + title: "Successful", + message: `${res.name} state set to default successfuly.`, + }); + setIsSubmitting(false); + }) + .catch((err) => { + setToastAlert({ + type: "error", + title: "Error", + message: "Error in setting the state to default.", + }); + setIsSubmitting(false); + }); + }); + else + stateService + .patchState(workspaceSlug as string, projectId as string, stateId, { + default: true, + }) + .then((res) => { + mutate(STATE_LIST(projectId as string)); + setToastAlert({ + type: "success", + title: "Successful", + message: `${res.name} state set to default successfuly.`, + }); + setIsSubmitting(false); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error", + message: "Error in setting the state to default.", + }); + setIsSubmitting(false); + }); + }; + + const handleMove = (state: IState, index: number, direction: "up" | "down") => { + let newSequence = 15000; + + if (direction === "up") { + if (index === 1) newSequence = statesList[0].sequence - 15000; + else newSequence = (statesList[index - 2].sequence + statesList[index - 1].sequence) / 2; + } else { + if (index === groupLength - 2) newSequence = statesList[groupLength - 1].sequence + 15000; + else newSequence = (statesList[index + 2].sequence + statesList[index + 1].sequence) / 2; + } + + let newStatesList = statesList.map((s) => { + if (s.id === state.id) + return { + ...s, + sequence: newSequence, + }; + + return s; + }); + newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); + mutate( + STATE_LIST(projectId as string), + orderStateGroups(groupBy(newStatesList, "group")), + false + ); + + stateService + .patchState(workspaceSlug as string, projectId as string, state.id, { + sequence: newSequence, + }) + .then((res) => { + console.log(res); + mutate(STATE_LIST(projectId as string)); + }) + .catch((err) => { + console.error(err); + }); + }; + + return ( +
+
+
+
{addSpaceIfCamelCase(state.name)}
+
+
+ {index !== 0 && ( + + )} + {!(index === groupLength - 1) && ( + + )} + {state.default ? ( + Default + ) : ( + + )} + + + + +
+
+ ); +}; diff --git a/apps/app/components/ui/tooltip.tsx b/apps/app/components/ui/tooltip.tsx new file mode 100644 index 000000000..5c91b7f0d --- /dev/null +++ b/apps/app/components/ui/tooltip.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from "react"; + +export type Props = { + direction?: "top" | "right" | "bottom" | "left"; + content: string | React.ReactNode; + margin?: string; + children: React.ReactNode; + className?: string; + disabled?: boolean; +}; + +export const Tooltip: React.FC = ({ + content, + direction = "top", + children, + margin = "24px", + className = "", + disabled = false, +}) => { + const [active, setActive] = useState(false); + const [styleConfig, setStyleConfig] = useState(`top-[calc(-100%-${margin})]`); + let timeout: any; + + const showToolTip = () => { + timeout = setTimeout(() => { + setActive(true); + }, 300); + }; + + const hideToolTip = () => { + clearInterval(timeout); + setActive(false); + }; + + const tooltipStyles = { + top: "left-[50%] translate-x-[-50%] before:contents-[''] before:border-solid before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:top-full before:border-t-black", + + right: "right-[-100%] top-[50%] translate-x-0 translate-y-[-50%]", + + bottom: + "left-[50%] translate-x-[-50%] before:contents-[''] before:border-solid before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:bottom-full before:border-b-black", + + left: "left-[-100%] top-[50%] translate-x-0 translate-y-[-50%]", + }; + + useEffect(() => { + const styleConfig = `${direction}-[calc(-100%-${margin})]`; + setStyleConfig(styleConfig); + }, [margin, direction]); + + return ( +
+ {children} + {active && ( +
+ {content} +
+ )} +
+ ); +}; diff --git a/apps/app/components/ui/tooltip/index.tsx b/apps/app/components/ui/tooltip/index.tsx deleted file mode 100644 index d49013a8b..000000000 --- a/apps/app/components/ui/tooltip/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect, useState } from "react"; - -export type Props = { - direction?: "top" | "right" | "bottom" | "left"; - content: string | React.ReactNode; - margin?: string; - children: React.ReactNode; - customStyle?: string; -}; - -const Tooltip: React.FC = ({ - content, - direction = "top", - children, - margin = "24px", - customStyle, -}) => { - const [active, setActive] = useState(false); - const [styleConfig, setStyleConfig] = useState("top-[calc(-100%-24px)]"); - let timeout: any; - - const showToolTip = () => { - timeout = setTimeout(() => { - setActive(true); - }, 300); - }; - - const hideToolTip = () => { - clearInterval(timeout); - setActive(false); - }; - - const tooltipStyles = { - top: ` - left-[50%] translate-x-[-50%] before:contents-[""] before:border-solid - before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none - before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:top-full before:border-t-black`, - - right: ` - right-[-100%] top-[50%] - translate-x-0 translate-y-[-50%] `, - - bottom: ` - left-[50%] translate-x-[-50%] before:contents-[""] before:border-solid - before:border-transparent before:h-0 before:w-0 before:absolute before:pointer-events-none - before:border-[6px] before:left-[50%] before:ml-[calc(6px*-1)] before:bottom-full before:border-b-black`, - - left: ` - left-[-100%] top-[50%] - translate-x-0 translate-y-[-50%] `, - }; - - useEffect(() => { - const styleConfig = direction + "-[calc(-100%-" + margin + ")]"; - setStyleConfig(styleConfig); - }, [margin, direction]); - - return ( -
- {children} - {active && ( -
- {content} -
- )} -
- ); -}; - -export default Tooltip; diff --git a/apps/app/hooks/use-issue-properties.tsx b/apps/app/hooks/use-issue-properties.tsx index c33cc3e61..08ff54362 100644 --- a/apps/app/hooks/use-issue-properties.tsx +++ b/apps/app/hooks/use-issue-properties.tsx @@ -17,7 +17,6 @@ const initialValues: Properties = { sub_issue_count: false, }; -// TODO: CHECK THIS LOGIC const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { const [properties, setProperties] = useState(initialValues); @@ -34,6 +33,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { useEffect(() => { if (!issueProperties || !workspaceSlug || !projectId || !user) return; + setProperties({ ...initialValues, ...issueProperties.properties }); if (Object.keys(issueProperties).length === 0) @@ -53,6 +53,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => { if (!workspaceSlug || !user) return; setProperties((prev) => ({ ...prev, [key]: !prev[key] })); + if (issueProperties && projectId) { mutateIssueProperties( (prev) => diff --git a/apps/app/layouts/app-layout/index.tsx b/apps/app/layouts/app-layout/index.tsx index d6462fdd3..fddc5f2f5 100644 --- a/apps/app/layouts/app-layout/index.tsx +++ b/apps/app/layouts/app-layout/index.tsx @@ -13,7 +13,7 @@ import useUser from "hooks/use-user"; import { Button, Spinner } from "components/ui"; // components import { NotAuthorizedView } from "components/core"; -import CommandPalette from "components/command-palette"; +import { CommandPalette } from "components/command-palette"; import { JoinProject } from "components/project"; // local components import Container from "layouts/container"; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx index 41ca87343..1915dc389 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/labels.tsx @@ -4,24 +4,22 @@ import { useRouter } from "next/router"; import useSWR from "swr"; -// react-hook-form -import { Controller, SubmitHandler, useForm } from "react-hook-form"; -// react-color -import { TwitterPicker } from "react-color"; -// headless ui -import { Popover, Transition } from "@headlessui/react"; // services import projectService from "services/project.service"; -import workspaceService from "services/workspace.service"; import issuesService from "services/issues.service"; // lib import { requiredAdmin } from "lib/auth"; // layouts import AppLayout from "layouts/app-layout"; // components -import { SingleLabel } from "components/labels"; +import { + CreateUpdateLabelInline, + LabelsListModal, + SingleLabel, + SingleLabelGroup, +} from "components/labels"; // ui -import { Button, Input, Loader } from "components/ui"; +import { Button, Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons import { PlusIcon } from "@heroicons/react/24/outline"; @@ -29,19 +27,21 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import { IIssueLabels, UserAuth } from "types"; import type { NextPageContext, NextPage } from "next"; // fetch-keys -import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS, WORKSPACE_DETAILS } from "constants/fetch-keys"; - -const defaultValues: Partial = { - name: "", - color: "#ff0000", -}; +import { PROJECT_DETAILS, PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; const LabelsSettings: NextPage = (props) => { const { isMember, isOwner, isViewer, isGuest } = props; + // create/edit label form const [labelForm, setLabelForm] = useState(false); + + // edit label const [isUpdating, setIsUpdating] = useState(false); - const [labelIdForUpdate, setLabelIdForUpdate] = useState(null); + const [labelToUpdate, setLabelToUpdate] = useState(null); + + // labels list modal + const [labelsListModal, setLabelsListModal] = useState(false); + const [parentLabel, setParentLabel] = useState(undefined); const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -60,57 +60,20 @@ const LabelsSettings: NextPage = (props) => { : null ); - const { - register, - handleSubmit, - reset, - control, - setValue, - formState: { errors, isSubmitting }, - watch, - } = useForm({ defaultValues }); - const newLabel = () => { - reset(); setIsUpdating(false); setLabelForm(true); }; + const addLabelToGroup = (parentLabel: IIssueLabels) => { + setLabelsListModal(true); + setParentLabel(parentLabel); + }; + const editLabel = (label: IIssueLabels) => { setLabelForm(true); - setValue("color", label.color); - setValue("name", label.name); setIsUpdating(true); - setLabelIdForUpdate(label.id); - }; - - const handleLabelCreate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectDetails || isSubmitting) return; - - await issuesService - .createIssueLabel(workspaceSlug as string, projectDetails.id, formData) - .then((res) => { - mutate((prevData) => [res, ...(prevData ?? [])], false); - reset(defaultValues); - setLabelForm(false); - }); - }; - - const handleLabelUpdate: SubmitHandler = async (formData) => { - if (!workspaceSlug || !projectDetails || isSubmitting) return; - - await issuesService - .patchIssueLabel(workspaceSlug as string, projectDetails.id, labelIdForUpdate ?? "", formData) - .then((res) => { - console.log(res); - reset(defaultValues); - mutate( - (prevData) => - prevData?.map((p) => (p.id === labelIdForUpdate ? { ...p, ...formData } : p)), - false - ); - setLabelForm(false); - }); + setLabelToUpdate(label); }; const handleLabelDelete = (labelId: string) => { @@ -128,146 +91,85 @@ const LabelsSettings: NextPage = (props) => { }; return ( - - - - - } - > -
-
-

Labels

-

Manage the labels of this project.

-
-
-

Manage labels

- -
-
-
-
- - {({ open }) => ( - <> - - {watch("color") && watch("color") !== "" && ( - - )} - - - - - ( - onChange(value.hex)} - /> - )} - /> - - - - )} - -
-
- -
- - {isUpdating ? ( - - ) : ( - - )} + <> + setLabelsListModal(false)} + parent={parentLabel} + /> + + + + + } + > +
+
+

Labels

+

Manage the labels of this project.

- <> - {issueLabels ? ( - issueLabels.map((label) => ( - - )) - ) : ( - - - - - - - )} - -
-
-
+
+

Manage labels

+ +
+
+ + <> + {issueLabels ? ( + issueLabels.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); + + if (children && children.length === 0) { + if (!label.parent) + return ( + addLabelToGroup(label)} + editLabel={editLabel} + handleLabelDelete={handleLabelDelete} + /> + ); + } else + return ( + + ); + }) + ) : ( + + + + + + + )} + +
+ + + ); }; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index 267f812a3..4cec9379e 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -4,22 +4,26 @@ import { useRouter } from "next/router"; import useSWR from "swr"; +// lib +import { requiredAdmin } from "lib/auth"; // services import stateService from "services/state.service"; import projectService from "services/project.service"; -// lib -import { requiredAdmin } from "lib/auth"; // layouts import AppLayout from "layouts/app-layout"; // components -import { CreateUpdateStateInline, DeleteStateModal, StateGroup } from "components/states"; +import { + CreateUpdateStateInline, + DeleteStateModal, + SingleState, + StateGroup, +} from "components/states"; // ui import { Loader } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PencilSquareIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { PlusIcon } from "@heroicons/react/24/outline"; // helpers -import { addSpaceIfCamelCase } from "helpers/string.helper"; import { getStatesList, orderStateGroups } from "helpers/state.helper"; // types import { UserAuth } from "types"; @@ -34,9 +38,8 @@ const StatesSettings: NextPage = (props) => { const [selectedState, setSelectedState] = useState(null); const [selectDeleteState, setSelectDeleteState] = useState(null); - const { - query: { workspaceSlug, projectId }, - } = useRouter(); + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, @@ -99,54 +102,33 @@ const StatesSettings: NextPage = (props) => {
{key === activeGroup && ( { setActiveGroup(null); setSelectedState(null); }} - workspaceSlug={workspaceSlug as string} data={null} selectedGroup={key as keyof StateGroup} /> )} - {orderedStateGroups[key].map((state) => + {orderedStateGroups[key].map((state, index) => state.id !== selectedState ? ( -
-
-
-
{addSpaceIfCamelCase(state.name)}
-
-
- - -
-
+ index={index} + currentGroup={key} + state={state} + statesList={statesList} + activeGroup={activeGroup} + handleEditState={() => setSelectedState(state.id)} + handleDeleteState={() => setSelectDeleteState(state.id)} + /> ) : (
{ setActiveGroup(null); setSelectedState(null); }} - workspaceSlug={workspaceSlug as string} data={ statesList?.find((state) => state.id === selectedState) ?? null } diff --git a/apps/app/types/state.d.ts b/apps/app/types/state.d.ts index 5f69ae49f..29096c302 100644 --- a/apps/app/types/state.d.ts +++ b/apps/app/types/state.d.ts @@ -1,17 +1,18 @@ export interface IState { readonly id: string; - readonly created_at: Date; - readonly updated_at: Date; - name: string; - description: string; color: string; - readonly slug: string; + readonly created_at: Date; readonly created_by: string; - readonly updated_by: string; - project: string; - workspace: string; - sequence: number; + default: boolean; + description: string; group: "backlog" | "unstarted" | "started" | "completed" | "cancelled"; + name: string; + project: string; + sequence: number; + readonly slug: string; + readonly updated_at: Date; + readonly updated_by: string; + workspace: string; } export interface StateResponse { From af22dc9c5896c648439fe3f18a2069affc0b74b2 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 10 Feb 2023 18:39:23 +0530 Subject: [PATCH 06/52] fix: remirror buttons (#267) --- .../rich-text-editor/toolbar/bold.tsx | 27 ------------------ .../rich-text-editor/toolbar/index.tsx | 27 +++++++++--------- .../rich-text-editor/toolbar/italic.tsx | 28 ------------------- .../rich-text-editor/toolbar/redo.tsx | 18 ------------ .../rich-text-editor/toolbar/strike.tsx | 26 ----------------- .../rich-text-editor/toolbar/underline.tsx | 28 ------------------- .../rich-text-editor/toolbar/undo.tsx | 21 -------------- apps/app/styles/editor.css | 20 +++++++++++++ 8 files changed, 34 insertions(+), 161 deletions(-) delete mode 100644 apps/app/components/rich-text-editor/toolbar/bold.tsx delete mode 100644 apps/app/components/rich-text-editor/toolbar/italic.tsx delete mode 100644 apps/app/components/rich-text-editor/toolbar/redo.tsx delete mode 100644 apps/app/components/rich-text-editor/toolbar/strike.tsx delete mode 100644 apps/app/components/rich-text-editor/toolbar/underline.tsx delete mode 100644 apps/app/components/rich-text-editor/toolbar/undo.tsx diff --git a/apps/app/components/rich-text-editor/toolbar/bold.tsx b/apps/app/components/rich-text-editor/toolbar/bold.tsx deleted file mode 100644 index 59c51b80c..000000000 --- a/apps/app/components/rich-text-editor/toolbar/bold.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useCommands, useActive } from "@remirror/react"; - -export const BoldButton = () => { - const { toggleBold, focus } = useCommands(); - const active = useActive(); - - return ( - - ); -}; diff --git a/apps/app/components/rich-text-editor/toolbar/index.tsx b/apps/app/components/rich-text-editor/toolbar/index.tsx index 1167c8d22..de42077cd 100644 --- a/apps/app/components/rich-text-editor/toolbar/index.tsx +++ b/apps/app/components/rich-text-editor/toolbar/index.tsx @@ -1,11 +1,12 @@ -// history -import { RedoButton } from "./redo"; -import { UndoButton } from "./undo"; -// formats -import { BoldButton } from "./bold"; -import { ItalicButton } from "./italic"; -import { UnderlineButton } from "./underline"; -import { StrikeButton } from "./strike"; +// buttons +import { + ToggleBoldButton, + ToggleItalicButton, + ToggleUnderlineButton, + ToggleStrikeButton, + RedoButton, + UndoButton, +} from "@remirror/react"; // headings import HeadingControls from "./heading-controls"; // list @@ -15,17 +16,17 @@ import { UnorderedListButton } from "./unordered-list"; export const RichTextToolbar: React.FC = () => (
- +
- - - - + + + +
diff --git a/apps/app/components/rich-text-editor/toolbar/italic.tsx b/apps/app/components/rich-text-editor/toolbar/italic.tsx deleted file mode 100644 index 6d7ab328a..000000000 --- a/apps/app/components/rich-text-editor/toolbar/italic.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useCommands, useActive } from "@remirror/react"; - -export const ItalicButton = () => { - const { toggleItalic, focus } = useCommands(); - - const active = useActive(); - - return ( - - ); -}; diff --git a/apps/app/components/rich-text-editor/toolbar/redo.tsx b/apps/app/components/rich-text-editor/toolbar/redo.tsx deleted file mode 100644 index 6fea794e0..000000000 --- a/apps/app/components/rich-text-editor/toolbar/redo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useCommands } from "@remirror/react"; - -export const RedoButton = () => { - const { redo } = useCommands(); - return ( - - ); -}; diff --git a/apps/app/components/rich-text-editor/toolbar/strike.tsx b/apps/app/components/rich-text-editor/toolbar/strike.tsx deleted file mode 100644 index 52c22926e..000000000 --- a/apps/app/components/rich-text-editor/toolbar/strike.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useCommands, useActive } from "@remirror/react"; - -export const StrikeButton = () => { - const { toggleStrike } = useCommands(); - - const active = useActive(); - - return ( - - ); -}; diff --git a/apps/app/components/rich-text-editor/toolbar/underline.tsx b/apps/app/components/rich-text-editor/toolbar/underline.tsx deleted file mode 100644 index 955a6a356..000000000 --- a/apps/app/components/rich-text-editor/toolbar/underline.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useCommands, useActive } from "@remirror/react"; - -export const UnderlineButton = () => { - const { toggleUnderline, focus } = useCommands(); - - const active = useActive(); - - return ( - - ); -}; diff --git a/apps/app/components/rich-text-editor/toolbar/undo.tsx b/apps/app/components/rich-text-editor/toolbar/undo.tsx deleted file mode 100644 index 6c35b96ae..000000000 --- a/apps/app/components/rich-text-editor/toolbar/undo.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useCommands } from "@remirror/react"; - -// icons - -export const UndoButton = () => { - const { undo } = useCommands(); - - return ( - - ); -}; diff --git a/apps/app/styles/editor.css b/apps/app/styles/editor.css index 319f3207d..4c7f49915 100644 --- a/apps/app/styles/editor.css +++ b/apps/app/styles/editor.css @@ -419,3 +419,23 @@ img.ProseMirror-separator { text-decoration: underline; } /* end link styling */ + +/* format buttons styling */ +.MuiButtonBase-root { + border: none !important; + border-radius: 0.25rem !important; + padding: 0.25rem !important; +} + +.MuiButtonBase-root:hover { + background-color: rgb(229 231 235); +} + +.MuiButtonBase-root svg { + fill: #000 !important; +} + +.MuiButtonBase-root.Mui-selected { + background-color: rgb(229 231 235) !important; +} +/* end format buttons styling */ From bb4ffec7e8f94c5b31a7d0aac5791a5027f72316 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 10 Feb 2023 18:40:02 +0530 Subject: [PATCH 07/52] feat: burndown chart (#268) * chore: recharts dependencie added * chore: tpye added for issue completed at * feat: date range helper fn added * feat: progress chart added * feat: ideal task line added in progress chart * feat: chart legends added --- .../core/sidebar/progress-chart.tsx | 97 ++++++ .../core/sidebar/sidebar-progress-stats.tsx | 242 +++++++------ apps/app/components/modules/sidebar.tsx | 9 +- .../cycles/cycle-detail-sidebar/index.tsx | 10 +- apps/app/helpers/date-time.helper.ts | 10 + apps/app/package.json | 1 + apps/app/types/issues.d.ts | 1 + pnpm-lock.yaml | 323 +++++++++++++++--- 8 files changed, 528 insertions(+), 165 deletions(-) create mode 100644 apps/app/components/core/sidebar/progress-chart.tsx diff --git a/apps/app/components/core/sidebar/progress-chart.tsx b/apps/app/components/core/sidebar/progress-chart.tsx new file mode 100644 index 000000000..d787e7077 --- /dev/null +++ b/apps/app/components/core/sidebar/progress-chart.tsx @@ -0,0 +1,97 @@ +import React from "react"; + +import { + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + AreaChart, + Area, + ReferenceLine, +} from "recharts"; + +//types +import { IIssue } from "types"; +// helper +import { getDatesInRange, renderShortNumericDateFormat } from "helpers/date-time.helper"; + +type Props = { + issues: IIssue[]; + start: string; + end: string; +}; + +const ProgressChart: React.FC = ({ issues, start, end }) => { + const startDate = new Date(start); + const endDate = new Date(end); + const getChartData = () => { + const dateRangeArray = getDatesInRange(startDate, endDate); + let count = 0; + const dateWiseData = dateRangeArray.map((d) => { + const current = d.toISOString().split("T")[0]; + const total = issues.length; + const currentData = issues.filter( + (i) => i.completed_at && i.completed_at.toString().split("T")[0] === current + ); + count = currentData ? currentData.length + count : count; + + return { + currentDate: renderShortNumericDateFormat(current), + currentDateData: currentData, + pending: new Date(current) < new Date() ? total - count : null, + }; + }); + return dateWiseData; + }; + const ChartData = getChartData(); + return ( +
+
+ + + + + + + + + +
+
+
+ + Ideal +
+
+ + Current +
+
+
+ ); +}; + +export default ProgressChart; diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index 9a3e53723..49c5be771 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -53,130 +53,128 @@ export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) : null ); return ( -
- - + + + `w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + } > - - `w-1/2 rounded py-1 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + Assignees + + + `w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}` + } + > + Labels + + + `w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}` + } + > + States + + + + + {members?.map((member, index) => { + const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id)); + const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed"); + if (totalArray.length > 0) { + return ( + + + {member.member.first_name} + + } + completed={completeArray.length} + total={totalArray.length} + /> + ); } - > - Assignees - - - `w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}` - } - > - Labels - - - `w-1/2 rounded py-1 ${selected ? "bg-gray-300 font-semibold" : "hover:bg-gray-200 "}` - } - > - States - - - - - {members?.map((member, index) => { - const totalArray = issues?.filter((i) => i.assignees?.includes(member.member.id)); - const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed"); - if (totalArray.length > 0) { - return ( - - - {member.member.first_name} - - } - completed={completeArray.length} - total={totalArray.length} - /> - ); - } - })} - {issues?.filter((i) => i.assignees?.length === 0).length > 0 ? ( - -
- User -
- No assignee - - } - completed={ - issues?.filter( - (i) => i.state_detail.group === "completed" && i.assignees?.length === 0 - ).length - } - total={issues?.filter((i) => i.assignees?.length === 0).length} - /> - ) : ( - "" - )} -
- - {issueLabels?.map((issue, index) => { - const totalArray = issues?.filter((i) => i.labels?.includes(issue.id)); - const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed"); - if (totalArray.length > 0) { - return ( - - - {issue.name} - - } - completed={completeArray.length} - total={totalArray.length} - /> - ); - } - })} - - - {Object.keys(groupedIssues).map((group, index) => ( - - i.assignees?.length === 0).length > 0 ? ( + +
+ User - {group} - - } - completed={groupedIssues[group].length} - total={issues.length} - /> - ))} - - - -
+
+ No assignee + + } + completed={ + issues?.filter( + (i) => i.state_detail.group === "completed" && i.assignees?.length === 0 + ).length + } + total={issues?.filter((i) => i.assignees?.length === 0).length} + /> + ) : ( + "" + )} + + + {issueLabels?.map((issue, index) => { + const totalArray = issues?.filter((i) => i.labels?.includes(issue.id)); + const completeArray = totalArray?.filter((i) => i.state_detail.group === "completed"); + if (totalArray.length > 0) { + return ( + + + {issue.name} + + } + completed={completeArray.length} + total={totalArray.length} + /> + ); + } + })} + + + {Object.keys(groupedIssues).map((group, index) => ( + + + {group} + + } + completed={groupedIssues[group].length} + total={issues.length} + /> + ))} + + + ); }; diff --git a/apps/app/components/modules/sidebar.tsx b/apps/app/components/modules/sidebar.tsx index 3ea95cec3..235c9c7b2 100644 --- a/apps/app/components/modules/sidebar.tsx +++ b/apps/app/components/modules/sidebar.tsx @@ -42,6 +42,7 @@ import { groupBy } from "helpers/array.helper"; import { IIssue, IModule, ModuleIssueResponse } from "types"; // fetch-keys import { MODULE_DETAILS } from "constants/fetch-keys"; +import ProgressChart from "components/core/sidebar/progress-chart"; const defaultValues: Partial = { lead: "", @@ -295,7 +296,13 @@ export const ModuleDetailsSidebar: React.FC = ({
-
+
+ +
diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index c55f30591..cdbac8b17 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -29,6 +29,7 @@ import { renderShortNumericDateFormat } from "helpers/date-time.helper"; import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; +import ProgressChart from "components/core/sidebar/progress-chart"; type Props = { issues: IIssue[]; @@ -246,7 +247,14 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue
-
+
+
+ +
diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts index fa490d5d9..4d179ef0d 100644 --- a/apps/app/helpers/date-time.helper.ts +++ b/apps/app/helpers/date-time.helper.ts @@ -25,6 +25,16 @@ export const findHowManyDaysLeft = (date: string | Date) => { return Math.ceil(timeDiff / (1000 * 3600 * 24)); }; +export const getDatesInRange = (startDate: Date, endDate: Date) => { + const date = new Date(startDate.getTime()); + const dates = []; + while (date <= endDate) { + dates.push(new Date(date)); + date.setDate(date.getDate() + 1); + } + return dates; +}; + export const timeAgo = (time: any) => { switch (typeof time) { case "number": diff --git a/apps/app/package.json b/apps/app/package.json index 8b1d8944c..5b0cccdcb 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -31,6 +31,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", + "recharts": "^2.3.2", "remirror": "^2.0.23", "swr": "^1.3.0", "uuid": "^9.0.0" diff --git a/apps/app/types/issues.d.ts b/apps/app/types/issues.d.ts index 3dcb8c918..08dbd5245 100644 --- a/apps/app/types/issues.d.ts +++ b/apps/app/types/issues.d.ts @@ -74,6 +74,7 @@ export interface IIssue { blockers_list: string[]; blocks_list: string[]; bridge: string; + completed_at: Date; created_at: Date; created_by: string; cycle: string | null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 492366154..c67aca649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: turbo: latest devDependencies: config: link:packages/config - prettier: 2.8.3 - turbo: 1.7.2 + prettier: 2.8.4 + turbo: 1.7.4 apps/app: specifiers: @@ -50,6 +50,7 @@ importers: react-dom: 18.2.0 react-dropzone: ^14.2.3 react-hook-form: ^7.38.0 + recharts: ^2.3.2 remirror: ^2.0.23 swr: ^1.3.0 tailwindcss: ^3.1.6 @@ -79,6 +80,7 @@ importers: react-dom: 18.2.0_react@18.2.0 react-dropzone: 14.2.3_react@18.2.0 react-hook-form: 7.40.0_react@18.2.0 + recharts: 2.3.2_biqbaboplfbrettd7655fr4n2y remirror: 2.0.23_yxmznn53udiq54cecqhape5kke swr: 1.3.0_react@18.2.0 uuid: 9.0.0 @@ -1462,13 +1464,13 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 + dev: false /@babel/runtime/7.20.7: resolution: {integrity: sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.11 - dev: false /@babel/template/7.20.7: resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} @@ -1842,7 +1844,7 @@ packages: resolution: {integrity: sha512-owjYOn/3xaWVW01p32Ylt3cXkCP79oudJCHdcNOn4noxd/9BhyFX2wLiVf02DxGYnkAgAD3KCp3Z4iyKlueymg==} engines: {node: '>=10.0.0'} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 make-plural: 6.2.2 messageformat-parser: 4.1.3 dev: false @@ -2414,13 +2416,13 @@ packages: /@remirror/core-constants/2.0.0: resolution: {integrity: sha512-vpePPMecHJllBqCWXl6+FIcZqS+tRUM2kSCCKFeEo1H3XUEv3ocijBIPhnlSAa7g6maX+12ATTgxrOsLpWVr2g==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 dev: false /@remirror/core-helpers/2.0.1: resolution: {integrity: sha512-s8M1pn33aBUhduvD1QR02uUQMegnFkGaTr4c1iBzxTTyg0rbQstzuQ7Q8TkL6n64JtgCdJS9jLz2dONb2meBKQ==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@linaria/core': 3.0.0-beta.13 '@remirror/core-constants': 2.0.0 '@remirror/types': 1.0.0 @@ -2456,7 +2458,7 @@ packages: '@types/node': optional: true dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@remirror/core-constants': 2.0.0 '@remirror/core-helpers': 2.0.1 '@remirror/core-types': 2.0.3_@remirror+pm@2.0.3 @@ -3358,7 +3360,7 @@ packages: /@remirror/i18n/2.0.2: resolution: {integrity: sha512-NYKGdMf3DGILMHKZfOrmiGW8XlhQ7w4edkenjQfa5FPTyvZTbQzKT0urxeoF9B1Pfu7VR6yiZZLp4yRMIiGqvw==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@lingui/core': 3.15.0 '@lingui/detect-locale': 3.15.0 '@remirror/core-helpers': 2.0.1 @@ -3368,14 +3370,14 @@ packages: /@remirror/icons/2.0.1: resolution: {integrity: sha512-dk+F9c38seXFnJLfPNDXKCUonUAg53LFxhpBXNkZfPxmf6bVqv2p3L5QhwEvbYmvOgdY0w4BM1Po1NYHUJfATg==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@remirror/core-helpers': 2.0.1 dev: false /@remirror/messages/2.0.2: resolution: {integrity: sha512-Cf78RL2OXQCWkiO0QgV+1FCpmDwjoVctZ/ZAXT2RgBBmNKQHauPBZaG0Z3Cpb9I0vdz3xIaaEghwVS0cJNWtYA==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@lingui/core': 3.15.0 '@remirror/core-helpers': 2.0.1 dev: false @@ -4075,6 +4077,48 @@ packages: '@types/tern': 0.23.4 dev: false + /@types/d3-array/3.0.4: + resolution: {integrity: sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==} + dev: false + + /@types/d3-color/3.1.0: + resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==} + dev: false + + /@types/d3-ease/3.0.0: + resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==} + dev: false + + /@types/d3-interpolate/3.0.1: + resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==} + dependencies: + '@types/d3-color': 3.1.0 + dev: false + + /@types/d3-path/3.0.0: + resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==} + dev: false + + /@types/d3-scale/4.0.3: + resolution: {integrity: sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==} + dependencies: + '@types/d3-time': 3.0.0 + dev: false + + /@types/d3-shape/3.1.1: + resolution: {integrity: sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==} + dependencies: + '@types/d3-path': 3.0.0 + dev: false + + /@types/d3-time/3.0.0: + resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + dev: false + + /@types/d3-timer/3.0.0: + resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==} + dev: false + /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: @@ -4698,7 +4742,7 @@ packages: resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} engines: {node: '>=6.0'} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@babel/runtime-corejs3': 7.20.6 /array-includes/3.1.6: @@ -5210,6 +5254,10 @@ packages: source-map: 0.6.1 dev: false + /css-unit-converter/1.1.2: + resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==} + dev: false + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -5218,6 +5266,77 @@ packages: /csstype/3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + /d3-array/3.2.2: + resolution: {integrity: sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color/3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease/3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format/3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate/3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path/3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale/4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.2 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape/3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format/4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time/3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.2 + dev: false + + /d3-timer/3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /damerau-levenshtein/1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -5261,6 +5380,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js-light/2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /decode-named-character-reference/1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} dependencies: @@ -5355,6 +5478,12 @@ packages: dependencies: esutils: 2.0.3 + /dom-helpers/3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.20.7 + dev: false + /dom-helpers/5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -5836,7 +5965,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 aria-query: 4.2.2 array-includes: 3.1.6 ast-types-flow: 0.0.7 @@ -5858,7 +5987,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 aria-query: 4.2.2 array-includes: 3.1.6 ast-types-flow: 0.0.7 @@ -5880,7 +6009,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 aria-query: 4.2.2 array-includes: 3.1.6 ast-types-flow: 0.0.7 @@ -6314,6 +6443,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + /eventemitter3/4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /extend/3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: false @@ -6325,6 +6458,10 @@ packages: /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + /fast-equals/2.0.4: + resolution: {integrity: sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==} + dev: false + /fast-glob/3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -6803,6 +6940,11 @@ packages: has: 1.0.3 side-channel: 1.0.4 + /internmap/2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /is-alphabetical/1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: false @@ -8423,6 +8565,10 @@ packages: cssesc: 3.0.0 util-deprecate: 1.0.2 + /postcss-value-parser/3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + dev: false + /postcss-value-parser/4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -8469,8 +8615,8 @@ packages: hasBin: true dev: true - /prettier/2.8.3: - resolution: {integrity: sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==} + /prettier/2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} engines: {node: '>=10.13.0'} hasBin: true dev: true @@ -8576,7 +8722,7 @@ packages: prosemirror-state: ^1 prosemirror-view: ^1 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@remirror/core-constants': 2.0.0 '@remirror/core-helpers': 2.0.1 escape-string-regexp: 4.0.0 @@ -8622,7 +8768,7 @@ packages: prosemirror-state: ^1 prosemirror-view: ^1 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@remirror/core-constants': 2.0.0 '@remirror/core-helpers': 2.0.1 '@remirror/types': 1.0.0 @@ -8649,7 +8795,7 @@ packages: prosemirror-state: ^1 prosemirror-view: ^1 dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@remirror/core-constants': 2.0.0 '@remirror/core-helpers': 2.0.1 escape-string-regexp: 4.0.0 @@ -8805,6 +8951,10 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false + /react-lifecycles-compat/3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + /react-onclickoutside/6.12.2_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==} peerDependencies: @@ -8841,7 +8991,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 '@types/react-redux': 7.1.24 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -8851,6 +9001,44 @@ packages: react-is: 17.0.2 dev: false + /react-resize-detector/7.1.2_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + + /react-smooth/2.0.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-Own9TA0GPPf3as4vSwFhDouVfXP15ie/wIHklhyKBH5AN6NFtdk0UpHBnonV11BtqDkAWlt40MOUc+5srmW7NA==} + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 2.0.4 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-transition-group: 2.9.0_biqbaboplfbrettd7655fr4n2y + dev: false + + /react-transition-group/2.9.0_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-lifecycles-compat: 3.0.4 + dev: false + /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -8937,6 +9125,33 @@ packages: dependencies: picomatch: 2.3.1 + /recharts-scale/0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts/2.3.2_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-2II30fGzKaypHfHNQNUhCfiLMxrOS/gF0WFahDIEFgXtJkVEe2DpZWFfEfAn+RU3B7/h2V/B05Bwmqq3rTXwLw==} + engines: {node: '>=12'} + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + classnames: 2.3.2 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + react-is: 16.13.1 + react-resize-detector: 7.1.2_biqbaboplfbrettd7655fr4n2y + react-smooth: 2.0.1_biqbaboplfbrettd7655fr4n2y + recharts-scale: 0.4.5 + reduce-css-calc: 2.1.8 + victory-vendor: 36.6.8 + dev: false + /recma-nextjs-static-props/1.0.0: resolution: {integrity: sha512-szo+rOZFU6mR0YWZi3e3dSqcEQU+E0f7GIyfMfntHeJccH1s9ODP0HWUeK7No0lcY1smRCcC43JrpoekzuX4Aw==} engines: {node: '>=14.0.0'} @@ -8946,10 +9161,17 @@ packages: unified: 10.1.2 dev: false + /reduce-css-calc/2.1.8: + resolution: {integrity: sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==} + dependencies: + css-unit-converter: 1.1.2 + postcss-value-parser: 3.3.1 + dev: false + /redux/4.2.0: resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} dependencies: - '@babel/runtime': 7.20.6 + '@babel/runtime': 7.20.7 dev: false /refractor/3.6.0: @@ -9867,65 +10089,65 @@ packages: typescript: 4.9.4 dev: false - /turbo-darwin-64/1.7.2: - resolution: {integrity: sha512-Sml3WR8MSu80W+gS8SnoKNImcDOlIX7zlvezzds65mW11yGniIFfZ18aKWGOm92Nj2SvXCQ2+UmyGghbFaHNmQ==} + /turbo-darwin-64/1.7.4: + resolution: {integrity: sha512-ZyYrQlUl8K/mYN1e6R7bEhPPYjMakz0DYMaexkyD7TAijQtWmTSd4a+I7VknOYNEssnUZ/v41GU3gPV1JAzxxQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64/1.7.2: - resolution: {integrity: sha512-JnlgGLScboUJGJxvmSsF+5xkImEDTMPg2FHzX4n8AMB9az9ZlPQAMtc+xu4p6Xp9eaykKiV2RG81YS3H0fxDLA==} + /turbo-darwin-arm64/1.7.4: + resolution: {integrity: sha512-CKIXg9uqp1a+Yeq/c4U0alPOqvwLUq5SBZf1PGYhGqJsfG0fRBtJfkUjHuBsuJIOGXg8rCmcGSWGIsIF6fqYuw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64/1.7.2: - resolution: {integrity: sha512-vbLJw6ovG+lpiPqxniscBjljKJ2jbsHuKp8uK4j/wqgp68wAVKeAZW77GGDAUgDb88XH6Kvhh2hcizL+iWduww==} + /turbo-linux-64/1.7.4: + resolution: {integrity: sha512-RIUl4RUFFyzD2T024vL7509Ygwcw+SEa8NOwPfaN6TtJHK7RZV/SBP3fLNVOptG9WRLnOWX3OvsLMbiOqDLLyA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64/1.7.2: - resolution: {integrity: sha512-zLnuS8WdHonKL74KqOopOH/leBOWumlVGF8/8hldbDPq0mwY+6myRR5/5LdveB51rkG4UJh/sQ94xV67tjBoyw==} + /turbo-linux-arm64/1.7.4: + resolution: {integrity: sha512-Bg65F0AjYYYxqE6RPf2H5TIGuA/EyWMeGOATHVSZOWAbYcnG3Ly03GZii8AHnUi7ntWBdjwvXf/QbOS1ayNB6A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64/1.7.2: - resolution: {integrity: sha512-oE5PMoXjmR09okvVzteFb6FjA6yo+nMsacsgKH2yLNq4sOrVo9tG98JkRurOv5+L6ZQ3yGXPxWHiqeH7hLkAVQ==} + /turbo-windows-64/1.7.4: + resolution: {integrity: sha512-rTaV50XZ2BRxRHOHqt1UsWfeDmYLbn8UKE6g2D2ED+uW+kmnTvR9s01nmlGWd2sAuWcRYQyQ2V+O09VfKPKcQw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64/1.7.2: - resolution: {integrity: sha512-mdTUJk23acRv5qxA/yEstYhM1VFenVE3FDrssxGRFq7S80smtCGK1xUd4BEDDzDlVXOqBohmM5jRh9516rcjPQ==} + /turbo-windows-arm64/1.7.4: + resolution: {integrity: sha512-h8sxdKPvHTnWUPtwnYszFMmSO0P/iUUwmYY9n7iYThA71zSao28UeZ0H0Gw75cY3MPjvkjn2C4EBAUGPjuZJLw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo/1.7.2: - resolution: {integrity: sha512-YR/x3GZEx0C1RV6Yvuw/HB1Ixx3upM6ZTTa4WqKz9WtLWN8u2g+u2h5KpG5YtjCS3wl/8zVXgHf2WiMK6KIghg==} + /turbo/1.7.4: + resolution: {integrity: sha512-8RLedDoUL0kkVKWEZ/RMM70BvKLyDFen06QuKKhYC2XNOfNKqFDqzIdcY/vGick869bNIWalChoy4O07k0HLsA==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.7.2 - turbo-darwin-arm64: 1.7.2 - turbo-linux-64: 1.7.2 - turbo-linux-arm64: 1.7.2 - turbo-windows-64: 1.7.2 - turbo-windows-arm64: 1.7.2 + turbo-darwin-64: 1.7.4 + turbo-darwin-arm64: 1.7.4 + turbo-linux-64: 1.7.4 + turbo-linux-arm64: 1.7.4 + turbo-windows-64: 1.7.4 + turbo-windows-arm64: 1.7.4 dev: true /turndown-plugin-gfm/1.0.2: @@ -10187,6 +10409,25 @@ packages: vfile-message: 3.1.3 dev: false + /victory-vendor/36.6.8: + resolution: {integrity: sha512-H3kyQ+2zgjMPvbPqAl7Vwm2FD5dU7/4bCTQakFQnpIsfDljeOMDojRsrmJfwh4oAlNnWhpAf+mbAoLh8u7dwyQ==} + dependencies: + '@types/d3-array': 3.0.4 + '@types/d3-ease': 3.0.0 + '@types/d3-interpolate': 3.0.1 + '@types/d3-scale': 4.0.3 + '@types/d3-shape': 3.1.1 + '@types/d3-time': 3.0.0 + '@types/d3-timer': 3.0.0 + d3-array: 3.2.2 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /vscode-oniguruma/1.7.0: resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} dev: false From 0a88b3ed842601e480e2de96975cfe0d0d0023e7 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:30:44 +0530 Subject: [PATCH 08/52] fix: state reordering (#269) * fix: state reordering * refactor: remove unnecessary argument * refactor: mutation after setting default --- apps/app/components/states/single-state.tsx | 89 ++++++++----------- .../projects/[projectId]/settings/states.tsx | 1 - 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index 5e9aff80c..2c0f4071e 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -6,8 +6,6 @@ import { mutate } from "swr"; // services import stateService from "services/state.service"; -// hooks -import useToast from "hooks/use-toast"; // ui import { Tooltip } from "components/ui"; // icons @@ -29,7 +27,6 @@ import { STATE_LIST } from "constants/fetch-keys"; type Props = { index: number; - currentGroup: string; state: IState; statesList: IState[]; activeGroup: StateGroup; @@ -39,7 +36,6 @@ type Props = { export const SingleState: React.FC = ({ index, - currentGroup, state, statesList, activeGroup, @@ -51,15 +47,26 @@ export const SingleState: React.FC = ({ const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { setToastAlert } = useToast(); + const groupStates = statesList.filter((s) => s.group === state.group); + const groupLength = groupStates.length; - const groupLength = statesList.filter((s) => s.group === currentGroup).length; - - const handleMakeDefault = (stateId: string) => { + const handleMakeDefault = () => { setIsSubmitting(true); const currentDefaultState = statesList.find((s) => s.default); + let newStatesList = statesList.map((s) => ({ + ...s, + default: s.id === state.id ? true : s.id === currentDefaultState?.id ? false : s.default, + })); + newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); + + mutate( + STATE_LIST(projectId as string), + orderStateGroups(groupBy(newStatesList, "group")), + false + ); + if (currentDefaultState) stateService .patchState(workspaceSlug as string, projectId as string, currentDefaultState?.id ?? "", { @@ -67,72 +74,48 @@ export const SingleState: React.FC = ({ }) .then(() => { stateService - .patchState(workspaceSlug as string, projectId as string, stateId, { + .patchState(workspaceSlug as string, projectId as string, state.id, { default: true, }) - .then((res) => { + .then(() => { mutate(STATE_LIST(projectId as string)); - setToastAlert({ - type: "success", - title: "Successful", - message: `${res.name} state set to default successfuly.`, - }); setIsSubmitting(false); }) - .catch((err) => { - setToastAlert({ - type: "error", - title: "Error", - message: "Error in setting the state to default.", - }); + .catch(() => { setIsSubmitting(false); }); }); else stateService - .patchState(workspaceSlug as string, projectId as string, stateId, { + .patchState(workspaceSlug as string, projectId as string, state.id, { default: true, }) - .then((res) => { + .then(() => { mutate(STATE_LIST(projectId as string)); - setToastAlert({ - type: "success", - title: "Successful", - message: `${res.name} state set to default successfuly.`, - }); setIsSubmitting(false); }) .catch(() => { - setToastAlert({ - type: "error", - title: "Error", - message: "Error in setting the state to default.", - }); setIsSubmitting(false); }); }; - const handleMove = (state: IState, index: number, direction: "up" | "down") => { + const handleMove = (state: IState, direction: "up" | "down") => { let newSequence = 15000; if (direction === "up") { - if (index === 1) newSequence = statesList[0].sequence - 15000; - else newSequence = (statesList[index - 2].sequence + statesList[index - 1].sequence) / 2; + if (index === 1) newSequence = groupStates[0].sequence - 15000; + else newSequence = (groupStates[index - 2].sequence + groupStates[index - 1].sequence) / 2; } else { - if (index === groupLength - 2) newSequence = statesList[groupLength - 1].sequence + 15000; - else newSequence = (statesList[index + 2].sequence + statesList[index + 1].sequence) / 2; + if (index === groupLength - 2) newSequence = groupStates[groupLength - 1].sequence + 15000; + else newSequence = (groupStates[index + 2].sequence + groupStates[index + 1].sequence) / 2; } - let newStatesList = statesList.map((s) => { - if (s.id === state.id) - return { - ...s, - sequence: newSequence, - }; - - return s; - }); + let newStatesList = statesList.map((s) => ({ + ...s, + sequence: s.id === state.id ? newSequence : s.sequence, + })); newStatesList = orderArrayBy(newStatesList, "sequence", "ascending"); + mutate( STATE_LIST(projectId as string), orderStateGroups(groupBy(newStatesList, "group")), @@ -155,7 +138,7 @@ export const SingleState: React.FC = ({ return (
@@ -165,14 +148,16 @@ export const SingleState: React.FC = ({ backgroundColor: state.color, }} /> -
{addSpaceIfCamelCase(state.name)}
+
+ {addSpaceIfCamelCase(state.name)} {state.sequence} +
{index !== 0 && ( @@ -181,7 +166,7 @@ export const SingleState: React.FC = ({ @@ -192,7 +177,7 @@ export const SingleState: React.FC = ({ +
+ {!isNotAllowed && ( +
+ +
+ )} + + + {properties.key && ( +
+ {issue.project_detail.identifier}-{issue.sequence_id}
)} - -
- {properties.key && ( -
- {issue.project_detail.identifier}-{issue.sequence_id} -
- )} -
- {issue.name} -
-
- -
- {properties.priority && ( - - )} - {properties.state && ( - - )} - {properties.due_date && ( - - )} - {properties.sub_issue_count && ( -
- {issue.sub_issues_count}{" "} - {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"} -
- )} - {properties.assignee && ( - - )} +
+ {issue.name} +
+ + +
+ {properties.priority && ( + + )} + {properties.state && ( + + )} + {properties.due_date && ( + + )} + {properties.sub_issue_count && ( +
+ {issue.sub_issues_count} {issue.sub_issues_count === 1 ? "sub-issue" : "sub-issues"}
-
+ )} + {properties.assignee && ( + + )}
- )} - +
+
); }; diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index ec4c5b688..2edb7f804 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; // react-beautiful-dnd -import { DropResult } from "react-beautiful-dnd"; +import { DragDropContext, DropResult } from "react-beautiful-dnd"; // services import issuesService from "services/issues.service"; import stateService from "services/state.service"; @@ -16,6 +16,9 @@ import useIssueView from "hooks/use-issue-view"; // components import { AllLists, AllBoards } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +// icons +import { TrashIcon } from "@heroicons/react/24/outline"; // helpers import { getStatesList } from "helpers/state.helper"; // types @@ -58,6 +61,9 @@ export const IssuesView: React.FC = ({ const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [issueToDelete, setIssueToDelete] = useState(null); + // trash box + const [trashBox, setTrashBox] = useState(false); + const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -78,271 +84,308 @@ export const IssuesView: React.FC = ({ : null ); + const handleDeleteIssue = useCallback( + (issue: IIssue) => { + setDeleteIssueModal(true); + setIssueToDelete(issue); + }, + [setDeleteIssueModal, setIssueToDelete] + ); + const handleOnDragEnd = useCallback( (result: DropResult) => { + setTrashBox(false); + if (!result.destination || !workspaceSlug || !projectId) return; const { source, destination } = result; const draggedItem = groupedByIssues[source.droppableId][source.index]; - if (source.droppableId !== destination.droppableId) { - const sourceGroup = source.droppableId; // source group id - const destinationGroup = destination.droppableId; // destination group id + if (destination.droppableId === "trashBox") { + handleDeleteIssue(draggedItem); + } else { + if (source.droppableId !== destination.droppableId) { + const sourceGroup = source.droppableId; // source group id + const destinationGroup = destination.droppableId; // destination group id - if (!sourceGroup || !destinationGroup) return; + if (!sourceGroup || !destinationGroup) return; - if (selectedGroup === "priority") { - // update the removed item for mutation - draggedItem.priority = destinationGroup; + if (selectedGroup === "priority") { + // update the removed item for mutation + draggedItem.priority = destinationGroup; - if (cycleId) - mutate( - CYCLE_ISSUES(cycleId as string), + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + priority: destinationGroup, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + priority: destinationGroup, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); + + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => { if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { + + const updatedIssues = prevData.results.map((issue) => { + if (issue.id === draggedItem.id) return { - ...issue, - issue_detail: { - ...draggedItem, - priority: destinationGroup, - }, + ...draggedItem, + priority: destinationGroup, }; - } + return issue; }); - return [...updatedIssues]; + + return { + ...prevData, + results: updatedIssues, + }; }, false ); - if (moduleId) - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => { - if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { - return { - ...issue, - issue_detail: { - ...draggedItem, - priority: destinationGroup, - }, - }; - } - return issue; - }); - return [...updatedIssues]; - }, - false - ); + // patch request + issuesService + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + priority: destinationGroup, + }) + .then((res) => { + if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); + if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), - (prevData) => { - if (!prevData) return prevData; - - const updatedIssues = prevData.results.map((issue) => { - if (issue.id === draggedItem.id) - return { - ...draggedItem, - priority: destinationGroup, - }; - - return issue; + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); }); + } else if (selectedGroup === "state_detail.name") { + const destinationState = states?.find((s) => s.name === destinationGroup); + const destinationStateId = destinationState?.id; - return { - ...prevData, - results: updatedIssues, - }; - }, - false - ); + // update the removed item for mutation + if (!destinationStateId || !destinationState) return; + draggedItem.state = destinationStateId; + draggedItem.state_detail = destinationState; - // patch request - issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { - priority: destinationGroup, - }) - .then((res) => { - if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); - if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); + if (cycleId) + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); - }); - } else if (selectedGroup === "state_detail.name") { - const destinationState = states?.find((s) => s.name === destinationGroup); - const destinationStateId = destinationState?.id; + if (moduleId) + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => { + if (!prevData) return prevData; + const updatedIssues = prevData.map((issue) => { + if (issue.issue_detail.id === draggedItem.id) { + return { + ...issue, + issue_detail: { + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, + }, + }; + } + return issue; + }); + return [...updatedIssues]; + }, + false + ); - // update the removed item for mutation - if (!destinationStateId || !destinationState) return; - draggedItem.state = destinationStateId; - draggedItem.state_detail = destinationState; - - if (cycleId) - mutate( - CYCLE_ISSUES(cycleId as string), + mutate( + PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), (prevData) => { if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { + + const updatedIssues = prevData.results.map((issue) => { + if (issue.id === draggedItem.id) return { - ...issue, - issue_detail: { - ...draggedItem, - state_detail: destinationState, - state: destinationStateId, - }, + ...draggedItem, + state_detail: destinationState, + state: destinationStateId, }; - } + return issue; }); - return [...updatedIssues]; + + return { + ...prevData, + results: updatedIssues, + }; }, false ); - if (moduleId) - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => { - if (!prevData) return prevData; - const updatedIssues = prevData.map((issue) => { - if (issue.issue_detail.id === draggedItem.id) { - return { - ...issue, - issue_detail: { - ...draggedItem, - state_detail: destinationState, - state: destinationStateId, - }, - }; - } - return issue; - }); - return [...updatedIssues]; - }, - false - ); + // patch request + issuesService + .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { + state: destinationStateId, + }) + .then((res) => { + if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); + if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - mutate( - PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), - (prevData) => { - if (!prevData) return prevData; - - const updatedIssues = prevData.results.map((issue) => { - if (issue.id === draggedItem.id) - return { - ...draggedItem, - state_detail: destinationState, - state: destinationStateId, - }; - - return issue; + mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); }); - - return { - ...prevData, - results: updatedIssues, - }; - }, - false - ); - - // patch request - issuesService - .patchIssue(workspaceSlug as string, projectId as string, draggedItem.id, { - state: destinationStateId, - }) - .then((res) => { - if (cycleId) mutate(CYCLE_ISSUES(cycleId as string)); - if (moduleId) mutate(MODULE_ISSUES(moduleId as string)); - - mutate(PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string)); - }); + } } } }, - [workspaceSlug, cycleId, moduleId, groupedByIssues, projectId, selectedGroup, states] + [ + workspaceSlug, + cycleId, + moduleId, + groupedByIssues, + projectId, + selectedGroup, + states, + handleDeleteIssue, + ] ); - const addIssueToState = (groupTitle: string, stateId: string | null) => { - setCreateIssueModal(true); - if (selectedGroup) - setPreloadedData({ - state: stateId ?? undefined, - [selectedGroup]: groupTitle, - actionType: "createIssue", + const addIssueToState = useCallback( + (groupTitle: string, stateId: string | null) => { + setCreateIssueModal(true); + if (selectedGroup) + setPreloadedData({ + state: stateId ?? undefined, + [selectedGroup]: groupTitle, + actionType: "createIssue", + }); + else setPreloadedData({ actionType: "createIssue" }); + }, + [setCreateIssueModal, setPreloadedData, selectedGroup] + ); + + const handleEditIssue = useCallback( + (issue: IIssue) => { + setEditIssueModal(true); + setIssueToEdit({ + ...issue, + actionType: "edit", + cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, + module: issue.issue_module ? issue.issue_module.module : null, }); - else setPreloadedData({ actionType: "createIssue" }); - }; + }, + [setEditIssueModal, setIssueToEdit] + ); - const handleEditIssue = (issue: IIssue) => { - setEditIssueModal(true); - setIssueToEdit({ - ...issue, - actionType: "edit", - cycle: issue.issue_cycle ? issue.issue_cycle.cycle : null, - module: issue.issue_module ? issue.issue_module.module : null, - }); - }; + const removeIssueFromCycle = useCallback( + (bridgeId: string) => { + if (!workspaceSlug || !projectId) return; - const handleDeleteIssue = (issue: IIssue) => { - setDeleteIssueModal(true); - setIssueToDelete(issue); - }; + mutate( + CYCLE_ISSUES(cycleId as string), + (prevData) => prevData?.filter((p) => p.id !== bridgeId), + false + ); - const removeIssueFromCycle = (bridgeId: string) => { - if (!workspaceSlug || !projectId) return; + issuesService + .removeIssueFromCycle( + workspaceSlug as string, + projectId as string, + cycleId as string, + bridgeId + ) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + }, + [workspaceSlug, projectId, cycleId] + ); - mutate( - CYCLE_ISSUES(cycleId as string), - (prevData) => prevData?.filter((p) => p.id !== bridgeId), - false - ); + const removeIssueFromModule = useCallback( + (bridgeId: string) => { + if (!workspaceSlug || !projectId) return; - issuesService - .removeIssueFromCycle( - workspaceSlug as string, - projectId as string, - cycleId as string, - bridgeId - ) - .then((res) => { - console.log(res); - }) - .catch((e) => { - console.log(e); - }); - }; + mutate( + MODULE_ISSUES(moduleId as string), + (prevData) => prevData?.filter((p) => p.id !== bridgeId), + false + ); - const removeIssueFromModule = (bridgeId: string) => { - if (!workspaceSlug || !projectId) return; + modulesService + .removeIssueFromModule( + workspaceSlug as string, + projectId as string, + moduleId as string, + bridgeId + ) + .then((res) => { + console.log(res); + }) + .catch((e) => { + console.log(e); + }); + }, + [workspaceSlug, projectId, moduleId] + ); - mutate( - MODULE_ISSUES(moduleId as string), - (prevData) => prevData?.filter((p) => p.id !== bridgeId), - false - ); - - modulesService - .removeIssueFromModule( - workspaceSlug as string, - projectId as string, - moduleId as string, - bridgeId - ) - .then((res) => { - console.log(res); - }) - .catch((e) => { - console.log(e); - }); - }; + const handleTrashBox = useCallback( + (isDragging: boolean) => { + if (isDragging && !trashBox) setTrashBox(true); + }, + [trashBox, setTrashBox] + ); return ( <> @@ -364,38 +407,59 @@ export const IssuesView: React.FC = ({ isOpen={deleteIssueModal} data={issueToDelete} /> - {issueView === "list" ? ( - - ) : ( - - )} + +
+ + + {(provided, snapshot) => ( +
+ + Drop issue here to delete +
+ )} +
+ {issueView === "list" ? ( + + ) : ( + + )} +
+
); }; From ebf294af5599e49ba918380e7bb6669e7b1c7b9e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:33:02 +0530 Subject: [PATCH 10/52] feat : cycle sidebar revamp (#271) * feat: range date picker added * feat: cycle status ui improved --- .../cycles/cycle-detail-sidebar/index.tsx | 171 ++++++++++-------- apps/app/components/project/cycles/index.ts | 1 - .../project/cycles/sidebar-select/index.ts | 1 - .../cycles/sidebar-select/select-status.tsx | 69 ------- 4 files changed, 99 insertions(+), 143 deletions(-) delete mode 100644 apps/app/components/project/cycles/sidebar-select/index.ts delete mode 100644 apps/app/components/project/cycles/sidebar-select/select-status.tsx diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index cdbac8b17..e1aa46692 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import Image from "next/image"; @@ -10,11 +10,18 @@ import { Controller, useForm } from "react-hook-form"; // react-circular-progressbar import { CircularProgressbar } from "react-circular-progressbar"; import "react-circular-progressbar/dist/styles.css"; +import { Popover, Transition } from "@headlessui/react"; +import DatePicker from "react-datepicker"; // icons -import { CalendarDaysIcon, ChartPieIcon, LinkIcon, UserIcon } from "@heroicons/react/24/outline"; -import { CycleSidebarStatusSelect } from "components/project/cycles"; +import { + CalendarDaysIcon, + ChartPieIcon, + LinkIcon, + Squares2X2Icon, + UserIcon, +} from "@heroicons/react/24/outline"; // ui -import { Loader, CustomDatePicker } from "components/ui"; +import { CustomSelect, Loader } from "components/ui"; // hooks import useToast from "hooks/use-toast"; // services @@ -24,12 +31,13 @@ import { SidebarProgressStats } from "components/core"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; -import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-time.helper"; // types import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; import ProgressChart from "components/core/sidebar/progress-chart"; +import { CYCLE_STATUS } from "constants/cycle"; type Props = { issues: IIssue[]; @@ -42,6 +50,9 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; + const [startDateRange, setStartDateRange] = useState(new Date()); + const [endDateRange, setEndDateRange] = useState(null); + const { setToastAlert } = useToast(); const defaultValues: Partial = { @@ -98,21 +109,90 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue > {cycle ? ( <> -
-
- {cycle.status} -
-
- - {renderShortNumericDateFormat(`${cycle.start_date}`) - ? renderShortNumericDateFormat(`${cycle.start_date}`) - : "N/A"}{" "} - -{" "} - {renderShortNumericDateFormat(`${cycle.end_date}`) - ? renderShortNumericDateFormat(`${cycle.end_date}`) - : "N/A"} - +
+
+ ( + + + {watch("status")} + + } + value={value} + onChange={(value: any) => { + submitChanges({ status: value }); + }} + > + {CYCLE_STATUS.map((option) => ( + + {option.label} + + ))} + + )} + />
+ + {({ open }) => ( + <> + + + + {renderShortNumericDateFormat(`${cycle.start_date}`) + ? renderShortNumericDateFormat(`${cycle.start_date}`) + : "N/A"}{" "} + -{" "} + {renderShortNumericDateFormat(`${cycle.end_date}`) + ? renderShortNumericDateFormat(`${cycle.end_date}`) + : "N/A"} + + + + + + { + const [start, end] = dates; + submitChanges({ + start_date: renderDateFormat(start), + end_date: renderDateFormat(end), + }); + if (setStartDateRange) { + setStartDateRange(start); + } + if (setEndDateRange) { + setEndDateRange(end); + } + }} + startDate={startDateRange} + endDate={endDateRange} + selectsRange + inline + /> + + + + )} +

{cycle.name}

@@ -192,59 +272,6 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue
-
-
-
- -

Start date

-
-
- ( - - submitChanges({ - start_date: val, - }) - } - isClearable={false} - /> - )} - /> -
-
-
-
- -

End date

-
-
- ( - - submitChanges({ - end_date: val, - }) - } - isClearable={false} - /> - )} - /> -
-
- -
diff --git a/apps/app/components/project/cycles/index.ts b/apps/app/components/project/cycles/index.ts index 9c6e55594..d330ba698 100644 --- a/apps/app/components/project/cycles/index.ts +++ b/apps/app/components/project/cycles/index.ts @@ -1,3 +1,2 @@ -export * from "./sidebar-select"; export * from "./stats-view"; export * from "./cycle-detail-sidebar"; \ No newline at end of file diff --git a/apps/app/components/project/cycles/sidebar-select/index.ts b/apps/app/components/project/cycles/sidebar-select/index.ts deleted file mode 100644 index efa8c553e..000000000 --- a/apps/app/components/project/cycles/sidebar-select/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./select-status"; \ No newline at end of file diff --git a/apps/app/components/project/cycles/sidebar-select/select-status.tsx b/apps/app/components/project/cycles/sidebar-select/select-status.tsx deleted file mode 100644 index 0c53083bd..000000000 --- a/apps/app/components/project/cycles/sidebar-select/select-status.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// react -import React from "react"; -// react-hook-form -import { Control, Controller, UseFormWatch } from "react-hook-form"; -// icons -import { Squares2X2Icon } from "@heroicons/react/24/outline"; -// ui -import { CustomSelect } from "components/ui"; -// types -import { ICycle } from "types"; -// common -// constants -import { CYCLE_STATUS } from "constants/cycle"; - -type Props = { - control: Control, any>; - submitChanges: (formData: Partial) => void; - watch: UseFormWatch>; -}; - -export const CycleSidebarStatusSelect: React.FC = ({ control, submitChanges, watch }) => ( -
-
- -

Status

-
-
- ( - - option.value === value)?.color, - }} - /> - {watch("status")} - - } - value={value} - onChange={(value: any) => { - submitChanges({ status: value }); - }} - > - {CYCLE_STATUS.map((option) => ( - - <> - - {option.label} - - - ))} - - )} - /> -
-
-); From 8fb34fe1e380b4edec51e5e1b00b3499d890f790 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 13 Feb 2023 13:14:23 +0530 Subject: [PATCH 11/52] feat : sidebar progress improvement (#272) * feat: progress chart render validation * fix: sidebar stats tab * feat: sidebar active tab context --- .../core/sidebar/sidebar-progress-stats.tsx | 30 ++++++++++++++++++- apps/app/components/modules/sidebar.tsx | 23 +++++++++----- .../cycles/cycle-detail-sidebar/index.tsx | 26 +++++++++++----- apps/app/hooks/use-local-storage.tsx | 22 ++++++++++++++ 4 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 apps/app/hooks/use-local-storage.tsx diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index 49c5be771..ea8e1e401 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -20,6 +20,7 @@ import User from "public/user.png"; import { IIssue, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; +import useLocalStorage from "hooks/use-local-storage"; // types type Props = { groupedIssues: any; @@ -38,6 +39,7 @@ const stateGroupColours: { export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { const router = useRouter(); + const [tab, setTab] = useLocalStorage("tab", "Assignees"); const { workspaceSlug, projectId } = router.query; const { data: issueLabels } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, @@ -52,8 +54,34 @@ export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) ? () => projectService.projectMembers(workspaceSlug as string, projectId as string) : null ); + + const currentValue = (tab: string) => { + switch (tab) { + case "Assignees": + return 0; + case "Labels": + return 1; + case "States": + return 2; + } + }; return ( - + { + switch (i) { + case 0: + return setTab("Assignees"); + case 1: + return setTab("Labels"); + case 2: + return setTab("States"); + + default: + return setTab("Assignees"); + } + }} + > = ({ }); }, [module, reset]); + const isStartValid = new Date(`${module?.start_date}`) <= new Date(); + const isEndValid = new Date(`${module?.target_date}`) >= new Date(`${module?.start_date}`); return ( <> = ({
- - - + {isStartValid && isEndValid ? ( + + ) : ( + "" + )} + {issues.length > 0 ? ( + + ) : ( + "" + )}
) : ( diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index e1aa46692..ef3788314 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -101,6 +101,8 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue }); }, [cycle, reset]); + const isStartValid = new Date(`${cycle?.start_date}`) <= new Date(); + const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`); return (
= ({ issues, cycle, isOpen, cycleIssue
-
- -
- + {isStartValid && isEndValid ? ( +
+ +
+ ) : ( + "" + )} + {issues.length > 0 ? ( + + ) : ( + "" + )}
) : ( diff --git a/apps/app/hooks/use-local-storage.tsx b/apps/app/hooks/use-local-storage.tsx new file mode 100644 index 000000000..8b863899c --- /dev/null +++ b/apps/app/hooks/use-local-storage.tsx @@ -0,0 +1,22 @@ +import React, { useEffect, useState } from "react"; + +const getSavedValue = (key: any, value: any) => { + const savedValue = localStorage.getItem(key); + if (savedValue) { + return savedValue; + } + return value; +}; + +const useLocalStorage = (key: any, value: any) => { + const [updatedvalue, seUpdatedvalue] = useState(() => getSavedValue(key, value)); + + useEffect(() => { + localStorage.setItem(key, updatedvalue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [updatedvalue]); + + return [updatedvalue, seUpdatedvalue]; +}; + +export default useLocalStorage; From 214e860e675cbd14cfdcf50c89a1fb2a7638d76d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 13 Feb 2023 19:38:58 +0530 Subject: [PATCH 12/52] chore: removed minor bugs (#273) --- .../components/account/email-code-form.tsx | 48 +- .../core/board-view/single-board.tsx | 4 +- .../components/issues/view-select/state.tsx | 8 +- apps/app/components/project/card.tsx | 23 +- .../project/cycles/confirm-cycle-deletion.tsx | 4 - .../cycles/cycle-detail-sidebar/index.tsx | 428 +++++++++--------- apps/app/components/states/single-state.tsx | 4 +- .../components/workspace/home-cards-list.tsx | 2 +- .../pages/[workspaceSlug]/me/my-issues.tsx | 92 ++-- 9 files changed, 326 insertions(+), 287 deletions(-) diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 93298b4e0..98ab10cb7 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -34,8 +34,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => { reValidateMode: "onChange", }); - const onSubmit = ({ email }: EmailCodeFormValues) => { - authenticationService + const onSubmit = async ({ email }: EmailCodeFormValues) => { + await authenticationService .emailCode({ email }) .then((res) => { setValue("key", res.key); @@ -46,8 +46,8 @@ export const EmailCodeForm = ({ onSuccess }: any) => { }); }; - const handleSignin = (formData: EmailCodeFormValues) => { - authenticationService + const handleSignin = async (formData: EmailCodeFormValues) => { + await authenticationService .magicSignIn(formData) .then((response) => { onSuccess(response); @@ -68,10 +68,7 @@ export const EmailCodeForm = ({ onSuccess }: any) => { return ( <> -
+ {codeSent && (
@@ -117,16 +114,37 @@ export const EmailCodeForm = ({ onSuccess }: any) => { error={errors.token} placeholder="Enter code" /> + {/* { + console.log("Triggered"); + handleSubmit(onSubmit); + }} + > + Resend code + */}
)}
- + {codeSent ? ( + + ) : ( + + )}
diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index 2063f6e14..e7cb49798 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -69,6 +69,8 @@ export const SingleBoard: React.FC = ({ ? (bgColor = "#22c55e") : (bgColor = "#ff0000"); + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; + return (
@@ -95,7 +97,7 @@ export const SingleBoard: React.FC = ({ key={issue.id} draggableId={issue.id} index={index} - isDragDisabled={selectedGroup === "created_by"} + isDragDisabled={isNotAllowed || selectedGroup === "created_by"} > {(provided, snapshot) => ( = ({ isNotAllowed, }) => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug } = router.query; const { data: stateGroups } = useSWR( - workspaceSlug && projectId ? STATE_LIST(issue.project) : null, - workspaceSlug ? () => stateService.getStates(workspaceSlug as string, issue.project) : null + workspaceSlug && issue ? STATE_LIST(issue.project) : null, + workspaceSlug && issue + ? () => stateService.getStates(workspaceSlug as string, issue.project) + : null ); const states = getStatesList(stateGroups ?? {}); diff --git a/apps/app/components/project/card.tsx b/apps/app/components/project/card.tsx index 0ddaf1cd7..656f1d10b 100644 --- a/apps/app/components/project/card.tsx +++ b/apps/app/components/project/card.tsx @@ -12,13 +12,14 @@ import { ClipboardDocumentListIcon, } from "@heroicons/react/24/outline"; // types -import type { IProject } from "types"; // ui import { Button } from "components/ui"; // hooks import useProjectMembers from "hooks/use-project-members"; // helpers import { renderShortNumericDateFormat } from "helpers/date-time.helper"; +// types +import type { IProject } from "types"; export type ProjectCardProps = { workspaceSlug: string; @@ -85,6 +86,14 @@ export const ProjectCard: React.FC = (props) => {
+ {!isMember ? ( ) : ( - +
)} -
diff --git a/apps/app/components/project/cycles/confirm-cycle-deletion.tsx b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx index ab35d343b..c1a5f38ed 100644 --- a/apps/app/components/project/cycles/confirm-cycle-deletion.tsx +++ b/apps/app/components/project/cycles/confirm-cycle-deletion.tsx @@ -36,10 +36,6 @@ const ConfirmCycleDeletion: React.FC = ({ const { setToastAlert } = useToast(); - useEffect(() => { - data && setIsOpen(true); - }, [data, setIsOpen]); - const handleClose = () => { setIsOpen(false); setIsDeleteLoading(false); diff --git a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx index ef3788314..f552bef88 100644 --- a/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx +++ b/apps/app/components/project/cycles/cycle-detail-sidebar/index.tsx @@ -18,6 +18,7 @@ import { ChartPieIcon, LinkIcon, Squares2X2Icon, + TrashIcon, UserIcon, } from "@heroicons/react/24/outline"; // ui @@ -28,6 +29,8 @@ import useToast from "hooks/use-toast"; import cyclesService from "services/cycles.service"; // components import { SidebarProgressStats } from "components/core"; +import ProgressChart from "components/core/sidebar/progress-chart"; +import ConfirmCycleDeletion from "components/project/cycles/confirm-cycle-deletion"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; import { groupBy } from "helpers/array.helper"; @@ -36,7 +39,7 @@ import { renderDateFormat, renderShortNumericDateFormat } from "helpers/date-tim import { CycleIssueResponse, ICycle, IIssue } from "types"; // fetch-keys import { CYCLE_DETAILS } from "constants/fetch-keys"; -import ProgressChart from "components/core/sidebar/progress-chart"; +// constants import { CYCLE_STATUS } from "constants/cycle"; type Props = { @@ -47,6 +50,8 @@ type Props = { }; const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssues }) => { + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); + const router = useRouter(); const { workspaceSlug, projectId, cycleId } = router.query; @@ -103,212 +108,227 @@ const CycleDetailSidebar: React.FC = ({ issues, cycle, isOpen, cycleIssue const isStartValid = new Date(`${cycle?.start_date}`) <= new Date(); const isEndValid = new Date(`${cycle?.end_date}`) >= new Date(`${cycle?.start_date}`); - return ( -
- {cycle ? ( - <> -
-
- ( - - - {watch("status")} - - } - value={value} - onChange={(value: any) => { - submitChanges({ status: value }); - }} - > - {CYCLE_STATUS.map((option) => ( - - {option.label} - - ))} - - )} - /> -
- - {({ open }) => ( - <> - - - - {renderShortNumericDateFormat(`${cycle.start_date}`) - ? renderShortNumericDateFormat(`${cycle.start_date}`) - : "N/A"}{" "} - -{" "} - {renderShortNumericDateFormat(`${cycle.end_date}`) - ? renderShortNumericDateFormat(`${cycle.end_date}`) - : "N/A"} - - - - - { - const [start, end] = dates; - submitChanges({ - start_date: renderDateFormat(start), - end_date: renderDateFormat(end), - }); - if (setStartDateRange) { - setStartDateRange(start); - } - if (setEndDateRange) { - setEndDateRange(end); - } - }} - startDate={startDateRange} - endDate={endDateRange} - selectsRange - inline - /> - - - - )} - -
-
-

{cycle.name}

-
- -
-
-
-
-
-
- -

Owned by

-
-
- {cycle.owned_by && - (cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( -
- {cycle.owned_by?.first_name} -
- ) : ( -
- {cycle.owned_by?.first_name && cycle.owned_by.first_name !== "" - ? cycle.owned_by.first_name.charAt(0) - : cycle.owned_by?.email.charAt(0)} -
- ))} - {cycle.owned_by.first_name !== "" - ? cycle.owned_by.first_name - : cycle.owned_by.email} -
-
-
-
- -

Progress

-
-
-
- - - -
- {groupedIssues.completed.length}/{cycleIssues?.length} -
-
-
-
-
-
- {isStartValid && isEndValid ? ( -
- + +
+ {cycle ? ( + <> +
+
+ ( + + + {watch("status")} + + } + value={value} + onChange={(value: any) => { + submitChanges({ status: value }); + }} + > + {CYCLE_STATUS.map((option) => ( + + {option.label} + + ))} + + )} />
- ) : ( - "" - )} - {issues.length > 0 ? ( - - ) : ( - "" - )} -
- - ) : ( - -
- - -
-
- - - -
-
- )} -
+ + {({ open }) => ( + <> + + + + {renderShortNumericDateFormat(`${cycle.start_date}`) + ? renderShortNumericDateFormat(`${cycle.start_date}`) + : "N/A"}{" "} + -{" "} + {renderShortNumericDateFormat(`${cycle.end_date}`) + ? renderShortNumericDateFormat(`${cycle.end_date}`) + : "N/A"} + + + + + + { + const [start, end] = dates; + submitChanges({ + start_date: renderDateFormat(start), + end_date: renderDateFormat(end), + }); + if (setStartDateRange) { + setStartDateRange(start); + } + if (setEndDateRange) { + setEndDateRange(end); + } + }} + startDate={startDateRange} + endDate={endDateRange} + selectsRange + inline + /> + + + + )} + +
+
+

{cycle.name}

+
+ + +
+
+
+
+
+
+ +

Owned by

+
+
+ {cycle.owned_by && + (cycle.owned_by.avatar && cycle.owned_by.avatar !== "" ? ( +
+ {cycle.owned_by?.first_name} +
+ ) : ( +
+ {cycle.owned_by?.first_name && cycle.owned_by.first_name !== "" + ? cycle.owned_by.first_name.charAt(0) + : cycle.owned_by?.email.charAt(0)} +
+ ))} + {cycle.owned_by.first_name !== "" + ? cycle.owned_by.first_name + : cycle.owned_by.email} +
+
+
+
+ +

Progress

+
+
+
+ + + +
+ {groupedIssues.completed.length}/{cycleIssues?.length} +
+
+
+
+
+
+ {isStartValid && isEndValid ? ( +
+ +
+ ) : ( + "" + )} + {issues.length > 0 ? ( + + ) : ( + "" + )} +
+ + ) : ( + +
+ + +
+
+ + + +
+
+ )} +
+ ); }; diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index 2c0f4071e..ef558d302 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -148,9 +148,7 @@ export const SingleState: React.FC = ({ backgroundColor: state.color, }} /> -
- {addSpaceIfCamelCase(state.name)} {state.sequence} -
+
{addSpaceIfCamelCase(state.name)}
{index !== 0 && ( diff --git a/apps/app/components/workspace/home-cards-list.tsx b/apps/app/components/workspace/home-cards-list.tsx index 1aaebdc98..cca5989e7 100644 --- a/apps/app/components/workspace/home-cards-list.tsx +++ b/apps/app/components/workspace/home-cards-list.tsx @@ -16,7 +16,7 @@ export const WorkspaceHomeCardsList: FC = (props) = }, { title: "Issues pending", - number: myIssues?.length ?? 0 - groupedIssues.completed.length, + number: myIssues.length - groupedIssues.completed.length, }, { title: "Projects", diff --git a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx index 899b4974f..d5b8bbfaf 100644 --- a/apps/app/pages/[workspaceSlug]/me/my-issues.tsx +++ b/apps/app/pages/[workspaceSlug]/me/my-issues.tsx @@ -58,54 +58,56 @@ const MyIssuesPage: NextPage = () => { } right={
- - {({ open }) => ( - <> - - View - + {myIssues && myIssues.length > 0 && ( + + {({ open }) => ( + <> + + View + - - -
-
-

Properties

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

Properties

+
+ {Object.keys(properties).map((key) => ( + + ))} +
-
- - - - )} - + + + + )} + + )} Date: Mon, 13 Feb 2023 20:19:46 +0530 Subject: [PATCH 13/52] fix: ui bug (#274) * fix: shortcut search fix shortcut modal ui fixes shortcut search fix email us label change * fix: email us label updated --- .../command-palette/shortcuts-modal.tsx | 65 +++++++++++++------ .../app/components/workspace/help-section.tsx | 2 +- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/apps/app/components/command-palette/shortcuts-modal.tsx b/apps/app/components/command-palette/shortcuts-modal.tsx index c1800ab17..593c68324 100644 --- a/apps/app/components/command-palette/shortcuts-modal.tsx +++ b/apps/app/components/command-palette/shortcuts-modal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // icons @@ -15,7 +15,7 @@ const shortcuts = [ { title: "Navigation", shortcuts: [ - { keys: "Ctrl,Cmd,K", description: "To open navigator" }, + { keys: "Ctrl,/,Cmd,K", description: "To open navigator" }, { keys: "↑", description: "Move up" }, { keys: "↓", description: "Move down" }, { keys: "←", description: "Move left" }, @@ -34,22 +34,26 @@ const shortcuts = [ { keys: "Delete", description: "To bulk delete issues" }, { keys: "H", description: "To open shortcuts guide" }, { - keys: "Ctrl,Cmd,Alt,C", + keys: "Ctrl,/,Cmd,Alt,C", description: "To copy issue url when on issue detail page.", }, ], }, ]; +const allShortcuts = shortcuts.map((i) => i.shortcuts).flat(1); + export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { const [query, setQuery] = useState(""); - const filteredShortcuts = shortcuts.filter((shortcut) => - shortcut.shortcuts.some((item) => item.description.includes(query.trim())) || query === "" - ? true - : false + const filteredShortcuts = allShortcuts.filter((shortcut) => + shortcut.description.includes(query.trim()) || query === "" ? true : false ); + useEffect(() => { + if (!isOpen) setQuery(""); + }, [isOpen]); + return ( @@ -104,8 +108,40 @@ export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => { />
- {filteredShortcuts.length > 0 ? ( - filteredShortcuts.map(({ title, shortcuts }) => ( + {query.trim().length > 0 ? ( + filteredShortcuts.length > 0 ? ( + filteredShortcuts.map((shortcut) => ( +
+
+
+

{shortcut.description}

+
+ {shortcut.keys.split(",").map((key, index) => ( + + + {key} + + + ))} +
+
+
+
+ )) + ) : ( +
+

+ No shortcuts found for{" "} + + {`"`} + {query} + {`"`} + +

+
+ ) + ) : ( + shortcuts.map(({ title, shortcuts }) => (

{title}

@@ -126,17 +162,6 @@ export const ShortcutsModal: React.FC = ({ isOpen, setIsOpen }) => {
)) - ) : ( -
-

- No shortcuts found for{" "} - - {`"`} - {query} - {`"`} - -

-
)}
diff --git a/apps/app/components/workspace/help-section.tsx b/apps/app/components/workspace/help-section.tsx index cd6fce3c6..b8ac07e24 100644 --- a/apps/app/components/workspace/help-section.tsx +++ b/apps/app/components/workspace/help-section.tsx @@ -32,7 +32,7 @@ const helpOptions = [ Icon: GithubIcon, }, { - name: "Chat with us", + name: "Email us", href: "mailto:hello@plane.so", Icon: CommentIcon, }, From 97ffdc81242108c444fa04a9db5aa83ffb31bcf6 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 14 Feb 2023 01:12:32 +0530 Subject: [PATCH 14/52] feat: default state for project (#264) --- apiserver/plane/api/views/project.py | 21 +++------------------ apiserver/plane/api/views/state.py | 21 ++++++++++++++++++++- apiserver/plane/db/models/issue.py | 11 ++++++++--- apiserver/plane/db/models/state.py | 1 + 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 2ec6faf1e..e24477ecd 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -75,7 +75,6 @@ class ProjectViewSet(BaseViewSet): def create(self, request, slug): try: - workspace = Workspace.objects.get(slug=slug) serializer = ProjectSerializer( @@ -96,6 +95,7 @@ class ProjectViewSet(BaseViewSet): "color": "#5e6ad2", "sequence": 15000, "group": "backlog", + "default": True, }, { "name": "Todo", @@ -132,6 +132,7 @@ class ProjectViewSet(BaseViewSet): sequence=state["sequence"], workspace=serializer.instance.workspace, group=state["group"], + default=state.get("default", False), ) for state in states ] @@ -188,7 +189,7 @@ class ProjectViewSet(BaseViewSet): {"name": "The project name is already taken"}, status=status.HTTP_410_GONE, ) - except (Project.DoesNotExist or Workspace.DoesNotExist) as e: + except Project.DoesNotExist or Workspace.DoesNotExist as e: return Response( {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND ) @@ -206,14 +207,12 @@ class ProjectViewSet(BaseViewSet): class InviteProjectEndpoint(BaseAPIView): - permission_classes = [ ProjectBasePermission, ] def post(self, request, slug, project_id): try: - email = request.data.get("email", False) role = request.data.get("role", False) @@ -287,7 +286,6 @@ class InviteProjectEndpoint(BaseAPIView): class UserProjectInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer model = ProjectMemberInvite @@ -301,7 +299,6 @@ class UserProjectInvitationsViewset(BaseViewSet): def create(self, request): try: - invitations = request.data.get("invitations") project_invitations = ProjectMemberInvite.objects.filter( pk__in=invitations, accepted=True @@ -331,7 +328,6 @@ class UserProjectInvitationsViewset(BaseViewSet): class ProjectMemberViewSet(BaseViewSet): - serializer_class = ProjectMemberSerializer model = ProjectMember permission_classes = [ @@ -356,14 +352,12 @@ class ProjectMemberViewSet(BaseViewSet): class AddMemberToProjectEndpoint(BaseAPIView): - permission_classes = [ ProjectBasePermission, ] def post(self, request, slug, project_id): try: - member_id = request.data.get("member_id", False) role = request.data.get("role", False) @@ -412,13 +406,11 @@ class AddMemberToProjectEndpoint(BaseAPIView): class AddTeamToProjectEndpoint(BaseAPIView): - permission_classes = [ ProjectBasePermission, ] def post(self, request, slug, project_id): - try: team_members = TeamMember.objects.filter( workspace__slug=slug, team__in=request.data.get("teams", []) @@ -467,7 +459,6 @@ class AddTeamToProjectEndpoint(BaseAPIView): class ProjectMemberInvitationsViewset(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer model = ProjectMemberInvite @@ -489,7 +480,6 @@ class ProjectMemberInvitationsViewset(BaseViewSet): class ProjectMemberInviteDetailViewSet(BaseViewSet): - serializer_class = ProjectMemberInviteSerializer model = ProjectMemberInvite @@ -509,14 +499,12 @@ class ProjectMemberInviteDetailViewSet(BaseViewSet): class ProjectIdentifierEndpoint(BaseAPIView): - permission_classes = [ ProjectBasePermission, ] def get(self, request, slug): try: - name = request.GET.get("name", "").strip().upper() if name == "": @@ -541,7 +529,6 @@ class ProjectIdentifierEndpoint(BaseAPIView): def delete(self, request, slug): try: - name = request.data.get("name", "").strip().upper() if name == "": @@ -616,7 +603,6 @@ class ProjectJoinEndpoint(BaseAPIView): class ProjectUserViewsEndpoint(BaseAPIView): def post(self, request, slug, project_id): try: - project = Project.objects.get(pk=project_id, workspace__slug=slug) project_member = ProjectMember.objects.filter( @@ -655,7 +641,6 @@ class ProjectUserViewsEndpoint(BaseAPIView): class ProjectMemberUserEndpoint(BaseAPIView): def get(self, request, slug, project_id): try: - project_member = ProjectMember.objects.get( project_id=project_id, workspace__slug=slug, member=request.user ) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 8054b15dd..21ca6c714 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -1,3 +1,7 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response + # Module imports from . import BaseViewSet from plane.api.serializers import StateSerializer @@ -6,7 +10,6 @@ from plane.db.models import State class StateViewSet(BaseViewSet): - serializer_class = StateSerializer model = State permission_classes = [ @@ -27,3 +30,19 @@ class StateViewSet(BaseViewSet): .select_related("workspace") .distinct() ) + + def destroy(self, request, slug, project_id, pk): + try: + state = State.objects.get( + pk=pk, project_id=project_id, workspace__slug=slug + ) + + if state.default: + return Response( + {"error": "Default state cannot be deleted"}, status=False + ) + + state.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except State.DoesNotExist: + return Response({"error": "State does not exists"}, status=status.HTTP_404) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index a870eb93f..82e8343bb 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -83,9 +83,14 @@ class Issue(ProjectBaseModel): try: from plane.db.models import State - self.state, created = State.objects.get_or_create( - project=self.project, name="Backlog" - ) + default_state = State.objects.filter( + project=self.project, default=True + ).first() + # if there is no default state assign any random state + if default_state is None: + self.state = State.objects.filter(project=self.project).first() + else: + self.state = default_state except ImportError: pass else: diff --git a/apiserver/plane/db/models/state.py b/apiserver/plane/db/models/state.py index 2c6287918..d66ecfa72 100644 --- a/apiserver/plane/db/models/state.py +++ b/apiserver/plane/db/models/state.py @@ -23,6 +23,7 @@ class State(ProjectBaseModel): default="backlog", max_length=20, ) + default = models.BooleanField(default=False) def __str__(self): """Return name of the state""" From 0477db69a05fdae3619303e5979a9a6fef7637d2 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 14 Feb 2023 01:14:24 +0530 Subject: [PATCH 15/52] build: add channels requirement for the asgi configuration (#225) --- apiserver/requirements/base.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 29ae099ea..97ef2901f 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -25,4 +25,5 @@ google-auth==2.16.0 google-api-python-client==2.75.0 django-rq==2.6.0 django-redis==5.2.0 -uvicorn==0.20.0 \ No newline at end of file +uvicorn==0.20.0 +channels==4.0.0 \ No newline at end of file From af1d49bbf5377d47a8fb5e047edcde61fbdc070e Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 14 Feb 2023 01:14:56 +0530 Subject: [PATCH 16/52] refactor: combine sign in and sign up endpoint to a single endpoint (#263) --- apiserver/plane/api/urls.py | 2 - apiserver/plane/api/views/__init__.py | 1 - apiserver/plane/api/views/auth_extended.py | 2 +- apiserver/plane/api/views/authentication.py | 149 +++++++------------- 4 files changed, 52 insertions(+), 102 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 98c2e87d2..4af139bf5 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -5,7 +5,6 @@ from django.urls import path from plane.api.views import ( # Authentication - SignUpEndpoint, SignInEndpoint, SignOutEndpoint, MagicSignInEndpoint, @@ -95,7 +94,6 @@ urlpatterns = [ path("social-auth/", OauthEndpoint.as_view(), name="oauth"), # Auth path("sign-in/", SignInEndpoint.as_view(), name="sign-in"), - path("sign-up/", SignUpEndpoint.as_view(), name="sign-up"), path("sign-out/", SignOutEndpoint.as_view(), name="sign-out"), # Magic Sign In/Up path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 1212e0dca..4fb565e8d 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -64,7 +64,6 @@ from .auth_extended import ( from .authentication import ( - SignUpEndpoint, SignInEndpoint, SignOutEndpoint, MagicSignInEndpoint, diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/api/views/auth_extended.py index 487d10a22..56dc091f4 100644 --- a/apiserver/plane/api/views/auth_extended.py +++ b/apiserver/plane/api/views/auth_extended.py @@ -84,7 +84,7 @@ class ForgotPasswordEndpoint(BaseAPIView): ) return Response( - {"messgae": "Check your email to reset your password"}, + {"message": "Check your email to reset your password"}, status=status.HTTP_200_OK, ) return Response( diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/api/views/authentication.py index ac218837d..58d75a049 100644 --- a/apiserver/plane/api/views/authentication.py +++ b/apiserver/plane/api/views/authentication.py @@ -35,7 +35,7 @@ def get_tokens_for_user(user): ) -class SignUpEndpoint(BaseAPIView): +class SignInEndpoint(BaseAPIView): permission_classes = (AllowAny,) def post(self, request): @@ -62,114 +62,67 @@ class SignUpEndpoint(BaseAPIView): user = User.objects.filter(email=email).first() - if user is not None: - return Response( - {"error": "Email ID is already taken"}, - status=status.HTTP_400_BAD_REQUEST, - ) + # Sign up Process + if user is None: + user = User.objects.create(email=email, username=uuid.uuid4().hex) + user.set_password(password) - user = User.objects.create(email=email) - user.set_password(password) + # settings last actives for the user + user.last_active = timezone.now() + user.last_login_time = timezone.now() + user.last_login_ip = request.META.get("REMOTE_ADDR") + user.last_login_uagent = request.META.get("HTTP_USER_AGENT") + user.token_updated_at = timezone.now() + user.save() - # settings last actives for the user - user.last_active = timezone.now() - user.last_login_time = timezone.now() - user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_uagent = request.META.get("HTTP_USER_AGENT") - user.token_updated_at = timezone.now() - user.save() + serialized_user = UserSerializer(user).data - serialized_user = UserSerializer(user).data + access_token, refresh_token = get_tokens_for_user(user) - access_token, refresh_token = get_tokens_for_user(user) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + "user": serialized_user, + } - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } + return Response(data, status=status.HTTP_200_OK) + # Sign in Process + else: + if not user.check_password(password): + return Response( + { + "error": "Sorry, we could not find a user with the provided credentials. Please try again." + }, + status=status.HTTP_403_FORBIDDEN, + ) + if not user.is_active: + return Response( + { + "error": "Your account has been deactivated. Please contact your site administrator." + }, + status=status.HTTP_403_FORBIDDEN, + ) - return Response(data, status=status.HTTP_200_OK) + serialized_user = UserSerializer(user).data - except Exception as e: - capture_exception(e) - return Response( - { - "error": "Something went wrong. Please try again later or contact the support team." - }, - status=status.HTTP_400_BAD_REQUEST, - ) + # settings last active for the user + user.last_active = timezone.now() + user.last_login_time = timezone.now() + user.last_login_ip = request.META.get("REMOTE_ADDR") + user.last_login_uagent = request.META.get("HTTP_USER_AGENT") + user.token_updated_at = timezone.now() + user.save() + access_token, refresh_token = get_tokens_for_user(user) -class SignInEndpoint(BaseAPIView): - permission_classes = (AllowAny,) + data = { + "access_token": access_token, + "refresh_token": refresh_token, + "user": serialized_user, + } - def post(self, request): - try: - email = request.data.get("email", False) - password = request.data.get("password", False) + return Response(data, status=status.HTTP_200_OK) - ## Raise exception if any of the above are missing - if not email or not password: - return Response( - {"error": "Both email and password are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - email = email.strip().lower() - - try: - validate_email(email) - except ValidationError as e: - return Response( - {"error": "Please provide a valid email address."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - user = User.objects.get(email=email) - - if not user.check_password(password): - return Response( - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - status=status.HTTP_403_FORBIDDEN, - ) - if not user.is_active: - return Response( - { - "error": "Your account has been deactivated. Please contact your site administrator." - }, - status=status.HTTP_403_FORBIDDEN, - ) - - serialized_user = UserSerializer(user).data - - # settings last active for the user - user.last_active = timezone.now() - user.last_login_time = timezone.now() - user.last_login_ip = request.META.get("REMOTE_ADDR") - user.last_login_uagent = request.META.get("HTTP_USER_AGENT") - user.token_updated_at = timezone.now() - user.save() - - access_token, refresh_token = get_tokens_for_user(user) - - data = { - "access_token": access_token, - "refresh_token": refresh_token, - "user": serialized_user, - } - - return Response(data, status=status.HTTP_200_OK) - - except User.DoesNotExist: - return Response( - { - "error": "Sorry, we could not find a user with the provided credentials. Please try again." - }, - status=status.HTTP_403_FORBIDDEN, - ) except Exception as e: capture_exception(e) return Response( From 92d57499971a2776382b95c27662fabe3ff929ce Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 14 Feb 2023 01:16:35 +0530 Subject: [PATCH 17/52] feat: state grouping and ordering list (#253) * feat: state grouping and ordering list * fix: state grouping in state list endpoint --- apiserver/plane/api/views/state.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 21ca6c714..4616fcee7 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -1,6 +1,11 @@ +# Python imports +from itertools import groupby + # Third party imports -from rest_framework import status from rest_framework.response import Response +from rest_framework import status +from sentry_sdk import capture_exception + # Module imports from . import BaseViewSet @@ -31,6 +36,25 @@ class StateViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug, project_id): + try: + state_dict = dict() + states = StateSerializer(self.get_queryset(), many=True).data + + for key, value in groupby( + sorted(states, key=lambda state: state["group"]), + lambda state: state.get("group"), + ): + state_dict[str(key)] = list(value) + + return Response(state_dict, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def destroy(self, request, slug, project_id, pk): try: state = State.objects.get( From 7950f191e7e5a9dd7846b797d5379ff4830e5f59 Mon Sep 17 00:00:00 2001 From: vamsi Date: Tue, 14 Feb 2023 01:19:59 +0530 Subject: [PATCH 18/52] dev: added migrations for new models schema changes --- .../db/migrations/0020_auto_20230214_0118.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 apiserver/plane/db/migrations/0020_auto_20230214_0118.py diff --git a/apiserver/plane/db/migrations/0020_auto_20230214_0118.py b/apiserver/plane/db/migrations/0020_auto_20230214_0118.py new file mode 100644 index 000000000..192764078 --- /dev/null +++ b/apiserver/plane/db/migrations/0020_auto_20230214_0118.py @@ -0,0 +1,69 @@ +# Generated by Django 3.2.16 on 2023-02-13 19:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0019_auto_20230131_0049'), + ] + + operations = [ + migrations.RenameField( + model_name='label', + old_name='colour', + new_name='color', + ), + migrations.AddField( + model_name='apitoken', + name='workspace', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to='db.workspace'), + ), + migrations.AddField( + model_name='issue', + name='completed_at', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='issue', + name='sort_order', + field=models.FloatField(default=65535), + ), + migrations.AddField( + model_name='project', + name='cycle_view', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='project', + name='module_view', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='state', + name='default', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='issue', + name='description', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='issue', + name='description_html', + field=models.TextField(blank=True, default='

'), + ), + migrations.AlterField( + model_name='issuecomment', + name='comment_html', + field=models.TextField(blank=True, default='

'), + ), + migrations.AlterField( + model_name='issuecomment', + name='comment_json', + field=models.JSONField(blank=True, default=dict), + ), + ] From 9c8c7f1ddacc2baca70c9c3f7f1264fe1e1403a7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:35:14 +0530 Subject: [PATCH 19/52] fix: mac text copy fix (#277) --- apps/app/components/command-palette/command-pallette.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/components/command-palette/command-pallette.tsx b/apps/app/components/command-palette/command-pallette.tsx index 678e17d46..cb4f398b9 100644 --- a/apps/app/components/command-palette/command-pallette.tsx +++ b/apps/app/components/command-palette/command-pallette.tsx @@ -105,7 +105,7 @@ export const CommandPalette: React.FC = () => { if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); setIsPaletteOpen(true); - } else if (e.ctrlKey && (e.key === "c" || e.key === "C")) { + } else if ((e.ctrlKey || e.metaKey) && (e.key === "c" || e.key === "C")) { if (e.altKey) { e.preventDefault(); if (!router.query.issueId) return; From e53ff4c02e7c9dd42f88f422df49fd1b5267273a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:46:48 +0530 Subject: [PATCH 20/52] feat: state description in settings (#275) * chore: removed minor bugs * feat: state description in settings * feat: group by assignee --- .../core/board-view/board-header.tsx | 133 +++++++------- .../core/board-view/single-board.tsx | 12 +- .../components/core/list-view/single-list.tsx | 16 +- .../issues/sidebar-select/cycle.tsx | 6 +- .../issues/sidebar-select/module.tsx | 6 +- apps/app/components/issues/sidebar.tsx | 45 +++-- .../components/modules/single-module-card.tsx | 162 ++++++------------ apps/app/components/states/single-state.tsx | 7 +- .../app/components/ui/{avatar => }/avatar.tsx | 2 +- apps/app/components/ui/avatar/index.ts | 1 - apps/app/components/ui/index.ts | 3 +- apps/app/constants/issue.ts | 1 + .../projects/[projectId]/settings/states.tsx | 2 +- 13 files changed, 188 insertions(+), 208 deletions(-) rename apps/app/components/ui/{avatar => }/avatar.tsx (96%) delete mode 100644 apps/app/components/ui/avatar/index.ts diff --git a/apps/app/components/core/board-view/board-header.tsx b/apps/app/components/core/board-view/board-header.tsx index 3a7753366..9fe51b793 100644 --- a/apps/app/components/core/board-view/board-header.tsx +++ b/apps/app/components/core/board-view/board-header.tsx @@ -12,80 +12,97 @@ import { // helpers import { addSpaceIfCamelCase } from "helpers/string.helper"; // types -import { IIssue } from "types"; +import { IIssue, IProjectMember, NestedKeyOf } from "types"; type Props = { - isCollapsed: boolean; - setIsCollapsed: React.Dispatch>; groupedByIssues: { [key: string]: IIssue[]; }; + selectedGroup: NestedKeyOf | null; groupTitle: string; - createdBy: string | null; bgColor?: string; addIssueToState: () => void; + members: IProjectMember[] | undefined; + isCollapsed: boolean; + setIsCollapsed: React.Dispatch>; }; export const BoardHeader: React.FC = ({ - isCollapsed, - setIsCollapsed, groupedByIssues, + selectedGroup, groupTitle, - createdBy, bgColor, addIssueToState, -}) => ( -
-
-
-

{ + const createdBy = + selectedGroup === "created_by" + ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." + : null; + + let assignees: any; + if (selectedGroup === "assignees") { + assignees = groupTitle.split(","); + assignees = assignees + .map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name) + .join(", "); + } + + return ( +
+
+
- {groupTitle === null || groupTitle === "null" - ? "None" - : createdBy - ? createdBy - : addSpaceIfCamelCase(groupTitle)} -

- {groupedByIssues[groupTitle].length} +

+ {selectedGroup === "created_by" + ? createdBy + : selectedGroup === "assignees" + ? assignees + : addSpaceIfCamelCase(groupTitle)} +

+ {groupedByIssues[groupTitle].length} +
+
+ +
+ +
- -
- - -
-
-); + ); +}; diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index e7cb49798..e0e790da5 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -55,11 +55,6 @@ export const SingleBoard: React.FC = ({ const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); - const createdBy = - selectedGroup === "created_by" - ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." - : null; - if (selectedGroup === "priority") groupTitle === "high" ? (bgColor = "#dc2626") @@ -77,11 +72,12 @@ export const SingleBoard: React.FC = ({ {(provided, snapshot) => ( @@ -97,7 +93,9 @@ export const SingleBoard: React.FC = ({ key={issue.id} draggableId={issue.id} index={index} - isDragDisabled={isNotAllowed || selectedGroup === "created_by"} + isDragDisabled={ + isNotAllowed || selectedGroup === "created_by" || selectedGroup === "assignees" + } > {(provided, snapshot) => ( = ({ const createdBy = selectedGroup === "created_by" - ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." + ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "Loading..." : null; + let assignees: any; + if (selectedGroup === "assignees") { + assignees = groupTitle.split(","); + assignees = assignees + .map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name) + .join(", "); + } + return ( {({ open }) => ( @@ -67,10 +75,10 @@ export const SingleList: React.FC = ({ {selectedGroup !== null ? (

- {groupTitle === null || groupTitle === "null" - ? "None" - : createdBy + {selectedGroup === "created_by" ? createdBy + : selectedGroup === "assignees" + ? assignees : addSpaceIfCamelCase(groupTitle)}

) : ( diff --git a/apps/app/components/issues/sidebar-select/cycle.tsx b/apps/app/components/issues/sidebar-select/cycle.tsx index 353bc5121..b1243fe98 100644 --- a/apps/app/components/issues/sidebar-select/cycle.tsx +++ b/apps/app/components/issues/sidebar-select/cycle.tsx @@ -82,14 +82,14 @@ export const SidebarCycleSelect: React.FC = ({ {cycles ? ( cycles.length > 0 ? ( <> - - None - {cycles.map((option) => ( {option.name} ))} + + None + ) : (
No cycles found
diff --git a/apps/app/components/issues/sidebar-select/module.tsx b/apps/app/components/issues/sidebar-select/module.tsx index e57688887..44bef4d62 100644 --- a/apps/app/components/issues/sidebar-select/module.tsx +++ b/apps/app/components/issues/sidebar-select/module.tsx @@ -81,14 +81,14 @@ export const SidebarModuleSelect: React.FC = ({ {modules ? ( modules.length > 0 ? ( <> - - None - {modules.map((option) => ( {option.name} ))} + + None + ) : (
No modules found
diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index c321f0a31..3ab331244 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/router"; @@ -144,6 +144,12 @@ export const IssueDetailsSidebar: React.FC = ({ [workspaceSlug, projectId, issueId, issueDetail] ); + useEffect(() => { + if (!createLabelForm) return; + + reset(); + }, [createLabelForm, reset]); + const isNotAllowed = userAuth.isGuest || userAuth.isViewer; return ( @@ -431,24 +437,25 @@ export const IssueDetailsSidebar: React.FC = ({ )} /> - + {!isNotAllowed && ( + + )}
diff --git a/apps/app/components/modules/single-module-card.tsx b/apps/app/components/modules/single-module-card.tsx index 6bd59d14f..ed0d027b5 100644 --- a/apps/app/components/modules/single-module-card.tsx +++ b/apps/app/components/modules/single-module-card.tsx @@ -6,9 +6,10 @@ import { useRouter } from "next/router"; // components import { DeleteModuleModal } from "components/modules"; +// ui +import { AssigneesList, Avatar } from "components/ui"; // icons import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; -import User from "public/user.png"; // helpers import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types @@ -21,10 +22,12 @@ type Props = { }; export const SingleModuleCard: React.FC = ({ module }) => { - const router = useRouter(); - const { workspaceSlug } = router.query; const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [selectedModuleForDelete, setSelectedModuleForDelete] = useState(); + + const router = useRouter(); + const { workspaceSlug } = router.query; + const handleDeleteModule = () => { if (!module) return; @@ -33,16 +36,7 @@ export const SingleModuleCard: React.FC = ({ module }) => { }; return ( -
-
- -
+ <> = ({ module }) => { setIsOpen={setModuleDeleteModal} data={selectedModuleForDelete} /> - - - {module.name} -
-
-
LEAD
-
- {module.lead ? ( - module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? ( -
- {module.lead_detail.first_name} -
- ) : ( -
- {module.lead_detail?.first_name && module.lead_detail.first_name !== "" - ? module.lead_detail.first_name.charAt(0) - : module.lead_detail?.email.charAt(0)} -
- ) - ) : ( - "N/A" - )} +
- - -
+ + +
+ ); }; diff --git a/apps/app/components/states/single-state.tsx b/apps/app/components/states/single-state.tsx index ef558d302..d51684275 100644 --- a/apps/app/components/states/single-state.tsx +++ b/apps/app/components/states/single-state.tsx @@ -142,13 +142,16 @@ export const SingleState: React.FC = ({ }`} >
-
-
{addSpaceIfCamelCase(state.name)}
+
+
{addSpaceIfCamelCase(state.name)}
+

{state.description}

+
{index !== 0 && ( diff --git a/apps/app/components/ui/avatar/avatar.tsx b/apps/app/components/ui/avatar.tsx similarity index 96% rename from apps/app/components/ui/avatar/avatar.tsx rename to apps/app/components/ui/avatar.tsx index cb36d07b3..9d7e83700 100644 --- a/apps/app/components/ui/avatar/avatar.tsx +++ b/apps/app/components/ui/avatar.tsx @@ -13,7 +13,7 @@ import { IUser, IUserLite } from "types"; import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; type AvatarProps = { - user: Partial | Partial | undefined; + user?: Partial | Partial | IUser | IUserLite | undefined | null; index?: number; }; diff --git a/apps/app/components/ui/avatar/index.ts b/apps/app/components/ui/avatar/index.ts deleted file mode 100644 index 90fdb226b..000000000 --- a/apps/app/components/ui/avatar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./avatar"; diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 1a16e2072..8311c0237 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -1,4 +1,3 @@ -// components export * from "./button"; export * from "./custom-listbox"; export * from "./custom-menu"; @@ -11,6 +10,6 @@ export * from "./outline-button"; export * from "./select"; export * from "./spinner"; export * from "./text-area"; -export * from "./tooltip"; export * from "./avatar"; export * from "./datepicker"; +export * from "./tooltip"; diff --git a/apps/app/constants/issue.ts b/apps/app/constants/issue.ts index 00bee0aa8..e37a8079d 100644 --- a/apps/app/constants/issue.ts +++ b/apps/app/constants/issue.ts @@ -5,6 +5,7 @@ export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf | { name: "State", key: "state_detail.name" }, { name: "Priority", key: "priority" }, { name: "Created By", key: "created_by" }, + { name: "Assignee", key: "assignees" }, { name: "None", key: null }, ]; diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx index f5cd0f37e..b4f12ddfb 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/states.tsx @@ -80,7 +80,7 @@ const StatesSettings: NextPage = (props) => {

States

-

Manage the state of this project.

+

Manage the states of this project.

{states && projectDetails ? ( From fcba3325899c67eef4b5e05cddeb2ba6bd481e37 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:50:16 +0530 Subject: [PATCH 21/52] refactor: update django admin panel heading (#276) --- apiserver/templates/admin/base_site.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/templates/admin/base_site.html b/apiserver/templates/admin/base_site.html index 4fdb5e19b..fd1d89067 100644 --- a/apiserver/templates/admin/base_site.html +++ b/apiserver/templates/admin/base_site.html @@ -17,7 +17,7 @@ color: #FFFFFF; } -

{% trans 'plane Admin' %}

+

{% trans 'Plane Django Admin' %}

{% endblock %}{% block nav-global %}{% endblock %} From 6f0539f01da6b2752623cb33ae6b914427ccfeea Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 14 Feb 2023 20:05:32 +0530 Subject: [PATCH 22/52] feat: create label option in create issue modal (#281) --- .../components/account/email-code-form.tsx | 8 +- .../components/core/board-view/all-boards.tsx | 6 + .../core/board-view/single-board.tsx | 9 + .../core/board-view/single-issue.tsx | 29 +- .../components/core/issues-view-filter.tsx | 37 ++- apps/app/components/core/issues-view.tsx | 8 + .../core/sidebar/sidebar-progress-stats.tsx | 7 +- apps/app/components/issues/form.tsx | 18 +- apps/app/components/issues/select/label.tsx | 91 +----- .../components/labels/create-label-modal.tsx | 189 +++++++++++++ apps/app/components/labels/index.ts | 1 + .../modules/delete-module-modal.tsx | 4 - apps/app/components/modules/sidebar.tsx | 18 +- .../components/modules/single-module-card.tsx | 15 +- .../components/states/create-state-modal.tsx | 227 +++++++++++++++ .../states/create-update-state-modal.tsx | 263 ------------------ apps/app/components/states/index.ts | 2 +- apps/app/contexts/issue-view.context.tsx | 3 +- .../[projectId]/modules/[moduleId].tsx | 62 +---- .../index.tsx => invitations.tsx} | 61 ++-- 20 files changed, 580 insertions(+), 478 deletions(-) create mode 100644 apps/app/components/labels/create-label-modal.tsx create mode 100644 apps/app/components/states/create-state-modal.tsx delete mode 100644 apps/app/components/states/create-update-state-modal.tsx rename apps/app/pages/{invitations/index.tsx => invitations.tsx} (81%) diff --git a/apps/app/components/account/email-code-form.tsx b/apps/app/components/account/email-code-form.tsx index 98ab10cb7..003d515f4 100644 --- a/apps/app/components/account/email-code-form.tsx +++ b/apps/app/components/account/email-code-form.tsx @@ -114,15 +114,15 @@ export const EmailCodeForm = ({ onSuccess }: any) => { error={errors.token} placeholder="Enter code" /> - {/* { - console.log("Triggered"); handleSubmit(onSubmit); }} > Resend code - */} + */}
)}
diff --git a/apps/app/components/core/board-view/all-boards.tsx b/apps/app/components/core/board-view/all-boards.tsx index d4e995403..f5b03267c 100644 --- a/apps/app/components/core/board-view/all-boards.tsx +++ b/apps/app/components/core/board-view/all-boards.tsx @@ -11,9 +11,11 @@ type Props = { states: IState[] | undefined; members: IProjectMember[] | undefined; addIssueToState: (groupTitle: string, stateId: string | null) => void; + handleEditIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; handleTrashBox: (isDragging: boolean) => void; + removeIssue: ((bridgeId: string) => void) | null; userAuth: UserAuth; }; @@ -23,9 +25,11 @@ export const AllBoards: React.FC = ({ states, members, addIssueToState, + handleEditIssue, openIssuesListModal, handleDeleteIssue, handleTrashBox, + removeIssue, userAuth, }) => { const { groupedByIssues, groupByProperty: selectedGroup, orderBy } = useIssueView(issues); @@ -57,11 +61,13 @@ export const AllBoards: React.FC = ({ groupedByIssues={groupedByIssues} selectedGroup={selectedGroup} members={members} + handleEditIssue={handleEditIssue} addIssueToState={() => addIssueToState(singleGroup, stateId)} handleDeleteIssue={handleDeleteIssue} openIssuesListModal={openIssuesListModal ?? null} orderBy={orderBy} handleTrashBox={handleTrashBox} + removeIssue={removeIssue} userAuth={userAuth} /> ); diff --git a/apps/app/components/core/board-view/single-board.tsx b/apps/app/components/core/board-view/single-board.tsx index e0e790da5..b9600a47c 100644 --- a/apps/app/components/core/board-view/single-board.tsx +++ b/apps/app/components/core/board-view/single-board.tsx @@ -25,11 +25,13 @@ type Props = { }; selectedGroup: NestedKeyOf | null; members: IProjectMember[] | undefined; + handleEditIssue: (issue: IIssue) => void; addIssueToState: () => void; handleDeleteIssue: (issue: IIssue) => void; openIssuesListModal?: (() => void) | null; orderBy: NestedKeyOf | "manual" | null; handleTrashBox: (isDragging: boolean) => void; + removeIssue: ((bridgeId: string) => void) | null; userAuth: UserAuth; }; @@ -40,11 +42,13 @@ export const SingleBoard: React.FC = ({ groupedByIssues, selectedGroup, members, + handleEditIssue, addIssueToState, handleDeleteIssue, openIssuesListModal, orderBy, handleTrashBox, + removeIssue, userAuth, }) => { // collapse/expand @@ -104,10 +108,15 @@ export const SingleBoard: React.FC = ({ snapshot={snapshot} type={type} issue={issue} + selectedGroup={selectedGroup} properties={properties} + editIssue={() => handleEditIssue(issue)} handleDeleteIssue={handleDeleteIssue} orderBy={orderBy} handleTrashBox={handleTrashBox} + removeIssue={() => { + removeIssue && removeIssue(issue.bridge); + }} userAuth={userAuth} /> )} diff --git a/apps/app/components/core/board-view/single-issue.tsx b/apps/app/components/core/board-view/single-issue.tsx index 945592788..a19c683e2 100644 --- a/apps/app/components/core/board-view/single-issue.tsx +++ b/apps/app/components/core/board-view/single-issue.tsx @@ -23,6 +23,8 @@ import { ViewPrioritySelect, ViewStateSelect, } from "components/issues/view-select"; +// ui +import { CustomMenu } from "components/ui"; // types import { CycleIssueResponse, @@ -41,7 +43,10 @@ type Props = { provided: DraggableProvided; snapshot: DraggableStateSnapshot; issue: IIssue; + selectedGroup: NestedKeyOf | null; properties: Properties; + editIssue: () => void; + removeIssue?: (() => void) | null; handleDeleteIssue: (issue: IIssue) => void; orderBy: NestedKeyOf | "manual" | null; handleTrashBox: (isDragging: boolean) => void; @@ -53,7 +58,10 @@ export const SingleBoardIssue: React.FC = ({ provided, snapshot, issue, + selectedGroup, properties, + editIssue, + removeIssue, handleDeleteIssue, orderBy, handleTrashBox, @@ -170,13 +178,26 @@ export const SingleBoardIssue: React.FC = ({
{!isNotAllowed && (
- + */} + {type && !isNotAllowed && ( + + Edit + {type !== "issue" && removeIssue && ( + + <>Remove from {type} + + )} + handleDeleteIssue(issue)}> + Delete permanently + + + )}
)} @@ -195,7 +216,7 @@ export const SingleBoardIssue: React.FC = ({
- {properties.priority && ( + {properties.priority && selectedGroup !== "priority" && ( = ({ position="left" /> )} - {properties.state && ( + {properties.state && selectedGroup !== "state_detail.name" && ( = ({ issues }) => {

Display Properties

- {Object.keys(properties).map((key) => ( - - ))} + {Object.keys(properties).map((key) => { + if ( + issueView === "kanban" && + ((groupByProperty === "state_detail.name" && key === "state") || + (groupByProperty === "priority" && key === "priority")) + ) + return; + + return ( + + ); + })}
diff --git a/apps/app/components/core/issues-view.tsx b/apps/app/components/core/issues-view.tsx index 2edb7f804..b3278ec3e 100644 --- a/apps/app/components/core/issues-view.tsx +++ b/apps/app/components/core/issues-view.tsx @@ -452,9 +452,17 @@ export const IssuesView: React.FC = ({ states={states} members={members} addIssueToState={addIssueToState} + handleEditIssue={handleEditIssue} openIssuesListModal={type !== "issue" ? openIssuesListModal : null} handleDeleteIssue={handleDeleteIssue} handleTrashBox={handleTrashBox} + removeIssue={ + type === "cycle" + ? removeIssueFromCycle + : type === "module" + ? removeIssueFromModule + : null + } userAuth={userAuth} /> )} diff --git a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx index ea8e1e401..8fb073f54 100644 --- a/apps/app/components/core/sidebar/sidebar-progress-stats.tsx +++ b/apps/app/components/core/sidebar/sidebar-progress-stats.tsx @@ -10,6 +10,8 @@ import { Tab } from "@headlessui/react"; // services import issuesServices from "services/issues.service"; import projectService from "services/project.service"; +// hooks +import useLocalStorage from "hooks/use-local-storage"; // components import { SingleProgressStats } from "components/core"; // ui @@ -20,7 +22,6 @@ import User from "public/user.png"; import { IIssue, IIssueLabels } from "types"; // fetch-keys import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys"; -import useLocalStorage from "hooks/use-local-storage"; // types type Props = { groupedIssues: any; @@ -39,8 +40,10 @@ const stateGroupColours: { export const SidebarProgressStats: React.FC = ({ groupedIssues, issues }) => { const router = useRouter(); - const [tab, setTab] = useLocalStorage("tab", "Assignees"); const { workspaceSlug, projectId } = router.query; + + const [tab, setTab] = useLocalStorage("tab", "Assignees"); + const { data: issueLabels } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 5e034822a..3194481b6 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -16,8 +16,9 @@ import { IssueStateSelect, } from "components/issues/select"; import { CycleSelect as IssueCycleSelect } from "components/cycles/select"; -import { CreateUpdateStateModal } from "components/states"; +import { CreateStateModal } from "components/states"; import CreateUpdateCycleModal from "components/project/cycles/create-update-cycle-modal"; +import { CreateLabelModal } from "components/labels"; // ui import { Button, CustomDatePicker, CustomMenu, Input, Loader } from "components/ui"; // icons @@ -74,6 +75,7 @@ export const IssueForm: FC = ({ const [mostSimilarIssue, setMostSimilarIssue] = useState(); const [cycleModal, setCycleModal] = useState(false); const [stateModal, setStateModal] = useState(false); + const [labelModal, setLabelModal] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const router = useRouter(); @@ -121,7 +123,7 @@ export const IssueForm: FC = ({ <> {projectId && ( <> - setStateModal(false)} projectId={projectId} @@ -131,6 +133,11 @@ export const IssueForm: FC = ({ setIsOpen={setCycleModal} projectId={projectId} /> + setLabelModal(false)} + projectId={projectId} + /> )}
@@ -281,7 +288,12 @@ export const IssueForm: FC = ({ control={control} name="labels_list" render={({ field: { value, onChange } }) => ( - + )} />
diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index 2d4e5a179..f0a1d2d64 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -1,15 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -// react-hook-form -import { useForm } from "react-hook-form"; // headless ui import { Combobox, Transition } from "@headlessui/react"; // icons -import { RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline"; +import { PlusIcon, RectangleGroupIcon, TagIcon } from "@heroicons/react/24/outline"; // services import issuesServices from "services/issues.service"; // types @@ -18,55 +16,26 @@ import type { IIssueLabels } from "types"; import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; type Props = { + setIsOpen: React.Dispatch>; value: string[]; onChange: (value: string[]) => void; projectId: string; }; -const defaultValues: Partial = { - name: "", -}; - -export const IssueLabelSelect: React.FC = ({ value, onChange, projectId }) => { +export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, projectId }) => { // states const [query, setQuery] = useState(""); const router = useRouter(); const { workspaceSlug } = router.query; - const [isOpen, setIsOpen] = useState(false); - - const { data: issueLabels, mutate: issueLabelsMutate } = useSWR( + const { data: issueLabels } = useSWR( projectId ? PROJECT_ISSUE_LABELS(projectId) : null, workspaceSlug && projectId ? () => issuesServices.getIssueLabels(workspaceSlug as string, projectId) : null ); - const onSubmit = async (data: IIssueLabels) => { - if (!projectId || !workspaceSlug || isSubmitting) return; - await issuesServices - .createIssueLabel(workspaceSlug as string, projectId as string, data) - .then((response) => { - issueLabelsMutate((prevData) => [...(prevData ?? []), response], false); - setIsOpen(false); - reset(defaultValues); - }) - .catch((error) => { - console.log(error); - }); - }; - - const { - formState: { isSubmitting }, - setFocus, - reset, - } = useForm({ defaultValues }); - - useEffect(() => { - isOpen && setFocus("name"); - }, [isOpen, setFocus]); - const filteredOptions = query === "" ? issueLabels @@ -175,48 +144,14 @@ export const IssueLabelSelect: React.FC = ({ value, onChange, projectId } ) : (

Loading...

)} - {/*
- {isOpen ? ( -
- - - -
- ) : ( - - )} -
*/} +
diff --git a/apps/app/components/labels/create-label-modal.tsx b/apps/app/components/labels/create-label-modal.tsx new file mode 100644 index 000000000..89c286012 --- /dev/null +++ b/apps/app/components/labels/create-label-modal.tsx @@ -0,0 +1,189 @@ +import React, { useEffect } from "react"; + +import { useRouter } from "next/router"; + +import { mutate } from "swr"; + +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// react-color +import { TwitterPicker } from "react-color"; +// headless ui +import { Dialog, Popover, Transition } from "@headlessui/react"; +// services +import issuesService from "services/issues.service"; +// ui +import { Button, Input } from "components/ui"; +// icons +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +// types +import type { IIssueLabels, IState } from "types"; +// constants +import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys"; + +// types +type Props = { + isOpen: boolean; + projectId: string; + handleClose: () => void; +}; + +const defaultValues: Partial = { + name: "", + color: "#000000", +}; + +export const CreateLabelModal: React.FC = ({ isOpen, projectId, handleClose }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + watch, + control, + reset, + setError, + } = useForm({ + defaultValues, + }); + + const onClose = () => { + handleClose(); + reset(defaultValues); + }; + + const onSubmit = async (formData: IIssueLabels) => { + if (!workspaceSlug) return; + + await issuesService + .createIssueLabel(workspaceSlug as string, projectId as string, formData) + .then((res) => { + mutate( + PROJECT_ISSUE_LABELS(projectId), + (prevData) => [res, ...(prevData ?? [])], + false + ); + onClose(); + }) + .catch((error) => { + console.log(error); + }); + }; + + return ( + + + +
+ + +
+
+ + + +
+ + Create Label + +
+ + {({ open }) => ( + <> + + Color + {watch("color") && watch("color") !== "" && ( + + )} + + + + + ( + onChange(value.hex)} + /> + )} + /> + + + + )} + + +
+
+
+ + +
+ +
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/labels/index.ts b/apps/app/components/labels/index.ts index d407cd074..db02d29f0 100644 --- a/apps/app/components/labels/index.ts +++ b/apps/app/components/labels/index.ts @@ -1,3 +1,4 @@ +export * from "./create-label-modal"; export * from "./create-update-label-inline"; export * from "./labels-list-modal"; export * from "./single-label-group"; diff --git a/apps/app/components/modules/delete-module-modal.tsx b/apps/app/components/modules/delete-module-modal.tsx index 317f49a68..05ed67089 100644 --- a/apps/app/components/modules/delete-module-modal.tsx +++ b/apps/app/components/modules/delete-module-modal.tsx @@ -65,10 +65,6 @@ export const DeleteModuleModal: React.FC = ({ isOpen, setIsOpen, data }) }); }; - useEffect(() => { - data && setIsOpen(true); - }, [data, setIsOpen]); - return ( void; }; -export const ModuleDetailsSidebar: React.FC = ({ - issues, - module, - isOpen, - moduleIssues, - handleDeleteModule, -}) => { +export const ModuleDetailsSidebar: React.FC = ({ issues, module, isOpen, moduleIssues }) => { + const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleLinkModal, setModuleLinkModal] = useState(false); const router = useRouter(); @@ -127,6 +122,11 @@ export const ModuleDetailsSidebar: React.FC = ({ handleClose={() => setModuleLinkModal(false)} module={module} /> +
= ({ diff --git a/apps/app/components/modules/single-module-card.tsx b/apps/app/components/modules/single-module-card.tsx index ed0d027b5..a53fc4353 100644 --- a/apps/app/components/modules/single-module-card.tsx +++ b/apps/app/components/modules/single-module-card.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import Link from "next/link"; -import Image from "next/image"; import { useRouter } from "next/router"; // components @@ -13,7 +12,7 @@ import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; // helpers import { renderShortNumericDateFormat } from "helpers/date-time.helper"; // types -import { IModule, SelectModuleType } from "types"; +import { IModule } from "types"; // common import { MODULE_STATUS } from "constants/module"; @@ -23,7 +22,6 @@ type Props = { export const SingleModuleCard: React.FC = ({ module }) => { const [moduleDeleteModal, setModuleDeleteModal] = useState(false); - const [selectedModuleForDelete, setSelectedModuleForDelete] = useState(); const router = useRouter(); const { workspaceSlug } = router.query; @@ -31,23 +29,18 @@ export const SingleModuleCard: React.FC = ({ module }) => { const handleDeleteModule = () => { if (!module) return; - setSelectedModuleForDelete({ ...module, actionType: "delete" }); setModuleDeleteModal(true); }; return ( <>
-
+