Skip to content

Commit de8ca83

Browse files
committed
Merge branch 'development' into track-vendor-approvals-across-versions
2 parents 6ef4572 + 18fd21c commit de8ca83

File tree

9 files changed

+396
-81
lines changed

9 files changed

+396
-81
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import React, { useMemo, useState } from 'react';
2+
import { ThemeTable, ThemeTableUnavailable } from '../common/ThemeTable';
3+
import { dates } from 'shared';
4+
import { NoneText } from '../TestPlanVersionsPage';
5+
import SortableTableHeader, {
6+
TABLE_SORT_ORDERS
7+
} from '../common/SortableTableHeader';
8+
import FilterButtons from '../common/FilterButtons';
9+
import { IssuePropType } from '../common/proptypes';
10+
import PropTypes from 'prop-types';
11+
12+
const FILTER_OPTIONS = {
13+
OPEN: 'Open',
14+
CLOSED: 'Closed',
15+
ALL: 'All'
16+
};
17+
18+
const SORT_FIELDS = {
19+
AUTHOR: 'author',
20+
TITLE: 'title',
21+
STATUS: 'status',
22+
AT: 'at',
23+
CREATED_AT: 'createdAt',
24+
CLOSED_AT: 'closedAt'
25+
};
26+
27+
const SortableIssuesTable = ({ issues }) => {
28+
const [activeSort, setActiveSort] = useState(SORT_FIELDS.STATUS);
29+
const [sortOrder, setSortOrder] = useState(TABLE_SORT_ORDERS.ASC);
30+
const [activeFilter, setActiveFilter] = useState('OPEN');
31+
32+
const issueStats = useMemo(() => {
33+
const openIssues = issues.filter(issue => issue.isOpen).length;
34+
const closedIssues = issues.length - openIssues;
35+
return { openIssues, closedIssues };
36+
}, [issues]);
37+
38+
// Helper function to get sortable value from issue
39+
const getSortableValue = (issue, sortField) => {
40+
switch (sortField) {
41+
case SORT_FIELDS.AUTHOR:
42+
return issue.author;
43+
case SORT_FIELDS.TITLE:
44+
return issue.title;
45+
case SORT_FIELDS.AT:
46+
return issue.at?.name ?? '';
47+
case SORT_FIELDS.CREATED_AT:
48+
return new Date(issue.createdAt);
49+
case SORT_FIELDS.CLOSED_AT:
50+
return issue.closedAt ? new Date(issue.closedAt) : new Date(0);
51+
default:
52+
return '';
53+
}
54+
};
55+
56+
const compareByStatus = (a, b) => {
57+
if (a.isOpen !== b.isOpen) {
58+
if (sortOrder === TABLE_SORT_ORDERS.ASC) {
59+
return a.isOpen ? -1 : 1; // Open first for ascending
60+
}
61+
return a.isOpen ? 1 : -1; // Closed first for descending
62+
}
63+
// If status is the same, sort by date created (newest first)
64+
return new Date(b.createdAt) - new Date(a.createdAt);
65+
};
66+
67+
const compareValues = (aValue, bValue) => {
68+
return sortOrder === TABLE_SORT_ORDERS.ASC
69+
? aValue < bValue
70+
? -1
71+
: 1
72+
: aValue > bValue
73+
? -1
74+
: 1;
75+
};
76+
77+
const sortedAndFilteredIssues = useMemo(() => {
78+
// Filter issues
79+
const filtered =
80+
activeFilter === 'ALL'
81+
? issues
82+
: issues.filter(issue => issue.isOpen === (activeFilter === 'OPEN'));
83+
84+
// Sort issues
85+
return filtered.sort((a, b) => {
86+
// Special handling for status sorting
87+
if (activeSort === SORT_FIELDS.STATUS) {
88+
return compareByStatus(a, b);
89+
}
90+
91+
// Normal sorting for other fields
92+
const aValue = getSortableValue(a, activeSort);
93+
const bValue = getSortableValue(b, activeSort);
94+
return compareValues(aValue, bValue);
95+
});
96+
}, [issues, activeSort, sortOrder, activeFilter]);
97+
98+
const handleSort = column => newSortOrder => {
99+
setActiveSort(column);
100+
setSortOrder(newSortOrder);
101+
};
102+
103+
const renderTableHeader = () => (
104+
<thead>
105+
<tr>
106+
{[
107+
{ field: SORT_FIELDS.AUTHOR, title: 'Author' },
108+
{ field: SORT_FIELDS.TITLE, title: 'Issue' },
109+
{ field: SORT_FIELDS.STATUS, title: 'Status' },
110+
{ field: SORT_FIELDS.AT, title: 'Assistive Technology' },
111+
{ field: SORT_FIELDS.CREATED_AT, title: 'Created On' },
112+
{ field: SORT_FIELDS.CLOSED_AT, title: 'Closed On' }
113+
].map(({ field, title }) => (
114+
<SortableTableHeader
115+
key={field}
116+
title={title}
117+
active={activeSort === field}
118+
onSort={handleSort(field)}
119+
data-test={`sort-${field.toLowerCase()}`}
120+
/>
121+
))}
122+
</tr>
123+
</thead>
124+
);
125+
126+
const renderTableBody = () => (
127+
<tbody>
128+
{sortedAndFilteredIssues.map(issue => (
129+
<tr
130+
key={issue.link}
131+
data-test="issue-row"
132+
data-status={issue.isOpen ? 'open' : 'closed'}
133+
>
134+
<td>
135+
<a
136+
target="_blank"
137+
rel="noreferrer"
138+
href={`https://github.com/${issue.author}`}
139+
>
140+
{issue.author}
141+
</a>
142+
</td>
143+
<td>
144+
<a target="_blank" rel="noreferrer" href={issue.link}>
145+
{issue.title}
146+
</a>
147+
</td>
148+
<td data-test="issue-status">{issue.isOpen ? 'Open' : 'Closed'}</td>
149+
<td>{issue.at?.name ?? 'AT not specified'}</td>
150+
<td>{dates.convertDateToString(issue.createdAt, 'MMM D, YYYY')}</td>
151+
<td>
152+
{!issue.closedAt ? (
153+
<NoneText>N/A</NoneText>
154+
) : (
155+
dates.convertDateToString(issue.closedAt, 'MMM D, YYYY')
156+
)}
157+
</td>
158+
</tr>
159+
))}
160+
</tbody>
161+
);
162+
163+
return (
164+
<>
165+
<h2 id="github-issues">
166+
GitHub Issues ({issueStats.openIssues} open, {issueStats.closedIssues}
167+
&nbsp;closed)
168+
</h2>
169+
<FilterButtons
170+
filterLabel="Filter"
171+
filterAriaLabel="Filter GitHub issues"
172+
filterOptions={FILTER_OPTIONS}
173+
activeFilter={activeFilter}
174+
onFilterChange={setActiveFilter}
175+
/>
176+
{!sortedAndFilteredIssues.length ? (
177+
<ThemeTableUnavailable aria-labelledby="github-issues">
178+
No GitHub Issues
179+
</ThemeTableUnavailable>
180+
) : (
181+
<ThemeTable
182+
bordered
183+
aria-labelledby="github-issues"
184+
data-test="issues-table"
185+
>
186+
{renderTableHeader()}
187+
{renderTableBody()}
188+
</ThemeTable>
189+
)}
190+
</>
191+
);
192+
};
193+
194+
SortableIssuesTable.propTypes = {
195+
issues: PropTypes.arrayOf(IssuePropType).isRequired
196+
};
197+
198+
export default SortableIssuesTable;

