Skip to content

feat: display related milestones on public program page#3441

Merged
kasya merged 9 commits intoOWASP:mainfrom
HarshitVerma109:feat/display-related-milestones
Jan 24, 2026
Merged

feat: display related milestones on public program page#3441
kasya merged 9 commits intoOWASP:mainfrom
HarshitVerma109:feat/display-related-milestones

Conversation

@HarshitVerma109
Copy link
Contributor

Proposed change

Resolves #3245

This PR adds a "Related Milestones" section to the public program details page. It aggregates and displays milestone cards for all repositories associated with a program's modules, providing better visibility into ongoing work.

Key Changes:

  • Backend: Added recent_milestones field to the ProgramNode in program.py. This field aggregates open milestones from all repositories linked to the program's modules.
  • Frontend:
    • Updated the GraphQL query for program details to fetch recentMilestones including URL and author details.
    • Added a distinct "Recent Milestones" section to CardDetailsPage.
    • Ensured milestone cards link directly to the GitHub milestone page.

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Summary by CodeRabbit

  • New Features

    • Program pages and details cards now show recent milestones with status, open/closed counts, repo/org, author avatar/name, date, and links; program admins are also surfaced.
    • Milestone list limits to 4 with a "Show more"/"Show less" toggle.
  • API / Types

    • Program payload now includes recentMilestones for frontend use.
  • Tests

    • Unit tests added for milestone listing, initial limit, and expand/collapse behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds a ProgramNode.recent_milestones GraphQL field; extends program query and Program type with recentMilestones; passes recentMilestones to DetailsCard; implements Recent Milestones UI with show-more toggle and unit tests. (28 words)

Changes

Cohort / File(s) Summary
Backend GraphQL
backend/apps/mentorship/api/internal/nodes/program.py
Add recent_milestones(self) -> list[MilestoneNode] on ProgramNode; queries Milestone filtered by program module project IDs, uses select_related/prefetch_related, orders by created_at desc, applies distinct, exposed as a Strawberry field.
Frontend query & types
frontend/src/server/queries/programsQueries.ts, frontend/src/types/mentorship.ts
Extend GET_PROGRAM_AND_MODULES selection set to include recentMilestones with milestone fields and author subfields; add recentMilestones?: Milestone[] to Program type.
Frontend pages & components
frontend/src/app/mentorship/programs/[programKey]/page.tsx, frontend/src/components/CardDetailsPage.tsx
Pass admins and recentMilestones into DetailsCard; add Recent Milestones UI for programs (avatar tooltip, linked/truncated titles, formatted date, open/closed counts, optional repo/org links), show-more/less toggle, MILESTONE_LIMIT = 4.
Frontend tests
frontend/__tests__/unit/components/CardDetailsPage.test.tsx
Add tests for milestones UI: helper to generate milestones, assert initial slice of 4, toggle expands/collapses, no toggle when ≤ limit; import fireEvent.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • kasya
  • arkid15r
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a milestones display feature to the public program page.
Description check ✅ Passed The description is well-organized, explains the proposed changes, lists key backend and frontend modifications, and references the resolved issue.
Linked Issues check ✅ Passed The PR fully addresses issue #3245 requirements: added recent milestones section, aggregated milestones from program repositories, created milestone cards, and provided GitHub links.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the recent milestones feature on the program page; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
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

🤖 Fix all issues with AI agents
In `@backend/apps/mentorship/api/internal/nodes/program.py`:
- Around line 37-44: Update the recent_milestones resolver: change its return
type to list["MilestoneNode"] (no None), add an optional limit parameter with
default 5, order results by "-created_at", eager-load relations via
select_related("repository__organization", "author") and
prefetch_related("labels") to avoid N+1, and preserve .distinct() (apply
select_related/prefetch before calling distinct()) when filtering
Milestone.open_milestones by project_ids in recent_milestones so duplicates from
repositories linked to multiple projects are prevented.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 21, 2026
@HarshitVerma109
Copy link
Contributor Author

@kasya, please review it and let me know if any changes are required.

Copy link
Collaborator

@kasya kasya left a comment

Choose a reason for hiding this comment

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

@HarshitVerma109 looks good on the UI!

Left some comments ⬇️

