diff --git a/src/accessibility-page/AccessibilityBody/AccessibilityBody.jsx b/src/accessibility-page/AccessibilityBody/AccessibilityBody.jsx new file mode 100644 index 0000000000..40da33c91c --- /dev/null +++ b/src/accessibility-page/AccessibilityBody/AccessibilityBody.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Hyperlink, MailtoLink, Stack } from '@openedx/paragon'; + +import messages from './messages'; + +const AccessibilityBody = ({ + communityAccessibilityLink, + email, +}) => ( +
+
+

+ +

+
+ +
+ + Website Accessibility Policy + + ), + }} + /> +
+
+ +
+
    +
  1. + + {email} + + ), + }} + /> +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
  2. +
  3. + +
  4. +
  5. + +
  6. +
+
+ + {email} + + ), + }} + /> +
+
+
+); + +AccessibilityBody.propTypes = { + communityAccessibilityLink: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, +}; + +export default injectIntl(AccessibilityBody); diff --git a/src/accessibility-page/AccessibilityBody/AccessibilityBody.test.jsx b/src/accessibility-page/AccessibilityBody/AccessibilityBody.test.jsx new file mode 100644 index 0000000000..bcb5f3c498 --- /dev/null +++ b/src/accessibility-page/AccessibilityBody/AccessibilityBody.test.jsx @@ -0,0 +1,46 @@ +import { + render, + screen, +} from '@testing-library/react'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import initializeStore from '../../store'; + +import AccessibilityBody from './index'; + +let store; + +const renderComponent = () => { + render( + + + + + , + ); +}; + +describe('', () => { + describe('renders', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: false, + roles: [], + }, + }); + store = initializeStore({}); + }); + it('contains links', () => { + renderComponent(); + expect(screen.getAllByTestId('email-element')).toHaveLength(2); + expect(screen.getAllByTestId('accessibility-page-link')).toHaveLength(1); + }); + }); +}); diff --git a/src/accessibility-page/AccessibilityBody/index.js b/src/accessibility-page/AccessibilityBody/index.js new file mode 100644 index 0000000000..a46720ac60 --- /dev/null +++ b/src/accessibility-page/AccessibilityBody/index.js @@ -0,0 +1,3 @@ +import AccessibilityBody from './AccessibilityBody'; + +export default AccessibilityBody; diff --git a/src/accessibility-page/AccessibilityBody/messages.js b/src/accessibility-page/AccessibilityBody/messages.js new file mode 100644 index 0000000000..78384b80ee --- /dev/null +++ b/src/accessibility-page/AccessibilityBody/messages.js @@ -0,0 +1,111 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + a11yBodyPolicyLink: { + id: 'a11yBodyPolicyLink', + defaultMessage: 'Website Accessibility Policy', + description: 'Title for link to full accessibility policy.', + }, + a11yBodyPageHeader: { + id: 'a11yBodyPageHeader', + defaultMessage: 'Individualized Accessibility Process for Course Creators', + description: 'Heading for studio\'s accessibility policy page.', + }, + a11yBodyIntroGraph: { + id: 'a11yBodyIntroGraph', + defaultMessage: `At edX, we seek to understand and respect the unique needs and perspectives of the edX global community. + We value every course team and are committed to expanding access to all, including course team creators and authors with + disabilities. To that end, we have adopted a {communityAccessibilityLink} and this process to allow course team creators + and authors to request assistance if they are unable to develop and post content on our platform via Studio because of their + disabilities.`, + description: 'Introductory paragraph outlining why we care about accessibility, and what we\'re doing about it.', + }, + a11yBodyStepsHeader: { + id: 'a11yBodyStepsHeader', + defaultMessage: 'Course team creators and authors needing such assistance should take the following steps:', + description: 'Heading for list of steps authors can take for accessibility requests.', + }, + a11yBodyEdxResponse: { + id: 'a11yBodyEdxResponse', + defaultMessage: `'We will communicate with you about your preferences and needs in determining the appropriate solution, although + the ultimate decision will be ours, provided that the solution is effective and timely. The factors we will consider in choosing + an accessibility solution are: effectiveness; timeliness (relative to your deadlines); ease of implementation; and ease of use for + you. We will notify you of the decision and explain the basis for our decision within 10 business days of discussing with you.`, + description: 'Paragraph outlining how we will select an accessibility solution.', + }, + a11yBodyEdxFollowUp: { + id: 'a11yBodyEdxFollowUp', + defaultMessage: `Thereafter, we will communicate with you on a weekly basis regarding our evaluation, decision, and progress in + implementing the accessibility solution. We will notify you when implementation of your accessibility solution is complete and + will follow-up with you as may be necessary to see if the solution was effective.`, + description: 'Paragraph outlining how we will follow-up with you during and after implementing an accessibility solution.', + }, + a11yBodyOngoingSupport: { + id: 'a11yBodyOngoingSupport', + defaultMessage: 'EdX will provide ongoing technical support as needed and will address any additional issues that arise after the initial course creation.', + description: 'A statement of ongoing support.', + }, + a11yBodyA11yFeedback: { + id: 'a11yBodyA11yFeedback', + defaultMessage: 'Please direct any questions or suggestions on how to improve the accessibility of Studio to {emailElement} or use the form below. We welcome your feedback.', + description: 'Contact information heading for those with accessibility issues or suggestions.', + }, + a11yBodyEmailHeading: { + id: 'a11yBodyEmailHeading', + defaultMessage: 'Send an email to {emailElement} with the following information:', + description: 'Heading for list of information required when you email us.', + }, + a11yBodyNameEmail: { + id: 'a11yBodyNameEmail', + defaultMessage: 'your name and email address;', + description: 'Your contact information.', + }, + a11yBodyInstitution: { + id: 'a11yBodyInstitution', + defaultMessage: 'the edX member institution that you are affiliated with;', + description: 'edX affiliate information.', + }, + a11yBodyBarrier: { + id: 'a11yBodyBarrier', + defaultMessage: 'a brief description of the challenge or barrier to access that you are experiencing; and', + description: 'Accessibility problem information.', + }, + a11yBodyTimeConstraints: { + id: 'a11yBodyTimeConstraints', + defaultMessage: 'how soon you need access and for how long (e.g., a planned course start date or in connection with a course-related deadline such as a final essay).', + description: 'Time contstraint information.', + }, + a11yBodyReceipt: { + id: 'a11yBodyReceipt', + defaultMessage: 'The edX Support Team will respond to confirm receipt and forward your request to the edX Partner Manager for your institution and the edX Website Accessibility Specialist.', + description: 'Paragraph outlining what steps edX will take immediately.', + }, + a11yBodyExtraInfo: { + id: 'a11yBodyExtraInfo', + defaultMessage: `With guidance from the Website Accessibility Specialist, edX will contact you to discuss your request and gather + additional information from you about your preferences and needs, to determine if there's a workable solution that edX is able to support.`, + description: 'Paragraph outlining how and when edX will reach out to you.', + }, + a11yBodyFixesListHeader: { + id: 'a11yBodyFixesListHeader', + defaultMessage: 'EdX will assist you promptly and thoroughly so that you are able to create content on the CMS within your time constraints. Such efforts may include, but are not limited to:', + description: 'Heading for list of ways we might be able to assist.', + }, + a11yBodyThirdParty: { + id: 'a11yBodyThirdParty', + defaultMessage: 'Purchasing a third-party tool or software for use on an individual basis to assist your use of Studio;', + description: 'Buy third-party software.', + }, + a11yBodyContractor: { + id: 'a11yBodyContractor', + defaultMessage: 'Engaging a trained independent contractor to provide real-time visual, verbal and physical assistance; or', + description: 'Hire a contractor.', + }, + a11yBodyCodeFix: { + id: 'a11yBodyCodeFix', + defaultMessage: 'Developing new code to implement a technical fix.', + description: 'Make a technical fix.', + }, +}); + +export default messages; diff --git a/src/accessibility-page/AccessibilityForm/AccessibilityForm.jsx b/src/accessibility-page/AccessibilityForm/AccessibilityForm.jsx new file mode 100644 index 0000000000..19587e2a7e --- /dev/null +++ b/src/accessibility-page/AccessibilityForm/AccessibilityForm.jsx @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + injectIntl, FormattedMessage, intlShape, FormattedDate, FormattedTime, +} from '@edx/frontend-platform/i18n'; +import { + ActionRow, Alert, Form, Stack, StatefulButton, +} from '@openedx/paragon'; + +import { RequestStatus } from '../../data/constants'; +import { STATEFUL_BUTTON_STATES } from '../../constants'; +import submitAccessibilityForm from '../data/thunks'; +import useAccessibility from './hooks'; +import messages from './messages'; + +const AccessibilityForm = ({ + accessibilityEmail, + // injected + intl, +}) => { + const { + errors, + values, + isFormFilled, + dispatch, + handleBlur, + handleChange, + hasErrorField, + savingStatus, + } = useAccessibility({ name: '', email: '', message: '' }, intl); + + const formFields = [ + { + label: intl.formatMessage(messages.accessibilityPolicyFormEmailLabel), + name: 'email', + value: values.email, + }, + { + label: intl.formatMessage(messages.accessibilityPolicyFormNameLabel), + name: 'name', + value: values.name, + }, + { + label: intl.formatMessage(messages.accessibilityPolicyFormMessageLabel), + name: 'message', + value: values.message, + }, + ]; + + const createButtonState = { + labels: { + default: intl.formatMessage(messages.accessibilityPolicyFormSubmitLabel), + pending: intl.formatMessage(messages.accessibilityPolicyFormSubmittingFeedbackLabel), + }, + disabledStates: [STATEFUL_BUTTON_STATES.pending], + }; + + const handleSubmit = () => { + dispatch(submitAccessibilityForm(values)); + }; + + const start = new Date('Mon Jan 29 2018 13:00:00 GMT (UTC)'); + const end = new Date('Fri Feb 2 2018 21:00:00 GMT (UTC)'); + + return ( + <> +

+ +

+ {savingStatus === RequestStatus.SUCCESSFUL && ( + + +
+ +
+
+ ), + time_start: (), + day_end: (), + time_end: (), + }} + /> +
+
+
+ )} + {savingStatus === RequestStatus.FAILED && ( + +
+ {accessibilityEmail}, + }} + /> +
+
+ )} +
+ {formFields.map((field) => ( + + + {hasErrorField(field.name) && ( + + {errors[field.name]} + + )} + + ))} +
+ + + + + ); +}; + +AccessibilityForm.propTypes = { + accessibilityEmail: PropTypes.string.isRequired, + // injected + intl: intlShape.isRequired, +}; + +export default injectIntl(AccessibilityForm); diff --git a/src/accessibility-page/AccessibilityForm/AccessibilityForm.test.jsx b/src/accessibility-page/AccessibilityForm/AccessibilityForm.test.jsx new file mode 100644 index 0000000000..cc15f81260 --- /dev/null +++ b/src/accessibility-page/AccessibilityForm/AccessibilityForm.test.jsx @@ -0,0 +1,164 @@ +import { + render, + act, + screen, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { initializeMockApp } from '@edx/frontend-platform'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import initializeStore from '../../store'; +import { RequestStatus } from '../../data/constants'; + +import AccessibilityForm from './index'; +import { getZendeskrUrl } from '../data/api'; +import messages from './messages'; + +let axiosMock; +let store; + +const defaultProps = { + accessibilityEmail: 'accessibilityTest@test.com', +}; + +const initialState = { + accessibilityPage: { + savingStatus: '', + }, +}; + +const renderComponent = () => { + render( + + + + + , + ); +}; + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: false, + roles: [], + }, + }); + store = initializeStore(initialState); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + describe('renders', () => { + beforeEach(() => { + renderComponent(); + }); + + it('correct number of form fields', () => { + const formSections = screen.getAllByRole('textbox'); + const formButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage); + expect(formSections).toHaveLength(3); + expect(formButton).toBeVisible(); + }); + + it('hides StatusAlert on initial load', () => { + expect(screen.queryAllByRole('alert')).toHaveLength(0); + }); + }); + + describe('statusAlert', () => { + let formSections; + let submitButton; + beforeEach(async () => { + renderComponent(); + formSections = screen.getAllByRole('textbox'); + await act(async () => { + userEvent.type(formSections[0], 'email@email.com'); + userEvent.type(formSections[1], 'test name'); + userEvent.type(formSections[2], 'feedback message'); + }); + submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage); + }); + + it('shows correct success message', async () => { + axiosMock.onPost(getZendeskrUrl()).reply(200); + await act(async () => { + userEvent.click(submitButton); + }); + const { savingStatus } = store.getState().accessibilityPage; + expect(savingStatus).toEqual(RequestStatus.SUCCESSFUL); + + expect(screen.getAllByRole('alert')).toHaveLength(1); + + expect(screen.getByText(messages.accessibilityPolicyFormSuccess.defaultMessage)).toBeVisible(); + + formSections.forEach(input => { + expect(input.value).toBe(''); + }); + }); + + it('shows correct rate limiting message', async () => { + axiosMock.onPost(getZendeskrUrl()).reply(429); + await act(async () => { + userEvent.click(submitButton); + }); + const { savingStatus } = store.getState().accessibilityPage; + expect(savingStatus).toEqual(RequestStatus.FAILED); + + expect(screen.getAllByRole('alert')).toHaveLength(1); + + expect(screen.getByTestId('rate-limit-alert')).toBeVisible(); + + formSections.forEach(input => { + expect(input.value).not.toBe(''); + }); + }); + }); + + describe('input validation', () => { + let formSections; + let submitButton; + beforeEach(async () => { + renderComponent(); + formSections = screen.getAllByRole('textbox'); + await act(async () => { + userEvent.type(formSections[0], 'email@email.com'); + userEvent.type(formSections[1], 'test name'); + userEvent.type(formSections[2], 'feedback message'); + }); + submitButton = screen.getByText(messages.accessibilityPolicyFormSubmitLabel.defaultMessage); + }); + + it('adds validation checking on each input field', async () => { + await act(async () => { + userEvent.clear(formSections[0]); + userEvent.clear(formSections[1]); + userEvent.clear(formSections[2]); + }); + const emailError = screen.getByTestId('error-feedback-email'); + expect(emailError).toBeVisible(); + + const fullNameError = screen.getByTestId('error-feedback-email'); + expect(fullNameError).toBeVisible(); + + const messageError = screen.getByTestId('error-feedback-message'); + expect(messageError).toBeVisible(); + }); + + it('sumbit button is disabled when trying to submit with all empty fields', async () => { + await act(async () => { + userEvent.clear(formSections[0]); + userEvent.clear(formSections[1]); + userEvent.clear(formSections[2]); + userEvent.click(submitButton); + }); + + expect(submitButton.closest('button')).toBeDisabled(); + }); + }); +}); diff --git a/src/accessibility-page/AccessibilityForm/hooks.js b/src/accessibility-page/AccessibilityForm/hooks.js new file mode 100644 index 0000000000..c96e21181d --- /dev/null +++ b/src/accessibility-page/AccessibilityForm/hooks.js @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; + +import { RequestStatus } from '../../data/constants'; +import messages from './messages'; + +const useAccessibility = (initialValues, intl) => { + const dispatch = useDispatch(); + const savingStatus = useSelector(state => state.accessibilityPage.savingStatus); + const [isFormFilled, setFormFilled] = useState(false); + const validationSchema = Yup.object().shape({ + name: Yup.string().required( + intl.formatMessage(messages.accessibilityPolicyFormValidName), + ), + email: Yup.string() + .email(intl.formatMessage(messages.accessibilityPolicyFormValidEmail)) + .required(intl.formatMessage(messages.accessibilityPolicyFormValidEmail)), + message: Yup.string().required( + intl.formatMessage(messages.accessibilityPolicyFormValidMessage), + ), + }); + + const { + values, errors, touched, handleChange, handleBlur, handleReset, + } = useFormik({ + initialValues, + enableReinitialize: true, + validateOnBlur: false, + validationSchema, + }); + + useEffect(() => { + setFormFilled(Object.values(values).every((i) => i)); + }, [values]); + + useEffect(() => { + if (savingStatus === RequestStatus.SUCCESSFUL) { + handleReset(); + } + }, [savingStatus]); + + const hasErrorField = (fieldName) => !!errors[fieldName] && !!touched[fieldName]; + + return { + errors, + values, + isFormFilled, + dispatch, + handleBlur, + handleChange, + hasErrorField, + savingStatus, + }; +}; + +export default useAccessibility; diff --git a/src/accessibility-page/AccessibilityForm/index.js b/src/accessibility-page/AccessibilityForm/index.js new file mode 100644 index 0000000000..a925bed87c --- /dev/null +++ b/src/accessibility-page/AccessibilityForm/index.js @@ -0,0 +1,3 @@ +import AccessibilityForm from './AccessibilityForm'; + +export default AccessibilityForm; diff --git a/src/accessibility-page/AccessibilityForm/messages.js b/src/accessibility-page/AccessibilityForm/messages.js new file mode 100644 index 0000000000..8f8513dc79 --- /dev/null +++ b/src/accessibility-page/AccessibilityForm/messages.js @@ -0,0 +1,76 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + accessibilityPolicyFormEmailLabel: { + id: 'accessibilityPolicyFormEmailLabel', + defaultMessage: 'Email Address', + description: 'Label for the email form field', + }, + accessibilityPolicyFormErrorHighVolume: { + id: 'accessibilityPolicyFormErrorHighVolume', + defaultMessage: 'We are currently experiencing high volume. Try again later today or send an email message to {emailLink}.', + description: 'Error message when site is experiencing high volume that will include an email link', + }, + accessibilityPolicyFormErrorMissingFields: { + id: 'accessibilityPolicyFormErrorMissingFields', + defaultMessage: 'Make sure to fill in all fields.', + description: 'Error message to instruct user to fill in all fields', + }, + accessibilityPolicyFormHeader: { + id: 'accessibilityPolicyFormHeader', + defaultMessage: 'Studio Accessibility Feedback', + description: 'The heading for the form', + }, + accessibilityPolicyFormMessageLabel: { + id: 'accessibilityPolicyFormMessageLabel', + defaultMessage: 'Message', + description: 'Label for the message form field', + }, + accessibilityPolicyFormNameLabel: { + id: 'accessibilityPolicyFormNameLabel', + defaultMessage: 'Name', + description: 'Label for the name form field', + }, + accessibilityPolicyFormSubmitAria: { + id: 'accessibilityPolicyFormSubmitAria', + defaultMessage: 'Submit Accessibility Feedback Form', + description: 'Detailed aria-label for the submit button', + }, + accessibilityPolicyFormSubmitLabel: { + id: 'accessibilityPolicyFormSubmitLabel', + defaultMessage: 'Submit', + description: 'General label for the submit button', + }, + accessibilityPolicyFormSubmittingFeedbackLabel: { + id: 'accessibilityPolicyFormSubmittingFeedbackLabel', + defaultMessage: 'Submitting', + description: 'Loading message while form feedback is being submitted', + }, + accessibilityPolicyFormSuccess: { + id: 'accessibilityPolicyFormSuccess', + defaultMessage: 'Thank you for contacting edX!', + description: 'Simple thank you message when form submission is successful', + }, + accessibilityPolicyFormSuccessDetails: { + id: 'accessibilityPolicyFormSuccessDetails', + defaultMessage: 'Thank you for your feedback regarding the accessibility of Studio. We typically respond within one business day ({day_start} to {day_end}, {time_start} to {time_end}).', + description: 'Detailed thank you message when form submission is successful', + }, + accessibilityPolicyFormValidEmail: { + id: 'accessibilityPolicyFormValidEmail', + defaultMessage: 'Enter a valid email address.', + description: 'Error message for when an invalid email is entered into the form', + }, + accessibilityPolicyFormValidMessage: { + id: 'accessibilityPolicyFormValidMessage', + defaultMessage: 'Enter a message.', + description: 'Error message an invalid message is entered into the form', + }, + accessibilityPolicyFormValidName: { + id: 'accessibilityPolicyFormValidName', + defaultMessage: 'Enter a name.', + description: 'Error message an invalid name is entered into the form', + }, +}); + +export default messages; diff --git a/src/accessibility-page/AccessibilityPage.jsx b/src/accessibility-page/AccessibilityPage.jsx new file mode 100644 index 0000000000..d3dd1c99a9 --- /dev/null +++ b/src/accessibility-page/AccessibilityPage.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { Helmet } from 'react-helmet'; +import { Container } from '@openedx/paragon'; +import { StudioFooter } from '@edx/frontend-component-footer'; + +import Header from '../header'; +import messages from './messages'; +import AccessibilityBody from './AccessibilityBody'; +import AccessibilityForm from './AccessibilityForm'; + +const AccessibilityPage = ({ + // injected + intl, +}) => { + const communityAccessibilityLink = 'https://www.edx.org/accessibility'; + const email = 'accessibility@edx.org'; + return ( + <> + + + {intl.formatMessage(messages.pageTitle, { + siteName: process.env.SITE_NAME, + })} + + +
+ + + + + + + ); +}; + +AccessibilityPage.propTypes = { + // injected + intl: intlShape.isRequired, +}; + +export default injectIntl(AccessibilityPage); diff --git a/src/accessibility-page/AccessibilityPage.test.jsx b/src/accessibility-page/AccessibilityPage.test.jsx new file mode 100644 index 0000000000..f686daf4d5 --- /dev/null +++ b/src/accessibility-page/AccessibilityPage.test.jsx @@ -0,0 +1,46 @@ +import { + render, + screen, +} from '@testing-library/react'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import initializeStore from '../store'; +import AccessibilityPage from './index'; + +const initialState = { + accessibilityPage: { + status: {}, + }, +}; +let store; + +const renderComponent = () => { + render( + + + + + , + ); +}; + +describe('', () => { + describe('renders', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: false, + roles: [], + }, + }); + store = initializeStore(initialState); + }); + it('contains the policy body', () => { + renderComponent(); + expect(screen.getByText('Individualized Accessibility Process for Course Creators')).toBeVisible(); + }); + }); +}); diff --git a/src/accessibility-page/data/api.js b/src/accessibility-page/data/api.js new file mode 100644 index 0000000000..7381384b0f --- /dev/null +++ b/src/accessibility-page/data/api.js @@ -0,0 +1,28 @@ +import { ensureConfig, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +ensureConfig([ + 'STUDIO_BASE_URL', +], 'Course Apps API service'); + +export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +export const getZendeskrUrl = () => `${getApiBaseUrl()}/zendesk_proxy/v0`; + +/** + * Posts the form data to zendesk endpoint + * @param {string} courseId + * @returns {Promise<[{}]>} + */ +export async function postAccessibilityForm({ name, email, message }) { + const data = { + name, + tags: ['studio_a11y'], + email: { + from: email, + subject: 'Studio Accessibility Request', + message, + }, + }; + + await getAuthenticatedHttpClient().post(getZendeskrUrl(), data); +} diff --git a/src/accessibility-page/data/slice.js b/src/accessibility-page/data/slice.js new file mode 100644 index 0000000000..7d90356f1f --- /dev/null +++ b/src/accessibility-page/data/slice.js @@ -0,0 +1,23 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +const slice = createSlice({ + name: 'accessibilityPage', + initialState: { + savingStatus: '', + }, + reducers: { + updateSavingStatus: (state, { payload }) => { + state.savingStatus = payload.status; + }, + }, +}); + +export const { + updateLoadingStatus, + updateSavingStatus, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/accessibility-page/data/thunks.js b/src/accessibility-page/data/thunks.js new file mode 100644 index 0000000000..b6b2d121ad --- /dev/null +++ b/src/accessibility-page/data/thunks.js @@ -0,0 +1,22 @@ +import { RequestStatus } from '../../data/constants'; +import { postAccessibilityForm } from './api'; +import { updateSavingStatus } from './slice'; + +function submitAccessibilityForm({ email, name, message }) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS })); + + try { + await postAccessibilityForm({ email, name, message }); + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + if (error.response && error.response.status === 429) { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } else { + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } + } + }; +} + +export default submitAccessibilityForm; diff --git a/src/accessibility-page/index.js b/src/accessibility-page/index.js new file mode 100644 index 0000000000..089d0cbbf9 --- /dev/null +++ b/src/accessibility-page/index.js @@ -0,0 +1,3 @@ +import AccessibilityPage from './AccessibilityPage'; + +export default AccessibilityPage; diff --git a/src/accessibility-page/messages.js b/src/accessibility-page/messages.js new file mode 100644 index 0000000000..6b97fb96e9 --- /dev/null +++ b/src/accessibility-page/messages.js @@ -0,0 +1,10 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + pageTitle: { + id: 'course-authoring.import.page.title', + defaultMessage: 'Studio Accessibility Policy| {siteName}', + }, +}); + +export default messages; diff --git a/src/index.jsx b/src/index.jsx index 39c682cb66..a68c1f1f4d 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -26,6 +26,7 @@ import { StudioHome } from './studio-home'; import CourseRerun from './course-rerun'; import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy'; import { ContentTagsDrawer } from './content-tags-drawer'; +import AccessibilityPage from './accessibility-page'; import 'react-datepicker/dist/react-datepicker.css'; import './index.scss'; @@ -53,6 +54,9 @@ const App = () => { } /> } /> } /> + {getConfig().SHOW_ACCESSIBILITY_PAGE === 'true' && ( + } /> + )} {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( <> {/* TODO: remove this redirect once Studio's link is updated */} diff --git a/src/store.js b/src/store.js index 2d36dcf454..76864e93f3 100644 --- a/src/store.js +++ b/src/store.js @@ -24,6 +24,7 @@ import { reducer as courseImportReducer } from './import-page/data/slice'; import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice'; import { reducer as courseOutlineReducer } from './course-outline/data/slice'; import { reducer as courseUnitReducer } from './course-unit/data/slice'; +import { reducer as accessibilityPageReducer } from './accessibility-page/data/slice'; export default function initializeStore(preloadedState = undefined) { return configureStore({ @@ -49,6 +50,7 @@ export default function initializeStore(preloadedState = undefined) { videos: videosReducer, courseOutline: courseOutlineReducer, courseUnit: courseUnitReducer, + accessibilityPage: accessibilityPageReducer, }, preloadedState, });