Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import strawberry

from apps.github.api.internal.nodes.issue import IssueNode
from apps.github.api.internal.nodes.pull_request import PullRequestNode
from apps.github.api.internal.nodes.user import UserNode
from apps.github.models import Label
from apps.github.models.pull_request import PullRequest
from apps.github.models.user import User
from apps.mentorship.api.internal.nodes.enum import ExperienceLevelEnum
from apps.mentorship.api.internal.nodes.mentor import MentorNode
Expand Down Expand Up @@ -158,6 +160,17 @@ def task_assigned_at(self, issue_number: int) -> datetime | None:
.first()
)

@strawberry.field
def recent_pull_requests(self, limit: int = 5) -> list[PullRequestNode]:
"""Return recent pull requests linked to issues in this module."""
issue_ids = self.issues.values_list("id", flat=True)
return list(
PullRequest.objects.filter(related_issues__id__in=issue_ids)
.select_related("author")
.distinct()
.order_by("-created_at")[:limit]
)

Comment on lines +164 to +173
Copy link
Collaborator

Choose a reason for hiding this comment

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

@HarshitVerma109 What I meant by Show more button is more like a pagination. Right now the limit is still 5, so at most you'll go from 4 to 5 items when clicking Show more.

The idea is to allow loading more PRs on request from user. This might be a bit more tricky and as I said before we can (and probably should) work on that in a separate task.


@strawberry.input
class CreateModuleInput:
Expand Down
118 changes: 118 additions & 0 deletions frontend/__tests__/unit/components/CardDetailsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react'
import '@testing-library/jest-dom'
import { FaCode, FaTags } from 'react-icons/fa6'
import type { DetailsCardProps } from 'types/card'
import type { PullRequest } from 'types/pullRequest'
import CardDetailsPage, { type CardType } from 'components/CardDetailsPage'

