From e7f0fd0c433eafa021fd88f2ff367d03e2b91ca9 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Fri, 22 Sep 2023 15:32:34 -0400 Subject: [PATCH 1/4] chore: ui implement for self assessment chore: delete file --- src/components/FileUpload/ActionCell.jsx | 15 +++- .../__snapshots__/ActionCell.test.jsx.snap | 1 + .../__snapshots__/index.test.jsx.snap | 4 +- src/components/FileUpload/index.jsx | 7 +- src/components/FileUpload/index.test.jsx | 1 + src/components/InfoPopover/index.jsx | 2 +- .../CriterionContainer/RadioCriterion.jsx | 2 +- src/components/TextResponse/index.jsx | 6 +- src/components/TextResponse/index.scss | 8 ++ src/components/TextResponse/index.test.jsx | 1 + src/data/services/lms/hooks/actions.ts | 19 ++++- src/data/services/lms/hooks/data.ts | 2 +- .../SelfAssessmentView/AssessmentActions.jsx | 12 --- .../SelfAssessmentView/AssessmentContent.jsx | 66 +++++++++++--- .../AssessmentContent.test.jsx | 45 ++++++++++ .../AssessmentContentLayout.jsx | 39 +++++---- .../AssessmentContentLayout.test.jsx | 18 ++++ .../AssessmentContent.test.jsx.snap | 85 +++++++++++++++++++ .../AssessmentContentLayout.test.jsx.snap | 26 ++++++ .../__snapshots__/index.test.jsx.snap | 15 ++++ src/views/SelfAssessmentView/hooks.js | 48 +++++++++++ src/views/SelfAssessmentView/index.jsx | 15 ++-- src/views/SelfAssessmentView/index.test.jsx | 20 +++++ src/views/SelfAssessmentView/messages.js | 49 +++++++++++ .../SubmissionView/SubmissionContent.jsx | 2 + .../SubmissionView/SubmissionContent.test.jsx | 3 +- .../SubmissionContentLayout.jsx | 2 + .../SubmissionContentLayout.test.jsx | 1 + .../SubmissionContent.test.jsx.snap | 4 +- .../SubmissionContentLayout.test.jsx.snap | 2 + .../__snapshots__/index.test.jsx.snap | 1 + src/views/SubmissionView/hooks.js | 11 +-- src/views/SubmissionView/index.jsx | 2 + src/views/SubmissionView/index.test.jsx | 1 + 34 files changed, 468 insertions(+), 67 deletions(-) delete mode 100644 src/views/SelfAssessmentView/AssessmentActions.jsx create mode 100644 src/views/SelfAssessmentView/AssessmentContent.test.jsx create mode 100644 src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx create mode 100644 src/views/SelfAssessmentView/__snapshots__/AssessmentContent.test.jsx.snap create mode 100644 src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap create mode 100644 src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap create mode 100644 src/views/SelfAssessmentView/hooks.js create mode 100644 src/views/SelfAssessmentView/index.test.jsx create mode 100644 src/views/SelfAssessmentView/messages.js diff --git a/src/components/FileUpload/ActionCell.jsx b/src/components/FileUpload/ActionCell.jsx index 047795a5..193ea44f 100644 --- a/src/components/FileUpload/ActionCell.jsx +++ b/src/components/FileUpload/ActionCell.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { IconButton, Icon } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -6,19 +6,30 @@ import { Delete, Preview } from '@edx/paragon/icons'; import messages from './messages'; -const ActionCell = () => { +const ActionCell = ({ + onDeletedFile, + disabled, + row, +}) => { const { formatMessage } = useIntl(); + const deleteFile = useCallback(async () => { + console.log('deleteFile', row.index); + await onDeletedFile(row.index); + }, []); return ( <> ); diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap index c8b38339..2aa7d334 100644 --- a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap +++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap @@ -5,6 +5,7 @@ exports[` renders 1`] = ` renders default 1`] = ` "accessor": "fileSize", }, Object { - "Cell": "ActionCell", + "Cell": [Function], "Header": "Actions", "accessor": "actions", }, @@ -106,7 +106,7 @@ exports[` renders read only 1`] = ` "accessor": "fileSize", }, Object { - "Cell": "ActionCell", + "Cell": [Function], "Header": "Actions", "accessor": "actions", }, diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 476355db..85ecc7f4 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -14,7 +14,7 @@ import messages from './messages'; import './styles.scss'; -const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => { +const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded, onDeletedFile }) => { const { formatMessage } = useIntl(); const { @@ -55,7 +55,7 @@ const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => { { Header: formatMessage(messages.fileActionsTitle), accessor: 'actions', - Cell: ActionCell, + Cell: (props) => , }, ]} /> @@ -81,6 +81,7 @@ const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => { FileUpload.defaultProps = { isReadOnly: false, uploadedFiles: [], + onFileUploaded: () => { }, }; FileUpload.propTypes = { isReadOnly: PropTypes.bool, @@ -91,7 +92,7 @@ FileUpload.propTypes = { fileSize: PropTypes.number, }), ), - onFileUploaded: PropTypes.func.isRequired, + onFileUploaded: PropTypes.func, }; export default FileUpload; diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx index 62407c82..7a973495 100644 --- a/src/components/FileUpload/index.test.jsx +++ b/src/components/FileUpload/index.test.jsx @@ -26,6 +26,7 @@ describe('', () => { }, ], onFileUploaded: jest.fn(), + onDeletedFile: jest.fn().mockName('onDeletedFile'), }; const mockHooks = (overrides) => { diff --git a/src/components/InfoPopover/index.jsx b/src/components/InfoPopover/index.jsx index b8508892..cbc90895 100644 --- a/src/components/InfoPopover/index.jsx +++ b/src/components/InfoPopover/index.jsx @@ -22,7 +22,7 @@ export const InfoPopover = ({ onClick, children }) => { return ( diff --git a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx index f8ab2830..bde9997c 100644 --- a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx +++ b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx @@ -42,7 +42,7 @@ const RadioCriterion = ({ isGrading, criterion }) => { RadioCriterion.propTypes = { isGrading: PropTypes.bool.isRequired, criterion: PropTypes.shape({ - optionsValue: PropTypes.string.isRequired, + optionsValue: PropTypes.string, optionsIsInvalid: PropTypes.bool.isRequired, optionsOnChange: PropTypes.func.isRequired, name: PropTypes.string.isRequired, diff --git a/src/components/TextResponse/index.jsx b/src/components/TextResponse/index.jsx index 140aedb4..01a973c1 100644 --- a/src/components/TextResponse/index.jsx +++ b/src/components/TextResponse/index.jsx @@ -6,12 +6,12 @@ import RichTextEditor from 'components/TextResponse/RichTextEditor'; import './index.scss'; -const TextResponse = ({ submissionConfig, value, onChange }) => { +const TextResponse = ({ submissionConfig, value, onChange, isReadOnly }) => { const { textResponseConfig } = submissionConfig; const { optional, enabled } = textResponseConfig; const props = { optional, - disabled: !enabled, + disabled: !enabled || isReadOnly, value, onChange, }; @@ -34,7 +34,7 @@ TextResponse.propTypes = { }), }).isRequired, value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, + onChange: PropTypes.func, }; export default TextResponse; diff --git a/src/components/TextResponse/index.scss b/src/components/TextResponse/index.scss index b4477cfd..3a0f44e5 100644 --- a/src/components/TextResponse/index.scss +++ b/src/components/TextResponse/index.scss @@ -1,5 +1,13 @@ +@import "@edx/paragon/scss/core/core.scss"; + .textarea-response { min-height: 200px; max-height: 300px; overflow-y: scroll; +} + +.tox-tinymce--disabled { + background-color: $input-disabled-bg; + // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526. + opacity: 1; } \ No newline at end of file diff --git a/src/components/TextResponse/index.test.jsx b/src/components/TextResponse/index.test.jsx index 26cafd17..8c8610ed 100644 --- a/src/components/TextResponse/index.test.jsx +++ b/src/components/TextResponse/index.test.jsx @@ -15,6 +15,7 @@ describe('', () => { }, value: 'value', onChange: jest.fn().mockName('onChange'), + isReadOnly: false, }; it('render Text Editor ', () => { diff --git a/src/data/services/lms/hooks/actions.ts b/src/data/services/lms/hooks/actions.ts index 7094908f..daa3d3ba 100644 --- a/src/data/services/lms/hooks/actions.ts +++ b/src/data/services/lms/hooks/actions.ts @@ -95,4 +95,21 @@ export const uploadFiles = () => queryClient.invalidateQueries([queryKeys.pageData, false]) return Promise.resolve(files); - }); \ No newline at end of file + }); + +export const deleteFile = () => + createMutationAction(async (fileIndex, queryClient) => { + await new Promise((resolve) => setTimeout(() => { + fakeData.pageData.shapes.emptySubmission.submission.response = { + ...fakeData.pageData.shapes.emptySubmission.submission.response, + uploaded_files: [ + ...fakeData.pageData.shapes.emptySubmission.submission.response.uploaded_files.filter((_, index) => index !== fileIndex) + ], + } as any; + resolve(null); + }, 1000)); + + queryClient.invalidateQueries([queryKeys.pageData, false]); + + return Promise.resolve(fakeData.pageData.shapes.emptySubmission.submission.response.uploaded_files); + }); diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index 9796c2b6..8b4e361a 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -27,7 +27,7 @@ export const useORAConfig = (): types.QueryData => { export const usePageData = (): types.QueryData => { const route = useMatch(routes.peerAssessment); - const isAssessment = !!route && route.pattern.path === routes.peerAssessment; + const isAssessment = !!route && [routes.peerAssessment, routes.selfAssessment].includes(route.pattern.path) const { data, ...status } = useQuery({ queryKey: [queryKeys.pageData, isAssessment], diff --git a/src/views/SelfAssessmentView/AssessmentActions.jsx b/src/views/SelfAssessmentView/AssessmentActions.jsx deleted file mode 100644 index 2490d700..00000000 --- a/src/views/SelfAssessmentView/AssessmentActions.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -import { ActionRow, Button } from '@edx/paragon'; - -const AssessmentActions = () => ( - - - - -); - -export default AssessmentActions; diff --git a/src/views/SelfAssessmentView/AssessmentContent.jsx b/src/views/SelfAssessmentView/AssessmentContent.jsx index b728625f..f3ae25b0 100644 --- a/src/views/SelfAssessmentView/AssessmentContent.jsx +++ b/src/views/SelfAssessmentView/AssessmentContent.jsx @@ -1,26 +1,68 @@ 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 AssessmentContent = () => { - const { prompts } = useORAConfigData(); +import messages from './messages'; + +const AssessmentContent = ({ + submission, + oraConfigData, +}) => { + const { formatMessage } = useIntl(); + return (
- {React.Children.toArray( - prompts.map((prompt, index) => ( -
- - -
- )), - )} - +
+

{formatMessage(messages.yourResponse)}

+
+

+ {formatMessage(messages.instructions)}: + {formatMessage(messages.instructionsText)} +

+ {oraConfigData.prompts.map((prompt, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+ + +
+ ))} +
); }; +AssessmentContent.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, +}; + export default AssessmentContent; diff --git a/src/views/SelfAssessmentView/AssessmentContent.test.jsx b/src/views/SelfAssessmentView/AssessmentContent.test.jsx new file mode 100644 index 00000000..9b429909 --- /dev/null +++ b/src/views/SelfAssessmentView/AssessmentContent.test.jsx @@ -0,0 +1,45 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import AssessmentContent from './AssessmentContent'; + +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, + }, + } + }; + + 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/SelfAssessmentView/AssessmentContentLayout.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx index 55f600f5..19d1a79f 100644 --- a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx @@ -1,28 +1,37 @@ 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 AssessmentContent from './AssessmentContent'; import './AssessmentContentLayout.scss'; -const AssessmentContentLayout = () => { - console.log(useRubricConfig()); - const showRubric = useRubricConfig().showDuringResponse; - return ( -
-
- - - - - {showRubric && ()} - -
+const AssessmentContentLayout = ({ + submission, + oraConfigData, +}) => ( +
+
+ + + + + +
- ); +
+); + +AssessmentContentLayout.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, }; export default AssessmentContentLayout; diff --git a/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx new file mode 100644 index 00000000..356c4798 --- /dev/null +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx @@ -0,0 +1,18 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import AssessmentContentLayout from './AssessmentContentLayout'; + +jest.mock('components/Rubric', () => 'Rubric'); +jest.mock('./AssessmentContent', () => 'AssessmentContent'); + +describe('', () => { + const props = { + submission: 'submission' + }; + + it('render', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('Rubric')).toHaveLength(1); + }); +}); diff --git a/src/views/SelfAssessmentView/__snapshots__/AssessmentContent.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/AssessmentContent.test.jsx.snap new file mode 100644 index 00000000..01a70f7a --- /dev/null +++ b/src/views/SelfAssessmentView/__snapshots__/AssessmentContent.test.jsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render default 1`] = ` +
+
+

