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 +

Uploaded files @@ -57,20 +58,23 @@ exports[` render default 1`] = ` ] } /> - + + maxSize={123456} + onProcessUpload={[MockFunction onProcessUpload]} + progressVariant="bar" + /> +
`; @@ -82,6 +86,7 @@ exports[` render extra columns when activeStepName is submission 1

File upload +

Uploaded files @@ -140,20 +145,23 @@ exports[` render extra columns when activeStepName is submission 1 ] } /> - + + maxSize={123456} + onProcessUpload={[MockFunction onProcessUpload]} + progressVariant="bar" + /> +
`; @@ -161,6 +169,7 @@ exports[` render without dropzone and confirm modal when isReadOnl

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 ( -