From c1b329c156be799535a57c7d3a9dfaef8353488e Mon Sep 17 00:00:00 2001 From: Paul Clue <67766160+Paul-Clue@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:22:29 -0500 Subject: [PATCH] Add collapsible areas for Test Plan Versions History page (#794) * Fix bug that would allow testPlanVersion to be updated to RECOMMENDED before the associated reports were all marked as final (but the tests were 100% done in the Test Queue) * Refine raise an issue behavior (#753) * Refine raise an issue behavior * Address feedback * Fixed squished dot icon * Address last feedback * Hide closed issues on datamgmt page * Fix for test results not being automatically saved when navigating through Test Run * Filter and sort functionality for Data Management table (#750) * First pass on functioning sort buttons for DataManagePage * Functioning sort * Filter functionality for DataManagement * Refactor filter buttons on DataManagement to reduce complexity * Filter buttons as separate component * DataManagement page, break sort out into dedicated hook * DataManagement page, dedicated hook for filtering * DataManagement, add constant for test plan version phases to simplify hooks code * Add relevant dynamic aria attributes to DataManagement column sort elements * Add relevant dynamic aria attributes to DataManagement filter buttons * unit tests for SortableTableHeader * unit tests for FilterButtons * unit tests for useDataManagementTableSorting hook * Add unit tests for useDataManagementTableFiltering hook * Filter buttons only show buttons that have have label, handle testPlans with multiple versions, ... useDataManagementTableFiltering don't generate label for buttons that have no associated plans * Break out overall phase derivation logic into dedicated hooks, functional filter and sort * Simplify useDerivedTestPlanOverallPhase * Additional unit tests for additional hooks and to test scenarios with multiple test plan versions for a single test plan hook rename * Different UX click interaction sequence with SortableTableHeader * Correct interpretation of alphabetical ascending/descending * Rename DataManagement/hooks.js to filterSortHooks * Move sorting and filtering enums to more specific locations based on use * Fix file locations in unit tests, DataManagement, FilterButtons, SortableTableHeader * Smaller margin between buttons, FilterButtons * First pass on functioning sort buttons for DataManagePage * Functioning sort * Filter functionality for DataManagement * Refactor filter buttons on DataManagement to reduce complexity * Filter buttons as separate component * DataManagement page, break sort out into dedicated hook * DataManagement page, dedicated hook for filtering * DataManagement, add constant for test plan version phases to simplify hooks code * Add relevant dynamic aria attributes to DataManagement column sort elements * Add relevant dynamic aria attributes to DataManagement filter buttons * unit tests for SortableTableHeader * unit tests for FilterButtons * unit tests for useDataManagementTableSorting hook * Add unit tests for useDataManagementTableFiltering hook * Filter buttons only show buttons that have have label, handle testPlans with multiple versions, ... useDataManagementTableFiltering don't generate label for buttons that have no associated plans * Break out overall phase derivation logic into dedicated hooks, functional filter and sort * Simplify useDerivedTestPlanOverallPhase * Additional unit tests for additional hooks and to test scenarios with multiple test plan versions for a single test plan hook rename * Different UX click interaction sequence with SortableTableHeader * Correct interpretation of alphabetical ascending/descending * Rename DataManagement/hooks.js to filterSortHooks * Move sorting and filtering enums to more specific locations based on use * Fix file locations in unit tests, DataManagement, FilterButtons, SortableTableHeader * Smaller margin between buttons, FilterButtons * Cleanup after rebase * Render SortableTableHeader inside for test to dismiss warning * Add AriaLiveRegionProvider, Update DataManagement table to use it * Allow SortableTableHeader component to work without AriaLiveRegionProvider * Explicitly support 'DEPRECATED' phase for `TestPlanVersion.phase` (#749) * Start to support for sunset phase * Explicitly use 'DEPRECATED' phase for TestPlanVersion.phase * Add checks to prevent test plan versions in R&D or Deprecated from being shown in data management dropdown * Remove duplicate updateTestPlanVersion call * Reuse phase variable * Add copy for deprecated reports with DisclaimerInfo component * Exclude 'DEPRECATED' testPlanVersions on DataManagement page query * Ensure only reports marked as final are displayed on /embed/reports/ * Fix test * Update failing test * Adjust semantic structure on Data Management Page (#752) * Adjust cell items for data management row to use list-related roles; update aria-labels * Address PR feedback * Remove width:max-content * Address PR feedback * Formatting * Address feedback * Adjust BasicModal to support AtAndBrowserDetailsModal closing * Stop assign menu dropdown from creating unintended bottom space with the parent container * Formatting * Close #755 * Close #754 * Remove superfluous header from TestPlanReportStatusDialog (#766) * Revise required reports conditions (#764) * Revise required reports approach * Update tests * Revert dev.env * Update Version History Page (#767) * Apply correct sort order for Timeline for All Versions section * Update headings used on Versions page * Add aria-labelledby's for tables * Add   for applicable spaces so the text is properly announced by NVDA * Add migration to add missing deprecatedAt dates and to also properly set the candidatePhaseReachedAt dates to more practical dates after the migrations (if candidatePhaseReachedAt < draftPhaseReachedAt, set it candidatePhaseReachedAt to draftPhaseReachedAt + 1day) * Test Plan Versions Page: Use standard testPlanVersions descending sort and show all phases being included in Version Summary * Switch issues search support for checking against hidden body content instead * Fix TestPlanReportStatusDialog using d (day of the week) and y (era) * Explicit check for older date when deprecating existing RD test plan versions during import * Update hidden message content in github issue * Update query for anon user on TestRun page * Keep overall status pill from drifting to center of cell * Fix R&D only TestPlanVersion's table not being shown * Use aria-label for heading to prevent space being announced * Update date format used in aria-label * Make version history a drop down * comment out unused variable * Fix responsiveness * Remove comments and logs * Changes after merge * Fix icon color * Fix padding for timeline heading * Fix console errors and delete comments * Fix conflicts that caused error * Implement required changes from review --------- Co-authored-by: Howard Edwards Co-authored-by: Alexander Flenniken Co-authored-by: Stalgia Grigg --- .../DataManagementRow/index.jsx | 12 +- .../components/TestPlanVersionsPage/index.jsx | 436 ++++++++++++------ .../common/DisclosureComponent/index.jsx | 29 +- .../components/common/VersionString/index.js | 6 +- 4 files changed, 318 insertions(+), 165 deletions(-) diff --git a/client/components/DataManagement/DataManagementRow/index.jsx b/client/components/DataManagement/DataManagementRow/index.jsx index df2b31a43..0a98a0311 100644 --- a/client/components/DataManagement/DataManagementRow/index.jsx +++ b/client/components/DataManagement/DataManagementRow/index.jsx @@ -754,21 +754,15 @@ const DataManagementRow = ({ // If a version of the plan is not in the candidate phase and there is a recommended // version, show string "Review of VERSION_STRING completed DATE" if (otherTestPlanVersions.length) { - const { - latestVersion: otherLatestVersion, - latestVersionDate: otherLatestVersionDate - } = getVersionData(otherTestPlanVersions); + const { latestVersion: otherLatestVersion } = + getVersionData(otherTestPlanVersions); const completionDate = otherLatestVersion.recommendedPhaseReachedAt; return ( - + {otherLatestVersion.versionString} diff --git a/client/components/TestPlanVersionsPage/index.jsx b/client/components/TestPlanVersionsPage/index.jsx index d074a20b4..7a43c26fb 100644 --- a/client/components/TestPlanVersionsPage/index.jsx +++ b/client/components/TestPlanVersionsPage/index.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { useQuery } from '@apollo/client'; import { TEST_PLAN_VERSIONS_PAGE_QUERY } from './queries'; import PageStatus from '../common/PageStatus'; @@ -20,13 +20,93 @@ import { faCodeCommit } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import DisclosureComponentUnstyled from '../common/DisclosureComponent'; -const H2 = styled.h2` - font-size: 1.25em; - padding-top: 3rem; - padding-bottom: 15px; - border-bottom: solid 1px #d2d5d9; - margin-bottom: 2rem !important; +const DisclosureContainer = styled.div` + // Following directives are related to the ManageTestQueue component + > span { + display: block; + margin-bottom: 1rem; + } + + // Add Test Plan to Test Queue button + > button { + display: flex; + padding: 0.5rem 1rem; + margin-top: 1rem; + margin-left: auto; + margin-right: 0; + } + + .disclosure-row-manage-ats { + display: grid; + grid-auto-flow: column; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + grid-gap: 1rem; + + .ats-container { + grid-column: 1 / span 2; + } + + .at-versions-container { + display: flex; + flex-direction: column; + grid-column: 3 / span 3; + } + + .disclosure-buttons-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + + > button { + margin: 0; + padding: 0; + color: #275caa; + border: none; + background-color: transparent; + + &:nth-of-type(2) { + margin-left: auto; + } + + // remove button + &:nth-of-type(3) { + margin-left: 1rem; + color: #ce1b4c; + } + } + } + } + + .disclosure-row-test-plans { + display: grid; + grid-auto-flow: column; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-gap: 1rem; + } + + .disclosure-form-label { + font-weight: bold; + font-size: 1rem; + } + + .timeline-for-version-table { + padding: 0.5rem 1rem; + } +`; + +const DisclosureComponent = styled(DisclosureComponentUnstyled)` + & h2 { + padding: 0; + margin: 0; + font-size: 1.25em; + + button { + font-size: unset; + font-weight: unset; + } + } `; const NoneText = styled.span` @@ -65,6 +145,13 @@ const ThemeTableHeader = styled(UnstyledThemeTableHeader)` margin: 0 !important; `; +// https://stackoverflow.com/a/53215514 +const useForceUpdate = () => { + const [, updateState] = React.useState(); + const forceUpdate = React.useCallback(() => updateState({}), []); + return forceUpdate; +}; + const TestPlanVersionsPage = () => { const { testPlanDirectory } = useParams(); @@ -72,6 +159,10 @@ const TestPlanVersionsPage = () => { variables: { testPlanDirectory }, fetchPolicy: 'cache-and-network' }); + const forceUpdate = useForceUpdate(); + + const expandedVersionSections = useRef(); + const toggleVersionSections = useRef(); if (error) { return ( @@ -258,6 +349,20 @@ const TestPlanVersionsPage = () => { return new Date(dateA) - new Date(dateB); }); + if (!expandedVersionSections.current) { + expandedVersionSections.current = []; + toggleVersionSections.current = []; + + for (let i = 0; i < testPlanVersions.length; i += 1) { + expandedVersionSections.current[i] = false; + toggleVersionSections.current[i] = () => { + expandedVersionSections.current[i] = + !expandedVersionSections.current[i]; + forceUpdate(); + }; + } + } + return ( @@ -389,7 +494,6 @@ const TestPlanVersionsPage = () => { )} - GitHub Issues @@ -458,7 +562,6 @@ const TestPlanVersionsPage = () => { )} - Timeline for All Versions @@ -500,23 +603,13 @@ const TestPlanVersionsPage = () => { })} - - {testPlanVersions.map(testPlanVersion => { - // Gets the derived phase even if deprecated by checking - // the known dates on the testPlanVersion object - const derivedDeprecatedAtPhase = - deriveDeprecatedDuringPhase(testPlanVersion); - - const hasFinalReports = - (derivedDeprecatedAtPhase === 'CANDIDATE' || - derivedDeprecatedAtPhase === 'RECOMMENDED') && - !!testPlanVersion.testPlanReports.filter( - report => report.isFinal - ).length; - - return ( -
-

+ { + return ( + { )} on ${getEventDate(testPlanVersion)}`} > { {testPlanVersion.phase} -  on {getEventDate(testPlanVersion)} -

- -
  • - - - Commit {testPlanVersion.gitSha.substr(0, 7)} - : {testPlanVersion.gitMessage} - -
  • -
  • - + ); + })} + disclosureContainerView={testPlanVersions.map( + testPlanVersion => { + // Gets the derived phase even if deprecated by checking + // the known dates on the testPlanVersion object + const derivedDeprecatedAtPhase = + deriveDeprecatedDuringPhase(testPlanVersion); + + const hasFinalReports = + (derivedDeprecatedAtPhase === 'CANDIDATE' || + derivedDeprecatedAtPhase === 'RECOMMENDED') && + !!testPlanVersion.testPlanReports.filter( + report => report.isFinal + ).length; + + return ( +
  • - {!hasFinalReports ? null : ( -
  • - - - View reports generated from{' '} - {testPlanVersion.versionString} - -
  • - )} -
    - -
    Covered AT
    -
    -
      - {ats.map(at => ( -
    • {at.name}
    • - ))} -
    -
    -
    - - Timeline for {testPlanVersion.versionString} - - -
    - - - - - - - {(() => { - let events = [ - ['RD', testPlanVersion.updatedAt], - [ - 'DRAFT', - testPlanVersion.draftPhaseReachedAt - ], - [ - 'CANDIDATE', - testPlanVersion.candidatePhaseReachedAt - ], - [ - 'RECOMMENDED', - testPlanVersion.recommendedPhaseReachedAt - ], - [ - 'DEPRECATED', - testPlanVersion.deprecatedAt - ] - ] - .filter(event => event[1]) - .sort((a, b) => { - const dateSort = - new Date(a[1]) - new Date(b[1]); - if (dateSort === 0) return 1; // maintain order above - return dateSort; - }); - - return events.map(([phase, date]) => ( - - - - - )); - })()} - - - - ); - })} + : {testPlanVersion.gitMessage} + + +
  • + + + View tests in{' '} + {testPlanVersion.versionString} + +
  • + {!hasFinalReports ? null : ( +
  • + + + View reports generated from{' '} + { + testPlanVersion.versionString + } + +
  • + )} + + +
    Covered AT
    +
    + +
    +
    + + Timeline for{' '} + {testPlanVersion.versionString} + + + + + + + + + + {(() => { + let events = [ + [ + 'RD', + testPlanVersion.updatedAt + ], + [ + 'DRAFT', + testPlanVersion.draftPhaseReachedAt + ], + [ + 'CANDIDATE', + testPlanVersion.candidatePhaseReachedAt + ], + [ + 'RECOMMENDED', + testPlanVersion.recommendedPhaseReachedAt + ], + [ + 'DEPRECATED', + testPlanVersion.deprecatedAt + ] + ] + .filter(event => event[1]) + .sort((a, b) => { + const dateSort = + new Date(a[1]) - + new Date(b[1]); + if (dateSort === 0) + return 1; // maintain order above + return dateSort; + }); + + return events.map( + ([phase, date]) => ( + + + + + ) + ); + })()} + + + + + ); + } + )} + onClick={toggleVersionSections.current} + expanded={expandedVersionSections.current} + stacked={true} + headingLevel="2" + /> ); }; diff --git a/client/components/common/DisclosureComponent/index.jsx b/client/components/common/DisclosureComponent/index.jsx index ca8acd32c..4fcee9a3e 100644 --- a/client/components/common/DisclosureComponent/index.jsx +++ b/client/components/common/DisclosureComponent/index.jsx @@ -22,11 +22,10 @@ const DisclosureParent = styled.div` ${({ stacked }) => stacked && ` - h1:not(:first-of-type) button { - border-top: 1px solid #d3d5da; - } - - h3:not(:first-of-type) button { + h1:not(:first-of-type) button, + h2:not(:first-of-type) button, + h3:not(:first-of-type) button, + h4:not(:first-of-type) button { border-top: 1px solid #d3d5da; }`} `; @@ -58,7 +57,7 @@ const DisclosureButton = styled.button` cursor: pointer; } - svg { + .disclosure-icon { position: absolute; margin: 0; top: 50%; @@ -88,7 +87,8 @@ const DisclosureComponent = ({ onClick = null, expanded = false, stacked = false, - headingLevel = '3' + headingLevel = '3', + className = null }) => { const [isExpanded, setIsExpanded] = useState(expanded); const Tag = `h${headingLevel}`; @@ -96,7 +96,7 @@ const DisclosureComponent = ({ return ( <> {stacked ? ( - + {title.map((_, index) => { const buttonTitle = title[index]; const buttonExpanded = expanded[index]; @@ -105,7 +105,7 @@ const DisclosureComponent = ({ disclosureContainerView[index]; return ( - + {buttonTitle} ) : ( - + {title} @@ -173,7 +175,9 @@ DisclosureComponent.propTypes = { componentId: PropTypes.string, title: PropTypes.oneOfType([ PropTypes.string, - PropTypes.arrayOf(PropTypes.string) + PropTypes.node, + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.node) ]), disclosureContainerView: PropTypes.oneOfType([ PropTypes.node, @@ -188,7 +192,8 @@ DisclosureComponent.propTypes = { PropTypes.arrayOf(PropTypes.bool) ]), stacked: PropTypes.bool, - headingLevel: PropTypes.string + headingLevel: PropTypes.string, + className: PropTypes.string }; export default DisclosureComponent; diff --git a/client/components/common/VersionString/index.js b/client/components/common/VersionString/index.js index ea08146c5..1e19fe372 100644 --- a/client/components/common/VersionString/index.js +++ b/client/components/common/VersionString/index.js @@ -51,7 +51,11 @@ const VersionString = ({ }) => { const body = ( - + {versionString} );
    DateEvent
    - {convertDateToString( - date, - 'MMM D, YYYY' + +
  • + + + Commit{' '} + {testPlanVersion.gitSha.substr( + 0, + 7 )} -
  • {getEventBody(phase)}
    DateEvent
    + {convertDateToString( + date, + 'MMM D, YYYY' + )} + + {getEventBody( + phase + )} +