From 88165a8fdbc4ecc5ac74f18ad787d8127e4f2c63 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:58:27 +0530 Subject: [PATCH] chore: module and cycle sidebar stats item filter implementation (#4286) --- .../core/sidebar/sidebar-progress-stats.tsx | 102 ++++++++++-------- web/components/cycles/sidebar.tsx | 62 +++++++---- web/components/modules/sidebar.tsx | 39 ++++++- 3 files changed, 135 insertions(+), 68 deletions(-) diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 528b8aa18..db9d94a8f 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -4,6 +4,8 @@ import Image from "next/image"; // headless ui import { Tab } from "@headlessui/react"; import { + IIssueFilterOptions, + IIssueFilters, IModule, TAssigneesDistribution, TCompletionChartDistribution, @@ -37,6 +39,9 @@ type Props = { roundedTab?: boolean; noBackground?: boolean; isPeekView?: boolean; + isCompleted?: boolean; + filters?: IIssueFilters | undefined; + handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; }; export const SidebarProgressStats: React.FC = ({ @@ -47,6 +52,9 @@ export const SidebarProgressStats: React.FC = ({ roundedTab, noBackground, isPeekView = false, + isCompleted = false, + filters, + handleFiltersUpdate, }) => { const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees"); @@ -145,20 +153,11 @@ export const SidebarProgressStats: React.FC = ({ } completed={assignee.completed_issues} total={assignee.total_issues} - {...(!isPeekView && { - onClick: () => { - // TODO: set filters here - // if (filters?.assignees?.includes(assignee.assignee_id ?? "")) - // setFilters({ - // assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id), - // }); - // else - // setFilters({ - // assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""], - // }); - }, - // selected: filters?.assignees?.includes(assignee.assignee_id ?? ""), - })} + {...(!isPeekView && + !isCompleted && { + onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""), + selected: filters?.filters?.assignees?.includes(assignee.assignee_id ?? ""), + })} /> ); else @@ -192,35 +191,52 @@ export const SidebarProgressStats: React.FC = ({ className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm" > {distribution && distribution?.labels.length > 0 ? ( - distribution.labels.map((label, index) => ( - - - {label.label_name ?? "No labels"} - - } - completed={label.completed_issues} - total={label.total_issues} - {...(!isPeekView && { - // TODO: set filters here - onClick: () => { - // if (filters.labels?.includes(label.label_id ?? "")) - // setFilters({ - // labels: filters?.labels?.filter((l) => l !== label.label_id), - // }); - // else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] }); - }, - // selected: filters?.labels?.includes(label.label_id ?? ""), - })} - /> - )) + distribution.labels.map((label, index) => { + if (label.label_id) { + return ( + + + {label.label_name ?? "No labels"} + + } + completed={label.completed_issues} + total={label.total_issues} + {...(!isPeekView && + !isCompleted && { + onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""), + selected: filters?.filters?.labels?.includes(label.label_id ?? `no-label-${index}`), + })} + /> + ); + } else { + return ( + + + {label.label_name ?? "No labels"} + + } + completed={label.completed_issues} + total={label.total_issues} + /> + ); + } + }) ) : (
diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 106f3b3e0..9ada6cf63 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; @@ -16,7 +16,7 @@ import { } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; // types -import { ICycle } from "@plane/types"; +import { ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Avatar, ArchiveIcon, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui"; // components @@ -27,12 +27,13 @@ import { DateRangeDropdown } from "@/components/dropdowns"; // constants import { CYCLE_STATUS } from "@/constants/cycle"; import { CYCLE_UPDATED } from "@/constants/event-tracker"; +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { EUserWorkspaceRoles } from "@/constants/workspace"; // helpers import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store"; +import { useEventTracker, useCycle, useUser, useMember, useIssues } from "@/hooks/store"; // services import { CycleService } from "@/services/cycle.service"; @@ -191,25 +192,36 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { } }; - // TODO: refactor this - // const handleFiltersUpdate = useCallback( - // (key: keyof IIssueFilterOptions, value: string | string[]) => { - // if (!workspaceSlug || !projectId) return; - // const newValues = issueFilters?.filters?.[key] ?? []; + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.CYCLE); - // if (Array.isArray(value)) { - // value.forEach((val) => { - // if (!newValues.includes(val)) newValues.push(val); - // }); - // } else { - // if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - // else newValues.push(value); - // } + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; - // updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId); - // }, - // [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] - // ); + if (Array.isArray(value)) { + // this validation is majorly for the filter start_date, target_date custom + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + cycleId + ); + }, + [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] + ); const cycleStatus = cycleDetails?.status?.toLocaleLowerCase(); const isCompleted = cycleStatus === "completed"; @@ -251,8 +263,8 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 - ? "0 Issue" - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? "0 Issue" + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); @@ -551,6 +563,9 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }} totalIssues={cycleDetails.progress_snapshot.total_issues} isPeekView={Boolean(peekCycle)} + isCompleted={isCompleted} + filters={issueFilters} + handleFiltersUpdate={handleFiltersUpdate} />
)} @@ -570,6 +585,9 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }} totalIssues={cycleDetails.total_issues} isPeekView={Boolean(peekCycle)} + isCompleted={isCompleted} + filters={issueFilters} + handleFiltersUpdate={handleFiltersUpdate} />
)} diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index f219aeb95..91d69c2e1 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; @@ -15,7 +15,7 @@ import { UserCircle2, } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; -import { ILinkDetails, IModule, ModuleLink } from "@plane/types"; +import { IIssueFilterOptions, ILinkDetails, IModule, ModuleLink } from "@plane/types"; // ui import { CustomMenu, @@ -41,13 +41,14 @@ import { MODULE_LINK_UPDATED, MODULE_UPDATED, } from "@/constants/event-tracker"; +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; import { MODULE_STATUS } from "@/constants/module"; import { EUserProjectRoles } from "@/constants/project"; // helpers import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; import { copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useModule, useUser, useEventTracker } from "@/hooks/store"; +import { useModule, useUser, useEventTracker, useIssues } from "@/hooks/store"; // types const defaultValues: Partial = { @@ -82,6 +83,9 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } = useModule(); const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker(); + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); const moduleDetails = getModuleById(moduleId); const moduleState = moduleDetails?.status?.toLocaleLowerCase(); @@ -245,6 +249,33 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { }); }, [moduleDetails, reset]); + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; + + if (Array.isArray(value)) { + // this validation is majorly for the filter start_date, target_date custom + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters( + workspaceSlug.toString(), + projectId.toString(), + EIssueFilterType.FILTERS, + { [key]: newValues }, + moduleId + ); + }, + [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + ); + const startDate = getDate(moduleDetails?.start_date); const endDate = getDate(moduleDetails?.target_date); const isStartValid = startDate && startDate <= new Date(); @@ -599,6 +630,8 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { totalIssues={moduleDetails.total_issues} module={moduleDetails} isPeekView={Boolean(peekModule)} + filters={issueFilters} + handleFiltersUpdate={handleFiltersUpdate} /> )}