diff --git a/src/components/FileUpload/__snapshots__/index.test.jsx.snap b/src/components/FileUpload/__snapshots__/index.test.jsx.snap
index 7f519bbc..9790302f 100644
--- a/src/components/FileUpload/__snapshots__/index.test.jsx.snap
+++ b/src/components/FileUpload/__snapshots__/index.test.jsx.snap
@@ -4,6 +4,7 @@ exports[` render default 1`] = `
File upload
+
render without file preview if uploadedFiles are empty a
File upload
+
Uploaded files
@@ -312,20 +322,23 @@ exports[` render without header 1`] = `
]
}
/>
-
+
+ maxSize={123456}
+ onProcessUpload={[MockFunction onProcessUpload]}
+ progressVariant="bar"
+ />
+
`;
diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx
index b7d04bbc..813eb6d2 100644
--- a/src/components/FileUpload/index.jsx
+++ b/src/components/FileUpload/index.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import filesize from 'filesize';
-import { DataTable, Dropzone } from '@openedx/paragon';
+import { DataTable, Dropzone, Form } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'utils';
@@ -35,6 +35,7 @@ const FileUpload = ({
onDeletedFile,
defaultCollapsePreview,
hideHeader,
+ isInValid,
}) => {
const { formatMessage } = useIntl();
const {
@@ -47,7 +48,7 @@ const FileUpload = ({
const viewStep = useViewStep();
const activeStepName = useActiveStepName();
const {
- enabled, fileUploadLimit, allowedExtensions, maxFileSize,
+ enabled, fileUploadLimit, allowedExtensions, maxFileSize, required,
} = useFileUploadConfig() || {};
if (!enabled || viewStep === stepNames.studentTraining) {
@@ -78,7 +79,7 @@ const FileUpload = ({
return (
- {!hideHeader &&
{formatMessage(messages.fileUploadTitle)}
}
+ {!hideHeader &&
{formatMessage(messages.fileUploadTitle)} {required && (required)}
}
{uploadedFiles.length > 0 && isReadOnly && (
)}
@@ -93,15 +94,17 @@ const FileUpload = ({
columns={columns}
/>
{!isReadOnly && fileUploadLimit > uploadedFiles.length && (
-
`.${ext}`),
- }}
- maxSize={maxFileSize}
- />
+
+ `.${ext}`),
+ }}
+ maxSize={maxFileSize}
+ />
+ {isInValid && {formatMessage(messages.required)}}
+
)}
{!isReadOnly && isModalOpen && (
{
+const useConfirmAction = (validateBeforeOpen = () => true) => {
const [isOpen, setIsOpen] = useKeyedState(stateKeys.isOpen, false);
const close = React.useCallback(() => setIsOpen(false), [setIsOpen]);
- const open = React.useCallback(() => setIsOpen(true), [setIsOpen]);
+ const open = React.useCallback(() => {
+ if (validateBeforeOpen()) {
+ setIsOpen(true);
+ }
+ }, [setIsOpen, validateBeforeOpen]);
return React.useCallback((config) => {
const { description, title } = config;
const action = config.action.action ? config.action.action : config.action;
diff --git a/src/hooks/actions/useConfirmAction.test.js b/src/hooks/actions/useConfirmAction.test.js
index cca76dd2..e2bacbbd 100644
--- a/src/hooks/actions/useConfirmAction.test.js
+++ b/src/hooks/actions/useConfirmAction.test.js
@@ -23,12 +23,14 @@ const nestedActionConfig = {
action: { action: config.action },
};
+const validateBeforeOpen = jest.fn(() => true);
+
let out;
describe('useConfirmAction', () => {
beforeEach(() => {
jest.clearAllMocks();
state.mock();
- out = useConfirmAction();
+ out = useConfirmAction(validateBeforeOpen);
});
afterEach(() => { state.resetVals(); });
describe('behavior', () => {
@@ -44,7 +46,7 @@ describe('useConfirmAction', () => {
state.expectSetStateCalledWith(stateKeys.isOpen, false);
};
const testOpen = (openFn) => {
- expect(openFn.useCallback.prereqs).toEqual([state.setState[stateKeys.isOpen]]);
+ expect(openFn.useCallback.prereqs).toEqual([state.setState[stateKeys.isOpen], validateBeforeOpen]);
openFn.useCallback.cb();
state.expectSetStateCalledWith(stateKeys.isOpen, true);
};
@@ -62,13 +64,13 @@ describe('useConfirmAction', () => {
expect(out.useCallback.prereqs[1]).toEqual(true);
});
test('open callback', () => {
- out = useConfirmAction();
+ out = useConfirmAction(validateBeforeOpen);
testOpen(prereqs[2]);
});
});
describe('callback', () => {
it('returns action with labels from config action', () => {
- out = useConfirmAction().useCallback.cb(config);
+ out = useConfirmAction(validateBeforeOpen).useCallback.cb(config);
testOpen(out.action.onClick);
expect(out.action.children).toEqual(config.action.labels.default);
expect(out.confirmProps.isOpen).toEqual(false);
@@ -78,7 +80,7 @@ describe('useConfirmAction', () => {
testClose(out.confirmProps.close);
});
it('returns nested action from config action', () => {
- out = useConfirmAction().useCallback.cb(nestedActionConfig);
+ out = useConfirmAction(validateBeforeOpen).useCallback.cb(nestedActionConfig);
testOpen(out.action.onClick);
expect(out.action.children).toEqual(nestedActionConfig.action.action.labels.default);
expect(out.confirmProps.isOpen).toEqual(false);
@@ -89,7 +91,7 @@ describe('useConfirmAction', () => {
});
it('returns action with children from config action', () => {
state.mockVals({ [stateKeys.isOpen]: true });
- out = useConfirmAction().useCallback.cb(noLabelConfig);
+ out = useConfirmAction(validateBeforeOpen).useCallback.cb(noLabelConfig);
testOpen(out.action.onClick);
expect(out.action.children).toEqual(noLabelConfig.action.children);
expect(out.confirmProps.isOpen).toEqual(true);
diff --git a/src/hooks/actions/useSubmitResponseAction.js b/src/hooks/actions/useSubmitResponseAction.js
index 47fdcd57..1f26536c 100644
--- a/src/hooks/actions/useSubmitResponseAction.js
+++ b/src/hooks/actions/useSubmitResponseAction.js
@@ -13,8 +13,8 @@ import messages, { confirmDescriptions, confirmTitles } from './messages';
*/
const useSubmitResponseAction = ({ options }) => {
const { formatMessage } = useIntl();
- const { submit, submitStatus } = options;
- const confirmAction = useConfirmAction();
+ const { submit, submitStatus, validateBeforeConfirmation } = options;
+ const confirmAction = useConfirmAction(validateBeforeConfirmation);
return confirmAction({
action: {
onClick: submit,
diff --git a/src/hooks/actions/useSubmitResponseAction.test.js b/src/hooks/actions/useSubmitResponseAction.test.js
index 961b8b85..c4b11496 100644
--- a/src/hooks/actions/useSubmitResponseAction.test.js
+++ b/src/hooks/actions/useSubmitResponseAction.test.js
@@ -12,14 +12,15 @@ jest.mock('./useConfirmAction', () => ({
default: jest.fn(),
}));
-const mockConfirmAction = jest.fn(args => ({ confirmAction: args }));
-when(useConfirmAction).calledWith().mockReturnValue(mockConfirmAction);
-
const options = {
submit: jest.fn(),
submitStatus: 'test-submit-status',
+ validateBeforeConfirmation: jest.fn(),
};
+const mockConfirmAction = jest.fn(args => ({ confirmAction: args }));
+when(useConfirmAction).calledWith(options.validateBeforeConfirmation).mockReturnValue(mockConfirmAction);
+
let out;
describe('useSubmitResponseAction', () => {
beforeEach(() => {
@@ -28,7 +29,7 @@ describe('useSubmitResponseAction', () => {
describe('behavior', () => {
it('loads internatioonalization and confirm action from hooks', () => {
expect(useIntl).toHaveBeenCalledWith();
- expect(useConfirmAction).toHaveBeenCalledWith();
+ expect(useConfirmAction).toHaveBeenCalledWith(options.validateBeforeConfirmation);
});
});
describe('output confirmAction', () => {
diff --git a/src/views/SubmissionView/SubmissionPrompts.jsx b/src/views/SubmissionView/SubmissionPrompts.jsx
index f2986e21..05bc1ae8 100644
--- a/src/views/SubmissionView/SubmissionPrompts.jsx
+++ b/src/views/SubmissionView/SubmissionPrompts.jsx
@@ -11,11 +11,12 @@ const SubmissionPrompts = ({
textResponses,
onUpdateTextResponse,
isReadOnly,
+ promptStatuses,
}) => {
const submissionConfig = useSubmissionConfig();
const prompts = usePrompts();
- const response = (index) => {
+ const response = (index, isInValid) => {
if (!submissionConfig.textResponseConfig.enabled) {
return null;
}
@@ -25,6 +26,7 @@ const SubmissionPrompts = ({
);
};
@@ -35,7 +37,7 @@ const SubmissionPrompts = ({
// eslint-disable-next-line react/no-array-index-key
- {response(index)}
+ {response(index, !promptStatuses[index])}
))}
>
@@ -43,11 +45,13 @@ const SubmissionPrompts = ({
};
SubmissionPrompts.defaultProps = {
textResponses: null,
+ promptStatuses: {},
};
SubmissionPrompts.propTypes = {
textResponses: PropTypes.arrayOf(PropTypes.string),
onUpdateTextResponse: PropTypes.func.isRequired,
isReadOnly: PropTypes.bool.isRequired,
+ promptStatuses: PropTypes.objectOf(PropTypes.number),
};
export default SubmissionPrompts;
diff --git a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
index 2ad9c23d..42b4f91d 100644
--- a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
+++ b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { Form } from '@openedx/paragon';
import { Editor } from '@tinymce/tinymce-react';
import 'tinymce/tinymce.min';
@@ -19,6 +20,7 @@ const RichTextEditor = ({
disabled,
optional,
onChange,
+ isInValid,
}) => {
const { formatMessage } = useIntl();
@@ -56,6 +58,7 @@ const RichTextEditor = ({
onEditorChange={onChange}
disabled={disabled}
/>
+ { isInValid && {formatMessage(messages.requiredField)}}
);
};
@@ -65,6 +68,7 @@ RichTextEditor.defaultProps = {
value: '',
optional: false,
onChange: () => { },
+ isInValid: false,
};
RichTextEditor.propTypes = {
@@ -83,6 +87,7 @@ RichTextEditor.propTypes = {
value: PropTypes.string,
optional: PropTypes.bool,
onChange: PropTypes.func,
+ isInValid: PropTypes.bool,
};
export default RichTextEditor;
diff --git a/src/views/SubmissionView/TextResponseEditor/TextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/TextEditor.jsx
index 703d1545..ed682098 100644
--- a/src/views/SubmissionView/TextResponseEditor/TextEditor.jsx
+++ b/src/views/SubmissionView/TextResponseEditor/TextEditor.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { TextArea } from '@openedx/paragon';
+import { Form } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import './TextEditor.scss';
@@ -12,23 +12,28 @@ const TextEditor = ({
disabled,
optional,
onChange,
+ isInValid,
}) => {
const { formatMessage } = useIntl();
return (
-