Skip to content

feat(web): show estimate point sums in list view group headers#9214

Open
laurin-eichberger wants to merge 1 commit into
makeplane:previewfrom
laurin-eichberger:feature/estimate-sums-in-group-headers
Open

feat(web): show estimate point sums in list view group headers#9214
laurin-eichberger wants to merge 1 commit into
makeplane:previewfrom
laurin-eichberger:feature/estimate-sums-in-group-headers

Conversation

@laurin-eichberger
Copy link
Copy Markdown

@laurin-eichberger laurin-eichberger commented Jun 4, 2026

Description

Adds estimate point sum badges to list view group headers when using a summable estimate system (Points or Time).

  • New "Show estimate totals" toggle in the list layout's display filter extra options
  • Each group header shows a badge with the sum of estimate points for items in that group
  • Shows a prefix when the group has more items not yet loaded (partial sum)
  • Only appears for summable estimate systems (Points, Time), not Categories

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Screenshots and Media (if applicable)

Activated estimations for groups show group estimation totals

image

Activated estimations for paginated groups (>50) show a lower bound based on loaded issues

image

Test Scenarios

  • Enable/disable "Show estimate totals" toggle → badges appear/disappear
  • Points estimate system → sums display correctly per group
  • Time estimate system → sums display correctly per group (assumption, cannot test on CE)
  • Categories estimate system → no badges shown
  • No estimate system configured → no badges shown
  • Partially loaded group → badge shows prefix
  • Mix of estimated and unestimated items → only estimated items counted (not relevant after #settings
  • All items unestimated → no badge shown

References

Resolves #9213

Summary by CodeRabbit

  • New Features
    • Added a new display filter option to toggle and show estimate summaries for issue groups in list view.

Copilot AI review requested due to automatic review settings June 4, 2026 13:54
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a user-facing "Show estimate totals" toggle in Display Filters that computes and displays the sum of estimate points next to each grouped list view's header. The feature follows the existing "Show sub-work items" pattern: optional type definitions, UI selection, prop propagation through list components, estimate calculations using the project estimates hook, and conditional badge rendering in group headers.

Changes

Estimate totals display in list group headers

Layer / File(s) Summary
Type system and configuration foundation
packages/types/src/view-props.ts, packages/constants/src/issue/filter.ts, packages/i18n/src/locales/en/work-item.json, packages/utils/src/work-item/base.ts
show_estimates added to TIssueExtraOptions and TIssueParams type unions, IIssueDisplayFilterOptions interface, filter configuration extra options, translation keys, and computed display filters with a default of false.
Display filter UI selection
apps/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx, apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx
The show_estimates option is wired into ISSUE_EXTRA_OPTIONS, the selectedExtraOptions prop type is extended to include it, and DisplayFiltersSelection passes the current state to FilterExtraOptions.
List view props propagation
apps/web/core/components/issues/issue-layouts/list/base-list-root.tsx, apps/web/core/components/issues/issue-layouts/list/default.tsx
BaseListRoot derives showEstimates from displayFilters?.show_estimates, passes it to the List component via a new prop, and List threads it down to each ListGroup.
List group estimate sum computation
apps/web/core/components/issues/issue-layouts/list/list-group.tsx
useProjectEstimates hook is imported and used to compute a group-level estimateSum by summing numeric estimate_point values from issues when showEstimates is enabled and the active estimate system is not categories; pagination state is derived to flag partial groups.
Estimate badge rendering in group headers
apps/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx
IHeaderGroupByCard interface extends to accept estimateSum and isPartialCount props; HeaderGroupByCard conditionally renders an estimate badge with EstimatePropertyIcon and a "≥" prefix when the count is partial due to pagination.

🎯 2 (Simple) | ⏱️ ~12 minutes

🐰 A filter to count the estimates we raise,
With sums that shine in each grouped case,
No more guessing what effort's in store,
The badge shows exactly what's planned for the floor!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding estimate point sum displays to group headers in list view.
Description check ✅ Passed The description is comprehensive, covering the feature, type of change, visual evidence via screenshots, test scenarios, and relevant issue references.
Linked Issues check ✅ Passed Changes fully implement the objectives from issue #9213: toggle in display filters, estimate sum badges in group headers, support for summable systems only, partial-load indicator (≥), and reduction of cognitive load [#9213].
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the estimate totals feature; no unrelated modifications detected across display filters, list view components, types, translations, and utilities.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Jun 4, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new “show estimates” display option and surfaces estimate totals in list group headers when enabled.

Changes:

  • Added show_estimates to issue display filter types, computed filters, constants, and i18n.
  • Wired the new option through list root → list → list group, and rendered an estimate total badge in the group header.
  • Implemented client-side estimate total aggregation for each visible group.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/utils/src/work-item/base.ts Includes show_estimates in computed display filters.
packages/types/src/view-props.ts Extends filter option unions/interfaces to include show_estimates.
packages/i18n/src/locales/en/work-item.json Adds label for the new extra option.
packages/constants/src/issue/filter.ts Enables show_estimates as a selectable extra option for relevant pages.
apps/web/core/components/issues/issue-layouts/list/list-group.tsx Computes per-group estimate totals and passes them to the header.
apps/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx Renders estimate total badge (with partial indicator).
apps/web/core/components/issues/issue-layouts/list/default.tsx Threads showEstimates prop into list groups.
apps/web/core/components/issues/issue-layouts/list/base-list-root.tsx Reads show_estimates from display filters and passes it down.
apps/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx Adds show_estimates to UI extra options config / types.
apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx Initializes show_estimates in selected options state.

Comment on lines +120 to +151
const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0;
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
const isPaginating = !!getIssueLoader(group.id);
const isPartialGroup = groupIssueIds ? groupIssueIds.length < groupIssueCount || !!nextPageResults : false;
const estimateSum = (() => {
if (!showEstimates) return null;
if (!groupIssueIds || groupIssueIds.length === 0) return null;

const firstIssue = issuesMap[groupIssueIds[0]];
const issueProjectId = firstIssue?.project_id;
if (!issueProjectId) return null;

if (!areEstimateEnabledByProjectId(issueProjectId)) return null;

const activeEstimateId = currentActiveEstimateIdByProjectId(issueProjectId);
if (!activeEstimateId) return null;
const activeEstimate = estimateById(activeEstimateId);
if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) return null;

let sum = 0;
for (const issueId of groupIssueIds) {
const issue = issuesMap[issueId];
if (!issue?.estimate_point) continue;
const point = activeEstimate.estimatePointById(issue.estimate_point);
if (!point?.value) continue;
const numericValue = Number(point.value);
if (!Number.isNaN(numericValue)) {
sum += numericValue;
}
}
return sum;
})();
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that the current logic is the simplest approach and performance impact is limited, because groups are capped at 50 entries, correct? After that pagination kicks in. But I am ofc open for different perspectives and suggestions here.

