diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 3b6d40534..e0d7e3c50 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -70,6 +70,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { // store hooks const { issuesFilter: { issueFilters, updateFilters }, + issues: { issuesCount }, } = useIssues(EIssuesStoreType.CYCLE); const { currentProjectCycleIds, getCycleById } = useCycle(); const { toggleCreateIssueModal } = useCommandPalette(); @@ -145,12 +146,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const issueCount = cycleDetails - ? !issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues - ? cycleDetails.total_issues - cycleDetails?.sub_issues - : cycleDetails.total_issues - : undefined; - const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0; return ( @@ -209,16 +204,16 @@ export const CycleIssuesHeader: React.FC = observer(() => {

{cycleDetails?.name && cycleDetails.name}

- {issueCount && issueCount > 0 ? ( + {issuesCount && issuesCount > 0 ? ( 1 ? "issues" : "issue" + tooltipContent={`There are ${issuesCount} ${ + issuesCount > 1 ? "issues" : "issue" } in this cycle`} position="bottom" > - {issueCount} + {issuesCount} ) : null} diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 9a911103d..538eca2cd 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -71,6 +71,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { // store hooks const { issuesFilter: { issueFilters }, + issues: { issuesCount }, } = useIssues(EIssuesStoreType.MODULE); const { updateFilters } = useIssuesActions(EIssuesStoreType.MODULE); const { projectModuleIds, getModuleById } = useModule(); @@ -145,12 +146,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const issueCount = moduleDetails - ? !issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues - ? moduleDetails.total_issues - moduleDetails.sub_issues - : moduleDetails.total_issues - : undefined; - const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0; return ( @@ -209,16 +204,16 @@ export const ModuleIssuesHeader: React.FC = observer(() => {

{moduleDetails?.name && moduleDetails.name}

- {issueCount && issueCount > 0 ? ( + {issuesCount && issuesCount > 0 ? ( 1 ? "issues" : "issue" + tooltipContent={`There are ${issuesCount} ${ + issuesCount > 1 ? "issues" : "issue" } in this module`} position="bottom" > - {issueCount} + {issuesCount} ) : null} diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 95983d85a..8ba44719e 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -43,6 +43,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { } = useMember(); const { issuesFilter: { issueFilters, updateFilters }, + issues: { issuesCount }, } = useIssues(EIssuesStoreType.PROJECT); const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -105,12 +106,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const issueCount = currentProjectDetails - ? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails?.sub_issues - ? currentProjectDetails?.total_issues - currentProjectDetails?.sub_issues - : currentProjectDetails?.total_issues - : undefined; - const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0; return ( @@ -153,14 +148,14 @@ export const ProjectIssuesHeader: React.FC = observer(() => { link={} />} /> - {issueCount && issueCount > 0 ? ( + {issuesCount && issuesCount > 0 ? ( 1 ? "issues" : "issue"} in this project`} + tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`} position="bottom" > - {issueCount} + {issuesCount} ) : null} diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index 614994767..d01f996c7 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -2,12 +2,15 @@ import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; import { v4 as uuidv4 } from "uuid"; // types import { + TGroupedIssues, TIssue, TIssueGroupByOptions, TIssueLayouts, TIssueOrderByOptions, TIssueParams, TStateGroups, + TSubGroupedIssues, + TUnGroupedIssues, } from "@plane/types"; import { IGanttBlock } from "@/components/gantt-chart"; // constants @@ -211,3 +214,43 @@ export const getDescriptionPlaceholder = (isFocused: boolean, description: strin if (!isDescriptionEmpty || isFocused) return "Press '/' for commands..."; else return "Click to add description"; }; + +export const issueCountBasedOnFilters = ( + issueIds: TUnGroupedIssues | TGroupedIssues | TSubGroupedIssues, + layout: TIssueLayouts, + groupBy: string | undefined, + subGroupBy: string | undefined +): number => { + let issuesCount = 0; + if (!layout) return issuesCount; + + if (["spreadsheet", "gantt_chart"].includes(layout)) { + issuesCount = (issueIds as TUnGroupedIssues)?.length; + } else if (layout === "calendar") { + Object.keys(issueIds || {}).map((groupId) => { + issuesCount += (issueIds as TGroupedIssues)?.[groupId]?.length; + }); + } else if (layout === "list") { + if (groupBy) { + Object.keys(issueIds || {}).map((groupId) => { + issuesCount += (issueIds as TGroupedIssues)?.[groupId]?.length; + }); + } else { + issuesCount = (issueIds as TUnGroupedIssues)?.length; + } + } else if (layout === "kanban") { + if (groupBy && subGroupBy) { + Object.keys(issueIds || {}).map((groupId) => { + Object.keys((issueIds as TSubGroupedIssues)?.[groupId] || {}).map((subGroupId) => { + issuesCount += (issueIds as TSubGroupedIssues)?.[groupId]?.[subGroupId]?.length || 0; + }); + }); + } else if (groupBy) { + Object.keys(issueIds || {}).map((groupId) => { + issuesCount += (issueIds as TGroupedIssues)?.[groupId]?.length; + }); + } + } + + return issuesCount; +}; diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 7a264dad0..3632d03f7 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -6,6 +6,8 @@ import update from "lodash/update"; import { action, observable, makeObservable, computed, runInAction } from "mobx"; // types import { TIssue, TSubGroupedIssues, TGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +// helpers +import { issueCountBasedOnFilters } from "@/helpers/issue.helper"; // services import { CycleService } from "@/services/cycle.service"; import { IssueService } from "@/services/issue"; @@ -21,6 +23,7 @@ export interface ICycleIssues { issues: { [cycle_id: string]: string[] }; viewFlags: ViewFlags; // computed + issuesCount: number; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; @@ -60,7 +63,7 @@ export interface ICycleIssues { ) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; - removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise + removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; transferIssuesFromCycle: ( workspaceSlug: string, projectId: string, @@ -93,6 +96,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { loader: observable.ref, issues: observable, // computed + issuesCount: computed, groupedIssueIds: computed, // action fetchIssues: action, @@ -113,6 +117,22 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { this.cycleService = new CycleService(); } + get issuesCount() { + let issuesCount = 0; + + const displayFilters = this.rootStore?.cycleIssuesFilter?.issueFilters?.displayFilters; + const groupedIssueIds = this.groupedIssueIds; + if (!displayFilters || !groupedIssueIds) return issuesCount; + + const layout = displayFilters?.layout || undefined; + const groupBy = displayFilters?.group_by || undefined; + const subGroupBy = displayFilters?.sub_group_by || undefined; + + if (!layout) return issuesCount; + issuesCount = issueCountBasedOnFilters(groupedIssueIds, layout, groupBy, subGroupBy); + return issuesCount; + } + get groupedIssueIds() { const cycleId = this.rootIssueStore?.cycleId; if (!cycleId) return undefined; @@ -336,14 +356,14 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { /** * Remove a cycle from issue - * @param workspaceSlug - * @param projectId - * @param issueId - * @returns + * @param workspaceSlug + * @param projectId + * @param issueId + * @returns */ removeCycleFromIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id; - if(!issueCycleId) return; + if (!issueCycleId) return; try { // perform optimistic update, update store runInAction(() => { diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index e5c7b1cc1..35066a052 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -7,6 +7,8 @@ import update from "lodash/update"; import { action, observable, makeObservable, computed, runInAction } from "mobx"; // types import { TIssue, TLoader, TGroupedIssues, TSubGroupedIssues, TUnGroupedIssues, ViewFlags } from "@plane/types"; +// helpers +import { issueCountBasedOnFilters } from "@/helpers/issue.helper"; // services import { IssueService } from "@/services/issue"; import { ModuleService } from "@/services/module.service"; @@ -21,6 +23,7 @@ export interface IModuleIssues { issues: { [module_id: string]: string[] }; viewFlags: ViewFlags; // computed + issuesCount: number; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; // actions getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; @@ -95,6 +98,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { loader: observable.ref, issues: observable, // computed + issuesCount: computed, groupedIssueIds: computed, // action fetchIssues: action, @@ -113,6 +117,22 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { this.moduleService = new ModuleService(); } + get issuesCount() { + let issuesCount = 0; + + const displayFilters = this.rootIssueStore?.moduleIssuesFilter?.issueFilters?.displayFilters; + const groupedIssueIds = this.groupedIssueIds; + if (!displayFilters || !groupedIssueIds) return issuesCount; + + const layout = displayFilters?.layout || undefined; + const groupBy = displayFilters?.group_by || undefined; + const subGroupBy = displayFilters?.sub_group_by || undefined; + + if (!layout) return issuesCount; + issuesCount = issueCountBasedOnFilters(groupedIssueIds, layout, groupBy, subGroupBy); + return issuesCount; + } + get groupedIssueIds() { const moduleId = this.rootIssueStore?.moduleId; if (!moduleId) return undefined; @@ -370,9 +390,9 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { /** * change modules array in issue - * @param workspaceSlug - * @param projectId - * @param issueId + * @param workspaceSlug + * @param projectId + * @param issueId * @param addModuleIds array of modules to be added * @param removeModuleIds array of modules to be removed */ @@ -404,7 +424,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { }); }); }); - if(originalModuleIds){ + if (originalModuleIds) { // update the root issue map with the new module ids let currentModuleIds = concat([...originalModuleIds], addModuleIds); currentModuleIds = pull(currentModuleIds, ...removeModuleIds); @@ -420,7 +440,6 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { if (!isEmpty(removeModuleIds)) { await this.moduleService.removeModulesFromIssueBulk(workspaceSlug, projectId, issueId, removeModuleIds); } - } catch (error) { // revert the issue back to its original module ids set(this.rootStore.issues.issuesMap, [issueId, "module_ids"], originalModuleIds); diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 6e137b9d8..8591f494c 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -5,6 +5,8 @@ import update from "lodash/update"; import { action, makeObservable, observable, runInAction, computed } from "mobx"; // types import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; +// helpers +import { issueCountBasedOnFilters } from "@/helpers/issue.helper"; // base class import { IssueService, IssueArchiveService } from "@/services/issue"; import { IssueHelperStore } from "../helpers/issue-helper.store"; @@ -17,6 +19,7 @@ export interface IProjectIssues { issues: Record; // Record of project_id as key and issue_ids as value viewFlags: ViewFlags; // computed + issuesCount: number; groupedIssueIds: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined; getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined; // action @@ -51,6 +54,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { loader: observable.ref, issues: observable, // computed + issuesCount: computed, groupedIssueIds: computed, // action fetchIssues: action, @@ -68,6 +72,22 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { this.issueArchiveService = new IssueArchiveService(); } + get issuesCount() { + let issuesCount = 0; + + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + const groupedIssueIds = this.groupedIssueIds; + if (!displayFilters || !groupedIssueIds) return issuesCount; + + const layout = displayFilters?.layout || undefined; + const groupBy = displayFilters?.group_by || undefined; + const subGroupBy = displayFilters?.sub_group_by || undefined; + + if (!layout) return issuesCount; + issuesCount = issueCountBasedOnFilters(groupedIssueIds, layout, groupBy, subGroupBy); + return issuesCount; + } + get groupedIssueIds() { const projectId = this.rootStore?.projectId; if (!projectId) return undefined;