diff --git a/src/components/FileUpload/UploadConfirmModal.jsx b/src/components/FileUpload/UploadConfirmModal.jsx
new file mode 100644
index 00000000..8f537a1a
--- /dev/null
+++ b/src/components/FileUpload/UploadConfirmModal.jsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Form, FormLabel, ModalDialog, Button, ActionRow,
+} from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+import { useUploadConfirmModalHooks } from './hooks';
+
+const UploadConfirmModal = ({
+ open, files, closeHandler, uploadHandler,
+}) => {
+ const { formatMessage } = useIntl();
+
+ const {
+ errors, exitHandler, confirmUploadClickHandler, onFileDescriptionChange,
+ } = useUploadConfirmModalHooks({
+ files,
+ closeHandler,
+ uploadHandler,
+ });
+
+ return (
+
+
+
+ {formatMessage(messages.uploadFileModalTitle)}
+
+
+
+
+
+ {files.map((file, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ {formatMessage(messages.uploadFileDescriptionFieldLabel)}
+
+ {file.name}
+
+
+ {errors[i] && (
+
+ {errors[i] && formatMessage(messages.fileDescriptionMissingError)}
+
+ )}
+
+ ))}
+
+
+
+
+
+ {formatMessage(messages.cancelUploadFileButton)}
+
+
+ {formatMessage(messages.confirmUploadFileButton)}
+
+
+
+
+ );
+};
+
+UploadConfirmModal.defaultProps = {
+ open: false,
+ files: [],
+ closeHandler: () => {},
+ uploadHandler: () => {},
+};
+UploadConfirmModal.propTypes = {
+ open: PropTypes.bool,
+ files: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string,
+ description: PropTypes.string,
+ }),
+ ),
+ closeHandler: PropTypes.func,
+ uploadHandler: PropTypes.func,
+};
+
+export default UploadConfirmModal;
diff --git a/src/components/FileUpload/UploadConfirmModal.test.jsx b/src/components/FileUpload/UploadConfirmModal.test.jsx
new file mode 100644
index 00000000..ff56839d
--- /dev/null
+++ b/src/components/FileUpload/UploadConfirmModal.test.jsx
@@ -0,0 +1,57 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import UploadConfirmModal from './UploadConfirmModal';
+
+import { useUploadConfirmModalHooks } from './hooks';
+
+jest.mock('./hooks', () => ({
+ useUploadConfirmModalHooks: jest.fn(),
+}));
+
+describe('
', () => {
+ const props = {
+ open: true,
+ files: [],
+ closeHandler: jest.fn().mockName('closeHandler'),
+ uploadHandler: jest.fn().mockName('uploadHandler'),
+ };
+
+ const mockHooks = (overrides) => {
+ useUploadConfirmModalHooks.mockReturnValueOnce({
+ errors: [],
+ exitHandler: jest.fn().mockName('exitHandler'),
+ confirmUploadClickHandler: jest.fn().mockName('confirmUploadClickHandler'),
+ onFileDescriptionChange: () => jest.fn().mockName('onFileDescriptionChange'),
+ ...overrides,
+ });
+ };
+ describe('renders', () => {
+ test('no files', () => {
+ mockHooks();
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Group').length).toBe(0);
+ });
+
+ test('multiple files', () => {
+ mockHooks(
+ { errors: new Array(2) },
+ );
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Group').length).toBe(2);
+ expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0);
+ });
+
+ test('with errors', () => {
+ mockHooks({ errors: [true, false] });
+ const wrapper = shallow(
);
+ // wrapper.setState({ errors: [true, false] });
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Form.Group').length).toBe(2);
+ expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1);
+ });
+ });
+});
diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap
new file mode 100644
index 00000000..c8b38339
--- /dev/null
+++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders 1`] = `
+
+
+
+
+`;
diff --git a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap
new file mode 100644
index 00000000..7743f457
--- /dev/null
+++ b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap
@@ -0,0 +1,184 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders multiple files 1`] = `
+
+
+
+ Add a text description to your file
+
+
+
+
+
+
+
+ Description for:
+
+
+ file1
+
+
+
+
+
+
+
+ Description for:
+
+
+ file2
+
+
+
+
+
+
+
+
+
+ Cancel upload
+
+
+ Upload files
+
+
+
+
+`;
+
+exports[`
renders no files 1`] = `
+
+
+
+ Add a text description to your file
+
+
+
+
+
+
+
+
+ Cancel upload
+
+
+ Upload files
+
+
+
+
+`;
+
+exports[`
renders with errors 1`] = `
+
+
+
+ Add a text description to your file
+
+
+
+
+
+
+
+ Description for:
+
+
+ file1
+
+
+
+
+ Please enter a file description
+
+
+
+
+
+ Description for:
+
+
+ file2
+
+
+
+
+
+
+
+
+
+ Cancel upload
+
+
+ Upload files
+
+
+
+
+`;
diff --git a/src/components/FileUpload/__snapshots__/index.test.jsx.snap b/src/components/FileUpload/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..1056f241
--- /dev/null
+++ b/src/components/FileUpload/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders default 1`] = `
+
+
+ File Upload
+
+
+
+ Uploaded Files
+
+
+
+
+
+
+`;
+
+exports[`
renders no uploaded files 1`] = `
+
+
+ File Upload
+
+
+
+
+`;
+
+exports[`
renders read only 1`] = `
+
+
+ File Upload
+
+
+
+ Uploaded Files
+
+
+
+
+
+`;
diff --git a/src/components/FileUpload/hooks.js b/src/components/FileUpload/hooks.js
new file mode 100644
index 00000000..fec24c8c
--- /dev/null
+++ b/src/components/FileUpload/hooks.js
@@ -0,0 +1,73 @@
+import { useState, useReducer, useCallback } from 'react';
+
+export const useUploadConfirmModalHooks = ({
+ files, closeHandler, uploadHandler,
+}) => {
+ const [errors, setErrors] = useState([]);
+
+ const confirmUploadClickHandler = () => {
+ const errorList = files.map((file) => (!file.description));
+ setErrors(errorList);
+ if (errorList.some((error) => error)) {
+ return;
+ }
+ uploadHandler();
+ };
+
+ const exitHandler = () => {
+ setErrors([]);
+ closeHandler();
+ };
+
+ // Modifying pointer of file object. This is not a good practice.
+ // eslint-disable-next-line no-param-reassign, no-return-assign
+ const onFileDescriptionChange = (file) => (event) => file.description = event.target.value;
+
+ return {
+ errors,
+ confirmUploadClickHandler,
+ exitHandler,
+ onFileDescriptionChange,
+ };
+};
+
+export const useFileUploadHooks = ({
+ onFileUploaded,
+}) => {
+ const [uploadState, dispatchUploadState] = useReducer(
+ (state, payload) => ({ ...state, ...payload }),
+ {
+ onProcessUploadArgs: {},
+ openModal: false,
+ },
+ );
+
+ const confirmUpload = useCallback(async () => {
+ dispatchUploadState({ openModal: false });
+ await onFileUploaded(uploadState.onProcessUploadArgs);
+ dispatchUploadState({ onProcessUploadArgs: {} });
+ }, [uploadState, onFileUploaded]);
+
+ const closeUploadModal = useCallback(() => {
+ dispatchUploadState({ openModal: false, onProcessUploadArgs: {} });
+ }, []);
+
+ const onProcessUpload = useCallback(({ fileData, handleError, requestConfig }) => {
+ dispatchUploadState({
+ onProcessUploadArgs: { fileData, handleError, requestConfig },
+ openModal: true,
+ });
+ }, []);
+
+ return {
+ uploadState,
+ confirmUpload,
+ closeUploadModal,
+ onProcessUpload,
+ };
+};
+
+export default {
+ useUploadConfirmModalHooks,
+ useFileUploadHooks,
+};
diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx
index 02c4aa96..476355db 100644
--- a/src/components/FileUpload/index.jsx
+++ b/src/components/FileUpload/index.jsx
@@ -1,28 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { DataTable, Dropzone } from '@edx/paragon';
-
-import { useSubmissionResponse } from 'data/services/lms/hooks/selectors';
+import { DataTable, Dropzone } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
+
import filesize from 'filesize';
+import UploadConfirmModal from './UploadConfirmModal';
+import ActionCell from './ActionCell';
+
+import { useFileUploadHooks } from './hooks';
import messages from './messages';
-const FileUpload = ({ isReadOnly }) => {
- const { uploadedFiles } = useSubmissionResponse();
+import './styles.scss';
+
+const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => {
const { formatMessage } = useIntl();
+
+ const {
+ uploadState,
+ confirmUpload,
+ closeUploadModal,
+ onProcessUpload,
+ } = useFileUploadHooks({
+ onFileUploaded,
+ });
+
return (
File Upload
- {uploadedFiles && (
+ {uploadedFiles.length > 0 && (
<>
Uploaded Files
({
+ data={uploadedFiles.map((file) => ({
...file,
- size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown',
+ size:
+ typeof file.size === 'number' ? filesize(file.size) : 'Unknown',
}))}
columns={[
{
@@ -37,20 +52,46 @@ const FileUpload = ({ isReadOnly }) => {
Header: formatMessage(messages.fileSizeTitle),
accessor: 'fileSize',
},
+ {
+ Header: formatMessage(messages.fileActionsTitle),
+ accessor: 'actions',
+ Cell: ActionCell,
+ },
]}
/>
>
)}
- {!isReadOnly && }
+ {!isReadOnly && (
+
+ )}
+
);
};
FileUpload.defaultProps = {
isReadOnly: false,
+ uploadedFiles: [],
};
FileUpload.propTypes = {
isReadOnly: PropTypes.bool,
+ uploadedFiles: PropTypes.arrayOf(
+ PropTypes.shape({
+ fileDescription: PropTypes.string,
+ fileName: PropTypes.string,
+ fileSize: PropTypes.number,
+ }),
+ ),
+ onFileUploaded: PropTypes.func.isRequired,
};
export default FileUpload;
diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx
new file mode 100644
index 00000000..62407c82
--- /dev/null
+++ b/src/components/FileUpload/index.test.jsx
@@ -0,0 +1,69 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import FileUpload from '.';
+
+import { useFileUploadHooks } from './hooks';
+
+jest.mock('./hooks', () => ({
+ useFileUploadHooks: jest.fn(),
+}));
+
+jest.mock('./UploadConfirmModal', () => 'UploadConfirmModal');
+jest.mock('./ActionCell', () => 'ActionCell');
+
+describe('
', () => {
+ const props = {
+ isReadOnly: false,
+ uploadedFiles: [
+ {
+ fileName: 'file1',
+ fileDescription: 'file1 desc',
+ fileSize: 100,
+ },
+ {
+ fileName: 'file2',
+ fileDescription: 'file2 desc',
+ fileSize: 200,
+ },
+ ],
+ onFileUploaded: jest.fn(),
+ };
+
+ const mockHooks = (overrides) => {
+ useFileUploadHooks.mockReturnValueOnce({
+ uploadState: {
+ onProcessUploadArgs: {},
+ openModal: false,
+ },
+ confirmUpload: jest.fn().mockName('confirmUpload'),
+ closeUploadModal: jest.fn().mockName('closeUploadModal'),
+ onProcessUpload: jest.fn().mockName('onProcessUpload'),
+ ...overrides,
+ });
+ };
+ describe('renders', () => {
+ test('default', () => {
+ mockHooks();
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Dropzone')).toHaveLength(1);
+ expect(wrapper.instance.findByType('DataTable')).toHaveLength(1);
+ });
+
+ test('read only', () => {
+ mockHooks({ isReadOnly: true });
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Dropzone')).toHaveLength(0);
+ });
+
+ test('no uploaded files', () => {
+ mockHooks();
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('DataTable')).toHaveLength(0);
+ });
+ });
+});
diff --git a/src/components/FileUpload/messages.js b/src/components/FileUpload/messages.js
index 8a4164bd..a3e06213 100644
--- a/src/components/FileUpload/messages.js
+++ b/src/components/FileUpload/messages.js
@@ -16,6 +16,46 @@ const messages = defineMessages({
defaultMessage: 'File Size',
description: ' title for file size',
},
+ fileActionsTitle: {
+ id: 'ora-grading.FileCellContent.fileActionsTitle',
+ defaultMessage: 'Actions',
+ description: ' title for file actions',
+ },
+ deleteButtonAltText: {
+ id: 'ora-grading.FileCellContent.deleteButtonAltText',
+ defaultMessage: 'Delete',
+ description: ' alt text for delete button',
+ },
+ previewButtonAltText: {
+ id: 'ora-grading.FileCellContent.previewButtonAltText',
+ defaultMessage: 'Preview',
+ description: ' alt text for preview button',
+ },
+ uploadFileModalTitle: {
+ id: 'ora-grading.FileCellContent.uploadFileModalTitle',
+ defaultMessage: 'Add a text description to your file',
+ description: 'Ask user to add a text description to the file',
+ },
+ uploadFileDescriptionFieldLabel: {
+ id: 'ora-grading.FileCellContent.uploadFileDescriptionFieldLabel',
+ defaultMessage: 'Description for: ',
+ description: 'Label for file description field',
+ },
+ cancelUploadFileButton: {
+ id: 'ora-grading.FileCellContent.cancelUploadFileButton',
+ defaultMessage: 'Cancel upload',
+ description: 'Label for cancel button',
+ },
+ confirmUploadFileButton: {
+ id: 'ora-grading.FileCellContent.confirmUploadFileButton',
+ defaultMessage: 'Upload files',
+ description: 'Label for upload button',
+ },
+ fileDescriptionMissingError: {
+ id: 'ora-grading.FileCellContent.fileDescriptionMissingError',
+ defaultMessage: 'Please enter a file description',
+ description: 'Error message when file description is missing',
+ },
});
export default messages;
diff --git a/src/components/FileUpload/styles.scss b/src/components/FileUpload/styles.scss
new file mode 100644
index 00000000..7ed24d08
--- /dev/null
+++ b/src/components/FileUpload/styles.scss
@@ -0,0 +1,8 @@
+.file-name-ellipsis {
+ width: 50%;
+ display: inline-block;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ overflow: hidden;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/src/components/Prompt/__snapshots__/index.test.jsx.snap b/src/components/Prompt/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..7c4dfd9a
--- /dev/null
+++ b/src/components/Prompt/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders 1`] = `
+
+ prompt",
+ }
+ }
+ />
+
+`;
+
+exports[`
renders closed 1`] = `
+
+ prompt",
+ }
+ }
+ />
+
+`;
diff --git a/src/components/Prompt/hooks.js b/src/components/Prompt/hooks.js
new file mode 100644
index 00000000..2cc77124
--- /dev/null
+++ b/src/components/Prompt/hooks.js
@@ -0,0 +1,14 @@
+import { useState } from 'react';
+
+const usePromptHooks = () => {
+ const [open, setOpen] = useState(true);
+
+ const toggleOpen = () => setOpen(!open);
+
+ return {
+ open,
+ toggleOpen,
+ };
+};
+
+export default usePromptHooks;
diff --git a/src/components/Prompt/index.jsx b/src/components/Prompt/index.jsx
index 2e66cce2..4e37a4ba 100644
--- a/src/components/Prompt/index.jsx
+++ b/src/components/Prompt/index.jsx
@@ -1,20 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { useORAConfigData } from 'data/services/lms/hooks/selectors';
+import { Collapsible } from '@edx/paragon';
-export const Prompt = ({ promptIndex }) => {
- const { prompts } = useORAConfigData();
+import usePromptHooks from './hooks';
+
+const Prompt = ({ prompt }) => {
+ const { open, toggleOpen } = usePromptHooks();
return (
-
-
Prompt {promptIndex + 1}
-
-
+
+
+
);
};
Prompt.propTypes = {
- promptIndex: PropTypes.number.isRequired,
+ prompt: PropTypes.string.isRequired,
};
export default Prompt;
diff --git a/src/components/Prompt/index.test.jsx b/src/components/Prompt/index.test.jsx
new file mode 100644
index 00000000..627e22f4
--- /dev/null
+++ b/src/components/Prompt/index.test.jsx
@@ -0,0 +1,34 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import Prompt from '.';
+
+import usePromptHooks from './hooks';
+
+jest.mock('./hooks', () => jest.fn());
+
+describe('
', () => {
+ const props = {
+ prompt: '
prompt
',
+ };
+ const mockHooks = (overrides) => {
+ usePromptHooks.mockReturnValueOnce({
+ open: true,
+ toggleOpen: jest.fn().mockName('toggleOpen'),
+ ...overrides,
+ });
+ };
+ it('renders', () => {
+ mockHooks();
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Collapsible')[0].props.title).toEqual('');
+ });
+
+ it('renders closed', () => {
+ mockHooks({ open: false });
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Collapsible')[0].props.title).not.toEqual('');
+ });
+});
diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx
index 7d6630db..a508ed7b 100644
--- a/src/components/Rubric/index.jsx
+++ b/src/components/Rubric/index.jsx
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { Card, StatefulButton } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { StrictDict } from '@edx/react-unit-test-utils';
+import { MutationStatus } from 'data/services/lms/constants';
import CriterionContainer from './CriterionContainer';
import RubricFeedback from './RubricFeedback';
@@ -13,13 +13,6 @@ import messages from './messages';
import './Rubric.scss';
-export const ButtonStates = StrictDict({
- idle: 'idle',
- loading: 'loading',
- error: 'error',
- success: 'success',
-});
-
/**
*
*/
@@ -64,11 +57,11 @@ export const Rubric = ({ isGrading }) => {
diff --git a/src/components/TextResponse/RichTextEditor.jsx b/src/components/TextResponse/RichTextEditor.jsx
index f8139ff1..beecfc4b 100644
--- a/src/components/TextResponse/RichTextEditor.jsx
+++ b/src/components/TextResponse/RichTextEditor.jsx
@@ -11,21 +11,16 @@ import 'tinymce/plugins/image';
import 'tinymce/themes/silver';
import 'tinymce/skins/ui/oxide/skin.min.css';
-import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
-export const stateKeys = StrictDict({
- value: 'value',
-});
-
const RichTextEditor = ({
// id,
- initialValue,
+ value,
disabled,
optional,
+ onChange,
}) => {
- const [value, setValue] = useKeyedState(stateKeys.value, initialValue);
const { formatMessage } = useIntl();
const extraConfig = disabled ? {
@@ -43,7 +38,7 @@ const RichTextEditor = ({
setValue(e.target.getContent())}
+ onEditorChange={onChange}
disabled={disabled}
/>
@@ -63,25 +58,27 @@ const RichTextEditor = ({
RichTextEditor.defaultProps = {
disabled: false,
- initialValue: '',
+ value: '',
optional: false,
+ onChange: () => { },
};
RichTextEditor.propTypes = {
// id: PropTypes.string.isRequired,
- input: PropTypes.shape({
- value: PropTypes.string,
- name: PropTypes.string,
- onChange: PropTypes.func.isRequired,
- }).isRequired,
- meta: PropTypes.shape({
- touched: PropTypes.bool,
- submitFailed: PropTypes.bool,
- error: PropTypes.string,
- }).isRequired,
+ // input: PropTypes.shape({
+ // value: PropTypes.string,
+ // name: PropTypes.string,
+ // onChange: PropTypes.func.isRequired,
+ // }).isRequired,
+ // meta: PropTypes.shape({
+ // touched: PropTypes.bool,
+ // submitFailed: PropTypes.bool,
+ // error: PropTypes.string,
+ // }).isRequired,
disabled: PropTypes.bool,
- initialValue: PropTypes.string,
+ value: PropTypes.string,
optional: PropTypes.bool,
+ onChange: PropTypes.func,
};
export default RichTextEditor;
diff --git a/src/components/TextResponse/RichTextEditor.test.jsx b/src/components/TextResponse/RichTextEditor.test.jsx
new file mode 100644
index 00000000..01c82a57
--- /dev/null
+++ b/src/components/TextResponse/RichTextEditor.test.jsx
@@ -0,0 +1,45 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import RichTextEditor from './RichTextEditor';
+
+jest.mock('@tinymce/tinymce-react', () => ({
+ Editor: () => 'Editor',
+}));
+
+jest.mock('tinymce/tinymce.min', () => 'tinymce');
+jest.mock('tinymce/icons/default', () => 'default');
+jest.mock('tinymce/plugins/link', () => 'link');
+jest.mock('tinymce/plugins/lists', () => 'lists');
+jest.mock('tinymce/plugins/code', () => 'code');
+jest.mock('tinymce/plugins/image', () => 'image');
+jest.mock('tinymce/themes/silver', () => 'silver');
+
+describe(' ', () => {
+ const props = {
+ optional: true,
+ disabled: false,
+ value: 'value',
+ onChange: jest.fn().mockName('onChange'),
+ };
+
+ it('render optional', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('label')[0].el.children).toContain('Optional');
+ expect(wrapper.instance.findByType('Editor')[0].props.init.readonly).not.toEqual(1);
+ });
+
+ it('render required', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('label')[0].el.children).toContain('Required');
+ });
+
+ it('render disabled', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Editor')[0].props.init.readonly).toEqual(1);
+ });
+});
diff --git a/src/components/TextResponse/TextEditor.jsx b/src/components/TextResponse/TextEditor.jsx
index 6a0f613e..e9441aa4 100644
--- a/src/components/TextResponse/TextEditor.jsx
+++ b/src/components/TextResponse/TextEditor.jsx
@@ -1,23 +1,18 @@
-import React, { useState } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { TextArea } from '@edx/paragon';
-import { StrictDict } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
-export const stateKeys = StrictDict({
- value: 'value',
-});
-
const TextEditor = ({
// id,
- initialValue,
+ value,
disabled,
optional,
+ onChange,
}) => {
const { formatMessage } = useIntl();
- const [value, setValue] = useState(initialValue);
return (
@@ -36,25 +31,27 @@ const TextEditor = ({
TextEditor.defaultProps = {
disabled: false,
- initialValue: '',
+ value: '',
optional: false,
+ onChange: () => { },
};
TextEditor.propTypes = {
// id: PropTypes.string.isRequired,
- input: PropTypes.shape({
- value: PropTypes.string,
- name: PropTypes.string,
- onChange: PropTypes.func.isRequired,
- }).isRequired,
- meta: PropTypes.shape({
- touched: PropTypes.bool,
- submitFailed: PropTypes.bool,
- error: PropTypes.string,
- }).isRequired,
+ // input: PropTypes.shape({
+ // value: PropTypes.string,
+ // name: PropTypes.string,
+ // onChange: PropTypes.func.isRequired,
+ // }).isRequired,
+ // meta: PropTypes.shape({
+ // touched: PropTypes.bool,
+ // submitFailed: PropTypes.bool,
+ // error: PropTypes.string,
+ // }).isRequired,
disabled: PropTypes.bool,
- initialValue: PropTypes.string,
+ value: PropTypes.string,
optional: PropTypes.bool,
+ onChange: PropTypes.func,
};
export default TextEditor;
diff --git a/src/components/TextResponse/TextEditor.test.jsx b/src/components/TextResponse/TextEditor.test.jsx
new file mode 100644
index 00000000..3a3420f9
--- /dev/null
+++ b/src/components/TextResponse/TextEditor.test.jsx
@@ -0,0 +1,25 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import TextEditor from './TextEditor';
+
+describe(' ', () => {
+ const props = {
+ optional: true,
+ disabled: false,
+ value: 'value',
+ onChange: jest.fn().mockName('onChange'),
+ };
+
+ it('render optional', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('TextArea')[0].props.label).toContain('Optional');
+ });
+
+ it('render required', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('TextArea')[0].props.label).toContain('Required');
+ });
+});
diff --git a/src/components/TextResponse/__snapshots__/RichTextEditor.test.jsx.snap b/src/components/TextResponse/__snapshots__/RichTextEditor.test.jsx.snap
new file mode 100644
index 00000000..1048ca16
--- /dev/null
+++ b/src/components/TextResponse/__snapshots__/RichTextEditor.test.jsx.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` render disabled 1`] = `
+
+
+ Your response
+ (
+ Optional
+ )
+
+
+
+`;
+
+exports[` render optional 1`] = `
+
+
+ Your response
+ (
+ Optional
+ )
+
+
+
+`;
+
+exports[` render required 1`] = `
+
+
+ Your response
+ (
+ Required
+ )
+
+
+
+`;
diff --git a/src/components/TextResponse/__snapshots__/TextEditor.test.jsx.snap b/src/components/TextResponse/__snapshots__/TextEditor.test.jsx.snap
new file mode 100644
index 00000000..bd5c0607
--- /dev/null
+++ b/src/components/TextResponse/__snapshots__/TextEditor.test.jsx.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` render optional 1`] = `
+
+`;
+
+exports[` render required 1`] = `
+
+`;
diff --git a/src/components/TextResponse/__snapshots__/index.test.jsx.snap b/src/components/TextResponse/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..e9bbaa66
--- /dev/null
+++ b/src/components/TextResponse/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` render Rich Text Editor 1`] = `
+
+
+
+`;
+
+exports[` render Text Editor 1`] = `
+
+
+
+`;
diff --git a/src/components/TextResponse/index.jsx b/src/components/TextResponse/index.jsx
index c2baabaa..140aedb4 100644
--- a/src/components/TextResponse/index.jsx
+++ b/src/components/TextResponse/index.jsx
@@ -1,24 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {
- useSubmissionConfig,
- // useSubmissionResponse,
- // useSubmissionStatus,
- // useSubmissionTeamInfo,
-} from 'data/services/lms/hooks/selectors';
-
import TextEditor from 'components/TextResponse/TextEditor';
import RichTextEditor from 'components/TextResponse/RichTextEditor';
import './index.scss';
-export const TextResponse = () => {
- const { textResponseConfig } = useSubmissionConfig();
+const TextResponse = ({ submissionConfig, value, onChange }) => {
+ const { textResponseConfig } = submissionConfig;
const { optional, enabled } = textResponseConfig;
const props = {
optional,
disabled: !enabled,
+ value,
+ onChange,
};
return (
@@ -31,7 +26,15 @@ export const TextResponse = () => {
};
TextResponse.propTypes = {
- promptIndex: PropTypes.number.isRequired,
+ submissionConfig: PropTypes.shape({
+ textResponseConfig: PropTypes.shape({
+ optional: PropTypes.bool,
+ enabled: PropTypes.bool,
+ editorType: PropTypes.string,
+ }),
+ }).isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
};
export default TextResponse;
diff --git a/src/components/TextResponse/index.test.jsx b/src/components/TextResponse/index.test.jsx
new file mode 100644
index 00000000..26cafd17
--- /dev/null
+++ b/src/components/TextResponse/index.test.jsx
@@ -0,0 +1,35 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import TextResponse from '.';
+
+jest.mock('./TextEditor', () => 'TextEditor');
+jest.mock('./RichTextEditor', () => 'RichTextEditor');
+
+describe(' ', () => {
+ const props = {
+ submissionConfig: {
+ textResponseConfig: {
+ optional: false,
+ enabled: true,
+ editorType: 'text',
+ },
+ },
+ value: 'value',
+ onChange: jest.fn().mockName('onChange'),
+ };
+
+ it('render Text Editor ', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('TextEditor').length).toEqual(1);
+ expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(0);
+ });
+
+ it('render Rich Text Editor ', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('TextEditor').length).toEqual(0);
+ expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(1);
+ });
+});
diff --git a/src/components/UploadedFiles/index.jsx b/src/components/UploadedFiles/index.jsx
deleted file mode 100644
index 86cf78fb..00000000
--- a/src/components/UploadedFiles/index.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-
-import {
- useSubmissionConfig,
- useSubmissionTeamInfo,
- useSubmissionStatus,
- useSubmissionResponse,
-} from 'data/services/lms/hooks/selectors';
-
-export const UploadedFiles = () => {
- const config = useSubmissionConfig().fileResponseConfig;
- const files = useSubmissionResponse().uploadedFiles;
- const submissionStatus = useSubmissionStatus();
- const teamInfo = useSubmissionTeamInfo();
- console.log({
- TextResponse: {
- config,
- files,
- submissionStatus,
- teamInfo,
- },
- });
- return (
-
-
UploadedFiles
-
- );
-};
-
-export default UploadedFiles;
diff --git a/src/data/services/lms/constants.js b/src/data/services/lms/constants.js
index e1101c18..f9b4b111 100644
--- a/src/data/services/lms/constants.js
+++ b/src/data/services/lms/constants.js
@@ -11,4 +11,11 @@ export const queryKeys = StrictDict({
pageData: 'pageData',
});
-export default { feedbackRequirement, queryKeys };
+export const MutationStatus = StrictDict({
+ idle: 'idle',
+ loading: 'loading',
+ error: 'error',
+ success: 'success',
+});
+
+export default { feedbackRequirement, queryKeys, MutationStatus };
diff --git a/src/data/services/lms/fakeData/pageData/index.jsx b/src/data/services/lms/fakeData/pageData/index.jsx
index b3ac5afe..fb680cd4 100644
--- a/src/data/services/lms/fakeData/pageData/index.jsx
+++ b/src/data/services/lms/fakeData/pageData/index.jsx
@@ -5,7 +5,7 @@ import submissionStates from './submission';
export const emptySubmission = {
progress: progressStates.submission,
rubric: rubricStates.criteriaFeedbackEnabled.empty,
- submission: submissionStates.emptySubmission,
+ submission: submissionStates.individialSubmission,
};
export const peerAssessment = {
diff --git a/src/data/services/lms/hooks/actions.ts b/src/data/services/lms/hooks/actions.ts
index 1645180b..7094908f 100644
--- a/src/data/services/lms/hooks/actions.ts
+++ b/src/data/services/lms/hooks/actions.ts
@@ -27,4 +27,72 @@ export const submitRubric = () =>
queryClient.invalidateQueries([queryKeys.pageData, true])
return Promise.resolve(data);
+ });
+
+export const submitResponse = () =>
+ createMutationAction(async (data: any, queryClient) => {
+ // TODO: submit response
+ await new Promise((resolve) => setTimeout(() => {
+ fakeData.pageData.shapes.emptySubmission.submission.response = {
+ uploaded_files: [
+ ...data.response.uploadedFiles,
+ ],
+ text_responses: [
+ ...data.response.textResponses,
+ ],
+ } as any;
+ resolve(null);
+ }, 1000));
+
+ queryClient.invalidateQueries([queryKeys.pageData, false])
+
+ return Promise.resolve(data);
+ });
+
+export const saveResponse = () =>
+ createMutationAction(async (data: any, queryClient) => {
+ // TODO: save response for later
+ await new Promise((resolve) => setTimeout(() => {
+ fakeData.pageData.shapes.emptySubmission.submission.response = {
+ uploaded_files: [
+ ...data.response.uploadedFiles,
+ ],
+ text_responses: [
+ ...data.response.textResponses,
+ ],
+ } as any;
+ resolve(null);
+ }, 1000));
+
+ queryClient.invalidateQueries([queryKeys.pageData, false])
+
+ return Promise.resolve(data);
+ });
+
+export const uploadFiles = () =>
+ createMutationAction(async (data: any, queryClient) => {
+ const { fileData, requestConfig } = data;
+ // TODO: upload files
+ const files = fileData.getAll('file');
+ for (let i = 0; i <= 50; i++) {
+ // eslint-disable-next-line no-await-in-loop, no-promise-executor-return
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ requestConfig.onUploadProgress({ loaded: i, total: 50 });
+ }
+
+ fakeData.pageData.shapes.emptySubmission.submission.response = {
+ ...fakeData.pageData.shapes.emptySubmission.submission.response,
+ uploaded_files: [
+ ...fakeData.pageData.shapes.emptySubmission.submission.response.uploaded_files,
+ ...files.map((file: any) => ({
+ fileDescription: file.description,
+ fileName: file.name,
+ fileSize: file.size,
+ })),
+ ],
+ } as any;
+
+ queryClient.invalidateQueries([queryKeys.pageData, false])
+
+ return Promise.resolve(files);
});
\ No newline at end of file
diff --git a/src/data/services/lms/hooks/data.test.ts b/src/data/services/lms/hooks/data.test.ts
index 07f1ab2b..65b8233c 100644
--- a/src/data/services/lms/hooks/data.test.ts
+++ b/src/data/services/lms/hooks/data.test.ts
@@ -75,7 +75,7 @@ describe('lms data hooks', () => {
});
describe('usePageData', () => {
const pageDataCamelCase = (data: any) => ({
- ...data,
+ ...camelCaseObject(data),
rubric: {
optionsSelected: {...data.rubric.options_selected},
criterionFeedback: {...data.rubric.criterion_feedback},
diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts
index c6cc9171..abfd8b58 100644
--- a/src/data/services/lms/hooks/data.ts
+++ b/src/data/services/lms/hooks/data.ts
@@ -38,7 +38,7 @@ export const usePageData = (): types.QueryData => {
: fakeData.pageData.shapes.emptySubmission;
const returnData = assessmentData ? {
- ...assessmentData,
+ ...camelCaseObject(assessmentData),
rubric: {
optionsSelected: {...assessmentData.rubric.options_selected},
criterionFeedback: {...assessmentData.rubric.criterion_feedback},
diff --git a/src/setupTest.js b/src/setupTest.js
index 00e08236..250b1c8f 100644
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -44,6 +44,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
TableController: 'DataTable.TableController',
TableFooter: 'DataTable.TableFooter',
},
+ Dropzone: 'Dropzone',
Dropdown: {
Item: 'Dropdown.Item',
Menu: 'Dropdown.Menu',
@@ -59,10 +60,18 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
RadioSet: 'Form.RadioSet',
},
FormControlFeedback: 'FormControlFeedback',
+ FormLabel: 'FormLabel',
FullscreenModal: 'FullscreenModal',
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
+ ModalDialog: {
+ Body: 'ModalDialog.Body',
+ Footer: 'ModalDialog.Footer',
+ Header: 'ModalDialog.Header',
+ Title: 'ModalDialog.Title',
+ CloseButton: 'ModalDialog.CloseButton',
+ },
MultiSelectDropdownFilter: 'MultiSelectDropdownFilter',
OverlayTrigger: 'OverlayTrigger',
PageBanner: 'PageBanner',
@@ -72,5 +81,6 @@ jest.mock('@edx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
Row: 'Row',
StatefulButton: 'StatefulButton',
TextFilter: 'TextFilter',
+ TextArea: 'TextArea',
Spinner: 'Spinner',
}));
diff --git a/src/views/SubmissionView/SubmissionActions.jsx b/src/views/SubmissionView/SubmissionActions.jsx
index a49c3d8f..442462c7 100644
--- a/src/views/SubmissionView/SubmissionActions.jsx
+++ b/src/views/SubmissionView/SubmissionActions.jsx
@@ -1,12 +1,57 @@
import React from 'react';
+import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { ActionRow, StatefulButton } from '@edx/paragon';
+import { MutationStatus } from 'data/services/lms/constants';
+import messages from './messages';
-import { ActionRow, Button } from '@edx/paragon';
+const SubmissionActions = ({
+ submitResponseHandler,
+ submitResponseStatus,
+ saveResponseHandler,
+ saveResponseStatus,
+}) => {
+ const { formatMessage } = useIntl();
-const SubmissionActions = () => (
-
- Secondary Action
- Primary Action
-
-);
+ return (
+
+
+
+
+ );
+};
+
+SubmissionActions.propTypes = {
+ submitResponseHandler: PropTypes.func.isRequired,
+ submitResponseStatus: PropTypes.oneOf(Object.values(MutationStatus))
+ .isRequired,
+ saveResponseHandler: PropTypes.func.isRequired,
+ saveResponseStatus: PropTypes.oneOf(Object.values(MutationStatus))
+ .isRequired,
+};
export default SubmissionActions;
diff --git a/src/views/SubmissionView/SubmissionActions.test.jsx b/src/views/SubmissionView/SubmissionActions.test.jsx
new file mode 100644
index 00000000..ada8b249
--- /dev/null
+++ b/src/views/SubmissionView/SubmissionActions.test.jsx
@@ -0,0 +1,16 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import SubmissionActions from './SubmissionActions';
+
+describe(' ', () => {
+ const props = {
+ submitResponseHandler: jest.fn().mockName('submitResponseHandler'),
+ submitResponseStatus: 'idle',
+ saveResponseHandler: jest.fn().mockName('saveResponseHandler'),
+ saveResponseStatus: 'idle',
+ };
+
+ it('renders', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+ });
+});
diff --git a/src/views/SubmissionView/SubmissionContent.jsx b/src/views/SubmissionView/SubmissionContent.jsx
index a86f8ad6..3a5230de 100644
--- a/src/views/SubmissionView/SubmissionContent.jsx
+++ b/src/views/SubmissionView/SubmissionContent.jsx
@@ -1,26 +1,80 @@
import React from 'react';
+import PropTypes from 'prop-types';
-import { useORAConfigData } from 'data/services/lms/hooks/selectors';
+import { Icon } from '@edx/paragon';
+import { CheckCircle } from '@edx/paragon/icons';
+import { useIntl } from '@edx/frontend-platform/i18n';
import Prompt from 'components/Prompt';
import TextResponse from 'components/TextResponse';
import FileUpload from 'components/FileUpload';
-const SubmissionContent = () => {
- const { prompts } = useORAConfigData();
+import messages from './messages';
+
+const SubmissionContent = ({
+ submission,
+ oraConfigData,
+ onTextResponseChange,
+ onFileUploaded,
+ draftSaved,
+}) => {
+ const { formatMessage } = useIntl();
+
return (
- {
- prompts.map((prompt, index) => (
-
- ))
- }
-
+
+
{formatMessage(messages.yourResponse)}
+ {draftSaved && (
+
+
+ {formatMessage(messages.draftSaved)}
+
+ )}
+
+
+ {formatMessage(messages.instructions)}:
+ {formatMessage(messages.instructionsText)}
+
+ {oraConfigData.prompts.map((prompt, index) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
);
};
+SubmissionContent.propTypes = {
+ submission: PropTypes.shape({
+ response: PropTypes.shape({
+ textResponses: PropTypes.arrayOf(PropTypes.string),
+ uploadedFiles: PropTypes.arrayOf(
+ PropTypes.shape({
+ fileDescription: PropTypes.string,
+ fileName: PropTypes.string,
+ fileSize: PropTypes.number,
+ }),
+ ),
+ }),
+ }).isRequired,
+ oraConfigData: PropTypes.shape({
+ prompts: PropTypes.arrayOf(PropTypes.string),
+ // eslint-disable-next-line react/forbid-prop-types
+ submissionConfig: PropTypes.any,
+ }).isRequired,
+ onTextResponseChange: PropTypes.func.isRequired,
+ onFileUploaded: PropTypes.func.isRequired,
+ draftSaved: PropTypes.bool.isRequired,
+};
+
export default SubmissionContent;
diff --git a/src/views/SubmissionView/SubmissionContent.test.jsx b/src/views/SubmissionView/SubmissionContent.test.jsx
new file mode 100644
index 00000000..a6e441fc
--- /dev/null
+++ b/src/views/SubmissionView/SubmissionContent.test.jsx
@@ -0,0 +1,48 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import SubmissionContent from './SubmissionContent';
+
+jest.mock('@edx/paragon/icons', () => ({
+ CheckCircle: 'CheckCircle',
+}));
+
+jest.mock('components/Prompt', () => 'Prompt');
+jest.mock('components/TextResponse', () => 'TextResponse');
+jest.mock('components/FileUpload', () => 'FileUpload');
+
+describe(' ', () => {
+ const props = {
+ submission: {
+ response: {
+ textResponses: ['test'],
+ uploadedFiles: [{
+ fileDescription: 'test',
+ }],
+ },
+ },
+ oraConfigData: {
+ prompts: ['test
'],
+ submissionConfig: {
+ maxFileSize: 100,
+ },
+ },
+ onTextResponseChange: jest.fn().mockName('onTextResponseChange'),
+ onFileUploaded: jest.fn().mockName('onFileUploaded'),
+ draftSaved: true,
+ };
+
+ describe('render', () => {
+ test('default', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Prompt')).toHaveLength(1);
+ });
+
+ test('no prompts', () => {
+ const wrapper = shallow( );
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Prompt')).toHaveLength(0);
+ });
+ });
+});
diff --git a/src/views/SubmissionView/SubmissionContentLayout.jsx b/src/views/SubmissionView/SubmissionContentLayout.jsx
index f0b50e59..d1e9cb82 100644
--- a/src/views/SubmissionView/SubmissionContentLayout.jsx
+++ b/src/views/SubmissionView/SubmissionContentLayout.jsx
@@ -1,27 +1,46 @@
import React from 'react';
+import PropTypes from 'prop-types';
import { Col, Row } from '@edx/paragon';
-import { useRubricConfig } from 'data/services/lms/hooks/selectors';
import Rubric from 'components/Rubric';
import SubmissionContent from './SubmissionContent';
import './SubmissionContentLayout.scss';
-const SubmissionContentLayout = () => {
- const showRubric = useRubricConfig().showDuringResponse;
- return (
-
-
-
-
-
-
- {showRubric && ( )}
-
-
+const SubmissionContentLayout = ({
+ submission,
+ oraConfigData,
+ onTextResponseChange,
+ onFileUploaded,
+ draftSaved,
+}) => (
+
+
+
+
+
+
+ {oraConfigData.showDuringResponse && }
+
- );
+
+);
+
+SubmissionContentLayout.propTypes = {
+ // eslint-disable-next-line react/forbid-prop-types
+ submission: PropTypes.any.isRequired,
+ // eslint-disable-next-line react/forbid-prop-types
+ oraConfigData: PropTypes.any.isRequired,
+ onTextResponseChange: PropTypes.func.isRequired,
+ onFileUploaded: PropTypes.func.isRequired,
+ draftSaved: PropTypes.bool.isRequired,
};
export default SubmissionContentLayout;
diff --git a/src/views/SubmissionView/SubmissionContentLayout.test.jsx b/src/views/SubmissionView/SubmissionContentLayout.test.jsx
new file mode 100644
index 00000000..95efdf66
--- /dev/null
+++ b/src/views/SubmissionView/SubmissionContentLayout.test.jsx
@@ -0,0 +1,31 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import SubmissionContentLayout from './SubmissionContentLayout';
+
+jest.mock('components/Rubric', () => 'Rubric');
+jest.mock('./SubmissionContent', () => 'SubmissionContent');
+
+describe('
', () => {
+ const props = {
+ submission: 'submission',
+ oraConfigData: {
+ showDuringResponse: true,
+ },
+ onTextResponseChange: jest.fn().mockName('onTextResponseChange'),
+ onFileUploaded: jest.fn().mockName('onFileUploaded'),
+ draftSaved: true,
+ };
+
+ it('show rubric', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Rubric')).toHaveLength(1);
+ });
+
+ it('hide rubric', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+
+ expect(wrapper.instance.findByType('Rubric')).toHaveLength(0);
+ });
+});
diff --git a/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap
new file mode 100644
index 00000000..e3cb598b
--- /dev/null
+++ b/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders 1`] = `
+
+
+
+
+`;
diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap
new file mode 100644
index 00000000..7b07e027
--- /dev/null
+++ b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
render default 1`] = `
+
+
+
+ Your response
+
+
+
+ Draft saved
+
+
+
+
+ Instructions
+ :
+
+ Create a response to the prompt below.
+ Progress will be saved automatically and you can return to complete your
+ progress at any time. After you submit your response, you cannot edit
+ it.
+
+
+
+
+`;
+
+exports[`
render no prompts 1`] = `
+
+
+
+ Your response
+
+
+
+ Draft saved
+
+
+
+
+ Instructions
+ :
+
+ Create a response to the prompt below.
+ Progress will be saved automatically and you can return to complete your
+ progress at any time. After you submit your response, you cannot edit
+ it.
+
+
+
+`;
diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap
new file mode 100644
index 00000000..8c80eb56
--- /dev/null
+++ b/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
hide rubric 1`] = `
+
+`;
+
+exports[`
show rubric 1`] = `
+
+`;
diff --git a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..9e33db18
--- /dev/null
+++ b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
renders 1`] = `
+
+ }
+ isOpen={true}
+ modalBodyClassName="content-body"
+ onClose={[Function]}
+ title="ORA Submission"
+>
+
+
+`;
diff --git a/src/views/SubmissionView/hooks.js b/src/views/SubmissionView/hooks.js
new file mode 100644
index 00000000..fb53678d
--- /dev/null
+++ b/src/views/SubmissionView/hooks.js
@@ -0,0 +1,70 @@
+import { useEffect, useReducer } from 'react';
+
+import { useORAConfigData, usePageData } from 'data/services/lms/hooks/selectors';
+
+import { submitResponse, saveResponse, uploadFiles } from 'data/services/lms/hooks/actions';
+import { MutationStatus } from 'data/services/lms/constants';
+
+const useSubmissionViewHooks = () => {
+ const submitResponseMutation = submitResponse();
+ const saveResponseMutation = saveResponse();
+ const uploadFilesMutation = uploadFiles();
+ const pageData = usePageData();
+ const oraConfigData = useORAConfigData();
+
+ const [submission, dispatchSubmission] = useReducer(
+ (state, payload) => ({ ...state, isDirty: true, ...payload }),
+ { ...pageData?.submission, isDirty: false },
+ );
+
+ useEffect(() => {
+ // a workaround to update the submission state when the pageData changes
+ if (pageData?.submission) {
+ dispatchSubmission({ ...pageData.submission, isDirty: false });
+ }
+ }, [pageData?.submission]);
+
+ const onTextResponseChange = (index) => (textResponse) => {
+ dispatchSubmission({
+ response: {
+ ...submission.response,
+ textResponses: [
+ ...submission.response.textResponses.slice(0, index),
+ textResponse,
+ ...submission.response.textResponses.slice(index + 1),
+ ],
+ },
+ });
+ };
+
+ const onFileUploaded = (args) => {
+ const fileUploads = uploadFilesMutation.mutate(args);
+ dispatchSubmission({ response: { ...submission.response, fileUploads } });
+ };
+
+ const submitResponseHandler = () => {
+ dispatchSubmission({ isDirty: false });
+ submitResponseMutation.mutate(submission);
+ };
+
+ const saveResponseHandler = () => {
+ dispatchSubmission({ isDirty: false });
+ saveResponseMutation.mutate(submission);
+ };
+
+ return {
+ submitResponseHandler,
+ submitResponseStatus: submitResponseMutation.status,
+ saveResponseHandler,
+ saveResponseStatus: saveResponseMutation.status,
+ draftSaved: saveResponseMutation.status === MutationStatus.success && !submission.isDirty,
+ pageData,
+ oraConfigData,
+ submission,
+ dispatchSubmission,
+ onTextResponseChange,
+ onFileUploaded,
+ };
+};
+
+export default useSubmissionViewHooks;
diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx
index f3e61ffd..89909668 100644
--- a/src/views/SubmissionView/index.jsx
+++ b/src/views/SubmissionView/index.jsx
@@ -1,33 +1,23 @@
import React from 'react';
-import { FullscreenModal, Spinner } from '@edx/paragon';
-
-import {
- // useORAConfigData,
- useIsORAConfigLoaded,
- useIsPageDataLoaded,
- // usePageData,
-} from 'data/services/lms/hooks/selectors';
+import { FullscreenModal } from '@edx/paragon';
import SubmissionContentLayout from './SubmissionContentLayout';
import SubmissionActions from './SubmissionActions';
+import useSubmissionViewHooks from './hooks';
export const SubmissionView = () => {
- const isConfigLoaded = useIsORAConfigLoaded();
- const isPageLoaded = useIsPageDataLoaded();
-
- if (!isConfigLoaded || !isPageLoaded) {
- return (
-
-
-
- );
- }
+ const {
+ submission,
+ oraConfigData,
+ onFileUploaded,
+ onTextResponseChange,
+ submitResponseHandler,
+ submitResponseStatus,
+ saveResponseHandler,
+ saveResponseStatus,
+ draftSaved,
+ } = useSubmissionViewHooks();
return (
{
onClose={() => ({})}
title="ORA Submission"
modalBodyClassName="content-body"
+ footerNode={(
+
+ )}
>
-
-
+
);
};
diff --git a/src/views/SubmissionView/index.test.jsx b/src/views/SubmissionView/index.test.jsx
new file mode 100644
index 00000000..2c2ff6ad
--- /dev/null
+++ b/src/views/SubmissionView/index.test.jsx
@@ -0,0 +1,24 @@
+import { shallow } from '@edx/react-unit-test-utils';
+import { SubmissionView } from '.';
+
+jest.mock('./SubmissionContentLayout', () => 'SubmissionContentLayout');
+jest.mock('./SubmissionActions', () => 'SubmissionActions');
+
+jest.mock('./hooks', () => jest.fn().mockReturnValue({
+ submission: 'submission',
+ oraConfigData: 'oraConfigData',
+ onFileUploaded: jest.fn().mockName('onFileUploaded'),
+ onTextResponseChange: jest.fn().mockName('onTextResponseChange'),
+ submitResponseHandler: jest.fn().mockName('submitResponseHandler'),
+ submitResponseStatus: 'submitResponseStatus',
+ saveResponseHandler: jest.fn().mockName('saveResponseHandler'),
+ saveResponseStatus: 'saveResponseStatus',
+ draftSaved: true,
+}));
+
+describe('
', () => {
+ it('renders', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+ });
+});
diff --git a/src/views/SubmissionView/messages.js b/src/views/SubmissionView/messages.js
new file mode 100644
index 00000000..23762b09
--- /dev/null
+++ b/src/views/SubmissionView/messages.js
@@ -0,0 +1,54 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ yourResponse: {
+ id: 'ora-grading.SubmissionView.yourResponse',
+ defaultMessage: 'Your response',
+ description: 'Label for the response textarea',
+ },
+ draftSaved: {
+ id: 'ora-grading.SubmissionView.draftSaved',
+ defaultMessage: 'Draft saved',
+ description: 'Label for the draft saved message',
+ },
+ instructions: {
+ id: 'ora-grading.SubmissionView.instructions',
+ defaultMessage: 'Instructions',
+ description: 'Label for the instructions textarea',
+ },
+ instructionsText: {
+ id: 'ora-grading.SubmissionView.instructionsText',
+ defaultMessage: `Create a response to the prompt below.
+ Progress will be saved automatically and you can return to complete your
+ progress at any time. After you submit your response, you cannot edit
+ it.`,
+ description: 'Description for the instructions textarea',
+ },
+ submissionActionSubmit: {
+ id: 'ora-grading.SubmissionAction.submit',
+ defaultMessage: 'Submit response',
+ description: 'Submit button text',
+ },
+ submissionActionSubmitting: {
+ id: 'ora-grading.SubmissionAction.submitting',
+ defaultMessage: 'Submitting response',
+ description: 'Submit button text while submitting',
+ },
+ submissionActionSubmitted: {
+ id: 'ora-grading.SubmissionAction.submitted',
+ defaultMessage: 'Response submitted',
+ description: 'Submit button text after successful submission',
+ },
+ saveActionSave: {
+ id: 'ora-grading.SaveAction.save',
+ defaultMessage: 'Finish later',
+ description: 'Save for later button text',
+ },
+ saveActionSaving: {
+ id: 'ora-grading.SaveAction.saving',
+ defaultMessage: 'Saving response',
+ description: 'Save for later button text while saving',
+ },
+});
+
+export default messages;