Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import '@testing-library/jest-dom'
import { fireEvent, render, screen } from '@testing-library/react'

import { MAX_RELEASES_TO_SHOW } from 'utils/constants'
import { ReleasesSection } from 'components/SnapshotReleaseSection'

describe('ReleasesSection', () => {
const mockReleases = Array.from({ length: MAX_RELEASES_TO_SHOW + 1 }, (_, i) => ({
id: `release-${i}`,
name: `Release v${i}`,
publishedAt: new Date().toISOString(),
tagName: `v${i}`,
}))

it('renders only MAX_RELEASES_TO_SHOW items when showAll is false', () => {
render(<ReleasesSection releases={mockReleases} showAll={false} onToggle={jest.fn()} />)

const items = screen.getAllByText(/Release v/)
expect(items.length).toBe(MAX_RELEASES_TO_SHOW)
})

it('renders all releases when showAll is true', () => {
render(<ReleasesSection releases={mockReleases} showAll={true} onToggle={jest.fn()} />)

const items = screen.getAllByText(/Release v/)
expect(items.length).toBe(MAX_RELEASES_TO_SHOW + 1)
})

it('renders the show more button for releases > MAX_RELEASES_TO_SHOW', () => {
render(<ReleasesSection releases={mockReleases} showAll={false} onToggle={jest.fn()} />)
expect(screen.getByRole('button', { name: 'Show more' })).toBeInTheDocument()
})

it('calls onToggle when the show more button is clicked', () => {
const onToggle = jest.fn()

render(<ReleasesSection releases={mockReleases} showAll={false} onToggle={onToggle} />)

fireEvent.click(screen.getByRole('button', { name: 'Show more' }))
expect(onToggle).toHaveBeenCalledTimes(1)
})
Comment thread
kasya marked this conversation as resolved.
})
31 changes: 14 additions & 17 deletions frontend/src/app/community/snapshots/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'
import { useQuery } from '@apollo/client/react'
import { useRouter, useParams } from 'next/navigation'
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { FaCalendar, FaRightToBracket } from 'react-icons/fa6'
import { handleAppError, ErrorDisplay } from 'app/global-error'
import { GetSnapshotDetailsDocument } from 'types/__generated__/snapshotQueries.generated'
Expand All @@ -14,12 +14,14 @@ import { getFilteredIcons, handleSocialUrls } from 'utils/utility'
import Card from 'components/Card'
import ChapterMapWrapper from 'components/ChapterMapWrapper'
import LoadingSpinner from 'components/LoadingSpinner'
import Release from 'components/Release'
import { ReleasesSection } from 'components/SnapshotReleaseSection'