Comment on lines +274 to +280
group,
orderBy,
getGroupIndex,
setDragColumnOrientation,
setIsDraggingOverColumn,
isWorkflowDropDisabled
]);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/core/components/issues/issue-layouts/list/list-group.tsx`:
- Around line 124-151: The group total in estimateSum currently uses the first
issue's project context; change the logic to compute per-issue project
estimates: inside the loop over groupIssueIds (using variables groupIssueIds and
issuesMap), for each issue read its project_id, skip if missing or if
areEstimateEnabledByProjectId(project_id) is false, fetch activeEstimateId via
currentActiveEstimateIdByProjectId(project_id) and resolve activeEstimate via
estimateById(activeEstimateId), skip if missing or of type
EEstimateSystem.CATEGORIES, then lookup the point with
activeEstimate.estimatePointById(issue.estimate_point) and add its numeric value
to sum; remove dependence on firstIssue/issueProjectId outside the loop so
mixed-project groups are handled correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84f3368b-89b1-493f-a420-a3e0e23c8f36

📥 Commits

Reviewing files that changed from the base of the PR and between 9a30a07 and 38cdf94.

📒 Files selected for processing (10)
  • apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx
  • apps/web/core/components/issues/issue-layouts/filters/header/display-filters/extra-options.tsx
  • apps/web/core/components/issues/issue-layouts/list/base-list-root.tsx
  • apps/web/core/components/issues/issue-layouts/list/default.tsx
  • apps/web/core/components/issues/issue-layouts/list/headers/group-by-card.tsx
  • apps/web/core/components/issues/issue-layouts/list/list-group.tsx
  • packages/constants/src/issue/filter.ts
  • packages/i18n/src/locales/en/work-item.json
  • packages/types/src/view-props.ts
  • packages/utils/src/work-item/base.ts

Comment on lines +124 to +151
const estimateSum = (() => {
if (!showEstimates) return null;
if (!groupIssueIds || groupIssueIds.length === 0) return null;

const firstIssue = issuesMap[groupIssueIds[0]];
const issueProjectId = firstIssue?.project_id;
if (!issueProjectId) return null;

if (!areEstimateEnabledByProjectId(issueProjectId)) return null;

const activeEstimateId = currentActiveEstimateIdByProjectId(issueProjectId);
if (!activeEstimateId) return null;
const activeEstimate = estimateById(activeEstimateId);
if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) return null;

let sum = 0;
for (const issueId of groupIssueIds) {
const issue = issuesMap[issueId];
if (!issue?.estimate_point) continue;
const point = activeEstimate.estimatePointById(issue.estimate_point);
if (!point?.value) continue;
const numericValue = Number(point.value);
if (!Number.isNaN(numericValue)) {
sum += numericValue;
}
}
return sum;
})();
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jun 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use per-issue project estimate context when calculating group totals.

The current sum logic uses the first issue’s project_id for the whole group. In mixed-project groups, this can undercount or miscalculate totals for issues from other projects.

Suggested fix
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
@@
-  const estimateSum = (() => {
+  const estimateSum = useMemo(() => {
     if (!showEstimates) return null;
     if (!groupIssueIds || groupIssueIds.length === 0) return null;
-
-    const firstIssue = issuesMap[groupIssueIds[0]];
-    const issueProjectId = firstIssue?.project_id;
-    if (!issueProjectId) return null;
-
-    if (!areEstimateEnabledByProjectId(issueProjectId)) return null;
-
-    const activeEstimateId = currentActiveEstimateIdByProjectId(issueProjectId);
-    if (!activeEstimateId) return null;
-    const activeEstimate = estimateById(activeEstimateId);
-    if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) return null;
-
     let sum = 0;
+    let hasEstimatedItems = false;
+
     for (const issueId of groupIssueIds) {
       const issue = issuesMap[issueId];
-      if (!issue?.estimate_point) continue;
-      const point = activeEstimate.estimatePointById(issue.estimate_point);
-      if (!point?.value) continue;
+      if (!issue?.project_id || !issue?.estimate_point) continue;
+      if (!areEstimateEnabledByProjectId(issue.project_id)) continue;
+
+      const activeEstimateId = currentActiveEstimateIdByProjectId(issue.project_id);
+      if (!activeEstimateId) continue;
+      const activeEstimate = estimateById(activeEstimateId);
+      if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) continue;
+
+      const point = activeEstimate.estimatePointById(issue.estimate_point);
+      if (point?.value == null) continue;
       const numericValue = Number(point.value);
       if (!Number.isNaN(numericValue)) {
         sum += numericValue;
+        hasEstimatedItems = true;
       }
     }
-    return sum;
-  })();
+    return hasEstimatedItems ? sum : null;
+  }, [
+    showEstimates,
+    groupIssueIds,
+    issuesMap,
+    areEstimateEnabledByProjectId,
+    currentActiveEstimateIdByProjectId,
+    estimateById,
+  ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const estimateSum = (() => {
if (!showEstimates) return null;
if (!groupIssueIds || groupIssueIds.length === 0) return null;
const firstIssue = issuesMap[groupIssueIds[0]];
const issueProjectId = firstIssue?.project_id;
if (!issueProjectId) return null;
if (!areEstimateEnabledByProjectId(issueProjectId)) return null;
const activeEstimateId = currentActiveEstimateIdByProjectId(issueProjectId);
if (!activeEstimateId) return null;
const activeEstimate = estimateById(activeEstimateId);
if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) return null;
let sum = 0;
for (const issueId of groupIssueIds) {
const issue = issuesMap[issueId];
if (!issue?.estimate_point) continue;
const point = activeEstimate.estimatePointById(issue.estimate_point);
if (!point?.value) continue;
const numericValue = Number(point.value);
if (!Number.isNaN(numericValue)) {
sum += numericValue;
}
}
return sum;
})();
const estimateSum = useMemo(() => {
if (!showEstimates) return null;
if (!groupIssueIds || groupIssueIds.length === 0) return null;
let sum = 0;
let hasEstimatedItems = false;
for (const issueId of groupIssueIds) {
const issue = issuesMap[issueId];
if (!issue?.project_id || !issue?.estimate_point) continue;
if (!areEstimateEnabledByProjectId(issue.project_id)) continue;
const activeEstimateId = currentActiveEstimateIdByProjectId(issue.project_id);
if (!activeEstimateId) continue;
const activeEstimate = estimateById(activeEstimateId);
if (!activeEstimate?.type || activeEstimate.type === EEstimateSystem.CATEGORIES) continue;
const point = activeEstimate.estimatePointById(issue.estimate_point);
if (point?.value == null) continue;
const numericValue = Number(point.value);
if (!Number.isNaN(numericValue)) {
sum += numericValue;
hasEstimatedItems = true;
}
}
return hasEstimatedItems ? sum : null;
}, [
showEstimates,
groupIssueIds,
issuesMap,
areEstimateEnabledByProjectId,
currentActiveEstimateIdByProjectId,
estimateById,
]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/issues/issue-layouts/list/list-group.tsx` around
lines 124 - 151, The group total in estimateSum currently uses the first issue's
project context; change the logic to compute per-issue project estimates: inside
the loop over groupIssueIds (using variables groupIssueIds and issuesMap), for
each issue read its project_id, skip if missing or if
areEstimateEnabledByProjectId(project_id) is false, fetch activeEstimateId via
currentActiveEstimateIdByProjectId(project_id) and resolve activeEstimate via
estimateById(activeEstimateId), skip if missing or of type
EEstimateSystem.CATEGORIES, then lookup the point with
activeEstimate.estimatePointById(issue.estimate_point) and add its numeric value
to sum; remove dependence on firstIssue/issueProjectId outside the loop so
mixed-project groups are handled correctly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a valid point, I did not think of multi-project views. I will adapt the function to handle this correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurin-eichberger, sounds great! The suggested useMemo-based fix in the comment above should cover multi-project groups correctly by resolving the estimate context per-issue rather than anchoring on the first issue's project. Feel free to ping if you run into anything while adapting it.