client/components/TestPlanVersionsPage/index.jsx

+3-55
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
77
import { Container } from 'react-bootstrap';
88
import {
99
ThemeTable,
10-
ThemeTableUnavailable,
1110
ThemeTableHeaderH3 as UnstyledThemeTableHeader
1211
} from '../common/ThemeTable';
1312
import VersionString from '../common/VersionString';
@@ -22,6 +21,7 @@ import {
2221
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2322
import DisclosureComponentUnstyled from '../common/DisclosureComponent';
2423
import useForceUpdate from '../../hooks/useForceUpdate';
24+
import SortableIssuesTable from '../SortableIssuesTable';
2525

2626
const DisclosureContainer = styled.div`
2727
.timeline-for-version-table {
@@ -40,7 +40,7 @@ const DisclosureComponent = styled(DisclosureComponentUnstyled)`
4040
}
4141
`;
4242

43-
const NoneText = styled.span`
43+
export const NoneText = styled.span`
4444
font-style: italic;
4545
color: #6a7989;
4646
`;
@@ -390,59 +390,7 @@ const TestPlanVersionsPage = () => {
390390
<PageSpacer />
391391
</>
392392
)}
393-
<ThemeTableHeader id="github-issues">GitHub Issues</ThemeTableHeader>
394-
{!issues.length ? (
395-
<ThemeTableUnavailable aria-labelledby="github-issues">
396-
No GitHub Issues
397-
</ThemeTableUnavailable>
398-
) : (
399-
<ThemeTable bordered responsive aria-labelledby="github-issues">
400-
<thead>
401-
<tr>
402-
<th>Author</th>
403-
<th>Issue</th>
404-
<th>Status</th>
405-
<th>AT</th>
406-
<th>Created On</th>
407-
<th>Closed On</th>
408-
</tr>
409-
</thead>
410-
<tbody>
411-
{issues.map(issue => {
412-
return (
413-
<tr key={issue.link}>
414-
<td>
415-
<a
416-
target="_blank"
417-
rel="noreferrer"
418-
href={`https://github.com/${issue.author}`}
419-
>
420-
{issue.author}
421-
</a>
422-
</td>
423-
<td>
424-
<a target="_blank" rel="noreferrer" href={issue.link}>
425-
{issue.title}
426-
</a>
427-
</td>
428-
<td>{issue.isOpen ? 'Open' : 'Closed'}</td>
429-
<td>{issue.at?.name ?? 'AT not specified'}</td>
430-
<td>
431-
{dates.convertDateToString(issue.createdAt, 'MMM D, YYYY')}
432-
</td>
433-
<td>
434-
{!issue.closedAt ? (
435-
<NoneText>N/A</NoneText>
436-
) : (
437-
dates.convertDateToString(issue.closedAt, 'MMM D, YYYY')
438-
)}
439-
</td>
440-
</tr>
441-
);
442-
})}
443-
</tbody>
444-
</ThemeTable>
445-
)}
393+
<SortableIssuesTable issues={issues} />
446394
<PageSpacer />
447395
<ThemeTableHeader id="timeline-for-all-versions">
448396
Timeline for All Versions