+ Your response +

+
+

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

+
+

+ + 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/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap new file mode 100644 index 00000000..682a45ad --- /dev/null +++ b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render 1`] = ` +
+
+ + + + + + +
+
+`; diff --git a/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap new file mode 100644 index 00000000..2126d32c --- /dev/null +++ b/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders 1`] = ` + + + +`; diff --git a/src/views/SelfAssessmentView/hooks.js b/src/views/SelfAssessmentView/hooks.js new file mode 100644 index 00000000..f702bdd5 --- /dev/null +++ b/src/views/SelfAssessmentView/hooks.js @@ -0,0 +1,48 @@ +import { useEffect, useReducer } from 'react'; + +import { useORAConfigData, usePageData } from 'data/services/lms/hooks/selectors'; + +import { submitResponse, saveResponse, uploadFiles, deleteFile } from 'data/services/lms/hooks/actions'; +import { MutationStatus } from 'data/services/lms/constants'; + +const useAssessmentViewHooks = () => { + const submitResponseMutation = submitResponse(); + const saveResponseMutation = saveResponse(); + const pageData = usePageData(); + const oraConfigData = useORAConfigData(); + + const [submission, dispatchAssessment] = 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) { + dispatchAssessment({ ...pageData.submission, isDirty: false }); + } + }, [pageData?.submission]); + + const submitResponseHandler = () => { + dispatchAssessment({ isDirty: false }); + submitResponseMutation.mutate(submission); + }; + + const saveResponseHandler = () => { + dispatchAssessment({ isDirty: false }); + saveResponseMutation.mutate(submission); + }; + + return { + submitResponseHandler, + submitResponseStatus: submitResponseMutation.status, + saveResponseHandler, + saveResponseStatus: saveResponseMutation.status, + pageData, + oraConfigData, + submission, + dispatchAssessment, + }; +}; + +export default useAssessmentViewHooks; diff --git a/src/views/SelfAssessmentView/index.jsx b/src/views/SelfAssessmentView/index.jsx index 18ad1fcb..04d5f23d 100644 --- a/src/views/SelfAssessmentView/index.jsx +++ b/src/views/SelfAssessmentView/index.jsx @@ -1,20 +1,21 @@ import React from 'react'; -import { useIsORAConfigLoaded } from 'data/services/lms/hooks/selectors'; import ProgressBar from 'components/ProgressBar'; import AssessmentContentLayout from './AssessmentContentLayout'; -import AssessmentActions from './AssessmentActions'; +import useAssessmentViewHooks from './hooks'; -export const SelfAssessmentView = () => { - const isORAConfigLoaded = useIsORAConfigLoaded(); +export const AssessmentView = () => { + const { submission, oraConfigData } = useAssessmentViewHooks(); return ( <> - {isORAConfigLoaded && ()} - + ); }; -export default SelfAssessmentView; +export default AssessmentView; diff --git a/src/views/SelfAssessmentView/index.test.jsx b/src/views/SelfAssessmentView/index.test.jsx new file mode 100644 index 00000000..928eb329 --- /dev/null +++ b/src/views/SelfAssessmentView/index.test.jsx @@ -0,0 +1,20 @@ +import { shallow } from '@edx/react-unit-test-utils'; +import { AssessmentView } from '.'; + +jest.mock('./AssessmentContentLayout', () => 'AssessmentContentLayout'); + +jest.mock('./hooks', () => jest.fn().mockReturnValue({ + submission: 'submission', + oraConfigData: 'oraConfigData', + submitResponseHandler: jest.fn().mockName('submitResponseHandler'), + submitResponseStatus: 'submitResponseStatus', + saveResponseHandler: jest.fn().mockName('saveResponseHandler'), + saveResponseStatus: 'saveResponseStatus', +})); + +describe('', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/views/SelfAssessmentView/messages.js b/src/views/SelfAssessmentView/messages.js new file mode 100644 index 00000000..00cff185 --- /dev/null +++ b/src/views/SelfAssessmentView/messages.js @@ -0,0 +1,49 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + yourResponse: { + id: 'ora-grading.AssessmentView.yourResponse', + defaultMessage: 'Your response', + description: 'Label for the response textarea', + }, + instructions: { + id: 'ora-grading.AssessmentView.instructions', + defaultMessage: 'Instructions', + description: 'Label for the instructions textarea', + }, + instructionsText: { + id: 'ora-grading.AssessmentView.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.AssessmentAction.submit', + defaultMessage: 'Submit response', + description: 'Submit button text', + }, + submissionActionSubmitting: { + id: 'ora-grading.AssessmentAction.submitting', + defaultMessage: 'Submitting response', + description: 'Submit button text while submitting', + }, + submissionActionSubmitted: { + id: 'ora-grading.AssessmentAction.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; diff --git a/src/views/SubmissionView/SubmissionContent.jsx b/src/views/SubmissionView/SubmissionContent.jsx index 3a5230de..ccc0304b 100644 --- a/src/views/SubmissionView/SubmissionContent.jsx +++ b/src/views/SubmissionView/SubmissionContent.jsx @@ -16,6 +16,7 @@ const SubmissionContent = ({ oraConfigData, onTextResponseChange, onFileUploaded, + onDeletedFile, draftSaved, }) => { const { formatMessage } = useIntl(); @@ -49,6 +50,7 @@ const SubmissionContent = ({
); diff --git a/src/views/SubmissionView/SubmissionContent.test.jsx b/src/views/SubmissionView/SubmissionContent.test.jsx index a6e441fc..7e125c1f 100644 --- a/src/views/SubmissionView/SubmissionContent.test.jsx +++ b/src/views/SubmissionView/SubmissionContent.test.jsx @@ -25,8 +25,9 @@ describe('', () => { maxFileSize: 100, }, }, - onTextResponseChange: jest.fn().mockName('onTextResponseChange'), + onTextResponseChange: () => jest.fn().mockName('onTextResponseChange'), onFileUploaded: jest.fn().mockName('onFileUploaded'), + onDeletedFile: jest.fn().mockName('onDeletedFile'), draftSaved: true, }; diff --git a/src/views/SubmissionView/SubmissionContentLayout.jsx b/src/views/SubmissionView/SubmissionContentLayout.jsx index d1e9cb82..8f96db46 100644 --- a/src/views/SubmissionView/SubmissionContentLayout.jsx +++ b/src/views/SubmissionView/SubmissionContentLayout.jsx @@ -13,6 +13,7 @@ const SubmissionContentLayout = ({ oraConfigData, onTextResponseChange, onFileUploaded, + onDeletedFile, draftSaved, }) => (
@@ -24,6 +25,7 @@ const SubmissionContentLayout = ({ oraConfigData={oraConfigData} onTextResponseChange={onTextResponseChange} onFileUploaded={onFileUploaded} + onDeletedFile={onDeletedFile} draftSaved={draftSaved} /> diff --git a/src/views/SubmissionView/SubmissionContentLayout.test.jsx b/src/views/SubmissionView/SubmissionContentLayout.test.jsx index 95efdf66..98069ed0 100644 --- a/src/views/SubmissionView/SubmissionContentLayout.test.jsx +++ b/src/views/SubmissionView/SubmissionContentLayout.test.jsx @@ -12,6 +12,7 @@ describe('', () => { }, onTextResponseChange: jest.fn().mockName('onTextResponseChange'), onFileUploaded: jest.fn().mockName('onFileUploaded'), + onDeletedFile: jest.fn().mockName('onDeletedFile'), draftSaved: true, }; diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap index 7b07e027..642b7975 100644 --- a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap +++ b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap @@ -36,7 +36,7 @@ exports[` render default 1`] = ` prompt="

test

" /> render default 1`] = ` />
render no prompts 1`] = ` it.

hide rubric 1`] = ` > show rubric 1`] = ` > renders 1`] = ` > { const submitResponseMutation = submitResponse(); const saveResponseMutation = saveResponse(); const uploadFilesMutation = uploadFiles(); + const deleteFileMutation = deleteFile(); const pageData = usePageData(); const oraConfigData = useORAConfigData(); @@ -37,10 +38,9 @@ const useSubmissionViewHooks = () => { }); }; - const onFileUploaded = (args) => { - const fileUploads = uploadFilesMutation.mutate(args); - dispatchSubmission({ response: { ...submission.response, fileUploads } }); - }; + const onFileUploaded = uploadFilesMutation.mutate + + const onDeletedFile = deleteFileMutation.mutate const submitResponseHandler = () => { dispatchSubmission({ isDirty: false }); @@ -64,6 +64,7 @@ const useSubmissionViewHooks = () => { dispatchSubmission, onTextResponseChange, onFileUploaded, + onDeletedFile, }; }; diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx index e4c4687c..5239589e 100644 --- a/src/views/SubmissionView/index.jsx +++ b/src/views/SubmissionView/index.jsx @@ -16,6 +16,7 @@ export const SubmissionView = () => { saveResponseHandler, saveResponseStatus, draftSaved, + onDeletedFile, } = useSubmissionViewHooks(); return ( <> @@ -25,6 +26,7 @@ export const SubmissionView = () => { oraConfigData={oraConfigData} onTextResponseChange={onTextResponseChange} onFileUploaded={onFileUploaded} + onDeletedFile={onDeletedFile} draftSaved={draftSaved} /> jest.fn().mockReturnValue({ submission: 'submission', oraConfigData: 'oraConfigData', onFileUploaded: jest.fn().mockName('onFileUploaded'), + onDeletedFile: jest.fn().mockName('onDeletedFile'), onTextResponseChange: jest.fn().mockName('onTextResponseChange'), submitResponseHandler: jest.fn().mockName('submitResponseHandler'), submitResponseStatus: 'submitResponseStatus', From bc5a0624f0f16ee5cdf50427acc28dbbf84a9060 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Mon, 25 Sep 2023 11:46:35 -0400 Subject: [PATCH 2/4] chore: update test --- src/components/FileUpload/ActionCell.jsx | 19 ++++++++-- src/components/FileUpload/ActionCell.test.jsx | 9 ++++- .../__snapshots__/ActionCell.test.jsx.snap | 2 ++ src/components/FileUpload/index.jsx | 7 +++- src/components/TextResponse/index.jsx | 10 +++++- .../SelfAssessmentView/AssessmentContent.jsx | 2 -- .../AssessmentContent.test.jsx | 2 +- .../AssessmentContentLayout.jsx | 2 +- .../AssessmentContentLayout.test.jsx | 3 +- .../AssessmentContentLayout.test.jsx.snap | 1 + .../__snapshots__/index.test.jsx.snap | 10 ++---- src/views/SelfAssessmentView/hooks.js | 35 +------------------ src/views/SelfAssessmentView/index.test.jsx | 4 --- .../SubmissionView/SubmissionContent.jsx | 1 + .../SubmissionContentLayout.jsx | 1 + .../__snapshots__/index.test.jsx.snap | 24 +++++-------- src/views/SubmissionView/hooks.js | 8 +++-- 17 files changed, 66 insertions(+), 74 deletions(-) diff --git a/src/components/FileUpload/ActionCell.jsx b/src/components/FileUpload/ActionCell.jsx index 193ea44f..9f97f288 100644 --- a/src/components/FileUpload/ActionCell.jsx +++ b/src/components/FileUpload/ActionCell.jsx @@ -1,8 +1,9 @@ import React, { useCallback } from 'react'; -import { IconButton, Icon } from '@edx/paragon'; +import PropTypes from 'prop-types'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { IconButton, Icon } from '@edx/paragon'; import { Delete, Preview } from '@edx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; @@ -15,7 +16,7 @@ const ActionCell = ({ const deleteFile = useCallback(async () => { console.log('deleteFile', row.index); await onDeletedFile(row.index); - }, []); + }, [onDeletedFile, row.index]); return ( <> {}, +}; + +ActionCell.propTypes = { + onDeletedFile: PropTypes.func, + disabled: PropTypes.bool.isRequired, + row: PropTypes.shape({ + index: PropTypes.number.isRequired, + }).isRequired, +}; + export default ActionCell; diff --git a/src/components/FileUpload/ActionCell.test.jsx b/src/components/FileUpload/ActionCell.test.jsx index f9992b6a..6ffbd72e 100644 --- a/src/components/FileUpload/ActionCell.test.jsx +++ b/src/components/FileUpload/ActionCell.test.jsx @@ -2,8 +2,15 @@ import { shallow } from '@edx/react-unit-test-utils'; import ActionCell from './ActionCell'; describe('', () => { + const props = { + onDeletedFile: jest.fn(), + disabled: false, + row: { + index: 0, + }, + }; it('renders', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.snapshot).toMatchSnapshot(); }); }); diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap index 2aa7d334..edb37f3e 100644 --- a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap +++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap @@ -4,12 +4,14 @@ exports[` renders 1`] = ` diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 85ecc7f4..251ca570 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -14,7 +14,9 @@ import messages from './messages'; import './styles.scss'; -const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded, onDeletedFile }) => { +const FileUpload = ({ + isReadOnly, uploadedFiles, onFileUploaded, onDeletedFile, +}) => { const { formatMessage } = useIntl(); const { @@ -55,6 +57,7 @@ const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded, onDeletedFile } { Header: formatMessage(messages.fileActionsTitle), accessor: 'actions', + // eslint-disable-next-line react/no-unstable-nested-components Cell: (props) => , }, ]} @@ -82,6 +85,7 @@ FileUpload.defaultProps = { isReadOnly: false, uploadedFiles: [], onFileUploaded: () => { }, + onDeletedFile: () => { }, }; FileUpload.propTypes = { isReadOnly: PropTypes.bool, @@ -93,6 +97,7 @@ FileUpload.propTypes = { }), ), onFileUploaded: PropTypes.func, + onDeletedFile: PropTypes.func, }; export default FileUpload; diff --git a/src/components/TextResponse/index.jsx b/src/components/TextResponse/index.jsx index 01a973c1..58149ffe 100644 --- a/src/components/TextResponse/index.jsx +++ b/src/components/TextResponse/index.jsx @@ -6,7 +6,9 @@ import RichTextEditor from 'components/TextResponse/RichTextEditor'; import './index.scss'; -const TextResponse = ({ submissionConfig, value, onChange, isReadOnly }) => { +const TextResponse = ({ + submissionConfig, value, onChange, isReadOnly, +}) => { const { textResponseConfig } = submissionConfig; const { optional, enabled } = textResponseConfig; const props = { @@ -25,6 +27,11 @@ const TextResponse = ({ submissionConfig, value, onChange, isReadOnly }) => { ); }; +TextResponse.defaultProps = { + onChange: () => {}, + isReadOnly: false, +}; + TextResponse.propTypes = { submissionConfig: PropTypes.shape({ textResponseConfig: PropTypes.shape({ @@ -35,6 +42,7 @@ TextResponse.propTypes = { }).isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func, + isReadOnly: PropTypes.bool, }; export default TextResponse; diff --git a/src/views/SelfAssessmentView/AssessmentContent.jsx b/src/views/SelfAssessmentView/AssessmentContent.jsx index f3ae25b0..9a841c55 100644 --- a/src/views/SelfAssessmentView/AssessmentContent.jsx +++ b/src/views/SelfAssessmentView/AssessmentContent.jsx @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Icon } from '@edx/paragon'; -import { CheckCircle } from '@edx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import Prompt from 'components/Prompt'; diff --git a/src/views/SelfAssessmentView/AssessmentContent.test.jsx b/src/views/SelfAssessmentView/AssessmentContent.test.jsx index 9b429909..7ec40eef 100644 --- a/src/views/SelfAssessmentView/AssessmentContent.test.jsx +++ b/src/views/SelfAssessmentView/AssessmentContent.test.jsx @@ -24,7 +24,7 @@ describe('', () => { submissionConfig: { maxFileSize: 100, }, - } + }, }; describe('render', () => { diff --git a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx index 19d1a79f..a53b5130 100644 --- a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx @@ -21,7 +21,7 @@ const AssessmentContentLayout = ({ oraConfigData={oraConfigData} /> - + diff --git a/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx index 356c4798..b08ed9db 100644 --- a/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.test.jsx @@ -6,7 +6,8 @@ jest.mock('./AssessmentContent', () => 'AssessmentContent'); describe('', () => { const props = { - submission: 'submission' + submission: 'submission', + oraConfigData: 'oraConfigData', }; it('render', () => { diff --git a/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap index 682a45ad..ca56c1f6 100644 --- a/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap +++ b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap @@ -14,6 +14,7 @@ exports[` render 1`] = ` className="p-0" > diff --git a/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap index 2126d32c..8cb7e67b 100644 --- a/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap +++ b/src/views/SelfAssessmentView/__snapshots__/index.test.jsx.snap @@ -1,15 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders 1`] = ` - + + - + `; diff --git a/src/views/SelfAssessmentView/hooks.js b/src/views/SelfAssessmentView/hooks.js index f702bdd5..5d62bd61 100644 --- a/src/views/SelfAssessmentView/hooks.js +++ b/src/views/SelfAssessmentView/hooks.js @@ -2,46 +2,13 @@ import { useEffect, useReducer } from 'react'; import { useORAConfigData, usePageData } from 'data/services/lms/hooks/selectors'; -import { submitResponse, saveResponse, uploadFiles, deleteFile } from 'data/services/lms/hooks/actions'; -import { MutationStatus } from 'data/services/lms/constants'; - const useAssessmentViewHooks = () => { - const submitResponseMutation = submitResponse(); - const saveResponseMutation = saveResponse(); const pageData = usePageData(); const oraConfigData = useORAConfigData(); - const [submission, dispatchAssessment] = 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) { - dispatchAssessment({ ...pageData.submission, isDirty: false }); - } - }, [pageData?.submission]); - - const submitResponseHandler = () => { - dispatchAssessment({ isDirty: false }); - submitResponseMutation.mutate(submission); - }; - - const saveResponseHandler = () => { - dispatchAssessment({ isDirty: false }); - saveResponseMutation.mutate(submission); - }; - return { - submitResponseHandler, - submitResponseStatus: submitResponseMutation.status, - saveResponseHandler, - saveResponseStatus: saveResponseMutation.status, - pageData, + submission: pageData?.submission, oraConfigData, - submission, - dispatchAssessment, }; }; diff --git a/src/views/SelfAssessmentView/index.test.jsx b/src/views/SelfAssessmentView/index.test.jsx index 928eb329..8667e5af 100644 --- a/src/views/SelfAssessmentView/index.test.jsx +++ b/src/views/SelfAssessmentView/index.test.jsx @@ -6,10 +6,6 @@ jest.mock('./AssessmentContentLayout', () => 'AssessmentContentLayout'); jest.mock('./hooks', () => jest.fn().mockReturnValue({ submission: 'submission', oraConfigData: 'oraConfigData', - submitResponseHandler: jest.fn().mockName('submitResponseHandler'), - submitResponseStatus: 'submitResponseStatus', - saveResponseHandler: jest.fn().mockName('saveResponseHandler'), - saveResponseStatus: 'saveResponseStatus', })); describe('', () => { diff --git a/src/views/SubmissionView/SubmissionContent.jsx b/src/views/SubmissionView/SubmissionContent.jsx index ccc0304b..3ad46b49 100644 --- a/src/views/SubmissionView/SubmissionContent.jsx +++ b/src/views/SubmissionView/SubmissionContent.jsx @@ -76,6 +76,7 @@ SubmissionContent.propTypes = { }).isRequired, onTextResponseChange: PropTypes.func.isRequired, onFileUploaded: PropTypes.func.isRequired, + onDeletedFile: PropTypes.func.isRequired, draftSaved: PropTypes.bool.isRequired, }; diff --git a/src/views/SubmissionView/SubmissionContentLayout.jsx b/src/views/SubmissionView/SubmissionContentLayout.jsx index 8f96db46..0e890b38 100644 --- a/src/views/SubmissionView/SubmissionContentLayout.jsx +++ b/src/views/SubmissionView/SubmissionContentLayout.jsx @@ -42,6 +42,7 @@ SubmissionContentLayout.propTypes = { oraConfigData: PropTypes.any.isRequired, onTextResponseChange: PropTypes.func.isRequired, onFileUploaded: PropTypes.func.isRequired, + onDeletedFile: PropTypes.func.isRequired, draftSaved: PropTypes.bool.isRequired, }; diff --git a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap index 7fa002c3..35995e67 100644 --- a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap +++ b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap @@ -1,20 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders 1`] = ` - - } - isOpen={true} - modalBodyClassName="content-body" - onClose={[Function]} - title="ORA Submission" -> + + renders 1`] = ` oraConfigData="oraConfigData" submission="submission" /> - + + `; diff --git a/src/views/SubmissionView/hooks.js b/src/views/SubmissionView/hooks.js index 30231089..acd4d812 100644 --- a/src/views/SubmissionView/hooks.js +++ b/src/views/SubmissionView/hooks.js @@ -2,7 +2,9 @@ import { useEffect, useReducer } from 'react'; import { useORAConfigData, usePageData } from 'data/services/lms/hooks/selectors'; -import { submitResponse, saveResponse, uploadFiles, deleteFile } from 'data/services/lms/hooks/actions'; +import { + submitResponse, saveResponse, uploadFiles, deleteFile, +} from 'data/services/lms/hooks/actions'; import { MutationStatus } from 'data/services/lms/constants'; const useSubmissionViewHooks = () => { @@ -38,9 +40,9 @@ const useSubmissionViewHooks = () => { }); }; - const onFileUploaded = uploadFilesMutation.mutate + const onFileUploaded = uploadFilesMutation.mutate; - const onDeletedFile = deleteFileMutation.mutate + const onDeletedFile = deleteFileMutation.mutate; const submitResponseHandler = () => { dispatchSubmission({ isDirty: false }); From 3595f50ac1e16e8cf50ffa90a7121b94cd57a880 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Tue, 26 Sep 2023 12:01:26 -0400 Subject: [PATCH 3/4] chore: update ui and messages --- src/components/ProgressBar/index.jsx | 6 +- src/components/StatefulStatus/index.jsx | 60 +++++++++++ src/index.scss | 22 ++++- .../AssessmentContentLayout.jsx | 24 +++-- .../SelfAssessmentView/AssessmentStatus.jsx | 99 +++++++++++++++++++ src/views/SelfAssessmentView/messages.js | 96 +++++++++++++----- 6 files changed, 271 insertions(+), 36 deletions(-) create mode 100644 src/components/StatefulStatus/index.jsx create mode 100644 src/views/SelfAssessmentView/AssessmentStatus.jsx diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx index fa9348b3..74692ba0 100644 --- a/src/components/ProgressBar/index.jsx +++ b/src/components/ProgressBar/index.jsx @@ -119,13 +119,13 @@ export const ProgressBar = () => { {stepConfig.order.map(step => { if (step === 'peer') { - return ; + return ; } if (step === 'training') { - return ; + return ; } if (step === 'self') { - return ; + return ; } return null; })} diff --git a/src/components/StatefulStatus/index.jsx b/src/components/StatefulStatus/index.jsx new file mode 100644 index 00000000..efb46275 --- /dev/null +++ b/src/components/StatefulStatus/index.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Alert, Badge } from '@edx/paragon'; +import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons'; + +const alertMap = { + success: { + variant: 'success', + icon: CheckCircle, + }, + error: { + variant: 'danger', + icon: Info, + }, + cancelled: { + variant: 'warning', + icon: WarningFilled, + }, + default: { + variant: 'dark', + icon: null, + } +}; + +export const statefulStates = Object.keys(alertMap); + +const StatefulStatus = ({ state, status }) => { + const { headerText, badgeText, content } = status[state]; + + const { variant, icon } = alertMap[state]; + + return ( + <> + {badgeText} + {headerText} + {state !== 'default' ? ( + + {content} + + ) : ( + content + )} + + ); +}; + +const statusProps = PropTypes.shape({ + badgeText: PropTypes.string.isRequired, + headerText: PropTypes.string.isRequired, + content: PropTypes.node, +}); + +StatefulStatus.propTypes = { + state: PropTypes.oneOf(statefulStates) + .isRequired, + status: PropTypes.objectOf(statusProps).isRequired, +}; + +export default StatefulStatus; diff --git a/src/index.scss b/src/index.scss index 6b4980cc..1bf84429 100644 --- a/src/index.scss +++ b/src/index.scss @@ -23,4 +23,24 @@ .spinner-md { height: 100px; width: 100px; -} \ No newline at end of file +} + +.gap-0 { + gap: map-get($spacers, 0); +} + +.gap-1 { + gap: map-get($spacers, 1); +} + +.gap-2 { + gap: map-get($spacers, 2); +} + +.gap-3 { + gap: map-get($spacers, 3); +} + +.gap-4 { + gap: map-get($spacers, 4); +} diff --git a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx index a53b5130..2359af91 100644 --- a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx @@ -4,18 +4,26 @@ import PropTypes from 'prop-types'; import { Col, Row } from '@edx/paragon'; import Rubric from 'components/Rubric'; +import { statefulStates } from 'components/StatefulStatus'; import AssessmentContent from './AssessmentContent'; +import AssessmentStatus from './AssessmentStatus'; import './AssessmentContentLayout.scss'; -const AssessmentContentLayout = ({ - submission, - oraConfigData, -}) => ( -
-
- - +const AssessmentContentLayout = ({ submission, oraConfigData }) => ( +
+
+ + + { + statefulStates.map((status) => ( + + )) + } { + const { formatMessage } = useIntl(); + return ( + +

+ {formatMessage(messages.instructions)}: {formatMessage(messages.inProgressText)} +

+ + +
+ ), + }, + success: { + badgeText: formatMessage(messages.completedBadge), + headerText: formatMessage(messages.completedHeader), + content: ( +
+
+ {formatMessage(messages.completedBodyHeader)} +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Nullam euismod, nisl eget ultricies aliquet, mauris quam + sodales +

+
+ +
+ ), + }, + error: { + badgeText: formatMessage(messages.errorBadge), + headerText: formatMessage(messages.errorHeader), + content: ( + <> + + {formatMessage(messages.errorBodyHeader)} + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + euismod, nisl eget ultricies aliquet, mauris quam sodales +

+ + ), + }, + cancelled: { + badgeText: formatMessage(messages.cancelledBadge), + headerText: formatMessage(messages.cancelledHeader, { dueDate }), + content: ( + <> + {formatMessage(messages.cancelledBodyHeader)} +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam + euismod, nisl eget ultricies aliquet, mauris quam sodales +

+ + ), + }, + }} + /> + ); +}; + +AssessmentStatus.propTypes = { + status: PropTypes.oneOf(['default', 'success', 'error', 'cancelled']) + .isRequired, + dueDate: PropTypes.string, +}; + +export default AssessmentStatus; diff --git a/src/views/SelfAssessmentView/messages.js b/src/views/SelfAssessmentView/messages.js index 00cff185..2024f9c1 100644 --- a/src/views/SelfAssessmentView/messages.js +++ b/src/views/SelfAssessmentView/messages.js @@ -19,30 +19,78 @@ const messages = defineMessages({ it.`, description: 'Description for the instructions textarea', }, - submissionActionSubmit: { - id: 'ora-grading.AssessmentAction.submit', - defaultMessage: 'Submit response', - description: 'Submit button text', - }, - submissionActionSubmitting: { - id: 'ora-grading.AssessmentAction.submitting', - defaultMessage: 'Submitting response', - description: 'Submit button text while submitting', - }, - submissionActionSubmitted: { - id: 'ora-grading.AssessmentAction.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', + inProgressBadge: { + id: 'ora-grading.AssessmentView.inProgressBadge', + defaultMessage: 'In Progress', + description: 'Label for the in progress badge', + }, + inProgressHeader: { + id: 'ora-grading.AssessmentView.inProgressHeader', + defaultMessage: 'Self-grading due by {dueDate}', + description: 'Header for the in progress badge', + }, + inProgressText: { + id: 'ora-grading.AssessmentView.inProgressBadgeText', + defaultMessage: `Assess your own response and give + yourself a grade. Progress will be saved automatically and you + can return to complete your self assessment at any time. After + you submit your grade, you cannot edit it.`, + description: 'Description for the in progress badge', + }, + inProgressButton: { + id: 'ora-grading.AssessmentView.inProgressButton', + defaultMessage: 'Begin self assessment', + description: 'Label for button to begin self assessment', + }, + completedBadge: { + id: 'ora-grading.AssessmentView.completedBadge', + defaultMessage: 'Completed', + description: 'Label for the completed badge', + }, + completedHeader: { + id: 'ora-grading.AssessmentView.completedHeader', + defaultMessage: 'Practice grading is complete!', + description: 'Header for the completed badge', + }, + completedBodyHeader: { + id: 'ora-grading.AssessmentView.completedBodyHeader', + defaultMessage: 'Practice grading complete', + description: 'Alert header for the completed badge', + }, + completedBodyButton: { + id: 'ora-grading.AssessmentView.completedBodyButton', + defaultMessage: 'Begin peer grading', + description: 'Label for button to view your grade', + }, + errorBadge: { + id: 'ora-grading.AssessmentView.errorBadge', + defaultMessage: 'Incomplete', + description: 'Label for the incomplete badge', + }, + errorHeader: { + id: 'ora-grading.AssessmentView.errorBadgeHeader', + defaultMessage: 'This step is past due!', + description: 'Header for the incomplete badge', + }, + errorBodyHeader: { + id: 'ora-grading.AssessmentView.errorBodyHeader', + defaultMessage: 'The due date for this step has passed', + description: 'Alert header for the incomplete badge', + }, + cancelledBadge: { + id: 'ora-grading.AssessmentView.cancelledBadge', + defaultMessage: 'Cancelled', + description: 'Label for the cancelled badge', + }, + cancelledHeader: { + id: 'ora-grading.AssessmentView.cancelledBadgeHeader', + defaultMessage: 'Self-grading Due by {dueDate}', + description: 'Header for the cancelled badge', + }, + cancelledBodyHeader: { + id: 'ora-grading.AssessmentView.cancelledBodyHeader', + defaultMessage: 'This step has been cancelled', + description: 'Alert header for the cancelled badge', }, }); From 7aa2628f2c4230ee9f867aa60a579a2c63d88871 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Wed, 27 Sep 2023 14:53:16 -0400 Subject: [PATCH 4/4] chore: add file preview --- .eslintrc.js | 9 + jest.config.js | 3 +- package-lock.json | 575 +++++++++++++++++- package.json | 5 +- src/App.jsx | 3 + .../FilePreview/Banners/ErrorBanner.jsx | 46 ++ .../FilePreview/Banners/ErrorBanner.test.jsx | 56 ++ .../FilePreview/Banners/LoadingBanner.jsx | 14 + .../Banners/LoadingBanner.test.jsx | 11 + .../__snapshots__/ErrorBanner.test.jsx.snap | 31 + .../__snapshots__/LoadingBanner.test.jsx.snap | 12 + src/components/FilePreview/Banners/index.jsx | 2 + .../BaseRenderers/ImageRenderer.jsx | 27 + .../BaseRenderers/ImageRenderer.test.jsx | 21 + .../FilePreview/BaseRenderers/PDFRenderer.jsx | 94 +++ .../BaseRenderers/PDFRenderer.test.jsx | 57 ++ .../FilePreview/BaseRenderers/TXTRenderer.jsx | 22 + .../BaseRenderers/TXTRenderer.test.jsx | 23 + .../__snapshots__/ImageRenderer.test.jsx.snap | 11 + .../__snapshots__/PDFRenderer.test.jsx.snap | 139 +++++ .../__snapshots__/TXTRenderer.test.jsx.snap | 9 + .../FilePreview/BaseRenderers/index.jsx | 3 + .../FilePreview/BaseRenderers/pdfHooks.js | 61 ++ .../BaseRenderers/pdfHooks.test.js | 151 +++++ .../FilePreview/BaseRenderers/textHooks.js | 38 ++ .../BaseRenderers/textHooks.test.js | 80 +++ src/components/FilePreview/FileCard.jsx | 35 ++ src/components/FilePreview/FileCard.scss | 33 + src/components/FilePreview/FileCard.test.jsx | 34 ++ src/components/FilePreview/FileRenderer.jsx | 47 ++ .../FilePreview/FileRenderer.test.jsx | 63 ++ .../__snapshots__/FileCard.test.jsx.snap | 28 + .../__snapshots__/FileRenderer.test.jsx.snap | 34 ++ src/components/FilePreview/hooks.js | 117 ++++ src/components/FilePreview/hooks.test.js | 101 +++ src/components/FilePreview/index.jsx | 2 + src/components/FilePreview/messages.js | 26 + .../__snapshots__/ActionCell.test.jsx.snap | 12 +- src/components/ProgressBar/index.jsx | 4 +- src/components/StatefulStatus/index.jsx | 4 +- src/routes.ts | 1 + src/setupTest.js | 8 + src/views/FilePreviewView/index.jsx | 34 ++ .../AssessmentContentLayout.jsx | 10 +- .../AssessmentContentLayout.test.jsx | 11 +- .../SelfAssessmentView/AssessmentStatus.jsx | 14 +- .../AssessmentContentLayout.test.jsx.snap | 32 +- src/views/SelfAssessmentView/hooks.js | 2 - 48 files changed, 2107 insertions(+), 48 deletions(-) create mode 100644 src/components/FilePreview/Banners/ErrorBanner.jsx create mode 100644 src/components/FilePreview/Banners/ErrorBanner.test.jsx create mode 100644 src/components/FilePreview/Banners/LoadingBanner.jsx create mode 100644 src/components/FilePreview/Banners/LoadingBanner.test.jsx create mode 100644 src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap create mode 100644 src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap create mode 100644 src/components/FilePreview/Banners/index.jsx create mode 100644 src/components/FilePreview/BaseRenderers/ImageRenderer.jsx create mode 100644 src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx create mode 100644 src/components/FilePreview/BaseRenderers/PDFRenderer.jsx create mode 100644 src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx create mode 100644 src/components/FilePreview/BaseRenderers/TXTRenderer.jsx create mode 100644 src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx create mode 100644 src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/BaseRenderers/index.jsx create mode 100644 src/components/FilePreview/BaseRenderers/pdfHooks.js create mode 100644 src/components/FilePreview/BaseRenderers/pdfHooks.test.js create mode 100644 src/components/FilePreview/BaseRenderers/textHooks.js create mode 100644 src/components/FilePreview/BaseRenderers/textHooks.test.js create mode 100644 src/components/FilePreview/FileCard.jsx create mode 100644 src/components/FilePreview/FileCard.scss create mode 100644 src/components/FilePreview/FileCard.test.jsx create mode 100644 src/components/FilePreview/FileRenderer.jsx create mode 100644 src/components/FilePreview/FileRenderer.test.jsx create mode 100644 src/components/FilePreview/__snapshots__/FileCard.test.jsx.snap create mode 100644 src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap create mode 100644 src/components/FilePreview/hooks.js create mode 100644 src/components/FilePreview/hooks.test.js create mode 100644 src/components/FilePreview/index.jsx create mode 100644 src/components/FilePreview/messages.js create mode 100644 src/views/FilePreviewView/index.jsx diff --git a/.eslintrc.js b/.eslintrc.js index bb529e64..301fb91d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,5 +4,14 @@ const { createConfig } = require('@edx/frontend-build'); module.exports = createConfig('eslint', { rules: { 'import/no-unresolved': 'off', + 'import/no-named-as-default': 'off', }, + overrides: [ + { + files: ['*{h,H}ooks.js'], + rules: { + 'react-hooks/rules-of-hooks': 'off', + }, + }, + ], }); diff --git a/jest.config.js b/jest.config.js index c641d5f2..18992858 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,6 +13,7 @@ const config = createConfig('jest', { }); config.moduleDirectories = ['node_modules', 'src']; -// config.moduleNameMapper['@/(.*)'] = '/src/$1'; +// add axios to the list of modules to not transform +config.transformIgnorePatterns = ['/node_modules/(?!@edx|axios)']; module.exports = config; diff --git a/package-lock.json b/package-lock.json index 0cd45674..eb6fc570 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@edx/frontend-component-footer": "12.2.1", "@edx/frontend-component-header": "4.6.0", - "@edx/frontend-platform": "5.3.2", + "@edx/frontend-platform": "5.4.0", "@edx/paragon": "^20.20.0", "@edx/react-unit-test-utils": "1.7.0", "@edx/tinymce-language-selector": "1.1.0", @@ -22,16 +22,19 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", "@tanstack/react-query": "^4.29.25", - "@tanstack/react-query-devtools": "^4.32.1", + "@tanstack/react-query-devtools": "^4.35.3", "@tinymce/tinymce-react": "3.14.0", + "axios": "^1.5.1", "classnames": "^2.3.2", "core-js": "3.32.2", "filesize": "^8.0.6", "jest-when": "^3.6.0", + "pdfjs-dist": "^3.11.174", "prop-types": "15.8.1", "query-string": "^8.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-pdf": "^7.4.0", "react-redux": "7.2.9", "react-router": "6.16.0", "react-router-dom": "6.16.0", @@ -3459,9 +3462,9 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.3.2.tgz", - "integrity": "sha512-wa381cOC2cNib5FJxKo4kEoX7f55O/nojUZTgSVYQQDElpTOucxMzjihKhc6wUHWBWENzS/CW1ikmgGK4BK7ww==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.4.0.tgz", + "integrity": "sha512-cz9yQfHJk1PMQdhxeyIXXiBNqaG9dQZpcBgodmVlLnL/PeN1CuRVjjW98WlKYSrxoZAH5wdgUOr0hKRW3OyBAA==", "dependencies": { "@cospired/i18n-iso-languages": "4.1.0", "@formatjs/intl-pluralrules": "4.3.3", @@ -3498,6 +3501,15 @@ "redux": "^4.0.4" } }, + "node_modules/@edx/frontend-platform/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/@edx/new-relic-source-map-webpack-plugin": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@edx/new-relic-source-map-webpack-plugin/-/new-relic-source-map-webpack-plugin-2.1.0.tgz", @@ -3678,6 +3690,15 @@ "redux": "^4.0.4" } }, + "node_modules/@edx/react-unit-test-utils/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/@edx/react-unit-test-utils/node_modules/core-js": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", @@ -5162,6 +5183,98 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/@newrelic/publish-sourcemap": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.0.tgz", @@ -6824,6 +6937,12 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6932,7 +7051,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, + "devOptional": true, "dependencies": { "debug": "4" }, @@ -7063,6 +7182,39 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -7358,12 +7510,13 @@ } }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios-cache-interceptor": { @@ -8106,6 +8259,56 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/canvas/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/canvas/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/canvas/node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -8390,6 +8593,14 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8458,6 +8669,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -8582,6 +8802,12 @@ "node": ">=0.8" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -9111,7 +9337,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -9357,6 +9583,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9388,7 +9620,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -11366,6 +11598,36 @@ "node": ">=10" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/fs-monkey": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", @@ -11426,6 +11688,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11769,6 +12051,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -12090,7 +12378,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, + "devOptional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -12735,7 +13023,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -16529,6 +16817,14 @@ "node": ">=6" } }, + "node_modules/make-cancellable-promise": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.1.tgz", + "integrity": "sha512-DWOzWdO3xhY5ESjVR+wVFy03rpt0ZccS4bunccNwngoX6rllKlMZm6S9ZnJ5nMuDDweqDMjtaO0g6tZeh+cCUA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -16557,6 +16853,14 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-event-props": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.1.tgz", + "integrity": "sha512-JhvWq/iz1BvlmnPvLJjXv+xnMPJZuychrDC68V+yCGQJn5chcA8rLGKo5EP1XwIKVrigSXKLmbeXAGkf36wdCQ==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -16627,6 +16931,17 @@ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "dev": true }, + "node_modules/merge-refs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.1.tgz", + "integrity": "sha512-pRPz39HQz2xzHdXAGvtJ9S8aEpNgpUjzb5yPC3ytozodmsHg+9nqgRs7/YOmn9fM/TLzntAC8AdGTidKxOq9TQ==", + "dependencies": { + "@types/react": "*" + }, + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -16769,6 +17084,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -16786,7 +17141,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -16813,7 +17168,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "devOptional": true }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -16828,6 +17183,12 @@ "multicast-dns": "cli.js" } }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -16968,6 +17329,48 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17049,6 +17452,21 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -17099,6 +17517,18 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -17652,6 +18082,27 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -18665,6 +19116,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -19160,6 +19616,34 @@ "react-dom": ">=16.3.0" } }, + "node_modules/react-pdf": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.4.0.tgz", + "integrity": "sha512-L2GIUc9eBLdb51DRqUe3zlkTmvOq7KQN5cKmgcrWdnWNwhYgrOGgpJQ3f56kG26G+B5GoasAHI6RjQrLfTjTnw==", + "dependencies": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -20588,7 +21072,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "devOptional": true }, "node_modules/set-function-name": { "version": "2.0.1", @@ -20769,13 +21253,13 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "devOptional": true }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -21480,7 +21964,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "devOptional": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -21489,7 +21973,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "devOptional": true }, "node_modules/string-length": { "version": "4.0.2", @@ -21508,7 +21992,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -21522,7 +22006,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "devOptional": true }, "node_modules/string.prototype.matchall": { "version": "4.0.10", @@ -21892,6 +22376,23 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -21914,6 +22415,21 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -22796,7 +23312,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "devOptional": true }, "node_modules/utila": { "version": "0.4.0", @@ -23460,6 +23976,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index 1a3cd135..3ee3201b 100644 --- a/package.json +++ b/package.json @@ -47,16 +47,19 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.0", "@tanstack/react-query": "^4.29.25", - "@tanstack/react-query-devtools": "^4.32.1", + "@tanstack/react-query-devtools": "^4.35.3", "@tinymce/tinymce-react": "3.14.0", + "axios": "^1.5.1", "classnames": "^2.3.2", "core-js": "3.32.2", "filesize": "^8.0.6", "jest-when": "^3.6.0", + "pdfjs-dist": "^3.11.174", "prop-types": "15.8.1", "query-string": "^8.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-pdf": "^7.4.0", "react-redux": "7.2.9", "react-router": "6.16.0", "react-router-dom": "6.16.0", diff --git a/src/App.jsx b/src/App.jsx index bf3bc43b..b76dde99 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,6 +12,7 @@ import SelfAssessmentView from 'views/SelfAssessmentView'; import StudentTrainingView from 'views/StudentTrainingView'; import SubmissionView from 'views/SubmissionView'; import XBlockView from 'views/XBlockView'; +import FilePreviewView from 'views/FilePreviewView'; import messages from './messages'; import routes from './routes'; @@ -30,6 +31,7 @@ const RouterRoot = () => { modalRoute(routes.embedded.selfAssessment, SelfAssessmentView, 'ORA Self Assessment'), modalRoute(routes.embedded.studentTraining, StudentTrainingView, 'ORA Student Training'), modalRoute(routes.embedded.submission, SubmissionView, 'ORA Submission'), + modalRoute(routes.preview, FilePreviewView, 'File Preview'), } />, ]; const baseRoutes = [ @@ -38,6 +40,7 @@ const RouterRoot = () => { appRoute(routes.selfAssessment, SelfAssessmentView), appRoute(routes.studentTraining, StudentTrainingView), appRoute(routes.submission, SubmissionView), + appRoute(routes.preview, FilePreviewView), } />, ]; diff --git a/src/components/FilePreview/Banners/ErrorBanner.jsx b/src/components/FilePreview/Banners/ErrorBanner.jsx new file mode 100644 index 00000000..22473bef --- /dev/null +++ b/src/components/FilePreview/Banners/ErrorBanner.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Alert, Button } from '@edx/paragon'; +import { Info } from '@edx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +const messageShape = PropTypes.shape({ + id: PropTypes.string, + defaultMessage: PropTypes.string, +}); + +export const ErrorBanner = ({ actions, headerMessage, children }) => { + const { formatMessage } = useIntl(); + const actionButtons = actions.map(action => ( + + )); + + return ( + + + {formatMessage(headerMessage)} + + {children} + + ); +}; +ErrorBanner.defaultProps = { + actions: [], + children: null, +}; +ErrorBanner.propTypes = { + actions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + onClick: PropTypes.func, + message: messageShape, + }), + ), + headerMessage: messageShape.isRequired, + children: PropTypes.node, +}; + +export default ErrorBanner; diff --git a/src/components/FilePreview/Banners/ErrorBanner.test.jsx b/src/components/FilePreview/Banners/ErrorBanner.test.jsx new file mode 100644 index 00000000..206c2102 --- /dev/null +++ b/src/components/FilePreview/Banners/ErrorBanner.test.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import ErrorBanner from './ErrorBanner'; + +import messages from '../messages'; + +describe('Error Banner component', () => { + const children =

Abitary Child

; + + const props = { + actions: [ + { + id: 'action1', + onClick: jest.fn().mockName('action1.onClick'), + message: messages.retryButton, + }, + { + id: 'action2', + onClick: jest.fn().mockName('action2.onClick'), + message: messages.retryButton, + }, + ], + headerMessage: messages.unknownError, + children, + }; + + let el; + beforeEach(() => { + el = shallow(); + }); + + test('snapshot', () => { + expect(el.snapshot).toMatchSnapshot(); + }); + + describe('component', () => { + test('children node', () => { + el.instance.findByType('p'); + const childEl = shallow(children); + el.instance.findByType('p')[0].matches(childEl); + }); + + test('verify actions', () => { + const { actions } = el.instance.findByType('Alert')[0].props; + expect(actions).toHaveLength(props.actions.length); + + actions.forEach((action, index) => { + expect(action.type).toEqual('Button'); + expect(action.props.onClick).toEqual(props.actions[index].onClick); + // action message + expect(action.props.children).toEqual(props.actions[index].message.defaultMessage); + }); + }); + }); +}); diff --git a/src/components/FilePreview/Banners/LoadingBanner.jsx b/src/components/FilePreview/Banners/LoadingBanner.jsx new file mode 100644 index 00000000..45575335 --- /dev/null +++ b/src/components/FilePreview/Banners/LoadingBanner.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Alert, Spinner } from '@edx/paragon'; + +export const LoadingBanner = () => ( + + + +); + +LoadingBanner.defaultProps = {}; +LoadingBanner.propTypes = {}; + +export default LoadingBanner; diff --git a/src/components/FilePreview/Banners/LoadingBanner.test.jsx b/src/components/FilePreview/Banners/LoadingBanner.test.jsx new file mode 100644 index 00000000..c05c6e83 --- /dev/null +++ b/src/components/FilePreview/Banners/LoadingBanner.test.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import LoadingBanner from './LoadingBanner'; + +describe('Loading Banner component', () => { + test('snapshot', () => { + const el = shallow(); + expect(el.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap b/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap new file mode 100644 index 00000000..ac9fd2f2 --- /dev/null +++ b/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Banner component snapshot 1`] = ` + + Retry + , + , + ] + } + icon={[Function]} + variant="danger" +> + + Unknown errors + +

+ Abitary Child +

+
+`; diff --git a/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap b/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap new file mode 100644 index 00000000..d6a2a516 --- /dev/null +++ b/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loading Banner component snapshot 1`] = ` + + + +`; diff --git a/src/components/FilePreview/Banners/index.jsx b/src/components/FilePreview/Banners/index.jsx new file mode 100644 index 00000000..fbba7f67 --- /dev/null +++ b/src/components/FilePreview/Banners/index.jsx @@ -0,0 +1,2 @@ +export { default as ErrorBanner } from './ErrorBanner'; +export { default as LoadingBanner } from './LoadingBanner'; diff --git a/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx b/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx new file mode 100644 index 00000000..c4c975c1 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ImageRenderer = ({ + url, fileName, onError, onSuccess, +}) => ( + {fileName} +); + +ImageRenderer.defaultProps = { + fileName: '', +}; + +ImageRenderer.propTypes = { + url: PropTypes.string.isRequired, + fileName: PropTypes.string, + onError: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; + +export default ImageRenderer; diff --git a/src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx new file mode 100644 index 00000000..9b919fda --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import ImageRenderer from './ImageRenderer'; + +describe('Image Renderer Component', () => { + const props = { + url: 'some_url.jpg', + fileName: 'some_file_name.jpg', + onError: jest.fn().mockName('onError'), + onSuccess: jest.fn().mockName('onSuccess'), + }; + + let el; + beforeEach(() => { + el = shallow(); + }); + test('snapshot', () => { + expect(el.snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/BaseRenderers/PDFRenderer.jsx b/src/components/FilePreview/BaseRenderers/PDFRenderer.jsx new file mode 100644 index 00000000..1a12fd86 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/PDFRenderer.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { pdfjs, Document, Page } from 'react-pdf'; +import { + Icon, Form, ActionRow, IconButton, +} from '@edx/paragon'; +import { ChevronLeft, ChevronRight } from '@edx/paragon/icons'; +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min'; + +import 'react-pdf/dist/esm/Page/TextLayer.css'; +import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; + +import { rendererHooks } from './pdfHooks'; + +pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker; + +/** + * + */ +export const PDFRenderer = ({ + onError, + onSuccess, + url, +}) => { + const { + pageNumber, + numPages, + relativeHeight, + wrapperRef, + onDocumentLoadSuccess, + onLoadPageSuccess, + onDocumentLoadError, + onInputPageChange, + onNextPageButtonClick, + onPrevPageButtonClick, + hasNext, + hasPrev, + } = rendererHooks({ onError, onSuccess }); + + return ( +
+ + {/* */} +
+ +
+
+ + + + Page + + of {numPages} + + + +
+ ); +}; + +PDFRenderer.defaultProps = {}; + +PDFRenderer.propTypes = { + url: PropTypes.string.isRequired, + onError: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; + +export default PDFRenderer; diff --git a/src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx new file mode 100644 index 00000000..d4c996c3 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import PDFRenderer from './PDFRenderer'; + +import * as hooks from './pdfHooks'; + +jest.mock('react-pdf', () => ({ + pdfjs: { GlobalWorkerOptions: {} }, + Document: () => 'Document', + Page: () => 'Page', +})); + +jest.mock('./pdfHooks', () => ({ + rendererHooks: jest.fn(), +})); + +describe('PDF Renderer Component', () => { + const props = { + url: 'some_url.pdf', + onError: jest.fn().mockName('this.props.onError'), + onSuccess: jest.fn().mockName('this.props.onSuccess'), + }; + const hookProps = { + pageNumber: 1, + numPages: 10, + relativeHeight: 200, + wrapperRef: { current: 'hooks.wrapperRef' }, + onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'), + onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'), + onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'), + onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'), + onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'), + onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'), + hasNext: true, + hasPref: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('snapshots', () => { + test('first page, prev is disabled', () => { + hooks.rendererHooks.mockReturnValue(hookProps); + expect(shallow().snapshot).toMatchSnapshot(); + }); + test('on last page, next is disabled', () => { + hooks.rendererHooks.mockReturnValue({ + ...hookProps, + pageNumber: hookProps.numPages, + hasNext: false, + hasPrev: true, + }); + expect(shallow().snapshot).toMatchSnapshot(); + }); + }); +}); diff --git a/src/components/FilePreview/BaseRenderers/TXTRenderer.jsx b/src/components/FilePreview/BaseRenderers/TXTRenderer.jsx new file mode 100644 index 00000000..340cc48d --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/TXTRenderer.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { rendererHooks } from './textHooks'; + +const TXTRenderer = ({ url, onError, onSuccess }) => { + const { content } = rendererHooks({ url, onError, onSuccess }); + return ( +
+      {content}
+    
+ ); +}; + +TXTRenderer.defaultProps = {}; + +TXTRenderer.propTypes = { + url: PropTypes.string.isRequired, + onError: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; + +export default TXTRenderer; diff --git a/src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx new file mode 100644 index 00000000..43f82247 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import TXTRenderer from './TXTRenderer'; + +jest.mock('./textHooks', () => { + const content = 'test-content'; + return { + content, + rendererHooks: (args) => ({ content, rendererHooks: args }), + }; +}); + +describe('TXT Renderer Component', () => { + const props = { + url: 'some_url.txt', + onError: jest.fn().mockName('this.props.onError'), + onSuccess: jest.fn().mockName('this.props.onSuccess'), + }; + test('snapshot', () => { + expect(shallow().snapshot).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap new file mode 100644 index 00000000..c9503872 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Image Renderer Component snapshot 1`] = ` +some_file_name.jpg +`; diff --git a/src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap new file mode 100644 index 00000000..5d7b3536 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = ` +
+ +
+ +
+
+ + + + + Page + + + + of + 10 + + + + +
+`; + +exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = ` +
+ +
+ +
+
+ + + + + Page + + + + of + 10 + + + + +
+`; diff --git a/src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap new file mode 100644 index 00000000..7675f907 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TXT Renderer Component snapshot 1`] = ` +
+  test-content
+
+`; diff --git a/src/components/FilePreview/BaseRenderers/index.jsx b/src/components/FilePreview/BaseRenderers/index.jsx new file mode 100644 index 00000000..e3906391 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/index.jsx @@ -0,0 +1,3 @@ +export { default as ImageRenderer } from './ImageRenderer'; +export { default as PDFRenderer } from './PDFRenderer'; +export { default as TXTRenderer } from './TXTRenderer'; diff --git a/src/components/FilePreview/BaseRenderers/pdfHooks.js b/src/components/FilePreview/BaseRenderers/pdfHooks.js new file mode 100644 index 00000000..01447e8f --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/pdfHooks.js @@ -0,0 +1,61 @@ +import { useRef } from 'react'; + +import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils'; + +export const stateKeys = StrictDict({ + pageNumber: 'pageNumber', + numPages: 'numPages', + relativeHeight: 'relativeHeight', +}); + +export const initialState = { + pageNumber: 1, + numPages: 1, + relativeHeight: 1, +}; + +export const safeSetPageNumber = ({ numPages, rawSetPageNumber }) => (pageNumber) => { + if (pageNumber > 0 && pageNumber <= numPages) { + rawSetPageNumber(pageNumber); + } +}; + +export const rendererHooks = ({ + onError, + onSuccess, +}) => { + const [pageNumber, rawSetPageNumber] = useKeyedState(stateKeys.pageNumber, initialState.pageNumber); + const [numPages, setNumPages] = useKeyedState(stateKeys.numPages, initialState.numPages); + const [relativeHeight, setRelativeHeight] = useKeyedState( + stateKeys.relativeHeight, + initialState.relativeHeight, + ); + + const setPageNumber = safeSetPageNumber({ numPages, rawSetPageNumber }); + + const wrapperRef = useRef(); + + return { + pageNumber, + numPages, + relativeHeight, + wrapperRef, + onDocumentLoadSuccess: (args) => { + onSuccess(); + setNumPages(args.numPages); + }, + onLoadPageSuccess: (page) => { + const pageWidth = page.view[2]; + const pageHeight = page.view[3]; + const wrapperHeight = wrapperRef.current.getBoundingClientRect().width; + const newHeight = (wrapperHeight * pageHeight) / pageWidth; + setRelativeHeight(newHeight); + }, + onDocumentLoadError: onError, + onInputPageChange: ({ target: { value } }) => setPageNumber(parseInt(value, 10)), + onPrevPageButtonClick: () => setPageNumber(pageNumber - 1), + onNextPageButtonClick: () => setPageNumber(pageNumber + 1), + hasNext: pageNumber < numPages, + hasPrev: pageNumber > 1, + }; +}; diff --git a/src/components/FilePreview/BaseRenderers/pdfHooks.test.js b/src/components/FilePreview/BaseRenderers/pdfHooks.test.js new file mode 100644 index 00000000..74272be0 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/pdfHooks.test.js @@ -0,0 +1,151 @@ +import React from 'react'; + +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; + +import { + stateKeys, + initialState, +} from './pdfHooks'; + +jest.mock('react-pdf', () => ({ + pdfjs: { GlobalWorkerOptions: {} }, + Document: () => 'Document', + Page: () => 'Page', +})); + +jest.mock('./pdfHooks', () => ({ + ...jest.requireActual('./pdfHooks'), + safeSetPageNumber: jest.fn(), + rendererHooks: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +const testValue = 'my-test-value'; + +describe('PDF Renderer hooks', () => { + const props = { + onError: jest.fn().mockName('this.props.onError'), + onSuccess: jest.fn().mockName('this.props.onSuccess'), + }; + + const actualHooks = jest.requireActual('./pdfHooks'); + + beforeEach(() => state.mock()); + afterEach(() => state.resetVals()); + + describe('state hooks', () => { + test('initialization', () => { + actualHooks.rendererHooks(props); + state.expectInitializedWith( + stateKeys.pageNumber, + initialState.pageNumber, + ); + state.expectInitializedWith(stateKeys.numPages, initialState.numPages); + state.expectInitializedWith( + stateKeys.relativeHeight, + initialState.relativeHeight, + ); + }); + }); + + test('safeSetPageNumber returns value handler that sets page number if valid', () => { + const { safeSetPageNumber } = actualHooks; + const rawSetPageNumber = jest.fn(); + const numPages = 10; + safeSetPageNumber({ numPages, rawSetPageNumber })(0); + expect(rawSetPageNumber).not.toHaveBeenCalled(); + safeSetPageNumber({ numPages, rawSetPageNumber })(numPages + 1); + expect(rawSetPageNumber).not.toHaveBeenCalled(); + safeSetPageNumber({ numPages, rawSetPageNumber })(numPages - 1); + expect(rawSetPageNumber).toHaveBeenCalledWith(numPages - 1); + }); + + describe('rendererHooks', () => { + const { rendererHooks } = actualHooks; + + test('wrapperRef passed as react ref', () => { + const hook = rendererHooks(props); + expect(hook.wrapperRef.useRef).toEqual(true); + }); + describe('onDocumentLoadSuccess', () => { + it('calls onSuccess and sets numPages based on args', () => { + const hook = rendererHooks(props); + hook.onDocumentLoadSuccess({ numPages: testValue }); + expect(props.onSuccess).toHaveBeenCalled(); + expect(state.setState.numPages).toHaveBeenCalledWith(testValue); + }); + }); + describe('onLoadPageSuccess', () => { + it('sets relative height based on page size', () => { + const width = 23; + React.useRef.mockReturnValueOnce({ + current: { + getBoundingClientRect: () => ({ width }), + }, + }); + const [pageWidth, pageHeight] = [20, 30]; + const page = { view: [0, 0, pageWidth, pageHeight] }; + const hook = rendererHooks(props); + const height = (width * pageHeight) / pageWidth; + hook.onLoadPageSuccess(page); + expect(state.setState.relativeHeight).toHaveBeenCalledWith(height); + }); + }); + test('onDocumentLoadError will call onError', () => { + const error = new Error('missingPDF'); + const hook = rendererHooks(props); + hook.onDocumentLoadError(error); + expect(props.onError).toHaveBeenCalledWith(error); + }); + + describe('pages hook', () => { + let oldNumPages; + let oldPageNumber; + let hook; + beforeEach(() => { + state.mock(); + // TODO: update state test instead of hacking initial state + oldNumPages = initialState.numPages; + oldPageNumber = initialState.pageNumber; + initialState.numPages = 10; + initialState.pageNumber = 5; + hook = rendererHooks(props); + }); + afterEach(() => { + initialState.numPages = oldNumPages; + initialState.pageNumber = oldPageNumber; + state.resetVals(); + }); + test('onInputPageChange will call setPageNumber with int event target value', () => { + hook.onInputPageChange({ target: { value: '3.3' } }); + expect(state.setState.pageNumber).toHaveBeenCalledWith(3); + }); + test('onPrevPageButtonClick will call setPageNumber with current page number - 1', () => { + hook.onPrevPageButtonClick(); + expect(state.setState.pageNumber).toHaveBeenCalledWith( + initialState.pageNumber - 1, + ); + }); + test('onNextPageButtonClick will call setPageNumber with current page number + 1', () => { + hook.onNextPageButtonClick(); + expect(state.setState.pageNumber).toHaveBeenCalledWith( + initialState.pageNumber + 1, + ); + }); + + test('hasNext returns true iff pageNumber is less than total number of pages', () => { + expect(hook.hasNext).toEqual(true); + initialState.pageNumber = initialState.numPages; + hook = rendererHooks(props); + expect(hook.hasNext).toEqual(false); + }); + test('hasPrev returns true iff pageNumber is greater than 1', () => { + expect(hook.hasPrev).toEqual(true); + initialState.pageNumber = 1; + hook = rendererHooks(props); + expect(hook.hasPrev).toEqual(false); + }); + }); + }); +}); diff --git a/src/components/FilePreview/BaseRenderers/textHooks.js b/src/components/FilePreview/BaseRenderers/textHooks.js new file mode 100644 index 00000000..e755c504 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/textHooks.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; + +import { get } from 'axios'; +import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils'; + +export const stateKeys = StrictDict({ + content: 'content', +}); + +export const fetchFile = async ({ + setContent, + url, + onError, + onSuccess, +}) => get(url) + .then(({ data }) => { + onSuccess(); + setContent(data); + }) + .catch((e) => onError(e.response.status)); + +export const rendererHooks = ({ url, onError, onSuccess }) => { + const [content, setContent] = useKeyedState(stateKeys.content, ''); + useEffect(() => { + fetchFile({ + setContent, + url, + onError, + onSuccess, + }); + }, [onError, onSuccess, setContent, url]); + return { content }; +}; + +export default { + rendererHooks, + fetchFile, +}; diff --git a/src/components/FilePreview/BaseRenderers/textHooks.test.js b/src/components/FilePreview/BaseRenderers/textHooks.test.js new file mode 100644 index 00000000..b73e4dc4 --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/textHooks.test.js @@ -0,0 +1,80 @@ +/* eslint-disable prefer-promise-reject-errors */ +import { get } from 'axios'; +import { mockUseKeyedState } from '@edx/react-unit-test-utils'; +import { when } from 'jest-when'; + +import { stateKeys, rendererHooks, fetchFile } from './textHooks'; + +jest.mock('axios', () => ({ + get: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +const testValue = 'test-value'; + +const props = { + url: 'test-url', + onError: jest.fn(), + onSuccess: jest.fn(), +}; + +describe('Text file preview hooks', () => { + beforeEach(() => state.mock()); + afterEach(() => state.resetVals()); + + test('state hooks', () => { + rendererHooks(props); + state.expectInitializedWith(stateKeys.content, ''); + }); + + describe('fetchFile', () => { + const setContent = jest.fn(); + + test('call setContent after fetch', async () => { + when(get).calledWith(props.url).mockResolvedValue({ data: testValue }); + await fetchFile({ setContent, ...props }); + expect(get).toHaveBeenCalledWith(props.url); + expect(setContent).toHaveBeenCalledWith(testValue); + }); + test('call onError if fetch fails', async () => { + const status = 404; + when(get).calledWith(props.url).mockRejectedValue({ response: { status } }); + await fetchFile({ setContent, ...props }); + expect(props.onError).toHaveBeenCalledWith(status); + }); + }); + + // describe('rendererHooks', () => { + // jest.mock('./textHooks', () => ({ + // ...jest.requireActual('./textHooks'), + // fetchFile: jest.fn(), + // })); + // let cb; + // let prereqs; + // let hook; + // const loadHook = () => { + // hook = rendererHooks(props); + // [[cb, prereqs]] = useEffect.mock.calls; + // }; + // it('calls fetchFile method, predicated on setContent, url, and callbacks', () => { + // loadHook(); + // expect(useEffect).toHaveBeenCalled(); + // expect(prereqs).toEqual([ + // props.onError, + // props.onSuccess, + // state.setState.content, + // props.url, + // ]); + // debugger + // // expect(fetchFile).not.toHaveBeenCalled(); + // cb(); + // expect(fetchFile).toHaveBeenCalledWith({ + // onError: props.onError, + // onSuccess: props.onSuccess, + // setContent: state.setState.content, + // url: props.url, + // }); + // }); + // }); +}); diff --git a/src/components/FilePreview/FileCard.jsx b/src/components/FilePreview/FileCard.jsx new file mode 100644 index 00000000..905aea1f --- /dev/null +++ b/src/components/FilePreview/FileCard.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Card, Collapsible } from '@edx/paragon'; + +import './FileCard.scss'; + +/** + * + */ +export const FileCard = ({ file, children }) => ( + + {file.name}} + > +
+ {children} +
+
+
+); +FileCard.defaultProps = { +}; +FileCard.propTypes = { + file: PropTypes.shape({ + name: PropTypes.string.isRequired, + downloadUrl: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + }).isRequired, + children: PropTypes.node.isRequired, +}; + +export default FileCard; diff --git a/src/components/FilePreview/FileCard.scss b/src/components/FilePreview/FileCard.scss new file mode 100644 index 00000000..89e6480c --- /dev/null +++ b/src/components/FilePreview/FileCard.scss @@ -0,0 +1,33 @@ +@import "@edx/paragon/scss/core/core"; + +.file-card { + margin: map-get($spacers, 1) 0; + + .file-card-title { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +} + +.image-renderer { + width: 100%; + height: auto; +} + +// .pdf-renderer { +// .react-pdf__Page__canvas { +// width: 100% !important; +// height: auto !important; +// } +// } + +.txt-renderer { + white-space: pre-wrap; +} + +@include media-breakpoint-down(sm) { + .file-card-title { + width: calc(map-get($container-max-widths, "sm")/2); + } +} \ No newline at end of file diff --git a/src/components/FilePreview/FileCard.test.jsx b/src/components/FilePreview/FileCard.test.jsx new file mode 100644 index 00000000..e5831ea5 --- /dev/null +++ b/src/components/FilePreview/FileCard.test.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import { Collapsible } from '@edx/paragon'; + +import FileCard from './FileCard'; + +describe('File Preview Card component', () => { + const props = { + file: { + name: 'test-file-name.pdf', + description: 'test-file description', + downloadUrl: 'destination/test-file-name.pdf', + }, + }; + const children = (

some children

); + let el; + beforeEach(() => { + el = shallow({children}); + }); + test('snapshot', () => { + expect(el.snapshot).toMatchSnapshot(); + }); + describe('Component', () => { + test('collapsible title is name header', () => { + const { title } = el.instance.findByType(Collapsible)[0].props; + expect(title).toEqual(

{props.file.name}

); + }); + // test('forwards children into preview-panel', () => { + // const previewPanelChildren = el.find('.preview-panel').children(); + // expect(previewPanelChildren.at(1).equals(children)).toEqual(true); + // }); + }); +}); diff --git a/src/components/FilePreview/FileRenderer.jsx b/src/components/FilePreview/FileRenderer.jsx new file mode 100644 index 00000000..f33b0783 --- /dev/null +++ b/src/components/FilePreview/FileRenderer.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; + +import FileCard from './FileCard'; +import { ErrorBanner, LoadingBanner } from './Banners'; +import { renderHooks } from './hooks'; + +/** + * + */ +export const FileRenderer = ({ + file, +}) => { + const { formatMessage } = useIntl(); + const { + Renderer, + isLoading, + errorStatus, + error, + rendererProps, + } = renderHooks({ file, formatMessage }); + + return ( + + {isLoading && } + {errorStatus ? ( + + ) : ( + + )} + + ); +}; + +FileRenderer.defaultProps = {}; +FileRenderer.propTypes = { + file: PropTypes.shape({ + name: PropTypes.string, + downloadUrl: PropTypes.string, + }).isRequired, + // injected + // intl: intlShape.isRequired, +}; + +export default FileRenderer; diff --git a/src/components/FilePreview/FileRenderer.test.jsx b/src/components/FilePreview/FileRenderer.test.jsx new file mode 100644 index 00000000..703f8644 --- /dev/null +++ b/src/components/FilePreview/FileRenderer.test.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from '@edx/react-unit-test-utils'; + +import { FileRenderer } from './FileRenderer'; +import { renderHooks, ErrorStatuses } from './hooks'; + +jest.mock('./FileCard', () => 'FileCard'); +jest.mock('./Banners', () => ({ + ErrorBanner: () => 'ErrorBanner', + LoadingBanner: () => 'LoadingBanner', +})); + +jest.mock('./hooks', () => ({ + ...jest.requireActual('./hooks'), + renderHooks: jest.fn(), +})); + +const props = { + file: { + downloadUrl: 'file download url', + name: 'filename.txt', + }, +}; +describe('FileRenderer', () => { + describe('component', () => { + describe('snapshot', () => { + test('isLoading, no Error', () => { + const hookProps = { + Renderer: () => 'Renderer', + isLoading: true, + errorStatus: null, + error: null, + rendererProps: { prop: 'hooks.rendererProps' }, + }; + renderHooks.mockReturnValueOnce(hookProps); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(1); + + expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(0); + expect(wrapper.instance.findByType('Renderer')).toHaveLength(1); + }); + test('is not loading, with error', () => { + const hookProps = { + Renderer: () => 'Renderer', + isLoading: false, + errorStatus: ErrorStatuses.serverError, + error: { prop: 'hooks.errorProps' }, + rendererProps: { prop: 'hooks.rendererProps' }, + }; + renderHooks.mockReturnValueOnce(hookProps); + const wrapper = shallow(); + expect(wrapper.snapshot).toMatchSnapshot(); + + expect(wrapper.instance.findByType('LoadingBanner')).toHaveLength(0); + + expect(wrapper.instance.findByType('ErrorBanner')).toHaveLength(1); + expect(wrapper.instance.findByType('Renderer')).toHaveLength(0); + }); + }); + }); +}); diff --git a/src/components/FilePreview/__snapshots__/FileCard.test.jsx.snap b/src/components/FilePreview/__snapshots__/FileCard.test.jsx.snap new file mode 100644 index 00000000..2cf08071 --- /dev/null +++ b/src/components/FilePreview/__snapshots__/FileCard.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`File Preview Card component snapshot 1`] = ` + + + test-file-name.pdf + + } + > +
+

