Skip to content

Commit 1139fb8

Browse files
authored
Merge branch 'main' into copilot/improve-carousel-tile-resizing
2 parents 9034a08 + e714e79 commit 1139fb8

12 files changed

Lines changed: 399 additions & 226 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ src
107107
│   │   │   └── SortingOrder.java
108108
│   │   ├── exception
109109
│   │   │   └── IronocJsonException.java
110+
│   │   ├── filter
111+
│   │   │   └── RequestRateLimitingInterceptor.java
110112
│   │   ├── graph
111113
│   │   │   ├── BrewsResolver.java
112114
│   │   │   └── DonateItemsResolver.java

frontend/package-lock.json

Lines changed: 281 additions & 211 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@conorheffron/ironoc-frontend",
3-
"version": "9.1.6",
3+
"version": "9.1.7",
44
"private": false,
55
"license": "GPL-3.0-or-later",
66
"dependencies": {

frontend/src/components/RepoIssues.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const RepoIssues = () => {
127127
enableStickyHeader: true,
128128
initialState: {
129129
showColumnFilters: true,
130+
columnFilters: [{ id: 'state', value: ['open'] }],
130131
columnVisibility: {
131132
state: false,
132133
body: false,

frontend/src/components/__tests__/RepoIssues.test.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React from 'react';
22
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
33
import RepoIssues from '../RepoIssues';
4-
import { useMaterialReactTable } from 'material-react-table';
4+
5+
// Track useMaterialReactTable call opts for assertions (must start with 'mock' for Jest hoisting)
6+
const mockUseMRTOpts = [];
57

68
// Mock react-router
79
jest.mock('react-router', () => ({
@@ -27,16 +29,31 @@ jest.mock('react-bootstrap', () => ({
2729
jest.mock('../../AppNavbar', () => () => <div data-testid="navbar">Navbar</div>);
2830
jest.mock('../../LoadingSpinner', () => () => <div data-testid="spinner">Loading...</div>);
2931

30-
// Mock MaterialReactTable and useMaterialReactTable
32+
// Mock MaterialReactTable and useMaterialReactTable.
33+
// useMaterialReactTable is a plain function (not jest.fn) so React 19 concurrent mode
34+
// commits re-renders correctly. Calls are tracked via mockUseMRTOpts for assertions.
35+
// columnFilters from initialState are applied to table.data so MaterialReactTable
36+
// only receives the filtered rows, allowing DOM-level assertions on visibility.
3137
jest.mock('material-react-table', () => ({
3238
MaterialReactTable: ({ table }) => (
3339
<div data-testid="mrt-table">
34-
{table && table.data && table.data.map((issue, idx) => (
40+
{(table?.data ?? []).map((issue, idx) => (
3541
<div key={idx} data-testid="mrt-row">{issue.title}</div>
3642
))}
3743
</div>
3844
),
39-
useMaterialReactTable: jest.fn((opts) => opts),
45+
useMaterialReactTable: (opts) => {
46+
mockUseMRTOpts.push(opts);
47+
const filters = opts?.initialState?.columnFilters ?? [];
48+
let data = opts?.data ?? [];
49+
filters.forEach((filter) => {
50+
data = data.filter((row) => {
51+
const val = row[filter.id];
52+
return Array.isArray(filter.value) ? filter.value.includes(val) : val === filter.value;
53+
});
54+
});
55+
return { ...opts, data };
56+
},
4057
}));
4158

4259
// Mock @mui/material theme functions
@@ -58,6 +75,7 @@ describe('RepoIssues', () => {
5875
const mockIssuesResponse = [];
5976

6077
beforeEach(() => {
78+
mockUseMRTOpts.length = 0;
6179
jest.clearAllMocks();
6280
useNavigate.mockReturnValue(mockNavigate);
6381
global.fetch = jest.fn(() =>
@@ -98,19 +116,21 @@ describe('RepoIssues', () => {
98116
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
99117
);
100118
expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
119+
expect(screen.getByText('Test Issue')).toBeInTheDocument();
101120
});
102121

103-
it('hides state and description columns by default', async () => {
122+
it('defaults to open issues while hiding state and description columns', async () => {
104123
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
105124
render(<RepoIssues />);
106125
await waitFor(() =>
107126
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
108127
);
109128

110-
expect(useMaterialReactTable).toHaveBeenCalledWith(
129+
expect(mockUseMRTOpts).toContainEqual(
111130
expect.objectContaining({
112131
initialState: expect.objectContaining({
113132
showColumnFilters: true,
133+
columnFilters: [{ id: 'state', value: ['open'] }],
114134
columnVisibility: {
115135
state: false,
116136
body: false,
@@ -120,6 +140,24 @@ describe('RepoIssues', () => {
120140
);
121141
});
122142

143+
it('renders open issues and excludes closed issues by default', async () => {
144+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
145+
global.fetch = jest.fn(() =>
146+
Promise.resolve({
147+
json: () => Promise.resolve([
148+
{ number: 1, state: 'open', labels: [], title: 'Open Issue', body: '' },
149+
{ number: 2, state: 'closed', labels: [], title: 'Closed Issue', body: '' },
150+
]),
151+
})
152+
);
153+
window.fetch = global.fetch;
154+
render(<RepoIssues />);
155+
await waitFor(() =>
156+
expect(screen.getByText('Open Issue')).toBeInTheDocument()
157+
);
158+
expect(screen.queryByText('Closed Issue')).not.toBeInTheDocument();
159+
});
160+
123161
it('navigates on form submit', async () => {
124162
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
125163
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>conorheffron</groupId>
77
<artifactId>ironoc</artifactId>
8-
<version>9.1.6</version>
8+
<version>9.1.7</version>
99
<packaging>war</packaging>
1010

1111
<distributionManagement>
@@ -29,8 +29,8 @@
2929
<spring.framework.version>7.0.7</spring.framework.version>
3030
<spring.webflux.version>7.0.7</spring.webflux.version>
3131
<frontend-maven-plugin.version>2.0.0</frontend-maven-plugin.version>
32-
<node.version>v24.15.0</node.version>
33-
<npm.version>11.12.1</npm.version>
32+
<node.version>v24.16.0</node.version>
33+
<npm.version>11.13.0</npm.version>
3434
<kotlin.version>1.6.0</kotlin.version>
3535
<graphql.vers>14.0.0</graphql.vers>
3636
<selenium.vers>4.43.0</selenium.vers>
@@ -190,7 +190,7 @@
190190
<dependency>
191191
<groupId>com.bucket4j</groupId>
192192
<artifactId>bucket4j_jdk17-core</artifactId>
193-
<version>8.18.0</version>
193+
<version>8.19.0</version>
194194
</dependency>
195195
<dependency>
196196
<groupId>com.graphql-java-kickstart</groupId>

src/main/java/net/ironoc/portfolio/config/IronocConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.swagger.v3.oas.models.OpenAPI;
77
import io.swagger.v3.oas.models.info.Info;
88
import io.swagger.v3.oas.models.info.License;
9+
import net.ironoc.portfolio.filter.RequestRateLimitingInterceptor;
910
import net.ironoc.portfolio.resolver.PushStateResourceResolver;
1011
import org.springframework.beans.factory.annotation.Autowired;
1112
import org.springframework.boot.info.BuildProperties;

src/main/java/net/ironoc/portfolio/controller/GitProjectsController.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class GitProjectsController extends AbstractLogger {
3030
private final GitDetailsService gitDetailsService;
3131

3232
protected static final String IRONOC_GIT_USER = "conorheffron";
33+
private static final String OPEN_ISSUE_STATE = "open";
3334

3435
// Cache for issue counts: key is "username/repo"
3536
private final ConcurrentHashMap<String, Integer> issueCountCache = new ConcurrentHashMap<>();
@@ -104,7 +105,7 @@ private ResponseEntity<List<RepositoryDetailDomain>> getReposSortedAndWithIssueC
104105
Integer cachedCount = issueCountCache.get(cacheKey);
105106
if (cachedCount == null) {
106107
List<RepositoryIssueDto> issues = gitDetailsService.getIssues(userId, domain.getName(), false);
107-
cachedCount = issues != null ? issues.size() : 0;
108+
cachedCount = countOpenIssues(issues);
108109
issueCountCache.put(cacheKey, cachedCount);
109110
}
110111
domain.setIssueCount(cachedCount);
@@ -152,6 +153,18 @@ private List<String> sanitizeValues(String... values) {
152153
return List.of(sanitizedValueUserId, sanitizedValueRepo);
153154
}
154155

156+
private int countOpenIssues(List<RepositoryIssueDto> issues) {
157+
if (issues == null) {
158+
return 0;
159+
}
160+
return (int) issues.stream()
161+
.filter(Objects::nonNull)
162+
.map(RepositoryIssueDto::getState)
163+
.filter(Objects::nonNull)
164+
.filter(OPEN_ISSUE_STATE::equalsIgnoreCase)
165+
.count();
166+
}
167+
155168
private String sanitizeValue(String value) {
156169
// trim leading and trailing whitespace
157170
String sanitizedValue = value.trim();

src/main/java/net/ironoc/portfolio/config/RequestRateLimitingInterceptor.java renamed to src/main/java/net/ironoc/portfolio/filter/RequestRateLimitingInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package net.ironoc.portfolio.config;
1+
package net.ironoc.portfolio.filter;
22

33
import module java.base;
44

src/test/java/net/ironoc/portfolio/config/IronocConfigurationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.swagger.v3.oas.models.OpenAPI;
77
import io.swagger.v3.oas.models.info.Info;
88
import io.swagger.v3.oas.models.info.License;
9+
import net.ironoc.portfolio.filter.RequestRateLimitingInterceptor;
910
import net.ironoc.portfolio.resolver.PushStateResourceResolver;
1011
import org.junit.jupiter.api.Test;
1112
import org.junit.jupiter.api.extension.ExtendWith;

0 commit comments

Comments
 (0)