client/components/common/FilterButtons/index.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const FilterButtons = ({
4242
return (
4343
<li key={value}>
4444
<StyledFilterButton
45+
data-test={`filter-${value.toLowerCase()}`}
4546
variant="secondary"
4647
aria-pressed={isActive}
4748
active={isActive}

client/components/common/SortableTableHeader/index.js

+16-18
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,38 @@ import {
1010
import { useAriaLiveRegion } from '../../providers/AriaLiveRegionProvider';
1111

1212
const SortableTableHeaderWrapper = styled.th`
13-
position: relative;
14-
padding: 0;
13+
background: #e9ebee;
14+
padding: 0 !important;
15+
height: 100%;
1516
`;
1617

1718
const SortableTableHeaderButton = styled(Button)`
18-
background: #e9ebee;
19+
background: transparent;
1920
border: none;
2021
color: black;
2122
font-size: 1rem;
22-
padding: 0;
2323
font-weight: 700;
2424
text-align: left;
25-
position: absolute;
26-
top: 0;
27-
bottom: 0;
28-
left: 0;
29-
right: 0;
25+
width: 100%;
26+
min-height: 100%;
27+
display: flex;
3028
justify-content: space-between;
3129
align-items: flex-end;
32-
padding: 8px 12px;
30+
padding: 0.5rem;
3331
border-radius: 0;
34-
z-index: 0;
35-
display: flex;
32+
margin: 0;
33+
34+
// Force the button to stretch
35+
position: relative;
36+
height: stretch;
37+
height: -webkit-fill-available;
38+
height: -moz-available;
39+
3640
&:hover,
3741
&:focus {
38-
background: #e9ebee;
39-
z-index: 1;
4042
color: #0b60ab;
4143
background-color: var(--bs-table-hover-bg);
4244
}
43-
44-
&:hover {
45-
border: none;
46-
}
4745
`;
4846

4947
const InactiveIcon = styled(FontAwesomeIcon)`

0 commit comments

Comments
 (0)