+ some children +

+
+
+
+`; diff --git a/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap b/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap new file mode 100644 index 00000000..d8af97d8 --- /dev/null +++ b/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileRenderer component snapshot is not loading, with error 1`] = ` + + + +`; + +exports[`FileRenderer component snapshot isLoading, no Error 1`] = ` + + + + +`; diff --git a/src/components/FilePreview/hooks.js b/src/components/FilePreview/hooks.js new file mode 100644 index 00000000..bf148103 --- /dev/null +++ b/src/components/FilePreview/hooks.js @@ -0,0 +1,117 @@ +import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils'; + +import { + PDFRenderer, + ImageRenderer, + TXTRenderer, +} from 'components/FilePreview/BaseRenderers'; + +import messages from './messages'; + +export const ErrorStatuses = StrictDict({ + badRequest: 400, + unauthorized: 401, + forbidden: 403, + notFound: 404, + conflict: 409, + serverError: 500, +}); + +export const FileTypes = StrictDict({ + pdf: 'pdf', + jpg: 'jpg', + jpeg: 'jpeg', + png: 'png', + bmp: 'bmp', + txt: 'txt', + gif: 'gif', + jfif: 'jfif', + pjpeg: 'pjpeg', + pjp: 'pjp', + svg: 'svg', +}); + +/** + * Config data + */ +export const RENDERERS = { + [FileTypes.pdf]: PDFRenderer, + [FileTypes.jpg]: ImageRenderer, + [FileTypes.jpeg]: ImageRenderer, + [FileTypes.bmp]: ImageRenderer, + [FileTypes.png]: ImageRenderer, + [FileTypes.txt]: TXTRenderer, + [FileTypes.gif]: ImageRenderer, + [FileTypes.jfif]: ImageRenderer, + [FileTypes.pjpeg]: ImageRenderer, + [FileTypes.pjp]: ImageRenderer, + [FileTypes.svg]: ImageRenderer, +}; + +export const SUPPORTED_TYPES = Object.keys(RENDERERS); + +export const ERROR_STATUSES = { + [ErrorStatuses.notFound]: messages.fileNotFoundError, + [ErrorStatuses.serverError]: messages.unknownError, +}; + +/** + * Util methods and transforms + */ +export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase(); +export const isSupported = (file) => SUPPORTED_TYPES.includes( + getFileType(file.name), +); + +export const stateKeys = StrictDict({ + errorStatus: 'errorStatus', + isLoading: 'isLoading', +}); + +/** + * component hooks + */ +export const renderHooks = ({ + file, + formatMessage, +}) => { + const [errorStatus, setErrorStatus] = useKeyedState(stateKeys.errorStatus, null); + const [isLoading, setIsLoading] = useKeyedState(stateKeys.isLoading, true); + + const setState = (newState) => { + setErrorStatus(newState.errorStatus); + setIsLoading(newState.isLoading); + }; + + const stopLoading = (status = null) => setState({ isLoading: false, errorStatus: status }); + + const errorMessage = ( + ERROR_STATUSES[errorStatus] || ERROR_STATUSES[ErrorStatuses.serverError] + ); + const errorAction = { + id: 'retry', + onClick: () => setState({ errorStatus: null, isLoading: true }), + message: messages.retryButton, + }; + const error = { + headerMessage: errorMessage, + children: formatMessage(errorMessage), + actions: [errorAction], + }; + + const Renderer = RENDERERS[getFileType(file.name)]; + const rendererProps = { + fileName: file.name, + url: file.downloadUrl, + onError: stopLoading, + onSuccess: () => stopLoading(), + }; + + return { + errorStatus, + isLoading, + error, + Renderer, + rendererProps, + }; +}; diff --git a/src/components/FilePreview/hooks.test.js b/src/components/FilePreview/hooks.test.js new file mode 100644 index 00000000..f5974061 --- /dev/null +++ b/src/components/FilePreview/hooks.test.js @@ -0,0 +1,101 @@ +import { mockUseKeyedState, formatMessage } from '@edx/react-unit-test-utils'; + +import { + ErrorStatuses, + RENDERERS, + SUPPORTED_TYPES, + ERROR_STATUSES, + getFileType, + isSupported, + stateKeys, +} from './hooks'; + +jest.mock('./hooks', () => ({ + ...jest.requireActual('./hooks'), + renderHooks: jest.fn(), +})); + +const state = mockUseKeyedState(stateKeys); + +describe('FilePreview hooks', () => { + const props = { + file: { + name: 'test-file-name.txt', + downloadUrl: 'my-test-download-url.jpg', + }, + formatMessage, + }; + + const actualHooks = jest.requireActual('./hooks'); + + beforeEach(() => state.mock()); + afterEach(() => state.resetVals()); + + test('state initialization', () => { + actualHooks.renderHooks(props); + state.expectInitializedWith(stateKeys.errorStatus, null); + state.expectInitializedWith(stateKeys.isLoading, true); + }); + + test('getFileType returns file extension if available, in lowercase', () => { + expect(getFileType('thing.TXT')).toEqual('txt'); + expect(getFileType(props.file.name)).toEqual('txt'); + }); + + test('isSupported returns true iff the filetype is included in SUPPORTED_TYPES', () => { + SUPPORTED_TYPES.forEach((type) => { + expect(isSupported({ name: `thing.${type}` })).toEqual(true); + }); + expect(isSupported({ name: 'thing' })).toEqual(false); + }); + + describe('renderHooks', () => { + test('errorStatus and isLoading tied to state, initialized to null and true', () => { + const hook = actualHooks.renderHooks(props); + expect(hook.errorStatus).toEqual(state.values.errorStatus); + expect(hook.errorStatus).toEqual(null); + expect(hook.isLoading).toEqual(state.values.isLoading); + expect(hook.isLoading).toEqual(true); + }); + + test('error', () => { + const hook = actualHooks.renderHooks(props); + expect(hook.error.headerMessage).toEqual( + ERROR_STATUSES[ErrorStatuses.serverError], + ); + expect(hook.error.children).toEqual( + props.formatMessage(ERROR_STATUSES[ErrorStatuses.serverError]), + ); + hook.rendererProps.onError(ErrorStatuses.notFound); + expect(state.setState.errorStatus).toHaveBeenCalledWith( + ErrorStatuses.notFound, + ); + }); + + test('Renderer', () => { + SUPPORTED_TYPES.forEach((type) => { + const hook = actualHooks.renderHooks({ + ...props, + file: { ...props.file, name: `thing.${type}` }, + }); + expect(hook.Renderer).toEqual(RENDERERS[type]); + }); + }); + + test('rendererProps', () => { + const hook = actualHooks.renderHooks(props); + expect(hook.rendererProps.fileName).toEqual(props.file.name); + expect(hook.rendererProps.url).toEqual(props.file.downloadUrl); + + hook.rendererProps.onSuccess(); + expect(state.setState.isLoading).toHaveBeenCalledWith(false); + expect(state.setState.errorStatus).toHaveBeenCalledWith(null); + + hook.rendererProps.onError(ErrorStatuses.notFound); + expect(state.setState.isLoading).toHaveBeenCalledWith(false); + expect(state.setState.errorStatus).toHaveBeenCalledWith( + ErrorStatuses.notFound, + ); + }); + }); +}); diff --git a/src/components/FilePreview/index.jsx b/src/components/FilePreview/index.jsx new file mode 100644 index 00000000..0addafe2 --- /dev/null +++ b/src/components/FilePreview/index.jsx @@ -0,0 +1,2 @@ +export { default as FileRenderer } from './FileRenderer'; +export { isSupported } from './hooks'; diff --git a/src/components/FilePreview/messages.js b/src/components/FilePreview/messages.js new file mode 100644 index 00000000..573a546c --- /dev/null +++ b/src/components/FilePreview/messages.js @@ -0,0 +1,26 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + fileInfo: { + id: 'ora-grading.InfoPopover.fileInfo', + defaultMessage: 'File info', + description: 'Popover trigger button text for file preview card', + }, + retryButton: { + id: 'ora-grading.ResponseDisplay.FileRenderer.retryButton', + defaultMessage: 'Retry', + description: 'Retry button for error in file renderer', + }, + fileNotFoundError: { + id: 'ora-grading.ResponseDisplay.FileRenderer.fileNotFound', + defaultMessage: 'File not found', + description: 'File not found error message', + }, + unknownError: { + id: 'ora-grading.ResponseDisplay.FileRenderer.unknownError', + defaultMessage: 'Unknown errors', + description: 'Unknown errors message', + }, +}); + +export default messages; diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap index edb37f3e..dd66fe44 100644 --- a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap +++ b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap @@ -6,7 +6,17 @@ exports[` renders 1`] = ` alt="Delete" disabled={false} iconAs="Icon" - onClick={[Function]} + onClick={ + Object { + "useCallback": Object { + "cb": [Function], + "prereqs": Array [ + [MockFunction], + 0, + ], + }, + } + } src={[Function]} /> { return ; } if (step === 'training') { - return ; + return ; } if (step === 'self') { - return ; + return ; } return null; })} diff --git a/src/components/StatefulStatus/index.jsx b/src/components/StatefulStatus/index.jsx index efb46275..6b66e190 100644 --- a/src/components/StatefulStatus/index.jsx +++ b/src/components/StatefulStatus/index.jsx @@ -20,7 +20,7 @@ const alertMap = { default: { variant: 'dark', icon: null, - } + }, }; export const statefulStates = Object.keys(alertMap); @@ -33,7 +33,7 @@ const StatefulStatus = ({ state, status }) => { return ( <> {badgeText} - {headerText} + {headerText} {state !== 'default' ? ( {content} diff --git a/src/routes.ts b/src/routes.ts index f787b9c6..57be9efb 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -12,5 +12,6 @@ export default { selfAssessment: '/self_assessment/:id', studentTraining: '/student_training/:id', submission: '/submission/:id', + preview: '/preview/:id', root: '/*', }; diff --git a/src/setupTest.js b/src/setupTest.js index 250b1c8f..de4589f7 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1,6 +1,14 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useRef: jest.fn((val) => ({ current: val, useRef: true })), + useCallback: jest.fn((cb, prereqs) => ({ useCallback: { cb, prereqs } })), + useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })), + useContext: jest.fn(context => context), +})); + jest.mock('@edx/frontend-platform/i18n', () => { const i18n = jest.requireActual('@edx/frontend-platform/i18n'); const { formatMessage } = jest.requireActual('@edx/react-unit-test-utils'); diff --git a/src/views/FilePreviewView/index.jsx b/src/views/FilePreviewView/index.jsx new file mode 100644 index 00000000..7537201f --- /dev/null +++ b/src/views/FilePreviewView/index.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { FileRenderer, isSupported } from 'components/FilePreview'; + +const FilePreviewView = () => ( +
+ {[ + { + name: 'test.png', + downloadUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Image_created_with_a_mobile_phone.png/1920px-Image_created_with_a_mobile_phone.png', + description: 'test description', + }, + { + name: 'test.txt', + downloadUrl: 'https://raw.githubusercontent.com/openedx/edx-ora2/master/README.rst', + description: 'test description', + }, + { + name: 'test.pdf', + downloadUrl: 'https://raw.githubusercontent.com/py-pdf/sample-files/main/004-pdflatex-4-pages/pdflatex-4-pages.pdf', + description: 'test description', + }, + { + name: 'error.pdf', + downloadUrl: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', + description: 'failed to load', + }, + ].filter(isSupported).map((file) => ( + + ))} +
+); + +export default FilePreviewView; diff --git a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx index 2359af91..ebe97da4 100644 --- a/src/views/SelfAssessmentView/AssessmentContentLayout.jsx +++ b/src/views/SelfAssessmentView/AssessmentContentLayout.jsx @@ -4,17 +4,17 @@ import PropTypes from 'prop-types'; import { Col, Row } from '@edx/paragon'; import Rubric from 'components/Rubric'; -import { statefulStates } from 'components/StatefulStatus'; import AssessmentContent from './AssessmentContent'; import AssessmentStatus from './AssessmentStatus'; +import { statefulStates } from 'components/StatefulStatus'; import './AssessmentContentLayout.scss'; const AssessmentContentLayout = ({ submission, oraConfigData }) => ( -
-
- - +
+
+ + { statefulStates.map((status) => ( 'Rubric'); jest.mock('./AssessmentContent', () => 'AssessmentContent'); +jest.mock('./AssessmentStatus', () => 'AssessmentStatus'); describe('', () => { const props = { submission: 'submission', - oraConfigData: 'oraConfigData', + oraConfigData: { + assessmentSteps: { + settings: { + self: { + endTime: 'endTime', + }, + }, + }, + }, }; it('render', () => { diff --git a/src/views/SelfAssessmentView/AssessmentStatus.jsx b/src/views/SelfAssessmentView/AssessmentStatus.jsx index 0b693d5c..3b7b8ed5 100644 --- a/src/views/SelfAssessmentView/AssessmentStatus.jsx +++ b/src/views/SelfAssessmentView/AssessmentStatus.jsx @@ -24,8 +24,8 @@ const AssessmentStatus = ({ status, dueDate }) => {