Skip to content
Merged
8 changes: 7 additions & 1 deletion backend/apps/github/api/internal/nodes/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ class IssueNode(strawberry.relay.Node):

assignees: list[UserNode] = strawberry_django.field()
author: UserNode | None = strawberry_django.field()
pull_requests: list[PullRequestNode] = strawberry_django.field()

@strawberry.field
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
def pull_requests(self, limit: int = 4, offset: int = 0) -> list[PullRequestNode]:
"""Return pull requests linked to this issue."""
limit = max(0, limit)
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
offset = max(0, offset)
return list(self.pull_requests.all().order_by("-created_at")[offset : offset + limit])

@strawberry_django.field(select_related=["repository__organization", "repository"])
def organization_name(self, root: Issue) -> str | None:
Expand Down
6 changes: 4 additions & 2 deletions backend/apps/mentorship/api/internal/nodes/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,16 @@ def task_assigned_at(self, issue_number: int) -> datetime | None:
)

@strawberry.field
def recent_pull_requests(self, limit: int = 5) -> list[PullRequestNode]:
def recent_pull_requests(self, limit: int = 4, offset: int = 0) -> list[PullRequestNode]:
"""Return recent pull requests linked to issues in this module."""
limit = max(0, limit)
offset = max(0, offset)
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]
.order_by("-created_at")[offset : offset + limit]
)


Expand Down
14 changes: 14 additions & 0 deletions backend/tests/apps/github/api/internal/nodes/issue_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,17 @@ def test_repository_name_without_repository(self):
field = self._get_field_by_name("repository_name", IssueNode)
result = field.base_resolver.wrapped_func(None, mock_issue)
assert result is None

def test_pull_requests_resolver(self):
"""Test pull_requests field resolver with pagination."""
mock_issue = Mock()
mock_qs = Mock()

mock_issue.pull_requests.all.return_value = mock_qs
mock_qs.order_by.return_value = ["pr1", "pr2", "pr3", "pr4", "pr5"]

field = self._get_field_by_name("pull_requests", IssueNode)
result = field.base_resolver.wrapped_func(mock_issue)
assert result == ["pr1", "pr2", "pr3", "pr4"]
result = field.base_resolver.wrapped_func(mock_issue, limit=2, offset=2)
assert result == ["pr3", "pr4"]
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@
import { useQuery } from '@apollo/client/react'
import capitalize from 'lodash/capitalize'
import { useParams } from 'next/navigation'
import { useEffect } from 'react'
import { useState, useEffect } from 'react'
import { ErrorDisplay, handleAppError } from 'app/global-error'
import { GetProgramAdminsAndModulesDocument } from 'types/__generated__/moduleQueries.generated'
import { Module } from 'types/mentorship'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
import { getSimpleDuration } from 'components/ModuleCard'