@strawberry.field
def recent_milestones(self, limit: int = 5) -> list["MilestoneNode"]:
"""Get the list of recent milestones for the program."""
from apps.github.models.milestone import Milestone
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move imports to the top of the module

Comment on lines 330 to 339
{type === 'program' ? (
recentMilestones && (
<div className="[&_.grid]:!grid-cols-1 md:[&_.grid]:!grid-cols-2">
<Milestones
data={recentMilestones}
showAvatar={showAvatar}
showSingleColumn={false}
/>
</div>
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this should be under Modules block.

return self.admins.all()

@strawberry.field
def recent_milestones(self, limit: int = 5) -> list["MilestoneNode"]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you think we should limit to 5 on this view?
I can see how 5 is suitable for projects page where we have 2 columns with 5 elements.
But on the program page 5 items look kinda unfinished:

Image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was a bit confused, so I just set the limit to 5. Additionally, I suggest we add a 'Recent Issues' column, similar to the one on the project page

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or should I limit this to 4 and add a "Show More" button? What do you think? Let me know if I'm wrong.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@HarshitVerma109 either 4 or 6 would work just fine! 👍🏼

As for recent issues - I don't think we need to add that, at least not right now. Issues are available per module view.

@kasya kasya mentioned this pull request Jan 23, 2026
47 tasks
Copy link
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: 2

🤖 Fix all issues with AI agents
In `@frontend/src/components/CardDetailsPage.tsx`:
- Around line 409-416: The Link currently uses href={milestone?.url || ''} which
makes an empty href navigate to the current page; update the JSX in
CardDetailsPage (the Link wrapping TruncatedText and the usage of milestone?.url
and milestone.title) to conditionally render: if milestone?.url is truthy render
the Link with target="_blank", otherwise render non-navigable plain text (e.g.,
a span/div with the same styling or TruncatedText alone) so clicks don’t trigger
unintended navigation.
- Around line 389-406: In CardDetailsPage, guard the avatar/link block against
missing author fields: check milestone?.author and required props (author.login
and author.avatarUrl) before rendering the Link/Image/Tooltip; if author is
missing render a safe fallback (e.g., plain text or a default avatar image URL
and no /members/ link) so you never create href={`/members/${undefined}`} or
pass undefined to Next/Image; update the block that currently uses Link, Image
and Tooltip to conditionally render based on those checks (or supply a
defaultAvatar constant) to prevent invalid hrefs and undefined src.
🧹 Nitpick comments (4)
frontend/src/components/CardDetailsPage.tsx (3)

65-72: Consider simplifying the type check logic.

Adding 'program' to showIssuesAndMilestones but then immediately excluding it at line 342 with type !== 'program' is somewhat confusing. The function now returns true for programs, but programs never use the standard issues/milestones block.

Consider either:

  1. Removing 'program' from this function since programs have their own milestone section, or
  2. Renaming/documenting to clarify the intent

This keeps the code self-documenting and avoids confusion for future maintainers.

Option 1: Remove 'program' from showIssuesAndMilestones
 const showIssuesAndMilestones = (type: CardType): boolean =>
-  ['organization', 'program', 'project', 'repository', 'user'].includes(type)
+  ['organization', 'project', 'repository', 'user'].includes(type)

Then simplify line 342:

-{showIssuesAndMilestones(type) && type !== 'program' && (
+{showIssuesAndMilestones(type) && (

373-376: Key fallback may cause collisions.

Using milestone.url || milestone.title as the key could produce duplicates if multiple milestones share the same title and lack URLs. This would cause React reconciliation issues.

Consider incorporating the index for uniqueness:

♻️ Suggested fix
-                  <div
-                    key={milestone.url || milestone.title}
+                  <div
+                    key={milestone.url || `${milestone.title}-${index}`}

432-441: Minor: Inconsistent icon sizing.

FaFolderOpen uses h-5 w-4 while other icons in the same row use h-4 w-4. This creates a subtle visual inconsistency.

✨ Suggested fix for consistency
-                            <FaFolderOpen className="mr-2 h-5 w-4 shrink-0" />
+                            <FaFolderOpen className="mr-2 h-4 w-4 shrink-0" />
frontend/__tests__/unit/components/CardDetailsPage.test.tsx (1)

1711-1787: Good test coverage for the milestone toggle feature.

The tests effectively cover:

  • Initial rendering with milestone limit
  • Expansion via "Show more"
  • Collapse via "Show less"
  • No toggle button at exactly the limit

Consider adding a test for the edge case when recentMilestones is empty or undefined for program type to ensure the section doesn't render:

📋 Optional: Add edge case test
it('does not render milestones section when recentMilestones is empty for program type', () => {
  const programProps: DetailsCardProps = {
    ...defaultProps,
    type: 'program' as const,
    recentMilestones: [],
    modules: [],
  }

  render(<CardDetailsPage {...programProps} />)

  expect(screen.queryByText('Recent Milestones')).not.toBeInTheDocument()
})

Copy link
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

🤖 Fix all issues with AI agents
In `@frontend/src/components/CardDetailsPage.tsx`:
- Around line 436-446: The Link href can produce "/organizations/undefined/..."
because milestone.organizationName is used without a guard even when
milestone.repositoryName exists; update the conditional rendering to require
both milestone.repositoryName and milestone.organizationName (or fallback to a
safe value) before rendering the Link, or build the href using a safe accessor
(e.g., only render Link when milestone.organizationName is truthy) so the Link
component and its TruncatedText remain inside a block that guarantees a valid
organizationName.
🧹 Nitpick comments (2)
frontend/src/components/CardDetailsPage.tsx (2)

65-66: Simplify the type guard logic.

Adding 'program' to showIssuesAndMilestones but then excluding it with type !== 'program' on line 342 is contradictory. If programs use a custom milestones section, don't include 'program' here.

♻️ Suggested fix
 const showIssuesAndMilestones = (type: CardType): boolean =>
-  ['organization', 'program', 'project', 'repository', 'user'].includes(type)
+  ['organization', 'project', 'repository', 'user'].includes(type)

Then remove the redundant guard on line 342:

-        {showIssuesAndMilestones(type) && type !== 'program' && (
+        {showIssuesAndMilestones(type) && (

374-376: Use a more unique key to avoid potential duplicates.

If two milestones share the same title and both lack URLs, the key will collide. Consider using the index as a fallback or a composite key.

♻️ Suggested fix
                   <div
-                    key={milestone.url || milestone.title}
+                    key={milestone.url || `${milestone.title}-${index}`}
                     className="mb-4 w-full rounded-lg bg-gray-200 p-4 dark:bg-gray-700"
                   >

Copy link
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

🤖 Fix all issues with AI agents
In `@frontend/src/components/CardDetailsPage.tsx`:
- Around line 425-432: The Link rendering for milestone URLs (the JSX block that
checks milestone?.url and renders <Link ... href={milestone?.url}
target="_blank"> with <TruncatedText /> inside) should include rel="noopener
noreferrer" to prevent reverse-tabnabbing; update the Link element that opens
external URLs in a new tab to add rel="noopener noreferrer" whenever
target="_blank" is used and ensure the href uses milestone?.url as before.

@sonarqubecloud
Copy link

@HarshitVerma109 HarshitVerma109 requested a review from kasya January 23, 2026 20:29
Copy link
Collaborator

@kasya kasya left a comment

Choose a reason for hiding this comment

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

@HarshitVerma109 Looks good 👍🏼 Thanks!

@codecov
Copy link

codecov bot commented Jan 24, 2026

Codecov Report

❌ Patch coverage is 77.27273% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.56%. Comparing base (c716b07) to head (1f1e95f).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
frontend/src/components/CardDetailsPage.tsx 77.27% 0 Missing and 5 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3441      +/-   ##
==========================================
- Coverage   85.56%   85.56%   -0.01%     
==========================================
  Files         461      461              
  Lines       14227    14249      +22     
  Branches     1895     1905      +10     
==========================================
+ Hits        12174    12192      +18     
  Misses       1680     1680              
- Partials      373      377       +4     
Flag Coverage Δ
backend 84.47% <ø> (ø)
frontend 88.58% <77.27%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
.../src/app/mentorship/programs/[programKey]/page.tsx 86.95% <ø> (ø)
frontend/src/components/CardDetailsPage.tsx 86.88% <77.27%> (-1.12%) ⬇️

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c716b07...1f1e95f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kasya kasya added this pull request to the merge queue Jan 24, 2026
Merged via the queue into OWASP:main with commit bffb260 Jan 24, 2026
56 of 61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Display Related Milestones on Public Program View

2 participants