diff --git a/client/components/DataManagement/queries.js b/client/components/DataManagement/queries.js index d5c40cc46..64398e0be 100644 --- a/client/components/DataManagement/queries.js +++ b/client/components/DataManagement/queries.js @@ -15,6 +15,10 @@ export const DATA_MANAGEMENT_PAGE_QUERY = gql` name releasedAt } + browsers { + id + name + } candidateBrowsers { id } diff --git a/client/components/ManageTestQueue/index.jsx b/client/components/ManageTestQueue/index.jsx index 1d884bf30..1339dc6d9 100644 --- a/client/components/ManageTestQueue/index.jsx +++ b/client/components/ManageTestQueue/index.jsx @@ -1,9 +1,13 @@ import React, { useEffect, useState, useRef } from 'react'; import { useMutation } from '@apollo/client'; -import { Form } from 'react-bootstrap'; +import { Button, Form, Dropdown } from 'react-bootstrap'; import styled from '@emotion/styled'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEdit, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { + faEdit, + faTrashAlt + // faChevronDown +} from '@fortawesome/free-solid-svg-icons'; import PropTypes from 'prop-types'; import BasicModal from '../common/BasicModal'; import UpdateVersionModal from '../common/UpdateVersionModal'; @@ -13,11 +17,34 @@ import { EDIT_AT_VERSION_MUTATION, DELETE_AT_VERSION_MUTATION } from '../TestQueue/queries'; +import { + CREATE_REQUIRED_REPORT_MUTATION, + UPDATE_REQUIRED_REPORT_MUTATION, + DELETE_REQUIRED_REPORT_MUTATION +} from './queries'; import { gitUpdatedDateToString } from '../../utils/gitUtils'; import { convertStringToDate } from '../../utils/formatter'; import { LoadingStatus, useTriggerLoad } from '../common/LoadingStatus'; import DisclosureComponent from '../common/DisclosureComponent'; import AddTestToQueueWithConfirmation from '../AddTestToQueueWithConfirmation'; +import { ThemeTable, ThemeTableHeaderH2 } from '../common/ThemeTable'; +import PhasePill from '../common/PhasePill'; + +const ModalInnerSectionContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const Row = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 1rem; +`; + +const TransparentButton = styled.button` + border: none; + background-color: transparent; +`; const DisclosureContainer = styled.div` // Following directives are related to the ManageTestQueue component @@ -76,11 +103,12 @@ const DisclosureContainer = styled.div` } } - .disclosure-row-test-plans { + .disclosure-row-controls { display: grid; grid-auto-flow: column; grid-template-columns: 1fr 1fr 1fr 1fr; grid-gap: 1rem; + align-items: end; } .disclosure-form-label { @@ -88,8 +116,119 @@ const DisclosureContainer = styled.div` font-size: 1rem; } `; +//section: +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; + /* 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; */ + 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; + /* margin-left: 20px; */ + white-space: nowrap; + background-color: ${props => + props.phaseLabel === 'Phase Selection' + ? '#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 === 'Phase Selection' ? 'black' : '#fff'}; +`; +//section: +// 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); + }} + > + { + e.preventDefault(); + onClick(e); + }} + > + + {children} + + {/* + + */} + + +)); + +const CustomMenu = React.forwardRef(({ children, className }, ref) => { + const value = ''; + + return ( +
+ +
+ ); +}); const ManageTestQueue = ({ + enableManageRequiredReports = false, ats = [], browsers = [], testPlanVersions = [], @@ -103,8 +242,33 @@ const ManageTestQueue = ({ const editAtVersionButtonRef = useRef(); const deleteAtVersionButtonRef = useRef(); + // Find Manage Required Reports Modal + const [showEditAtBrowserModal, setShowEditAtBrowserModal] = useState(true); + const [requiredReportsModalTitle, setRequiredReportsModalTitle] = + useState(''); + + const [isDelete, setIsDelete] = useState(false); + const [actionButtonLabel, setActionButtonLabel] = useState('Save Changes'); + const [updateAtIdForUpdate, setUpdateAtIdForUpdate] = useState(''); + const [updatePhaseForUpdate, setUpdatePhaseForUpdate] = useState(''); + const [updateBrowserIdForUpdate, setUpdateBrowserIdForUpdate] = + useState(''); + const [updateAtSelection, setUpdateAtSelection] = useState( + 'Select an Assistive Technology' + ); + const [updateAtForButton, setUpdateAtForButton] = useState(''); + const [updateListAtSelection, setUpdateListAtSelection] = useState( + 'Select an Assistive Technology' + ); + const [updateBrowserSelection, setUpdateBrowserSelection] = useState(''); + const [updateListBrowserSelection, setUpdateListBrowserSelection] = + useState(''); + const [updatePhaseSelection, setUpdatePhaseSelection] = + useState('Phase Selection'); + const [updatePhaseForButton, setUpdatePhaseForButton] = useState(''); const [showManageATs, setShowManageATs] = useState(false); const [showAddTestPlans, setShowAddTestPlans] = useState(false); + const [showManageReqReports, setShowManageReqReports] = useState(false); const [selectedManageAtId, setSelectedManageAtId] = useState('1'); const [selectedManageAtVersions, setSelectedManageAtVersions] = useState( [] @@ -144,9 +308,256 @@ const ManageTestQueue = ({ const [addAtVersion] = useMutation(ADD_AT_VERSION_MUTATION); const [editAtVersion] = useMutation(EDIT_AT_VERSION_MUTATION); const [deleteAtVersion] = useMutation(DELETE_AT_VERSION_MUTATION); + const [createRequiredReport] = useMutation(CREATE_REQUIRED_REPORT_MUTATION); + const [updateRequiredReport] = useMutation(UPDATE_REQUIRED_REPORT_MUTATION); + const [deleteRequiredReport] = useMutation(DELETE_REQUIRED_REPORT_MUTATION); + + const [atBrowserCombinations, setAtBrowserCombinations] = useState([ + ...ats.flatMap(at => + at.candidateBrowsers?.map(browser => ({ + at, + browser, + phase: 'CANDIDATE' + })) + ), + ...ats.flatMap(at => + at.recommendedBrowsers?.map(browser => ({ + at, + browser, + phase: 'RECOMMENDED' + })) + ) + ]); + + const setPhase = phase => { + setUpdatePhaseSelection(phase); + if (phase === 'Candidate' || phase === 'Recommended') { + setUpdatePhaseForButton(phase.toUpperCase()); + } + }; + + const onOpenShowEditAtBrowserModal = ( + type = 'edit', + phase, + at = '', + browser = '' + ) => { + if (type === 'edit') { + setRequiredReportsModalTitle( +

+ Edit the following AT/Browser pair for{' '} + + {phase} + {' '} + required reports +

+ ); + } + + if (type === 'delete') { + setRequiredReportsModalTitle( +

+ Delete {at} and {browser} pair for{' '} + + {phase} + {' '} + required reports +

+ ); + } + setShowEditAtBrowserModal(false); + }; + + const runMutationForRequiredReportTable = async mutation => { + let atId = updateAtForButton; + let browserId = updateListBrowserSelection; + + if (mutation === 'createRequiredReport') { + await triggerLoad(async () => { + try { + atBrowserCombinations.forEach(({ at, browser, phase }) => { + if ( + updateAtForButton === at.id && + updateListBrowserSelection === browser.id && + updatePhaseForButton === phase + ) { + throw new Error( + 'A duplicate Entry was detected in the table' + ); + } + }); + const { data } = await createRequiredReport({ + variables: { + atId: atId, + browserId: browserId, + phase: `IS_${updatePhaseForButton}` + } + }); + + const createdRequiredReport = + data.requiredReport.createRequiredReport; + + // Verify that the created required report was actually created before updating + // the dataset + if (createdRequiredReport) { + setAtBrowserCombinations( + [ + ...atBrowserCombinations, + { + at: ats.find( + at => + at.id === createdRequiredReport.atId + ), + browser: browsers.find( + browser => + browser.id === + createdRequiredReport.browserId + ), + phase: updatePhaseForButton + } + ].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); + }) + ); + } + } catch (error) { + setShowThemedModal(true); + setThemedModalTitle( + 'Error Updating Required Reports Table' + ); + setThemedModalContent(<>{error.message}); + } + }, 'Adding Phase requirement to the required reports table'); + } + if (mutation === 'updateRequiredReport') { + await triggerLoad(async () => { + try { + atBrowserCombinations.forEach(({ at, browser, phase }) => { + if ( + updateAtSelection === at.id && + updateBrowserSelection === browser.id && + updatePhaseForUpdate === phase + ) { + throw new Error( + 'Cannnot update to a duplicate entry' + ); + } + }); + + const { data } = await updateRequiredReport({ + variables: { + atId: updateAtIdForUpdate, + browserId: updateBrowserIdForUpdate, + phase: `IS_${updatePhaseForUpdate}`, + updateAtId: updateAtSelection, + updateBrowserId: updateBrowserSelection + } + }); + + const updatedRequiredReport = + data.requiredReport.updateRequiredReport; + + // Verify that the created required report was actually created before updating + // the dataset + if (updatedRequiredReport) { + setAtBrowserCombinations( + [ + ...atBrowserCombinations, + { + at: ats.find( + at => + at.id === updatedRequiredReport.atId + ), + browser: browsers.find( + browser => + browser.id === + updatedRequiredReport.browserId + ), + phase: updatePhaseForUpdate + } + ] + .filter(row => { + if ( + row.at.id === updateAtIdForUpdate && + row.browser.id === + updateBrowserIdForUpdate && + row.phase == updatePhaseForUpdate + ) { + return false; + } + return true; + }) + .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); + }) + ); + } + } catch (error) { + setShowThemedModal(true); + setThemedModalTitle( + 'Error Updating Required Reports Table' + ); + setThemedModalContent(<>{error.message}); + } + }, 'Adding Phase requirement to the required reports table'); + } + if (mutation === 'deleteRequiredReport') { + await triggerLoad(async () => { + const { data } = await deleteRequiredReport({ + variables: { + atId: updateAtIdForUpdate, + browserId: updateBrowserIdForUpdate, + phase: `IS_${updatePhaseForUpdate}` + } + }); + + const deletedRequiredReport = + data.requiredReport.deleteRequiredReport; + + if (deletedRequiredReport) { + setAtBrowserCombinations( + [...atBrowserCombinations] + .filter(row => { + if ( + row.at.id === updateAtIdForUpdate && + row.browser.id === + updateBrowserIdForUpdate && + row.phase == updatePhaseForUpdate + ) { + return false; + } + return true; + }) + .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); + }) + ); + } + }, 'Adding Phase requirement to the required reports table'); + } + }; const onManageAtsClick = () => setShowManageATs(!showManageATs); const onAddTestPlansClick = () => setShowAddTestPlans(!showAddTestPlans); + const onManageReqReportsClick = () => + setShowManageReqReports(!showManageReqReports); + + const disclosureTitle = enableManageRequiredReports + ? [ + 'Manage Assistive Technology Versions', + 'Add Test Plans to the Test Queue' + ] + : [ + 'Manage Assistive Technology Versions', + 'Add Test Plans to the Test Queue', + 'Manage Required Reports' + ]; useEffect(() => { const allTestPlanVersions = testPlanVersions @@ -298,7 +709,7 @@ const ManageTestQueue = ({ const onThemedModalClose = () => { setShowThemedModal(false); - focusButtonRef.current.focus(); + if (focusButtonRef.current) focusButtonRef.current.focus(); }; const getAtVersionFromId = id => { @@ -467,14 +878,31 @@ const ManageTestQueue = ({ setShowThemedModal(true); }; + const handleAtChange = e => { + const value = e.target.value; + setUpdateAtSelection(value); + }; + + const handleBrowserChange = e => { + const value = e.target.value; + setUpdateBrowserSelection(value); + }; + + const handleListAtChange = e => { + const value = e.target.value; + setUpdateListAtSelection(value); + setUpdateAtForButton(value); + }; + const handleListBrowserChange = e => { + const value = e.target.value; + setUpdateListBrowserSelection(value); + }; + return ( @@ -555,7 +983,7 @@ const ManageTestQueue = ({ Select a Test Plan and version and an Assistive Technology and Browser to add it to the Test Queue -
+
Test Plan @@ -675,13 +1103,239 @@ const ManageTestQueue = ({ !selectedBrowserId } /> + , + + + Add required reports for a specific AT and Browser + pair + +
+ + + Phase + + + + {updatePhaseSelection} + + + + + setPhase('Candidate') + } + > + Candidate + + + setPhase('Recommended') + } + > + Recommended + + + + + + + Assistive Technology + + + + {ats.map(item => { + return ( + + ); + })} + + + + + Browser + + + + {ats + .find( + at => + at.id === updateListAtSelection + ) + ?.browsers.map(item => ( + + ))} + + + + + +
+ + Required Reports + + + + + Phase + AT + Browser + Edit + + + + {atBrowserCombinations?.map( + ({ at, browser, phase }) => { + return ( + + + + {phase} + {' '} + + {at.name} + {browser.name} + + { + setIsDelete(false); + setActionButtonLabel( + 'Save Changes' + ); + setUpdateAtIdForUpdate( + at.id + ); + setUpdateBrowserIdForUpdate( + browser.id + ); + setUpdatePhaseForUpdate( + phase + ); + onOpenShowEditAtBrowserModal( + 'edit', + phase + ); + }} + > + + + Edit + + + { + setIsDelete(true); + setActionButtonLabel( + 'Confirm Delete' + ); + setUpdateAtIdForUpdate( + at.id + ); + setUpdateBrowserIdForUpdate( + browser.id + ); + setUpdatePhaseForUpdate( + phase + ); + onOpenShowEditAtBrowserModal( + 'delete', + phase, + at.name, + browser.name + ); + }} + > + + + Remove + + + + + ); + } + )} + +
]} - onClick={[onManageAtsClick, onAddTestPlansClick]} - expanded={[showManageATs, showAddTestPlans]} + onClick={[ + onManageAtsClick, + onAddTestPlansClick, + onManageReqReportsClick + ]} + expanded={[ + showManageATs, + showAddTestPlans, + showManageReqReports + ]} stacked /> - {showAtVersionModal && ( )} - {showThemedModal && ( )} - {showFeedbackModal && ( )} + {!showEditAtBrowserModal && ( + + {!isDelete ? ( + + + + Assistive Technology + + {updateListAtSelection === + 'Select an Assistive Technology' ? ( + + + {ats.map(item => { + return ( + + ); + })} + + ) : ( + + {ats.map(item => { + return ( + + ); + })} + + )} + + + Browser + + + {ats + .find( + at => + at.id === + updateAtSelection + ) + ?.browsers.map(item => ( + + ))} + + + + ) : null} + + } + actionLabel={actionButtonLabel} + handleAction={() => { + if (actionButtonLabel === 'Save Changes') { + runMutationForRequiredReportTable( + 'updateRequiredReport' + ); + } + if (actionButtonLabel === 'Confirm Delete') { + runMutationForRequiredReportTable( + 'deleteRequiredReport' + ); + } + setUpdateAtSelection('Select an Assistive Technology'); + setUpdateBrowserSelection(''); + setShowEditAtBrowserModal(true); + }} + handleClose={() => { + setUpdateAtSelection('Select an Assistive Technology'); + setShowEditAtBrowserModal(true); + }} + staticBackdrop={true} + /> + )} ); }; +CustomToggle.propTypes = { + children: PropTypes.string, + onClick: PropTypes.func +}; + +CustomMenu.propTypes = { + children: PropTypes.array, + className: PropTypes.string +}; + ManageTestQueue.propTypes = { ats: PropTypes.array, browsers: PropTypes.array, testPlanVersions: PropTypes.array, + enableManageRequiredReports: PropTypes.bool, triggerUpdate: PropTypes.func }; diff --git a/client/components/ManageTestQueue/queries.js b/client/components/ManageTestQueue/queries.js new file mode 100644 index 000000000..441c1594f --- /dev/null +++ b/client/components/ManageTestQueue/queries.js @@ -0,0 +1,88 @@ +import { gql } from '@apollo/client'; + +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 + } + } + } +`; + +export const ADD_TEST_QUEUE_MUTATION = gql` + mutation AddTestPlanReport( + $testPlanVersionId: ID! + $atId: ID! + $browserId: ID! + ) { + findOrCreateTestPlanReport( + input: { + testPlanVersionId: $testPlanVersionId + atId: $atId + browserId: $browserId + } + ) { + populatedData { + testPlanReport { + id + at { + id + } + browser { + id + } + } + testPlanVersion { + id + } + } + created { + locationOfData + } + } + } +`; diff --git a/client/components/TestQueue/TestQueue.css b/client/components/TestQueue/TestQueue.css index 03d9ca34d..39c00b93d 100644 --- a/client/components/TestQueue/TestQueue.css +++ b/client/components/TestQueue/TestQueue.css @@ -95,3 +95,16 @@ table button { .add-test-plan-queue-modal-normalize-row { margin-top: auto; } + +.phase-option:hover { + text-decoration: none; + cursor: default; +} + +.drop-down-div { + height: fit-content; +} + +.drop-down-div > ul { + margin-bottom: 0; +} diff --git a/client/components/TestQueue/index.jsx b/client/components/TestQueue/index.jsx index 2fb8ef344..b7012c078 100644 --- a/client/components/TestQueue/index.jsx +++ b/client/components/TestQueue/index.jsx @@ -253,6 +253,7 @@ const TestQueue = () => { {isAdmin && ( { +const PhasePill = ({ + fullWidth = true, + forHeader = false, + children: phase +}) => { + let className = fullWidth ? 'full-width' : ''; + className = forHeader ? `${className} for-header` : className; return ( str) .join(' ')} > @@ -59,6 +68,7 @@ const PhasePill = ({ fullWidth = true, children: phase }) => { }; PhasePill.propTypes = { + forHeader: PropTypes.bool, fullWidth: PropTypes.bool, children: PropTypes.string.isRequired }; diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js index 258b8119a..e5a8c8146 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageAdminPopulatedMock.js @@ -20,6 +20,36 @@ export default testQueuePageQuery => [ name: '2021.2103.174', releasedAt: '2022-08-02T14:36:02.659Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '2', + name: 'Chrome' + } + ], + recommendedBrowsers: [ + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } ] }, { @@ -51,6 +81,36 @@ export default testQueuePageQuery => [ name: '2019.3', releasedAt: '2022-01-01T12:00:00.000Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '2', + name: 'Chrome' + } + ], + recommendedBrowsers: [ + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } ] }, { @@ -62,6 +122,36 @@ export default testQueuePageQuery => [ name: '11.5.2', releasedAt: '2022-01-01T12:00:00.000Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '3', + name: 'Safari' + } + ], + recommendedBrowsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '2', + name: 'Chrome' + } ] } ], diff --git a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js index 3c1003e8c..b00937986 100644 --- a/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js +++ b/client/tests/__mocks__/GraphQLMocks/TestQueuePageTesterPopulatedMock.js @@ -21,6 +21,36 @@ export default testQueuePageQuery => [ name: '2021.2103.174', releasedAt: '2022-08-02T14:36:02.659Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '2', + name: 'Chrome' + } + ], + recommendedBrowsers: [ + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } ] }, { @@ -52,6 +82,36 @@ export default testQueuePageQuery => [ name: '2019.3', releasedAt: '2022-01-01T12:00:00.000Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '2', + name: 'Chrome' + } + ], + recommendedBrowsers: [ + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } ] }, { @@ -63,6 +123,36 @@ export default testQueuePageQuery => [ name: '11.5.2', releasedAt: '2022-01-01T12:00:00.000Z' } + ], + browsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '1', + name: 'Firefox' + }, + { + id: '2', + name: 'Chrome' + } + ], + candidateBrowsers: [ + { + id: '3', + name: 'Safari' + } + ], + recommendedBrowsers: [ + { + id: '3', + name: 'Safari' + }, + { + id: '2', + name: 'Chrome' + } ] } ], diff --git a/server/graphql-schema.js b/server/graphql-schema.js index 169d8f4d6..65e70d4f9 100644 --- a/server/graphql-schema.js +++ b/server/graphql-schema.js @@ -171,6 +171,15 @@ const graphqlSchema = gql` recommendedBrowsers: [Browser]! } + """ + The return type for createRequiredReport. + """ + type RequiredReport { + atId: ID! + browserId: ID! + phase: RequiredReportPhase! + } + """ The version for a given assistive technology. """ @@ -1084,6 +1093,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. """ @@ -1270,6 +1290,12 @@ const graphqlSchema = gql` Get the available mutations for the given browser. """ browser(id: ID!): BrowserOperations! + + requiredReport( + atId: ID! + browserId: ID! + phase: RequiredReportPhase! + ): RequiredReportOperations! """ Adds a report with the given TestPlanVersion, AT and Browser, and a state of "DRAFT", resulting in the report appearing in the Test Queue. diff --git a/server/models/services/AtBrowserService b/server/models/services/AtBrowserService new file mode 100644 index 000000000..07a292dae --- /dev/null +++ b/server/models/services/AtBrowserService @@ -0,0 +1,27 @@ +const ModelService = require('./ModelService.js'); +const { AtBrowsers } = require('../'); +const { AT_BROWSERS_ATTRIBUTES } = require('./helpers'); + +const updateAtBrowser = async ( + { atId, browserId }, + updateParams = {}, + atBrowsersAttributes = AT_BROWSERS_ATTRIBUTES, + options = {} +) => { + await ModelService.update( + AtBrowsers, + { atId, browserId }, + updateParams, + options + ); + + return await ModelService.getByQuery( + AtBrowsers, + { atId, browserId }, + atBrowsersAttributes, + null, + options + ); +}; + +module.exports = { updateAtBrowser }; diff --git a/server/models/services/helpers.js b/server/models/services/helpers.js index 577e76120..8115844cf 100644 --- a/server/models/services/helpers.js +++ b/server/models/services/helpers.js @@ -1,5 +1,6 @@ const { At, + AtBrowsers, AtMode, AtVersion, Browser, @@ -28,6 +29,7 @@ const getSequelizeModelAttributes = model => { module.exports = { getSequelizeModelAttributes, AT_ATTRIBUTES: getSequelizeModelAttributes(At), + AT_BROWSERS_ATTRIBUTES: getSequelizeModelAttributes(AtBrowsers), AT_MODE_ATTRIBUTES: getSequelizeModelAttributes(AtMode), AT_VERSION_ATTRIBUTES: getSequelizeModelAttributes(AtVersion), BROWSER_ATTRIBUTES: getSequelizeModelAttributes(Browser), diff --git a/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js b/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js new file mode 100644 index 000000000..0039cf017 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/createRequiredReportResolver.js @@ -0,0 +1,27 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const createRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + _, + { user } +) => { + 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); + + 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..ae9f15fe9 --- /dev/null +++ b/server/resolvers/RequiredReportOperations/deleteRequiredReportResolver.js @@ -0,0 +1,27 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const deleteRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + _, + { user } +) => { + 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); + + 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..e17558d01 --- /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..7f29cb18f --- /dev/null +++ b/server/resolvers/RequiredReportOperations/updateRequiredReportResolver.js @@ -0,0 +1,41 @@ +const { AuthenticationError } = require('apollo-server'); +const { updateAtBrowser } = require('../../models/services/AtBrowserService'); + +const updateRequiredReportResolver = async ( + { parentContext: { atId, browserId, phase } }, + { atId: inputAtId, browserId: inputBrowserId }, + { user } +) => { + 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); + updateParams = { isCandidate: true }; + await updateAtBrowser( + { atId: inputAtId, browserId: inputBrowserId }, + updateParams + ); + } + if (phase === 'IS_RECOMMENDED') { + updateParams = { isRecommended: false }; + await updateAtBrowser({ atId, browserId }, updateParams); + updateParams = { isRecommended: true }; + await updateAtBrowser( + { atId: inputAtId, browserId: inputBrowserId }, + updateParams + ); + } + + return { atId: inputAtId, browserId: inputBrowserId, phase }; +}; + +module.exports = updateRequiredReportResolver; diff --git a/server/resolvers/index.js b/server/resolvers/index.js index a8fad8916..00704ad3e 100644 --- a/server/resolvers/index.js +++ b/server/resolvers/index.js @@ -14,6 +14,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'); @@ -24,6 +25,7 @@ const User = require('./User'); const AtOperations = require('./AtOperations'); const AtVersionOperations = require('./AtVersionOperations'); const BrowserOperations = require('./BrowserOperations'); +const RequiredReportOperations = require('./RequiredReportOperations'); const TestPlan = require('./TestPlan'); const TestPlanVersion = require('./TestPlanVersion'); const TestPlanReport = require('./TestPlanReport'); @@ -54,6 +56,7 @@ const resolvers = { at: mutateAt, atVersion: mutateAtVersion, browser: mutateBrowser, + requiredReport: mutateRequiredReport, testPlanReport: mutateTestPlanReport, testPlanRun: mutateTestPlanRun, testResult: mutateTestResult, @@ -65,6 +68,7 @@ const resolvers = { AtOperations, AtVersionOperations, BrowserOperations, + RequiredReportOperations, User, TestPlan, TestPlanVersion, diff --git a/server/resolvers/mutateRequiredReportResolver.js b/server/resolvers/mutateRequiredReportResolver.js new file mode 100644 index 000000000..83ce4c83e --- /dev/null +++ b/server/resolvers/mutateRequiredReportResolver.js @@ -0,0 +1,5 @@ +const mutateRequiredReportResolver = (_, { atId, browserId, phase }) => { + 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 3a7a3f8db..6db37911d 100644 --- a/server/tests/integration/graphql.test.js +++ b/server/tests/integration/graphql.test.js @@ -514,7 +514,6 @@ describe('graphql', () => { } ` ); - // console.info(queryResult); await dbCleaner(async () => { const { @@ -699,6 +698,31 @@ describe('graphql', () => { ) { username } + 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 + } + } } `, {