const ModuleDetailsPage = () => {
const { programKey, moduleKey } = useParams<{ programKey: string; moduleKey: string }>()
const [hasMorePRs, setHasMorePRs] = useState(true)
const limit = 4

const {
data,
error,
loading: isLoading,
fetchMore,
} = useQuery(GetProgramAdminsAndModulesDocument, {
fetchPolicy: 'cache-and-network',
variables: {
programKey,
moduleKey,
limit,
offset: 0,
},
})

const programModule = data?.getModule
useEffect(() => {
const prCount = data?.getModule?.recentPullRequests?.length
if (prCount == null) return
if (prCount <= limit) {
setHasMorePRs(prCount >= limit)
}
}, [data, limit])

const programModule = data?.getModule as Module
const admins = data?.getProgram?.admins

useEffect(() => {
Expand Down Expand Up @@ -78,6 +92,62 @@ const ModuleDetailsPage = () => {
tags={programModule.tags}
title={programModule.name}
type="module"
onLoadMorePullRequests={
hasMorePRs
? () => {
const currentLength = programModule.recentPullRequests?.length || 0
fetchMore({
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
variables: {
programKey,
moduleKey,
offset: currentLength,
limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
const newPRs = fetchMoreResult.getModule?.recentPullRequests || []
if (newPRs.length < limit) setHasMorePRs(false)
if (newPRs.length === 0) return prevResult
return {
...prevResult,
getModule: {
...prevResult.getModule,
recentPullRequests: [
...(prevResult.getModule?.recentPullRequests || []),
...newPRs,
],
},
}
},
})
}
: undefined
}
onResetPullRequests={
hasMorePRs
? undefined
: () => {
setHasMorePRs(true)
fetchMore({
variables: {
programKey,
moduleKey,
offset: 0,
limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
return {
...prevResult,
getModule: {
...prevResult.getModule,
recentPullRequests: fetchMoreResult.getModule?.recentPullRequests || [],
},
}
},
})
}
}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ 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 { useState, useEffect } from 'react'
import {
FaCodeBranch,
FaLink,
FaPlus,
FaTags,
FaXmark,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import { ErrorDisplay } from 'app/global-error'
import { GetModuleIssueViewDocument } from 'types/__generated__/issueQueries.generated'
Expand All @@ -17,11 +25,11 @@ import LoadingSpinner from 'components/LoadingSpinner'
import Markdown from 'components/MarkdownWrapper'
import MentorshipPullRequest from 'components/MentorshipPullRequest'
import SecondaryCard from 'components/SecondaryCard'
import ShowMoreButton from 'components/ShowMoreButton'

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

const formatDeadline = (deadline: string | null) => {
Expand Down Expand Up @@ -65,13 +73,26 @@ const ModuleIssueDetailsPage = () => {
color,
}
}
const { data, loading, error } = useQuery(GetModuleIssueViewDocument, {
variables: { programKey, moduleKey, number: Number(issueId) },
const { data, loading, error, fetchMore } = useQuery(GetModuleIssueViewDocument, {
variables: {
programKey,
moduleKey,
number: Number(issueId),
limit,
offset: 0,
},
skip: !issueId,
fetchPolicy: 'cache-first',
nextFetchPolicy: 'cache-and-network',
})

useEffect(() => {
const prCount = data?.getModule?.issueByNumber?.pullRequests?.length
if (prCount == null) return
if (prCount <= limit) {
setHasMorePRs(prCount >= limit)
}
}, [data, limit])

const {
assignIssue,
unassignIssue,
Expand Down Expand Up @@ -100,7 +121,7 @@ const ModuleIssueDetailsPage = () => {
if (error) {
return <ErrorDisplay statusCode={500} title="Error Loading Issue" message={error.message} />
}
if (loading) return <LoadingSpinner />
if (loading && !data) return <LoadingSpinner />
if (!issue)
return <ErrorDisplay statusCode={404} title="Issue Not Found" message="Issue not found" />

Expand Down Expand Up @@ -320,15 +341,90 @@ const ModuleIssueDetailsPage = () => {

<SecondaryCard icon={FaCodeBranch} title="Pull Requests">
<div className="grid grid-cols-1 gap-3">
{(issue.pullRequests || []).slice(0, showAllPRs ? undefined : 4).map((pr) => (
{(issue.pullRequests || []).map((pr) => (
<MentorshipPullRequest key={pr.id} pr={pr} />
))}

{hasMorePRs && (
<div className="mt-4 flex justify-start gap-4">
<button
onClick={() => {
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
const currentLength = issue.pullRequests?.length || 0
fetchMore({
variables: {
programKey,
moduleKey,
number: Number(issueId),
offset: currentLength,
limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
const newPRs = fetchMoreResult.getModule?.issueByNumber?.pullRequests || []
if (newPRs.length < limit) setHasMorePRs(false)
if (newPRs.length === 0) return prevResult
return {
...prevResult,
getModule: {
...prevResult.getModule,
issueByNumber: {
...prevResult.getModule?.issueByNumber,
pullRequests: [
...(prevResult.getModule?.issueByNumber?.pullRequests || []),
...newPRs,
],
},
},
}
},
})
}}
className="flex items-center bg-transparent px-2 py-1 text-blue-400 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Show more <FaChevronDown aria-hidden="true" className="ml-2 text-sm" />
</button>
</div>
)}

{!hasMorePRs && (issue.pullRequests || []).length > 4 && (
<div className="mt-4 flex justify-start gap-4">
<button
onClick={() => {
setHasMorePRs(true)
fetchMore({
variables: {
programKey,
moduleKey,
number: Number(issueId),
offset: 0,
limit,
},
updateQuery: (prevResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return prevResult
return {
...prevResult,
getModule: {
...prevResult.getModule,
issueByNumber: {
...prevResult.getModule?.issueByNumber,
pullRequests:
fetchMoreResult.getModule?.issueByNumber?.pullRequests || [],
},
},
}
},
})
}}
className="flex items-center bg-transparent px-2 py-1 text-blue-400 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Show less <FaChevronUp aria-hidden="true" className="ml-2 text-sm" />
</button>
</div>
)}

{(!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
34 changes: 32 additions & 2 deletions frontend/src/components/CardDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
FaCircleExclamation,
FaSignsPost,
FaCodeBranch,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa6'
import { HiUserGroup } from 'react-icons/hi'
import type { ExtendedSession } from 'types/auth'
Expand Down Expand Up @@ -98,6 +100,8 @@ const DetailsCard = ({
isActive = true,
isArchived = false,
languages,
onLoadMorePullRequests,
onResetPullRequests,
programKey,
projectName,
pullRequests,
Expand Down Expand Up @@ -132,6 +136,8 @@ const DetailsCard = ({

const secondaryCardStyles = typeStylesMap[type] ?? 'gap-2 md:col-span-5'

const prDisplayLimit = onLoadMorePullRequests || onResetPullRequests || showAllPRs ? undefined : 4

return (
<div className="min-h-screen bg-white p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
Expand Down Expand Up @@ -370,13 +376,37 @@ 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) => (
{pullRequests.slice(0, prDisplayLimit).map((pr) => (
<MentorshipPullRequest key={pr.id} pr={pr} />
))}
{pullRequests.length > 4 && (

{onLoadMorePullRequests && (
<div className="mt-4 flex justify-start gap-4">
<button
Comment thread
HarshitVerma109 marked this conversation as resolved.
Outdated
onClick={onLoadMorePullRequests}
className="flex items-center bg-transparent px-2 py-1 text-blue-400 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Show more <FaChevronDown aria-hidden="true" className="ml-2 text-sm" />
</button>
</div>
)}

{onResetPullRequests && !onLoadMorePullRequests && (
<div className="mt-4 flex justify-start gap-4">
<button
onClick={onResetPullRequests}
className="flex items-center bg-transparent px-2 py-1 text-blue-400 hover:underline focus-visible:rounded focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
>
Show less <FaChevronUp aria-hidden="true" className="ml-2 text-sm" />
</button>
</div>
)}

{!onLoadMorePullRequests && !onResetPullRequests && pullRequests.length > 4 && (
<ShowMoreButton onToggle={() => setShowAllPRs(!showAllPRs)} />
)}
</div>
Expand Down
Loading