jest.mock('next/navigation', () => ({
Expand Down Expand Up @@ -259,6 +260,15 @@ jest.mock('components/RecentPullRequests', () => ({
),
}))

jest.mock('components/MentorshipPullRequest', () => ({
__esModule: true,
default: ({ pr, ...props }: { pr: PullRequest; [key: string]: unknown }) => (
<div data-testid="pull-request-item" {...props}>
MentorshipPullRequest: {pr.title}
</div>
),
}))

jest.mock('components/RecentReleases', () => ({
__esModule: true,
default: ({
Expand Down Expand Up @@ -769,6 +779,19 @@ describe('CardDetailsPage', () => {
expect(screen.getByText('Repositories')).toBeInTheDocument()
expect(screen.getByTestId('repositories-card')).toBeInTheDocument()
})

it('renders MentorshipPullRequest when type is module and PRs are provided', () => {
render(
<CardDetailsPage
{...defaultProps}
type="module"
pullRequests={mockPullRequests as unknown as PullRequest[]}
/>
)

expect(screen.getByText('Recent Pull Requests')).toBeInTheDocument()
expect(screen.getAllByTestId('pull-request-item').length).toBeGreaterThan(0)
})
})

describe('Event Handling', () => {
Expand Down Expand Up @@ -1800,4 +1823,99 @@ describe('CardDetailsPage', () => {
expect(screen.queryByText(/Show more/i)).not.toBeInTheDocument()
})
})

describe('Module Pull Requests Display', () => {
const createPullRequests = (count: number) => {
const pullRequests = []
for (let i = 0; i < count; i++) {
pullRequests.push({
id: `pr-${i}`,
author: mockUser,
createdAt: new Date().toISOString(),
organizationName: 'test-org',
title: `Pull Request ${i + 1}`,
url: `https://github.com/test/project/pull/${i + 1}`,
state: 'OPEN',
number: i + 1,
mergedAt: null,
repositoryName: 'test-repo',
})
}
return pullRequests
}

it('renders only first 4 PRs initially for module type', () => {
const manyPRs = createPullRequests(6)
const moduleProps: DetailsCardProps = {
...defaultProps,
type: 'module' as const,
pullRequests: manyPRs as unknown as PullRequest[],
}

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

expect(screen.getByText('Recent Pull Requests')).toBeInTheDocument()

expect(screen.getByText(/Pull Request 1/)).toBeInTheDocument()
expect(screen.getByText(/Pull Request 4/)).toBeInTheDocument()

expect(screen.queryByText(/Pull Request 5/)).not.toBeInTheDocument()
expect(screen.queryByText(/Pull Request 6/)).not.toBeInTheDocument()

expect(screen.getByText(/Show more/i)).toBeInTheDocument()
})

it('expands to show all PRs when "Show more" is clicked', () => {
const manyPRs = createPullRequests(6)
const moduleProps: DetailsCardProps = {
...defaultProps,
type: 'module' as const,
pullRequests: manyPRs as unknown as PullRequest[],
}

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

const showMoreBtn = screen.getByText(/Show more/i)
fireEvent.click(showMoreBtn)

expect(screen.getByText(/Pull Request 5/)).toBeInTheDocument()
expect(screen.getByText(/Pull Request 6/)).toBeInTheDocument()

expect(screen.getByText(/Show less/i)).toBeInTheDocument()
})

it('collapses back to 4 PRs when "Show less" is clicked', () => {
const manyPRs = createPullRequests(6)
const moduleProps: DetailsCardProps = {
...defaultProps,
type: 'module' as const,
pullRequests: manyPRs as unknown as PullRequest[],
}

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

fireEvent.click(screen.getByText(/Show more/i))
expect(screen.getByText(/Pull Request 5/)).toBeInTheDocument()

fireEvent.click(screen.getByText(/Show less/i))

expect(screen.queryByText(/Pull Request 5/)).not.toBeInTheDocument()
expect(screen.getByText(/Show more/i)).toBeInTheDocument()
})

it('does not show toggle button if PRs <= 4', () => {
const fewPRs = createPullRequests(4)
const moduleProps: DetailsCardProps = {
...defaultProps,
type: 'module' as const,
pullRequests: fewPRs as unknown as PullRequest[],
}

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

expect(screen.getByText(/Pull Request 1/)).toBeInTheDocument()
expect(screen.getByText(/Pull Request 4/)).toBeInTheDocument()
expect(screen.queryByText(/Show more/i)).not.toBeInTheDocument()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const ModuleDetailsPage = () => {
details={moduleDetails}
domains={programModule.domains}
mentors={programModule.mentors}
pullRequests={programModule.recentPullRequests || []}
summary={programModule.description}
tags={programModule.tags}
title={programModule.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useIssueMutations } from 'hooks/useIssueMutations'
import Image from 'next/image'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useState } from 'react'
import { FaCodeBranch, FaLink, FaPlus, FaTags, FaXmark } from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import { ErrorDisplay } from 'app/global-error'
Expand All @@ -14,11 +15,13 @@ import AnchorTitle from 'components/AnchorTitle'
import { LabelList } from 'components/LabelList'
import LoadingSpinner from 'components/LoadingSpinner'
import Markdown from 'components/MarkdownWrapper'
import MentorshipPullRequest from 'components/MentorshipPullRequest'
import SecondaryCard from 'components/SecondaryCard'
import { TruncatedText } from 'components/TruncatedText'
import ShowMoreButton from 'components/ShowMoreButton'

const ModuleIssueDetailsPage = () => {
const params = useParams<{ programKey: string; moduleKey: string; issueId: string }>()
const [showAllPRs, setShowAllPRs] = useState(false)
const { programKey, moduleKey, issueId } = params

const formatDeadline = (deadline: string | null) => {
Expand Down Expand Up @@ -118,22 +121,6 @@ const ModuleIssueDetailsPage = () => {
issueStatusLabel = 'Closed'
}

const getPRStatus = (pr: Exclude<typeof issue.pullRequests, undefined>[0]) => {
let backgroundColor: string
let label: string
if (pr.state === 'closed' && pr.mergedAt) {
backgroundColor = '#8657E5'
label = 'Merged'
} else if (pr.state === 'closed') {
backgroundColor = '#DA3633'
label = 'Closed'
} else {
backgroundColor = '#238636'
label = 'Open'
}
return { backgroundColor, label }
}

const getAssignButtonTitle = (assigning: boolean) => {
let title: string
if (!issueId) {
Expand Down Expand Up @@ -333,60 +320,15 @@ const ModuleIssueDetailsPage = () => {

<SecondaryCard icon={FaCodeBranch} title="Pull Requests">
<div className="grid grid-cols-1 gap-3">
{issue.pullRequests?.length ? (
issue.pullRequests.map((pr) => (
<div
key={pr.id}
className="flex items-center justify-between gap-3 rounded-lg bg-gray-200 p-4 dark:bg-gray-700"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
{pr.author?.avatarUrl ? (
<Image
src={pr.author.avatarUrl}
alt={pr.author?.login || 'Unknown'}
width={32}
height={32}
className="flex-shrink-0 rounded-full"
/>
) : (
<div
className="h-8 w-8 flex-shrink-0 rounded-full bg-gray-400"
aria-hidden="true"
/>
)}
<div className="min-w-0 flex-1">
<Link
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<TruncatedText text={pr.title} />
</Link>
<div className="text-sm text-gray-500 dark:text-gray-400">
by {pr.author?.login || 'Unknown'} •{' '}
{new Date(pr.createdAt).toLocaleDateString()}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{(() => {
const { backgroundColor, label } = getPRStatus(pr)
return (
<span
className="inline-flex items-center rounded-lg px-2 py-0.5 text-xs font-medium text-white"
style={{ backgroundColor }}
>
{label}
</span>
)
})()}
</div>
</div>
))
) : (
{(issue.pullRequests || []).slice(0, showAllPRs ? undefined : 4).map((pr) => (
<MentorshipPullRequest key={pr.id} pr={pr} />
))}
{(!issue.pullRequests || issue.pullRequests.length === 0) && (
<span className="text-sm text-gray-400">No linked pull requests.</span>
)}
{issue.pullRequests && issue.pullRequests.length > 4 && (
<ShowMoreButton onToggle={() => setShowAllPRs(!showAllPRs)} />
)}
</div>
</SecondaryCard>

Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
FaCircleCheck,
FaCircleExclamation,
FaSignsPost,
FaCodeBranch,
} from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import type { ExtendedSession } from 'types/auth'
Expand All @@ -35,6 +36,7 @@ import InfoBlock from 'components/InfoBlock'
import Leaders from 'components/Leaders'
import LeadersList from 'components/LeadersList'
import Markdown from 'components/MarkdownWrapper'
import MentorshipPullRequest from 'components/MentorshipPullRequest'
import MetricsScoreCircle from 'components/MetricsScoreCircle'
import Milestones from 'components/Milestones'
import ModuleCard from 'components/ModuleCard'
Expand Down Expand Up @@ -114,6 +116,7 @@ const DetailsCard = ({
userSummary,
}: DetailsCardProps) => {
const { data: session } = useSession() as { data: ExtendedSession | null }
const [showAllPRs, setShowAllPRs] = useState(false)
const [showAllMilestones, setShowAllMilestones] = useState(false)

// compute styles based on type prop
Expand Down Expand Up @@ -367,6 +370,18 @@ const DetailsCard = ({
<RecentReleases data={recentReleases} showAvatar={showAvatar} showSingleColumn={true} />
</div>
)}
{type === 'module' && pullRequests && pullRequests.length > 0 && (
<SecondaryCard icon={FaCodeBranch} title={<AnchorTitle title="Recent Pull Requests" />}>
<div className="grid grid-cols-1 gap-3">
{pullRequests.slice(0, showAllPRs ? undefined : 4).map((pr) => (
<MentorshipPullRequest key={pr.id} pr={pr} />
))}
{pullRequests.length > 4 && (
<ShowMoreButton onToggle={() => setShowAllPRs(!showAllPRs)} />
)}
</div>
</SecondaryCard>
)}
{(type === 'project' || type === 'user' || type === 'organization') &&
repositories.length > 0 && (
<SecondaryCard icon={FaFolderOpen} title={<AnchorTitle title="Repositories" />}>
Expand All @@ -376,7 +391,9 @@ const DetailsCard = ({
{type === 'program' && modules.length > 0 && (
<>
{modules.length === 1 ? (
<ModuleCard modules={modules} accessLevel={accessLevel} admins={admins} />
<div className="mb-8">
<ModuleCard modules={modules} accessLevel={accessLevel} admins={admins} />
</div>
) : (
<SecondaryCard icon={FaFolderOpen} title={<AnchorTitle title="Modules" />}>
<ModuleCard modules={modules} accessLevel={accessLevel} admins={admins} />
Expand Down
Loading