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 = () => (
-
- Secondary Action
- Primary Action
-
-);
-
-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)}
+
+
+ {}}
+ >
+
+ {formatMessage(messages.inProgressButton)}
+
+
+ ),
+ },
+ 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
+
+
+
{}}
+ >
+ {formatMessage(messages.completedBodyButton)}
+
+
+ ),
+ },
+ 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 => (
+
+ {formatMessage(action.message)}
+
+ ));
+
+ 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
+ ,
+
+ 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,
+}) => (
+
+);
+
+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`] = `
+
+`;
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 }) => {
{}}
>
@@ -38,8 +38,8 @@ const AssessmentStatus = ({ status, dueDate }) => {
badgeText: formatMessage(messages.completedBadge),
headerText: formatMessage(messages.completedHeader),
content: (
-
-
+
+
{formatMessage(messages.completedBodyHeader)}
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
@@ -48,8 +48,8 @@ const AssessmentStatus = ({ status, dueDate }) => {
{}}
>
{formatMessage(messages.completedBodyButton)}
@@ -93,7 +93,7 @@ const AssessmentStatus = ({ status, dueDate }) => {
AssessmentStatus.propTypes = {
status: PropTypes.oneOf(['default', 'success', 'error', 'cancelled'])
.isRequired,
- dueDate: PropTypes.string,
+ dueDate: PropTypes.string.isRequired,
};
export default AssessmentStatus;
diff --git a/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap
index ca56c1f6..f96726ba 100644
--- a/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap
+++ b/src/views/SelfAssessmentView/__snapshots__/AssessmentContentLayout.test.jsx.snap
@@ -13,8 +13,38 @@ exports[` render 1`] = `
+
+
+
+
diff --git a/src/views/SelfAssessmentView/hooks.js b/src/views/SelfAssessmentView/hooks.js
index 5d62bd61..35315795 100644
--- a/src/views/SelfAssessmentView/hooks.js
+++ b/src/views/SelfAssessmentView/hooks.js
@@ -1,5 +1,3 @@
-import { useEffect, useReducer } from 'react';
-
import { useORAConfigData, usePageData } from 'data/services/lms/hooks/selectors';
const useAssessmentViewHooks = () => {