From ced7c8fce99be1771033410e68665c78c2ad36ef Mon Sep 17 00:00:00 2001 From: Stalgia Grigg Date: Tue, 27 Aug 2024 15:46:03 -0700 Subject: [PATCH 01/15] Manage required report, migrate and refactor Co-authored-by: Paul-Clue --- client/components/ManageTestQueue/index.jsx | 75 ++--- client/components/ManageTestQueue/queries.js | 50 +++ client/components/TestQueue/index.jsx | 2 + client/components/TestQueue/queries.js | 9 + .../CreateRequiredReportForm.jsx | 209 +++++++++++++ .../DeleteRequiredReportModal.jsx | 55 ++++ .../UpdateRequiredReportModal.jsx | 136 ++++++++ .../ManageRequiredReportsDisclosure/index.jsx | 292 ++++++++++++++++++ .../ManageRequiredReportsDisclosure/utils.js | 20 ++ server/graphql-schema.js | 71 ++++- server/models/services/AtBrowserService | 25 ++ server/models/services/helpers.js | 4 +- .../createRequiredReportResolver.js | 28 ++ .../deleteRequiredReportResolver.js | 28 ++ .../RequiredReportOperations/index.js | 9 + .../updateRequiredReportResolver.js | 46 +++ server/resolvers/index.js | 4 + .../resolvers/mutateRequiredReportResolver.js | 9 + server/tests/integration/graphql.test.js | 21 ++ 19 files changed, 1048 insertions(+), 45 deletions(-) create mode 100644 client/components/common/ManageRequiredReportsDisclosure/CreateRequiredReportForm.jsx create mode 100644 client/components/common/ManageRequiredReportsDisclosure/DeleteRequiredReportModal.jsx create mode 100644 client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx create mode 100644 client/components/common/ManageRequiredReportsDisclosure/index.jsx create mode 100644 client/components/common/ManageRequiredReportsDisclosure/utils.js create mode 100644 server/models/services/AtBrowserService create mode 100644 server/resolvers/RequiredReportOperations/createRequiredReportResolver.js create mode 100644 server/resolvers/RequiredReportOperations/deleteRequiredReportResolver.js create mode 100644 server/resolvers/RequiredReportOperations/index.js create mode 100644 server/resolvers/RequiredReportOperations/updateRequiredReportResolver.js create mode 100644 server/resolvers/mutateRequiredReportResolver.js diff --git a/client/components/ManageTestQueue/index.jsx b/client/components/ManageTestQueue/index.jsx index 3bf82420d..09210f81b 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; -import styled from '@emotion/styled'; import PropTypes from 'prop-types'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; import DisclosureComponent from '../common/DisclosureComponent'; import ManageAtVersions from '@components/ManageTestQueue/ManageAtVersions'; import AddTestPlans from '@components/ManageTestQueue/AddTestPlans'; import { AtPropType, TestPlanVersionPropType } from '../common/proptypes'; +import ManageRequiredReportsDisclosure from '../common/ManageRequiredReportsDisclosure'; +import styled from '@emotion/styled'; export const DisclosureContainer = styled.div` // Following directives are related to the ManageTestQueue component @@ -16,8 +17,11 @@ export const DisclosureContainer = styled.div` // 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 { @@ -61,43 +65,12 @@ export const DisclosureContainer = styled.div` } } - .disclosure-row-test-plans { + .disclosure-row-controls { display: grid; - row-gap: 0.5rem; - grid-template-columns: 2fr 2fr 1fr; - column-gap: 2rem; - - & > :nth-of-type(3) { - display: block; - } - & > :nth-of-type(5) { - grid-column: span 2; - } - - @media (max-width: 768px) { - grid-template-columns: 1fr; - - & > :nth-of-type(3) { - display: none; - } - & > :nth-of-type(5) { - grid-column: initial; - } - } - } - - .form-group-at-version { - display: flex; - flex-wrap: wrap; - column-gap: 1rem; - row-gap: 0.75rem; - - select { - width: inherit; - @media (max-width: 767px) { - flex-grow: 1; - } - } + grid-auto-flow: column; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-gap: 1rem; + align-items: end; } .disclosure-form-label { @@ -109,15 +82,18 @@ export const DisclosureContainer = styled.div` const ManageTestQueue = ({ ats = [], testPlanVersions = [], - triggerUpdate = () => {} + triggerUpdate = () => {}, + browsers = [] }) => { const { loadingMessage } = useTriggerLoad(); - const [showManageATs, setShowManageATs] = useState(false); const [showAddTestPlans, setShowAddTestPlans] = useState(false); + const [showManageReqReports, setShowManageReqReports] = useState(false); const onManageAtsClick = () => setShowManageATs(!showManageATs); const onAddTestPlansClick = () => setShowAddTestPlans(!showAddTestPlans); + const onManageReqReportsClick = () => + setShowManageReqReports(!showManageReqReports); return ( @@ -125,7 +101,8 @@ const ManageTestQueue = ({ componentId="manage-test-queue" title={[ 'Manage Assistive Technology Versions', - 'Add Test Plans to the Test Queue' + 'Add Test Plans to the Test Queue', + 'Manage Required Reports' ]} disclosureContainerView={[ , + ]} - onClick={[onManageAtsClick, onAddTestPlansClick]} - expanded={[showManageATs, showAddTestPlans]} + onClick={[ + onManageAtsClick, + onAddTestPlansClick, + onManageReqReportsClick + ]} + expanded={[showManageATs, showAddTestPlans, showManageReqReports]} stacked /> @@ -151,7 +138,9 @@ const ManageTestQueue = ({ ManageTestQueue.propTypes = { ats: PropTypes.arrayOf(AtPropType).isRequired, testPlanVersions: PropTypes.arrayOf(TestPlanVersionPropType), - triggerUpdate: PropTypes.func + triggerUpdate: PropTypes.func, + browsers: PropTypes.array, + enableManageRequiredReports: PropTypes.bool }; export default ManageTestQueue; diff --git a/client/components/ManageTestQueue/queries.js b/client/components/ManageTestQueue/queries.js index 82ed8a3da..2f80b6d96 100644 --- a/client/components/ManageTestQueue/queries.js +++ b/client/components/ManageTestQueue/queries.js @@ -53,3 +53,53 @@ export const DELETE_AT_VERSION_MUTATION = gql` } } `; + +export const CREATE_REQUIRED_REPORT_MUTATION = gql` + mutation CreateRequiredReport( + $atId: ID! + $browserId: ID! + $phase: RequiredReportPhase! + ) { + requiredReport(atId: $atId, browserId: $browserId, phase: $phase) { + createRequiredReport { + atId + browserId + phase + } + } + } +`; + +export const UPDATE_REQUIRED_REPORT_MUTATION = gql` + mutation UpdateRequiredReport( + $atId: ID! + $browserId: ID! + $phase: RequiredReportPhase! + $updateAtId: ID! + $updateBrowserId: ID! + ) { + requiredReport(atId: $atId, browserId: $browserId, phase: $phase) { + updateRequiredReport(atId: $updateAtId, browserId: $updateBrowserId) { + atId + browserId + phase + } + } + } +`; + +export const DELETE_REQUIRED_REPORT_MUTATION = gql` + mutation DeleteRequiredReport( + $atId: ID! + $browserId: ID! + $phase: RequiredReportPhase! + ) { + requiredReport(atId: $atId, browserId: $browserId, phase: $phase) { + deleteRequiredReport { + atId + browserId + phase + } + } + } +`; diff --git a/client/components/TestQueue/index.jsx b/client/components/TestQueue/index.jsx index 3cec38396..27d1d766c 100644 --- a/client/components/TestQueue/index.jsx +++ b/client/components/TestQueue/index.jsx @@ -406,6 +406,8 @@ const TestQueue = () => { ats={data.ats} testPlanVersions={testPlanVersions} triggerUpdate={refetch} + enableManageRequiredReports={true} + browsers={data.browsers} /> )} diff --git a/client/components/TestQueue/queries.js b/client/components/TestQueue/queries.js index 7dd6f7411..156b90667 100644 --- a/client/components/TestQueue/queries.js +++ b/client/components/TestQueue/queries.js @@ -44,6 +44,15 @@ export const TEST_QUEUE_PAGE_QUERY = gql` browsers { ...BrowserFields } + candidateBrowsers { + ...BrowserFields + } + recommendedBrowsers { + ...BrowserFields + } + } + browsers { + ...BrowserFields } testPlans(testPlanVersionPhases: [DRAFT, CANDIDATE, RECOMMENDED]) { ...TestPlanFields diff --git a/client/components/common/ManageRequiredReportsDisclosure/CreateRequiredReportForm.jsx b/client/components/common/ManageRequiredReportsDisclosure/CreateRequiredReportForm.jsx new file mode 100644 index 000000000..8ee6ee3e1 --- /dev/null +++ b/client/components/common/ManageRequiredReportsDisclosure/CreateRequiredReportForm.jsx @@ -0,0 +1,209 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dropdown, Form } from 'react-bootstrap'; +import styled from '@emotion/styled'; +import { AtPropType } from '../proptypes'; + +const CustomToggleButton = styled.button` + background-color: transparent; + width: 100%; + height: 38px; + text-align: center; + + border: none; + margin: 0; + padding: 0; + display: block; + + .icon-container { + background-color: red; + /* position: relative; */ + float: right; + margin-top: 2px; + margin-right: 3px; + } + .icon-chevron { + font-size: 0.8rem; + } +`; + +const CustomToggleP = styled.p` + border: 1px solid #ced4da; + border-radius: 0.375rem; + background-color: #fff; + padding: 2px; + width: 100%; + height: 38px; + cursor: default; + display: inline-block; +`; + +const CustomToggleSpan = styled.span` + background-image: url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e'); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + float: left; + margin-top: 2px; + white-space: nowrap; + background-color: ${props => + props.phaseLabel === 'Select a Phase' + ? '#fff' + : props.phaseLabel === 'Candidate' + ? '#ff6c00' + : props.phaseLabel === 'Recommended' + ? '#8441de' + : 'black'}; + border-radius: 14px; + padding: 2px 32px 2px 14px; + text-align: left; + width: 100%; + font-size: 1rem; + font-weight: 400; + color: ${props => (props.phaseLabel === 'Select a Phase' ? 'black' : '#fff')}; +`; + +const CustomMenu = React.forwardRef(({ children, className }, ref) => { + const value = ''; + + return ( +
+
    + {React.Children.toArray(children).filter( + child => + !value || child.props.children.toLowerCase().startsWith(value) + )} +