(🐰 ✨)


🧠 Learnings used
Learnt from: aaryan610
Repo: makeplane/plane PR: 8537
File: apps/web/core/components/issues/preview-card/root.tsx:51-51
Timestamp: 2026-03-05T07:50:17.119Z
Learning: In Tailwind CSS v4, wrap-break-word is a built-in overflow-wrap utility (not a custom class) and should not be flagged as invalid. The v4 naming convention uses wrap-* for overflow-wrap utilities (e.g., wrap-break-word, wrap-anywhere, wrap-normal) instead of the older break-words class. When reviewing TSX files, recognize wrap-break-word as valid Tailwind usage and prefer wrap-* utilities for overflow-wrap; avoid flagging these as errors and consider updating any older references to break-words to the corresponding wrap-* form where appropriate.

Learnt from: dkumankov
Repo: makeplane/plane PR: 9216
File: apps/web/core/components/dropdowns/layout.tsx:77-77
Timestamp: 2026-06-05T06:06:41.884Z
Learning: In makeplane/plane, Tailwind CSS v4 uses the important modifier (`!`) as a **suffix** placed immediately after the utility class (e.g., `w-auto!`, `bg-layer-transparent-hover!`, `px-2!`). When reviewing files in this repo, do **not** flag these suffix-important utilities as invalid Tailwind syntax. The v3 prefix form `!utility` / `!classname` (e.g., `!w-auto`) is considered invalid for Tailwind v4—flag that form instead.

@laurin-eichberger
Copy link
Copy Markdown
Author

@sriramveeraghanta while working on this I noticed that the pre-commit hooks identified 14 warnings via eslint plugins, but AFAIK all were pre-existing and unrelated to the changes in this PR. How should I handle these?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature]: Show estimate point totals in list view group headers

3 participants