const SnapshotDetailsPage: React.FC = () => {
const { id: snapshotKey } = useParams<{ id: string }>()
const router = useRouter()

const [showAllReleases, setShowAllReleases] = useState(false)

const {
data,
error: graphQLRequestError,
Expand All @@ -29,6 +31,9 @@ const SnapshotDetailsPage: React.FC = () => {
})

const snapshot = data?.snapshot
useEffect(() => {
setShowAllReleases(false)
}, [snapshot])
Comment thread
devnchill marked this conversation as resolved.

useEffect(() => {
if (graphQLRequestError) {
Expand Down Expand Up @@ -183,21 +188,13 @@ const SnapshotDetailsPage: React.FC = () => {
<h2 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-200">
New Releases
</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{snapshot.newReleases.map((release, index) => {
return (
<Release
key={
release.id ||
`${release.tagName}-${release.repositoryName ?? 'unknown'}-${index}`
}
release={release as unknown as ReleaseType}
showAvatar={true}
index={index}
/>
)
})}
</div>
{
<ReleasesSection
releases={snapshot.newReleases as ReleaseType[]}
showAll={showAllReleases}
onToggle={() => setShowAllReleases((p) => !p)}
/>
}
</div>
)}
</div>
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/SnapshotReleaseSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Release as ReleaseType } from 'types/release'
import { MAX_RELEASES_TO_SHOW } from 'utils/constants'
import Release from 'components/Release'
import ShowMoreButton from 'components/ShowMoreButton'

type ReleasesSectionProps = {
releases: ReleaseType[]
showAll: boolean
onToggle: () => void
}

export const ReleasesSection = ({ releases, showAll, onToggle }: ReleasesSectionProps) => {
const visibleReleases = showAll ? releases : releases.slice(0, MAX_RELEASES_TO_SHOW)
return (
<>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{visibleReleases.map((release, index) => {
return (
<Release
key={
release.id || `${release.tagName}-${release.repositoryName ?? 'unknown'}-${index}`
}
release={release as unknown as ReleaseType}

Check warning on line 23 in frontend/src/components/SnapshotReleaseSection.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZ4P4eu0bkfPAHEeTcnY&open=AZ4P4eu0bkfPAHEeTcnY&pullRequest=4570
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
showAvatar={true}
index={index}
/>
)
})}
</div>
{releases.length > MAX_RELEASES_TO_SHOW && <ShowMoreButton onToggle={onToggle} />}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ShowMoreButton API and whether it receives expansion state.
fd -t f "ShowMoreButton.tsx" "ShowMoreButton.jsx" "ShowMoreButton/index.tsx" "ShowMoreButton/index.jsx"
rg -n -C3 "type .*ShowMoreButton|interface .*ShowMoreButton|function ShowMoreButton|const ShowMoreButton|aria-expanded|Show all|Show less|Show more|onToggle" --type ts --type tsx --type js --type jsx

# Verify call sites and passed props.
rg -n -C2 "<ShowMoreButton" --type tsx --type jsx

Repository: OWASP/Nest

Length of output: 358


🏁 Script executed:

#!/bin/bash
# Find ShowMoreButton component file
fd "ShowMoreButton" --type f

# Check the ShowMoreButton implementation
fd "ShowMoreButton" --type f -x cat -n {} \;

Repository: OWASP/Nest

Length of output: 2367


🏁 Script executed:

#!/bin/bash
# Find and examine SnapshotReleaseSection.tsx
fd "SnapshotReleaseSection" --type f -x cat -n {} \;

Repository: OWASP/Nest

Length of output: 3235


🏁 Script executed:

#!/bin/bash
# Find where ReleasesSection is used/imported
rg "ReleasesSection" --type ts --type tsx

Repository: OWASP/Nest

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Find where ReleasesSection is used
rg "ReleasesSection" -g "*.ts" -g "*.tsx" -g "*.js" -g "*.jsx"

Repository: OWASP/Nest

Length of output: 1353


🏁 Script executed:

#!/bin/bash
# Examine the page component where ReleasesSection is used
cat -n "frontend/src/app/community/snapshots/[id]/page.tsx"

Repository: OWASP/Nest

Length of output: 7942


ShowMoreButton needs to synchronize with parent showAll state and include aria-expanded attribute.

ShowMoreButton maintains internal isExpanded state that becomes out of sync when parent's showAll resets (e.g., when snapshot changes). This causes the button text to display "Show less" while only showing MAX_RELEASES_TO_SHOW items. Additionally, the button lacks an aria-expanded attribute for proper accessibility.

Consider either:

  1. Passing showAll prop to ShowMoreButton so it can reset when the parent state resets, or
  2. Using a React key on ShowMoreButton to force remount when showAll changes

Add aria-expanded={isExpanded} to the <Button> element for accessibility.

🤖 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 `@frontend/src/components/SnapshotReleaseSection.tsx` at line 30,
ShowMoreButton's internal isExpanded can get out of sync with the parent's
showAll (causing "Show less" to appear when only MAX_RELEASES_TO_SHOW items are
shown) and the button is missing an accessibility attribute; fix by wiring the
parent showAll into the component lifecycle (either pass showAll into
ShowMoreButton and use it to reset internal state inside ShowMoreButton or
render <ShowMoreButton key={showAll} .../> so it remounts when showAll changes),
ensure onToggle is still called to notify the parent, and add
aria-expanded={isExpanded} to the underlying <Button> element inside
ShowMoreButton so the expanded state is exposed to assistive tech.

</>
)
}
2 changes: 2 additions & 0 deletions frontend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export const themeToggleTooltip = {

export const desktopViewMinWidth = 768

export const MAX_RELEASES_TO_SHOW = 9

export const userAuthStatus = {
AUTHENTICATED: 'authenticated',
LOADING: 'loading',
Expand Down
Loading