+
+ ); +}); + +// You can learn everything about this component here: https://react-bootstrap.netlify.app/docs/components/dropdowns#custom-dropdown-components +const CustomToggle = React.forwardRef(({ children, onClick }, ref) => ( + { + e.preventDefault(); + onClick(e); + }} + > + + {children} + + +)); + +const FormGroup = ({ label, children }) => ( + + {label} + {children} + +); + +export const CreateRequiredReportForm = ({ ats, handleCreate }) => { + const [formState, setFormState] = useState({ + phase: '', + at: '', + browser: '' + }); + + const handleInputChange = (field, value) => { + setFormState(prev => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async () => { + await handleCreate({ + atId: formState.at, + browserId: formState.browser, + phase: formState.phase.toUpperCase() + }); + setFormState({ phase: '', at: '', browser: '' }); + }; + + return ( +
+ + + + {formState.phase || 'Select a Phase'} + + + {['Candidate', 'Recommended'].map(phase => ( + handleInputChange('phase', phase)} + > + {phase} + + ))} + + + + + handleInputChange('at', e.target.value)} + required + > + + {ats.map(item => ( + + ))} + + + + handleInputChange('browser', e.target.value)} + required + > + + {ats + .find(at => at.id === formState.at) + ?.browsers.map(item => ( + + ))} + + + + + +
+ ); +}; + +FormGroup.propTypes = { + label: PropTypes.string, + children: PropTypes.node.isRequired +}; + +CustomToggle.propTypes = { + children: PropTypes.string, + onClick: PropTypes.func +}; + +CustomMenu.propTypes = { + children: PropTypes.array, + className: PropTypes.string +}; + +CreateRequiredReportForm.propTypes = { + ats: PropTypes.arrayOf(AtPropType).isRequired, + handleCreate: PropTypes.func.isRequired +}; + +CreateRequiredReportForm.propTypes = {}; diff --git a/client/components/common/ManageRequiredReportsDisclosure/DeleteRequiredReportModal.jsx b/client/components/common/ManageRequiredReportsDisclosure/DeleteRequiredReportModal.jsx new file mode 100644 index 000000000..d61d2d4df --- /dev/null +++ b/client/components/common/ManageRequiredReportsDisclosure/DeleteRequiredReportModal.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import BasicModal from '../BasicModal'; +import PhasePill from '../PhasePill'; +import PropTypes from 'prop-types'; +import { AtPropType, BrowserPropType } from '../proptypes'; + +const DeleteRequiredReportModal = ({ + at, + browser, + phase, + handleClose, + handleDeleteReqReport +}) => { + const handleDelete = () => { + handleDeleteReqReport({ atId: at.id, browserId: browser.id, phase }); + handleClose(); + }; + + return ( + + Delete {`${at.name} and ${browser.name} pair for `} + + {phase} + + {' required reports'} +

+ } + dialogClassName="modal-50w" + actions={[ + { + label: 'Confirm Delete', + onClick: handleDelete + } + ]} + handleClose={handleClose} + staticBackdrop={true} + /> + ); +}; + +DeleteRequiredReportModal.propTypes = { + at: AtPropType.isRequired, + browser: BrowserPropType.isRequired, + phase: PropTypes.string.isRequired, + handleClose: PropTypes.func.isRequired, + handleDeleteReqReport: PropTypes.func.isRequired +}; + +export default DeleteRequiredReportModal; diff --git a/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx b/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx new file mode 100644 index 000000000..4b976a05e --- /dev/null +++ b/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx @@ -0,0 +1,136 @@ +import React, { useMemo, useState } from 'react'; +import PhasePill from '../PhasePill'; +import styled from '@emotion/styled'; +import BasicModal from '../BasicModal'; +import { Form } from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import { AtPropType } from '../proptypes'; + +const ModalInnerSectionContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const Row = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1rem; +`; + +const UpdateRequiredReportModal = ({ + atId, + browserId, + ats, + phase, + handleClose, + handleUpdate +}) => { + const [atSelection, setAtSelection] = useState(atId); + const [browserSelection, setBrowserSelection] = useState(browserId); + + const handleAtChange = e => { + const value = e.target.value; + setAtSelection(value); + }; + + const handleBrowserChange = e => { + const value = e.target.value; + setBrowserSelection(value); + }; + + const filteredBrowsers = useMemo( + () => ats.find(at => at.id === atSelection)?.browsers || [], + [ats, atSelection] + ); + + return ( + + Edit the following AT/Browser pair for{' '} + + {phase} + {' '} + required reports +

+ } + dialogClassName="modal-50w" + content={ + + { + + + Assistive Technology + + {ats.map(item => { + return ( + + ); + })} + + + + Browser + + + {filteredBrowsers.map(item => ( + + ))} + + + + } + + } + actions={[ + { + label: 'Save Changes', + onClick: () => { + // If the user has changed the AT or Browser, update the required report + if (atSelection !== atId || browserSelection !== browserId) { + handleUpdate({ + atId, + browserId, + phase, + updateAtId: atSelection, + updateBrowserId: browserSelection + }); + } + handleClose(); + } + } + ]} + handleClose={handleClose} + staticBackdrop={true} + /> + ); +}; + +UpdateRequiredReportModal.propTypes = { + atId: PropTypes.string.isRequired, + browserId: PropTypes.string.isRequired, + ats: PropTypes.arrayOf(AtPropType).isRequired, + phase: PropTypes.string.isRequired, + handleClose: PropTypes.func.isRequired, + handleUpdate: PropTypes.func.isRequired +}; + +export default UpdateRequiredReportModal; diff --git a/client/components/common/ManageRequiredReportsDisclosure/index.jsx b/client/components/common/ManageRequiredReportsDisclosure/index.jsx new file mode 100644 index 000000000..ea948e9da --- /dev/null +++ b/client/components/common/ManageRequiredReportsDisclosure/index.jsx @@ -0,0 +1,292 @@ +import { faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useTriggerLoad } from '../LoadingStatus'; +import styled from '@emotion/styled'; +import PhasePill from '../PhasePill'; +import { ThemeTable, ThemeTableHeaderH2 } from '../ThemeTable'; +import { useMutation } from '@apollo/client'; +import { + CREATE_REQUIRED_REPORT_MUTATION, + DELETE_REQUIRED_REPORT_MUTATION, + UPDATE_REQUIRED_REPORT_MUTATION +} from '../../ManageTestQueue/queries'; +import BasicThemedModal from '../BasicThemedModal'; +import PropTypes from 'prop-types'; +import { DisclosureContainer } from '../../ManageTestQueue'; +import UpdateRequiredReportModal from './UpdateRequiredReportModal'; +import DeleteRequiredReportModal from './DeleteRequiredReportModal'; +import { AtPropType, BrowserPropType } from '../proptypes'; +import { CreateRequiredReportForm } from './CreateRequiredReportForm'; +import { + filterAtBrowserCombinations, + sortAtBrowserCombinations +} from './utils'; + +const TransparentButton = styled.button` + border: none; + background-color: transparent; +`; + +const ManageRequiredReportsDisclosure = ({ + ats, + browsers, + triggerUpdate = () => {} +}) => { + const { triggerLoad } = useTriggerLoad(); + + const [pendingUpdateReport, setPendingUpdateReport] = useState(null); + const [pendingDeleteReport, setPendingDeleteReport] = useState(null); + + const [modalState, setModalState] = useState({ + show: false, + title: '', + content: '' + }); + + const [createRequiredReport] = useMutation(CREATE_REQUIRED_REPORT_MUTATION); + const [updateRequiredReport] = useMutation(UPDATE_REQUIRED_REPORT_MUTATION); + const [deleteRequiredReport] = useMutation(DELETE_REQUIRED_REPORT_MUTATION); + + useEffect(() => { + setAtBrowserCombinations(getAtBrowserCombos()); + }, [ats, browsers]); + + const showErrorModal = useCallback((title, message) => { + setModalState({ show: true, title, content: <>{message} }); + }, []); + + const getAtBrowserCombos = () => { + return [ + ...ats.flatMap(at => + at.candidateBrowsers?.map(browser => ({ + at, + browser, + phase: 'CANDIDATE' + })) + ), + ...ats.flatMap(at => + at.recommendedBrowsers?.map(browser => ({ + at, + browser, + phase: 'RECOMMENDED' + })) + ) + ]; + }; + + const [atBrowserCombinations, setAtBrowserCombinations] = useState( + getAtBrowserCombos() + ); + + const updateAtBrowserCombinations = useCallback( + (updatedReport, action) => { + setAtBrowserCombinations(prevCombinations => { + let newCombinations = [...prevCombinations]; + + if (action === 'add') { + newCombinations.push({ + at: ats.find(at => at.id === updatedReport.atId), + browser: browsers.find( + browser => browser.id === updatedReport.browserId + ), + phase: updatedReport.phase.replace('IS_', '') + }); + } + + newCombinations = filterAtBrowserCombinations( + newCombinations, + updatedReport + ); + return sortAtBrowserCombinations(newCombinations); + }); + }, + [ats, browsers] + ); + + const handleRequiredReportAction = useCallback( + async (action, actionData, mutationFn, successMessage) => { + try { + await triggerLoad(async () => { + const { data } = await mutationFn({ + variables: { + ...actionData, + phase: `IS_${actionData.phase}` + } + }); + + const updatedReport = data.requiredReport[`${action}RequiredReport`]; + + if (updatedReport) { + updateAtBrowserCombinations(updatedReport, action); + } + + await triggerUpdate(); + }, successMessage); + } catch (error) { + showErrorModal( + `Error ${ + action.charAt(0).toUpperCase() + action.slice(1) + }ing Required Report`, + error.message + ); + } + }, + [triggerLoad, triggerUpdate, updateAtBrowserCombinations, showErrorModal] + ); + + const handleCreateRequiredReport = useCallback( + createData => + handleRequiredReportAction( + 'create', + createData, + createRequiredReport, + 'Adding Phase requirement to the required reports table' + ), + [handleRequiredReportAction, createRequiredReport] + ); + + const handleDeleteReqReport = useCallback( + deletionData => { + handleRequiredReportAction( + 'delete', + deletionData, + deleteRequiredReport, + 'Deleting Phase requirement from the required reports table' + ), + handleCloseDeleteModal(); + }, + [handleRequiredReportAction, deleteRequiredReport, handleCloseDeleteModal] + ); + + const handleUpdateReport = useCallback( + updateData => { + handleRequiredReportAction( + 'update', + updateData, + updateRequiredReport, + 'Updating Phase requirement in the required reports table' + ), + handleCloseUpdateModal(); + }, + [handleRequiredReportAction, updateRequiredReport, handleCloseUpdateModal] + ); + + const handleCloseUpdateModal = () => { + setPendingUpdateReport(null); + }; + + const handleCloseDeleteModal = () => { + setPendingDeleteReport(null); + }; + + return ( + <> + + Add required reports for a specific AT and Browser pair + + + Required Reports + + + + + Phase + AT + Browser + Edit + + + + {atBrowserCombinations?.map(({ at, browser, phase }) => { + return ( + + + + {phase} + + + {at.name} + {browser.name} + + { + setPendingUpdateReport({ + atId: at.id, + browserId: browser.id, + phase: phase + }); + }} + > + + Edit + + { + setPendingDeleteReport({ + at, + browser, + phase + }); + }} + > + + Remove + + + + ); + })} + + + + {pendingUpdateReport && ( + + )} + {pendingDeleteReport && ( + + )} + + setModalState({ show: false, title: '', content: null }) + } + ]} + handleClose={() => + setModalState({ show: false, title: '', content: null }) + } + /> + + ); +}; + +ManageRequiredReportsDisclosure.propTypes = { + ats: PropTypes.arrayOf(AtPropType), + browsers: PropTypes.arrayOf(BrowserPropType), + triggerUpdate: PropTypes.func +}; + +export default ManageRequiredReportsDisclosure; diff --git a/client/components/common/ManageRequiredReportsDisclosure/utils.js b/client/components/common/ManageRequiredReportsDisclosure/utils.js new file mode 100644 index 000000000..6e7592844 --- /dev/null +++ b/client/components/common/ManageRequiredReportsDisclosure/utils.js @@ -0,0 +1,20 @@ +export const sortAtBrowserCombinations = combinations => { + return combinations.sort((a, b) => { + if (a.phase < b.phase) return -1; + if (a.phase > b.phase) return 1; + return a.at.name.localeCompare(b.at.name); + }); +}; + +export const filterAtBrowserCombinations = ( + combinations, + { atId, browserId, phase } +) => { + return combinations.filter(row => { + return !( + row.at.id === atId && + row.browser.id === browserId && + row.phase === phase + ); + }); +}; diff --git a/server/graphql-schema.js b/server/graphql-schema.js index b6b50adfa..30e5f5e56 100644 --- a/server/graphql-schema.js +++ b/server/graphql-schema.js @@ -736,7 +736,7 @@ const graphqlSchema = gql` """ input TestResultInput { """ - See TestResult type for more information. + The version for a given assistive technology. """ id: ID! """ @@ -786,6 +786,31 @@ const graphqlSchema = gql` unexpectedBehaviors: [UnexpectedBehavior] } + """ + See TestResult type for more information. + The return type for createRequiredReport. + """ + type RequiredReport { + atId: ID! + browserId: ID! + phase: RequiredReportPhase! + } + + """ + The fields on the RequiredReportOperations type which can be used or update the + RequiredReports. + """ + input RequiredReportOperationsInput { + """ + See AtVersion type for more information. + """ + inputAtId: ID! + """ + See AtVersion type for more information. + """ + inputBrowserId: ID! + } + """ Minimal plain representation of a ScenarioResult. """ @@ -840,6 +865,31 @@ const graphqlSchema = gql` failedReason: AssertionFailedReason } + enum TestPlanVersionPhase { + """ + Accepting new TestPlanRuns from testers. + """ + RD + """ + Accepting new TestPlanRuns from testers. + """ + DRAFT + """ + Testing is complete and consistent, and ready to be displayed in the + Candidate Tests and Reports section of the app. + """ + CANDIDATE + """ + Testing is complete and consistent, and ready to be displayed in the + Reports section of the app as being recommended. + """ + RECOMMENDED + """ + The TestPlanVersion is now outdated and replaced by another version. + """ + DEPRECATED + } + """ Minimal plain representation of an AssertionResult. """ @@ -1316,6 +1366,17 @@ const graphqlSchema = gql` findOrCreateAtVersion(input: AtVersionInput!): AtVersion! } + enum RequiredReportPhase { + IS_CANDIDATE + IS_RECOMMENDED + } + + type RequiredReportOperations { + createRequiredReport: RequiredReport! + updateRequiredReport(atId: ID!, browserId: ID!): RequiredReport! + deleteRequiredReport: RequiredReport! + } + """ Mutations scoped to an existing AtVersion. """ @@ -1571,6 +1632,14 @@ const graphqlSchema = gql` Delete a CollectionJob """ deleteCollectionJob(id: ID!): NoResponse! + """ + Get the available mutations for the given RequiredReport. + """ + requiredReport( + atId: ID! + browserId: ID! + phase: RequiredReportPhase! + ): RequiredReportOperations! } `; diff --git a/server/models/services/AtBrowserService b/server/models/services/AtBrowserService new file mode 100644 index 000000000..f3de0767e --- /dev/null +++ b/server/models/services/AtBrowserService @@ -0,0 +1,25 @@ +const ModelService = require('./ModelService.js'); +const { AtBrowsers } = require('../'); +const { AT_BROWSERS_ATTRIBUTES } = require('./helpers'); + +const updateAtBrowser = async ({ + atId, + browserId, + updateParams = {}, + atBrowsersAttributes = AT_BROWSERS_ATTRIBUTES, + transaction +}) => { + await ModelService.update(AtBrowsers, { + where: { atId, browserId }, + values: updateParams, + transaction + }); + + return ModelService.getByQuery(AtBrowsers, { + where: { atId, browserId }, + attributes: atBrowsersAttributes, + transaction + }); +}; + +module.exports = { updateAtBrowser }; diff --git a/server/models/services/helpers.js b/server/models/services/helpers.js index 402787495..3db63d195 100644 --- a/server/models/services/helpers.js +++ b/server/models/services/helpers.js @@ -12,7 +12,8 @@ const { UserRoles, UserAts, CollectionJob, - CollectionJobTestStatus + CollectionJobTestStatus, + AtBrowsers } = require('../index'); /** @@ -32,6 +33,7 @@ module.exports = { AT_VERSION_ATTRIBUTES: getSequelizeModelAttributes(AtVersion), BROWSER_ATTRIBUTES: getSequelizeModelAttributes(Browser), BROWSER_VERSION_ATTRIBUTES: getSequelizeModelAttributes(BrowserVersion), + AT_BROWSERS_ATTRIBUTES: getSequelizeModelAttributes(AtBrowsers), ROLE_ATTRIBUTES: getSequelizeModelAttributes(Role), TEST_PLAN_ATTRIBUTES: getSequelizeModelAttributes(TestPlan), TEST_PLAN_VERSION_ATTRIBUTES: getSequelizeModelAttributes(TestPlanVersion), diff --git a/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js b/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js new file mode 100644 index 000000000..ecbb7eeb6 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js @@ -0,0 +1,28 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const createRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + _, + context +) => { + const { user, transaction } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + let updateParams = {}; + + if (phase === 'IS_CANDIDATE') { + updateParams = { isCandidate: true }; + } + if (phase === 'IS_RECOMMENDED') { + updateParams = { isRecommended: true }; + } + + await updateAtBrowser({ atId, browserId, updateParams, transaction }); + + return { atId, browserId, phase }; +}; + +module.exports = createRequiredReportResolver; diff --git a/server/resolvers/RequiredReportOperations/deleteRequiredReportResolver.js b/server/resolvers/RequiredReportOperations/deleteRequiredReportResolver.js new file mode 100644 index 000000000..27b2535a2 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/deleteRequiredReportResolver.js @@ -0,0 +1,28 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const deleteRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + _, + context +) => { + const { user, transaction } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + let updateParams = {}; + + if (phase === 'IS_CANDIDATE') { + updateParams = { isCandidate: false }; + } + if (phase === 'IS_RECOMMENDED') { + updateParams = { isRecommended: false }; + } + + await updateAtBrowser({ atId, browserId, updateParams, transaction }); + + return { atId, browserId, phase }; +}; + +module.exports = deleteRequiredReportResolver; diff --git a/server/resolvers/RequiredReportOperations/index.js b/server/resolvers/RequiredReportOperations/index.js new file mode 100644 index 000000000..1a1a91c11 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/index.js @@ -0,0 +1,9 @@ +const createRequiredReport = require('./createRequiredReportResolver'); +const updateRequiredReport = require('./updateRequiredReportResolver'); +const deleteRequiredReport = require('./deleteRequiredReportResolver'); + +module.exports = { + createRequiredReport, + updateRequiredReport, + deleteRequiredReport +}; diff --git a/server/resolvers/RequiredReportOperations/updateRequiredReportResolver.js b/server/resolvers/RequiredReportOperations/updateRequiredReportResolver.js new file mode 100644 index 000000000..bd184eb19 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/updateRequiredReportResolver.js @@ -0,0 +1,46 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const updateRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + { atId: inputAtId, browserId: inputBrowserId }, + context +) => { + const { user, transaction } = context; + if (!user?.roles.find(role => role.name === 'ADMIN')) { + throw new AuthenticationError(); + } + + let updateParams = {}; + + // These conditionals will change values in the At/Browsers table + // in the database. Each updateAtBrowser() call changes the boolean value + // for a particular row in the database. The booleans for two row need to be + // changed. So we call updateAtBrowser() twice. + if (phase === 'IS_CANDIDATE') { + updateParams = { isCandidate: false }; + await updateAtBrowser({ atId, browserId, updateParams, transaction }); + updateParams = { isCandidate: true }; + await updateAtBrowser({ + atId: inputAtId, + browserId: inputBrowserId, + updateParams, + transaction + }); + } + if (phase === 'IS_RECOMMENDED') { + updateParams = { isRecommended: false }; + await updateAtBrowser({ atId, browserId, updateParams, transaction }); + updateParams = { isRecommended: true }; + await updateAtBrowser({ + atId: inputAtId, + browserId: inputBrowserId, + updateParams, + transaction + }); + } + + return { atId: inputAtId, browserId: inputBrowserId, phase }; +}; + +module.exports = updateRequiredReportResolver; diff --git a/server/resolvers/index.js b/server/resolvers/index.js index 58bfd1d80..cf7364cd4 100644 --- a/server/resolvers/index.js +++ b/server/resolvers/index.js @@ -15,6 +15,7 @@ const addViewer = require('./addViewerResolver'); const mutateAt = require('./mutateAtResolver'); const mutateAtVersion = require('./mutateAtVersionResolver'); const mutateBrowser = require('./mutateBrowserResolver'); +const mutateRequiredReport = require('./mutateRequiredReportResolver'); const mutateTestPlanReport = require('./mutateTestPlanReportResolver'); const mutateTestPlanRun = require('./mutateTestPlanRunResolver'); const mutateTestResult = require('./mutateTestResultResolver'); @@ -34,6 +35,7 @@ const AtOperations = require('./AtOperations'); const AtVersionOperations = require('./AtVersionOperations'); const BrowserOperations = require('./BrowserOperations'); const TestPlan = require('./TestPlan'); +const RequiredReportOperations = require('./RequiredReportOperations'); const TestPlanVersion = require('./TestPlanVersion'); const TestPlanReport = require('./TestPlanReport'); const TestPlanReportOperations = require('./TestPlanReportOperations'); @@ -75,6 +77,7 @@ const resolvers = { testResult: mutateTestResult, testPlanVersion: mutateTestPlanVersion, collectionJob: mutateCollectionJob, + requiredReport: mutateRequiredReport, createTestPlanReport, updateMe, addViewer, @@ -99,6 +102,7 @@ const resolvers = { TestResultOperations, TestPlanVersionOperations, CollectionJobOperations, + RequiredReportOperations, AtVersion }; diff --git a/server/resolvers/mutateRequiredReportResolver.js b/server/resolvers/mutateRequiredReportResolver.js new file mode 100644 index 000000000..e0b2b8533 --- /dev/null +++ b/server/resolvers/mutateRequiredReportResolver.js @@ -0,0 +1,9 @@ +const mutateRequiredReportResolver = ( + _, + { atId, browserId, phase }, + context // eslint-disable-line no-unused-vars +) => { + return { parentContext: { atId, browserId, phase } }; +}; + +module.exports = mutateRequiredReportResolver; diff --git a/server/tests/integration/graphql.test.js b/server/tests/integration/graphql.test.js index 7ffbf8c69..f10ac0785 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -801,6 +801,27 @@ describe('graphql', () => { } } deleteCollectionJob(id: 1) + requiredReport(atId: 1, browserId: 1, phase: IS_CANDIDATE) { + __typename + createRequiredReport { + __typename + atId + browserId + phase + } + updateRequiredReport(atId: 1, browserId: 1) { + __typename + atId + browserId + phase + } + deleteRequiredReport { + __typename + atId + browserId + phase + } + } } `, { From 3c2713c4aa22c8af7c5f6e2fd5770697873a8555 Mon Sep 17 00:00:00 2001 From: Stalgia Grigg Date: Tue, 27 Aug 2024 15:58:56 -0700 Subject: [PATCH 02/15] Update snapshots --- .../e2e/snapshots/saved/_data-management.html | 465 +++++++++++++++++- .../e2e/snapshots/saved/_test-queue.html | 465 +++++++++++++++++- 2 files changed, 926 insertions(+), 4 deletions(-) diff --git a/client/tests/e2e/snapshots/saved/_data-management.html b/client/tests/e2e/snapshots/saved/_data-management.html index 496638791..256197db1 100644 --- a/client/tests/e2e/snapshots/saved/_data-management.html +++ b/client/tests/e2e/snapshots/saved/_data-management.html @@ -114,7 +114,7 @@

id="disclosure-container-manage-test-queue-Manage Assistive Technology Versions" aria-labelledby="disclosure-btn-manage-test-queue-Manage Assistive Technology Versions" class="css-19fsyrg"> -
+
Select an assistive technology and manage its versions in the ARIA-AT App id="disclosure-container-manage-test-queue-Add Test Plans to the Test Queue" aria-labelledby="disclosure-btn-manage-test-queue-Add Test Plans to the Test Queue" class="css-19fsyrg"> -
+
Select a test plan, assistive technology and browser to add a new test plan report to the test queue.
+

+ +

+
+
+ Add required reports for a specific AT and Browser pair +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+

+ Required Reports +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PhaseATBrowserEdit
+ Candidate + JAWS + +
+ Candidate + NVDA + +
+ Candidate + VoiceOver for macOS + +
+ Recommended + JAWS + +
+ Recommended + JAWS + +
+ Recommended + NVDA + +
+ Recommended + NVDA + +
+ Recommended + VoiceOver for macOS + +
+ Recommended + VoiceOver for macOS + +
+
+
+

Test Plans Status Summary

    id="disclosure-container-manage-test-queue-Manage Assistive Technology Versions" aria-labelledby="disclosure-btn-manage-test-queue-Manage Assistive Technology Versions" class="css-19fsyrg"> -
    +
    Select an assistive technology and manage its versions in the ARIA-AT App id="disclosure-container-manage-test-queue-Add Test Plans to the Test Queue" aria-labelledby="disclosure-btn-manage-test-queue-Add Test Plans to the Test Queue" class="css-19fsyrg"> -
    +
    Select a test plan, assistive technology and browser to add a new test plan report to the test queue.
    +

    + +

    +
    +
    + Add required reports for a specific AT and Browser pair +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +

    + Required Reports +

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PhaseATBrowserEdit
    + Candidate + JAWSChrome + +
    + Candidate + NVDAChrome + +
    + Candidate + VoiceOver for macOSSafari + +
    + Recommended + JAWSFirefox + +
    + Recommended + JAWSChrome + +
    + Recommended + NVDAFirefox + +
    + Recommended + NVDAChrome + +
    + Recommended + VoiceOver for macOSSafari + +
    + Recommended + VoiceOver for macOSChrome + +
    +
    +
    +

    Alert Example

    From 0a861965c47ee3815fe58fbadbd65cd1c3cb2d3b Mon Sep 17 00:00:00 2001 From: Stalgia Grigg Date: Wed, 28 Aug 2024 08:40:13 -0700 Subject: [PATCH 03/15] Update mock for TestQueue --- .../GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js | 3 ++- .../GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js index 32c8de0cf..c7de62598 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminNotPopulatedMock.js @@ -40,7 +40,8 @@ export default testQueuePageQuery => [ ats: [], testPlans: [], testPlanVersions: [], - testPlanReports: [] + testPlanReports: [], + browsers: [] } } } diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js index 186f6ac4f..83a112fb2 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterNotPopulatedMock.js @@ -40,7 +40,8 @@ export default testQueuePageQuery => [ ats: [], testPlans: [], testPlanVersions: [], - testPlanReports: [] + testPlanReports: [], + browsers: [] } } } From 1474e8ab0784e3f845eef16a2330cbbab00035e8 Mon Sep 17 00:00:00 2001 From: Stalgia Grigg Date: Wed, 28 Aug 2024 10:27:20 -0700 Subject: [PATCH 04/15] Add e2e tests for ManageRequiredReports --- .../UpdateRequiredReportModal.jsx | 1 + .../ManageRequiredReportsDisclosure/index.jsx | 4 +- client/tests/e2e/TestQueue.e2e.test.js | 187 ++++++++++++++++++ 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx b/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx index 4b976a05e..fea184e74 100644 --- a/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx +++ b/client/components/common/ManageRequiredReportsDisclosure/UpdateRequiredReportModal.jsx @@ -84,6 +84,7 @@ const UpdateRequiredReportModal = ({