diff --git a/client/components/App/App.jsx b/client/components/App/App.jsx
index 143a245a3..286460186 100644
--- a/client/components/App/App.jsx
+++ b/client/components/App/App.jsx
@@ -19,7 +19,7 @@ const App = () => {
const [isNavbarExpanded, setIsNavbarExpanded] = useState(false);
const auth = evaluateAuth(data && data.me ? data.me : {});
- const { username, isSignedIn, isAdmin, isVendor } = auth;
+ const { username, isSignedIn, isAdmin, isVendor, isTester } = auth;
const signOut = async () => {
await fetch('/api/auth/signout', { method: 'POST' });
@@ -104,7 +104,7 @@ const App = () => {
)}
{isSignedIn && (
<>
- {!isVendor && (
+ {(isAdmin || isTester) && (
{
+ const { triggerLoad, loadingMessage, loadingNote } = useTriggerLoad();
+ const {
+ themedModal,
+ showThemedModal,
+ setShowThemedModal,
+ setThemedModalTitle,
+ setThemedModalContent,
+ setThemedModalType
+ } = useThemedModal({
+ type: THEMES.SUCCESS,
+ title: 'Success'
+ });
+
+ const handleImportTests = async () => {
+ await triggerLoad(
+ async () => {
+ try {
+ const response = await fetch('/api/test/import', { method: 'POST' });
+ if (!response.ok) {
+ throw new Error(
+ `Failed to import the latest Test Plan Versions: ${response.status}`
+ );
+ }
+
+ // Success
+ setThemedModalType(THEMES.SUCCESS);
+ setThemedModalTitle('Success');
+ setThemedModalContent(
+ <>The latest Test Plan Versions have been imported.>
+ );
+ setShowThemedModal(true);
+ await refetch();
+ } catch (e) {
+ // Failed, show themed message
+ setThemedModalType(THEMES.DANGER);
+ setThemedModalTitle('Error');
+ setThemedModalContent(<>{e.message}>);
+ setShowThemedModal(true);
+ }
+ },
+ 'Importing latest Test Plan Versions',
+ 'This may take a few minutes ...'
+ );
+ };
+
+ return (
+
+
+ Admin Actions
+
+
+ Date of latest test plan version:{' '}
+ {dates.convertDateToString(
+ latestTestPlanVersion?.updatedAt,
+ 'MMMM D, YYYY HH:mm z'
+ )}
+
+
+ {showThemedModal && themedModal}
+
+ );
+};
+
+AdminSettings.propTypes = {
+ latestTestPlanVersion: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ updatedAt: PropTypes.string.isRequired
+ }),
+ refetch: PropTypes.func
+};
+
+export default AdminSettings;
diff --git a/client/components/UserSettings/TesterSettings.jsx b/client/components/UserSettings/TesterSettings.jsx
new file mode 100644
index 000000000..c88695fe2
--- /dev/null
+++ b/client/components/UserSettings/TesterSettings.jsx
@@ -0,0 +1,106 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form } from 'react-bootstrap';
+import { useMutation } from '@apollo/client';
+import { TESTER_SETTINGS_QUERY, UPDATE_ME_MUTATION } from './queries';
+import { AtPropType } from '../common/proptypes';
+
+const TesterSettings = ({ ats, meAts }) => {
+ const [updateMe] = useMutation(UPDATE_ME_MUTATION, {
+ refetchQueries: [{ query: TESTER_SETTINGS_QUERY }]
+ });
+
+ const [checkedAts, setCheckedAts] = useState([]);
+
+ if (!ats || !meAts || !checkedAts) return null;
+
+ useEffect(() => {
+ if (!meAts) return;
+ setCheckedAts(meAts.map(at => at.id));
+ }, [meAts]);
+
+ const handleCheckedAt = useCallback(
+ event => {
+ const atId = event.target.id;
+ const isChecked = checkedAts.includes(atId);
+ if (isChecked) {
+ setCheckedAts(checkedAts.filter(item => item !== atId));
+ } else {
+ setCheckedAts([...checkedAts, atId]);
+ }
+ },
+ [checkedAts]
+ );
+
+ const handleSave = useCallback(
+ event => {
+ event.preventDefault();
+ updateMe({ variables: { input: { atIds: checkedAts } } });
+ },
+ [checkedAts]
+ );
+
+ const savedAts = meAts.map(at => at.id);
+
+ return (
+
+ Assistive Technology Settings
+
+ {savedAts.length > 0 ? (
+
+
+ You can currently test the following assistive technologies:
+
+
+ {ats
+ .filter(({ id: atId }) => savedAts.includes(atId))
+ .map(at => (
+ -
+ {at.name}
+
+ ))}
+
+
+ ) : (
+
+ You have not yet selected any assistive technologies.
+
+ )}
+
+
+ Update the assistive technologies you can test by selecting from the
+ options below:
+
+
+ {ats?.map(at => {
+ return (
+ atId === at.id)}
+ />
+ );
+ })}
+
+
+
+
+ );
+};
+
+TesterSettings.propTypes = {
+ ats: PropTypes.arrayOf(AtPropType),
+ meAts: PropTypes.arrayOf(AtPropType)
+};
+
+export default TesterSettings;
diff --git a/client/components/UserSettings/UserSettings.jsx b/client/components/UserSettings/UserSettings.jsx
deleted file mode 100644
index 958d27f79..000000000
--- a/client/components/UserSettings/UserSettings.jsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import React, { useState, useCallback, useEffect } from 'react';
-import { Helmet } from 'react-helmet';
-import { Button, Container, Form } from 'react-bootstrap';
-import { useMutation, useQuery } from '@apollo/client';
-import PageStatus from '../common/PageStatus';
-import { CURRENT_SETTINGS_QUERY, UPDATE_ME_MUTATION } from './queries';
-
-const UserSettings = () => {
- const { loading, data, error } = useQuery(CURRENT_SETTINGS_QUERY);
-
- const [updateMe] = useMutation(UPDATE_ME_MUTATION, {
- refetchQueries: [{ query: CURRENT_SETTINGS_QUERY }]
- });
-
- const [checkedAts, setCheckedAts] = useState(undefined);
-
- const handleCheckedAt = useCallback(
- event => {
- const atId = event.target.id;
- const isChecked = checkedAts.includes(atId);
- if (isChecked) {
- setCheckedAts(checkedAts.filter(item => item !== atId));
- } else {
- setCheckedAts([...checkedAts, atId]);
- }
- },
- [checkedAts]
- );
-
- const handleSave = useCallback(
- event => {
- event.preventDefault();
- updateMe({ variables: { input: { atIds: checkedAts } } });
- },
- [checkedAts]
- );
-
- useEffect(() => {
- if (!data) return;
- setCheckedAts(data.me.ats.map(at => at.id));
- }, [data]);
-
- if (error) {
- return (
-
- );
- }
-
- if (loading) {
- return (
-
- );
- }
-
- if (!data || !checkedAts) return null;
-
- const {
- ats,
- me: { username, ats: userAtsData }
- } = data;
- const savedAts = userAtsData.map(at => at.id);
-
- return (
-
-
-
- Settings | ARIA-AT
-
- Settings
-
- User Details
-
-
- {username}
-
-
- Assistive Technology Settings
-
- {savedAts.length > 0 ? (
-
-
- You can currently test the following assistive technologies:
-
-
- {ats
- .filter(({ id: atId }) => savedAts.includes(atId))
- .map(at => (
- -
- {at.name}
-
- ))}
-
-
- ) : (
-
- You have not yet selected any assistive technologies.
-
- )}
-
-
- Update the assistive technologies you can test by selecting from the
- options below:
-
-
- {ats &&
- ats.map(at => {
- return (
- atId === at.id)}
- />
- );
- })}
-
-
-
-
-
-
- );
-};
-
-export default UserSettings;
diff --git a/client/components/UserSettings/index.js b/client/components/UserSettings/index.js
index 48df754fd..f040be42d 100644
--- a/client/components/UserSettings/index.js
+++ b/client/components/UserSettings/index.js
@@ -1,3 +1,88 @@
-import UserSettings from './UserSettings';
+import React from 'react';
+import { Helmet } from 'react-helmet';
+import { Container } from 'react-bootstrap';
+import { useQuery } from '@apollo/client';
+import PageStatus from '../common/PageStatus';
+import TesterSettings from './TesterSettings';
+import AdminSettings from './AdminSettings';
+import { ME_QUERY } from '../App/queries';
+import { TESTER_SETTINGS_QUERY, ADMIN_SETTINGS_QUERY } from './queries';
+import { evaluateAuth } from '../../utils/evaluateAuth';
+
+const UserSettings = () => {
+ const { loading, data, error } = useQuery(ME_QUERY);
+
+ const auth = evaluateAuth(data?.me ? data?.me : {});
+ const { username, isAdmin, isVendor, isTester } = auth;
+
+ const {
+ data: testerQueryData,
+ loading: testerQueryLoading,
+ error: testerQueryError
+ } = useQuery(TESTER_SETTINGS_QUERY, {
+ skip: isAdmin || isVendor // no need to query if admin or vendor
+ });
+
+ const {
+ data: adminQueryData,
+ loading: adminQueryLoading,
+ error: adminQueryError,
+ refetch: adminQueryRefetch
+ } = useQuery(ADMIN_SETTINGS_QUERY, {
+ skip: !isAdmin // no need to query if not admin
+ });
+
+ if (error || testerQueryError || adminQueryError) {
+ return (
+
+ );
+ }
+
+ if (loading || testerQueryLoading || adminQueryLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Settings | ARIA-AT
+
+ Settings
+
+ {!isAdmin && isTester && !isVendor && (
+
+ )}
+ {isAdmin && (
+
+ )}
+
+
+ );
+};
export default UserSettings;
diff --git a/client/components/UserSettings/queries.js b/client/components/UserSettings/queries.js
index 016588602..7122f2b85 100644
--- a/client/components/UserSettings/queries.js
+++ b/client/components/UserSettings/queries.js
@@ -1,7 +1,7 @@
import { gql } from '@apollo/client';
import { AT_FIELDS, ME_FIELDS } from '@components/common/fragments';
-export const CURRENT_SETTINGS_QUERY = gql`
+export const TESTER_SETTINGS_QUERY = gql`
${AT_FIELDS}
${ME_FIELDS}
query CurrentSettings {
@@ -17,6 +17,15 @@ export const CURRENT_SETTINGS_QUERY = gql`
}
`;
+export const ADMIN_SETTINGS_QUERY = gql`
+ query AdminSettings {
+ latestTestPlanVersion {
+ id
+ updatedAt
+ }
+ }
+`;
+
export const UPDATE_ME_MUTATION = gql`
${AT_FIELDS}
mutation UpdateMe($input: UserInput) {
diff --git a/client/components/common/BasicThemedModal/index.jsx b/client/components/common/BasicThemedModal/index.jsx
index 5c53c5edb..4dbf8b1f2 100644
--- a/client/components/common/BasicThemedModal/index.jsx
+++ b/client/components/common/BasicThemedModal/index.jsx
@@ -2,9 +2,13 @@ import React, { useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+import {
+ faCheckCircle,
+ faExclamationTriangle
+} from '@fortawesome/free-solid-svg-icons';
import styled from '@emotion/styled';
import { uniqueId } from 'lodash';
+import { THEMES, THEME_COLOR } from '@client/hooks/useThemedModal';
import { ActionButtonPropType } from '../proptypes';
const ModalTitleStyle = styled.h1`
@@ -25,9 +29,7 @@ const ColorStrip = styled.div`
width: 100%;
height: 10px;
${props => props.hideHeadline && `display: none;`}
- background-color: ${({ theme }) =>
- theme === 'danger' ? '#ce1b4c' : '#fab700'};
-
+ background-color: ${({ theme }) => THEME_COLOR(theme)};
border-top-left-radius: calc(0.3rem - 1px);
border-top-right-radius: calc(0.3rem - 1px);
`;
@@ -36,7 +38,7 @@ const BasicThemedModal = ({
show = false,
centered = false,
animation = true,
- theme = 'warning', // warning, danger
+ theme = THEMES.WARNING, // warning, danger, success
dialogClassName = '',
title = null,
content = null,
@@ -78,9 +80,13 @@ const BasicThemedModal = ({
>
{title}
@@ -103,7 +109,7 @@ const BasicThemedModal = ({
{actionButtons.map(({ action, text }) => (
+
+ Settings
+