From 0986afae7b4ae82cd96efc4963f430ebdf3a2ecf Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Tue, 19 Sep 2023 14:56:11 -0400 Subject: [PATCH 01/13] chore: remove debris --- src/components/FileUpload/index.jsx | 3 ++- .../TextResponse/RichTextEditor.jsx | 20 +++++++++---------- src/data/services/lms/constants.js | 6 +++++- .../SubmissionView/SubmissionContent.jsx | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index 02c4aa96..b7d9a5ac 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -8,6 +8,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import filesize from 'filesize'; import messages from './messages'; +import { notImplemented } from 'data/services/lms/constants'; const FileUpload = ({ isReadOnly }) => { const { uploadedFiles } = useSubmissionResponse(); @@ -41,7 +42,7 @@ const FileUpload = ({ isReadOnly }) => { /> )} - {!isReadOnly && } + {!isReadOnly && } ); }; diff --git a/src/components/TextResponse/RichTextEditor.jsx b/src/components/TextResponse/RichTextEditor.jsx index f8139ff1..cfea7eb8 100644 --- a/src/components/TextResponse/RichTextEditor.jsx +++ b/src/components/TextResponse/RichTextEditor.jsx @@ -69,16 +69,16 @@ RichTextEditor.defaultProps = { RichTextEditor.propTypes = { // id: PropTypes.string.isRequired, - input: PropTypes.shape({ - value: PropTypes.string, - name: PropTypes.string, - onChange: PropTypes.func.isRequired, - }).isRequired, - meta: PropTypes.shape({ - touched: PropTypes.bool, - submitFailed: PropTypes.bool, - error: PropTypes.string, - }).isRequired, + // input: PropTypes.shape({ + // value: PropTypes.string, + // name: PropTypes.string, + // onChange: PropTypes.func.isRequired, + // }).isRequired, + // meta: PropTypes.shape({ + // touched: PropTypes.bool, + // submitFailed: PropTypes.bool, + // error: PropTypes.string, + // }).isRequired, disabled: PropTypes.bool, initialValue: PropTypes.string, optional: PropTypes.bool, diff --git a/src/data/services/lms/constants.js b/src/data/services/lms/constants.js index e1101c18..2c2406fe 100644 --- a/src/data/services/lms/constants.js +++ b/src/data/services/lms/constants.js @@ -11,4 +11,8 @@ export const queryKeys = StrictDict({ pageData: 'pageData', }); -export default { feedbackRequirement, queryKeys }; +export const notImplemented = () => { + throw 'Not implemented'; +}; + +export default { feedbackRequirement, queryKeys, notImplemented }; diff --git a/src/views/SubmissionView/SubmissionContent.jsx b/src/views/SubmissionView/SubmissionContent.jsx index a86f8ad6..f9cf48aa 100644 --- a/src/views/SubmissionView/SubmissionContent.jsx +++ b/src/views/SubmissionView/SubmissionContent.jsx @@ -12,7 +12,7 @@ const SubmissionContent = () => {
{ prompts.map((prompt, index) => ( -
+
From 06e8a00b5c8005890de7d974bbe0aabdc6888977 Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Thu, 21 Sep 2023 14:45:11 -0400 Subject: [PATCH 02/13] feat: interactive submission actions --- src/App.jsx | 19 ++++ src/components/FileUpload/ActionCell.jsx | 27 +++++ .../FileUpload/UploadConfirmModal.jsx | 102 ++++++++++++++++++ src/components/FileUpload/index.jsx | 86 +++++++++++++-- src/components/FileUpload/messages.js | 40 +++++++ src/components/FileUpload/styles.scss | 8 ++ src/components/Prompt/index.jsx | 15 ++- src/components/Rubric/index.jsx | 17 +-- .../TextResponse/RichTextEditor.jsx | 23 ++-- src/components/TextResponse/TextEditor.jsx | 39 ++++--- src/components/TextResponse/index.jsx | 23 ++-- src/data/services/lms/constants.js | 11 +- .../services/lms/fakeData/pageData/index.jsx | 2 +- src/data/services/lms/hooks/actions.ts | 31 ++++++ src/data/services/lms/hooks/data.ts | 2 +- .../SubmissionView/SubmissionActions.jsx | 35 ++++-- .../SubmissionView/SubmissionContent.jsx | 68 +++++++++--- .../SubmissionContentLayout.jsx | 44 +++++--- src/views/SubmissionView/hooks.js | 53 +++++++++ src/views/SubmissionView/index.jsx | 47 ++++---- src/views/SubmissionView/messages.js | 26 +++++ 21 files changed, 577 insertions(+), 141 deletions(-) create mode 100644 src/components/FileUpload/ActionCell.jsx create mode 100644 src/components/FileUpload/UploadConfirmModal.jsx create mode 100644 src/components/FileUpload/styles.scss create mode 100644 src/views/SubmissionView/hooks.js create mode 100644 src/views/SubmissionView/messages.js diff --git a/src/App.jsx b/src/App.jsx index fa36e6c5..b607c6f2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,9 @@ import { Routes, Route } from 'react-router-dom'; import { ErrorPage } from '@edx/frontend-platform/react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { Spinner } from '@edx/paragon'; + +import { useIsORAConfigLoaded, useIsPageDataLoaded } from 'data/services/lms/hooks/selectors'; import PeerAssessmentView from 'views/PeerAssessmentView'; import SelfAssessmentView from 'views/SelfAssessmentView'; @@ -12,6 +15,22 @@ import routes from './routes'; const RouterRoot = () => { const { formatMessage } = useIntl(); + const isConfigLoaded = useIsORAConfigLoaded(); + const isPageLoaded = useIsPageDataLoaded(); + + if (!isConfigLoaded || !isPageLoaded) { + return ( +
+ +
+ ); + } + return ( } /> diff --git a/src/components/FileUpload/ActionCell.jsx b/src/components/FileUpload/ActionCell.jsx new file mode 100644 index 00000000..047795a5 --- /dev/null +++ b/src/components/FileUpload/ActionCell.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { IconButton, Icon } from '@edx/paragon'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Delete, Preview } from '@edx/paragon/icons'; + +import messages from './messages'; + +const ActionCell = () => { + const { formatMessage } = useIntl(); + return ( + <> + + + + ); +}; + +export default ActionCell; diff --git a/src/components/FileUpload/UploadConfirmModal.jsx b/src/components/FileUpload/UploadConfirmModal.jsx new file mode 100644 index 00000000..badbc6f6 --- /dev/null +++ b/src/components/FileUpload/UploadConfirmModal.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Form, FormLabel, ModalDialog, Button, ActionRow, +} from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import messages from './messages'; + +const UploadConfirmModal = ({ + open, files, closeHandler, uploadHandler, +}) => { + const { formatMessage } = useIntl(); + + const [errors, setErrors] = React.useState([]); + + const confirmUploadClickHandler = () => { + const errorList = files.map((file) => (!file.description + ? formatMessage(messages.fileDescriptionMissingError) + : null)); + setErrors(errorList); + if (errorList.some((error) => error)) { + return; + } + uploadHandler(); + }; + + const exitHandler = () => { + setErrors([]); + closeHandler(); + }; + + return ( + + + + {formatMessage(messages.uploadFileModalTitle)} + + + + +
+ {files.map((file, i) => ( + // eslint-disable-next-line react/no-array-index-key + + + + {formatMessage(messages.uploadFileDescriptionFieldLabel)} + + {file.name} + + { file.description = e.target.value; }} + /> + {errors[i] && ( + + {errors[i]} + + )} + + ))} +
+
+ + + + {formatMessage(messages.cancelUploadFileButton)} + + + + +
+ ); +}; + +UploadConfirmModal.defaultProps = { + open: false, + files: [], + closeHandler: () => {}, + uploadHandler: () => {}, +}; +UploadConfirmModal.propTypes = { + open: PropTypes.bool, + files: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + description: PropTypes.string, + })), + closeHandler: PropTypes.func, + uploadHandler: PropTypes.func, +}; + +export default UploadConfirmModal; diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index b7d9a5ac..d59fa4d9 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -1,29 +1,64 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { DataTable, Dropzone } from '@edx/paragon'; -import { useSubmissionResponse } from 'data/services/lms/hooks/selectors'; - import { useIntl } from '@edx/frontend-platform/i18n'; + import filesize from 'filesize'; import messages from './messages'; -import { notImplemented } from 'data/services/lms/constants'; +import UploadConfirmModal from './UploadConfirmModal'; + +import './styles.scss'; +import ActionCell from './ActionCell'; -const FileUpload = ({ isReadOnly }) => { - const { uploadedFiles } = useSubmissionResponse(); +const FileUpload = ({ isReadOnly, uploadedFiles, onFileUploaded }) => { const { formatMessage } = useIntl(); + + const [uploadState, dispatchUploadState] = React.useReducer( + (state, payload) => ({ ...state, ...payload }), + { + onProcessUploadArgs: {}, + openModal: false, + }, + ); + + const confirmUpload = useCallback(async () => { + dispatchUploadState({ openModal: false }); + const { fileData, requestConfig } = uploadState.onProcessUploadArgs; + const files = fileData.getAll('file'); + + for (let i = 0; i <= 50; i++) { + // eslint-disable-next-line no-await-in-loop, no-promise-executor-return + await new Promise((resolve) => setTimeout(resolve, 100)); + requestConfig.onUploadProgress({ loaded: i, total: 50 }); + } + + dispatchUploadState({ + onProcessUploadArgs: {}, + }); + onFileUploaded([ + ...uploadedFiles, + ...files.map((file) => ({ + fileDescription: file.description, + fileName: file.name, + fileSize: file.size, + })), + ]); + }, [uploadState, uploadedFiles, onFileUploaded]); + return (

File Upload

- {uploadedFiles && ( + {uploadedFiles.length > 0 && ( <> Uploaded Files ({ + data={uploadedFiles.map((file) => ({ ...file, - size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown', + size: + typeof file.size === 'number' ? filesize(file.size) : 'Unknown', }))} columns={[ { @@ -38,20 +73,51 @@ const FileUpload = ({ isReadOnly }) => { Header: formatMessage(messages.fileSizeTitle), accessor: 'fileSize', }, + { + Header: formatMessage(messages.fileActionsTitle), + accessor: 'actions', + Cell: ActionCell, + }, ]} /> )} - {!isReadOnly && } + {!isReadOnly && ( + { + dispatchUploadState({ + onProcessUploadArgs: { fileData, handleError, requestConfig }, + openModal: true, + }); + }} + progressVariant="bar" + /> + )} + dispatchUploadState({ openModal: false, onProcessUploadArgs: {} })} + uploadHandler={confirmUpload} + />
); }; FileUpload.defaultProps = { isReadOnly: false, + uploadedFiles: [], }; FileUpload.propTypes = { isReadOnly: PropTypes.bool, + uploadedFiles: PropTypes.arrayOf( + PropTypes.shape({ + fileDescription: PropTypes.string, + fileName: PropTypes.string, + fileSize: PropTypes.number, + }), + ), + onFileUploaded: PropTypes.func.isRequired, }; export default FileUpload; diff --git a/src/components/FileUpload/messages.js b/src/components/FileUpload/messages.js index 8a4164bd..a3e06213 100644 --- a/src/components/FileUpload/messages.js +++ b/src/components/FileUpload/messages.js @@ -16,6 +16,46 @@ const messages = defineMessages({ defaultMessage: 'File Size', description: ' title for file size', }, + fileActionsTitle: { + id: 'ora-grading.FileCellContent.fileActionsTitle', + defaultMessage: 'Actions', + description: ' title for file actions', + }, + deleteButtonAltText: { + id: 'ora-grading.FileCellContent.deleteButtonAltText', + defaultMessage: 'Delete', + description: ' alt text for delete button', + }, + previewButtonAltText: { + id: 'ora-grading.FileCellContent.previewButtonAltText', + defaultMessage: 'Preview', + description: ' alt text for preview button', + }, + uploadFileModalTitle: { + id: 'ora-grading.FileCellContent.uploadFileModalTitle', + defaultMessage: 'Add a text description to your file', + description: 'Ask user to add a text description to the file', + }, + uploadFileDescriptionFieldLabel: { + id: 'ora-grading.FileCellContent.uploadFileDescriptionFieldLabel', + defaultMessage: 'Description for: ', + description: 'Label for file description field', + }, + cancelUploadFileButton: { + id: 'ora-grading.FileCellContent.cancelUploadFileButton', + defaultMessage: 'Cancel upload', + description: 'Label for cancel button', + }, + confirmUploadFileButton: { + id: 'ora-grading.FileCellContent.confirmUploadFileButton', + defaultMessage: 'Upload files', + description: 'Label for upload button', + }, + fileDescriptionMissingError: { + id: 'ora-grading.FileCellContent.fileDescriptionMissingError', + defaultMessage: 'Please enter a file description', + description: 'Error message when file description is missing', + }, }); export default messages; diff --git a/src/components/FileUpload/styles.scss b/src/components/FileUpload/styles.scss new file mode 100644 index 00000000..7ed24d08 --- /dev/null +++ b/src/components/FileUpload/styles.scss @@ -0,0 +1,8 @@ +.file-name-ellipsis { + width: 50%; + display: inline-block; + text-overflow: ellipsis; + vertical-align: middle; + overflow: hidden; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/components/Prompt/index.jsx b/src/components/Prompt/index.jsx index 2e66cce2..18503fd4 100644 --- a/src/components/Prompt/index.jsx +++ b/src/components/Prompt/index.jsx @@ -1,20 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useORAConfigData } from 'data/services/lms/hooks/selectors'; +import { Collapsible } from '@edx/paragon'; -export const Prompt = ({ promptIndex }) => { - const { prompts } = useORAConfigData(); +export const Prompt = ({ prompt }) => { + const [open, setOpen] = React.useState(true); return ( -
-

Prompt {promptIndex + 1}

-
-
+ setOpen(!open)}> +
+ ); }; Prompt.propTypes = { - promptIndex: PropTypes.number.isRequired, + prompt: PropTypes.string.isRequired, }; export default Prompt; diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx index 7d6630db..a508ed7b 100644 --- a/src/components/Rubric/index.jsx +++ b/src/components/Rubric/index.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { Card, StatefulButton } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { StrictDict } from '@edx/react-unit-test-utils'; +import { MutationStatus } from 'data/services/lms/constants'; import CriterionContainer from './CriterionContainer'; import RubricFeedback from './RubricFeedback'; @@ -13,13 +13,6 @@ import messages from './messages'; import './Rubric.scss'; -export const ButtonStates = StrictDict({ - idle: 'idle', - loading: 'loading', - error: 'error', - success: 'success', -}); - /** * */ @@ -64,11 +57,11 @@ export const Rubric = ({ isGrading }) => {
diff --git a/src/components/TextResponse/RichTextEditor.jsx b/src/components/TextResponse/RichTextEditor.jsx index cfea7eb8..e162bba4 100644 --- a/src/components/TextResponse/RichTextEditor.jsx +++ b/src/components/TextResponse/RichTextEditor.jsx @@ -11,21 +11,16 @@ import 'tinymce/plugins/image'; import 'tinymce/themes/silver'; import 'tinymce/skins/ui/oxide/skin.min.css'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - const RichTextEditor = ({ // id, - initialValue, + value, disabled, optional, + onChange, }) => { - const [value, setValue] = useKeyedState(stateKeys.value, initialValue); const { formatMessage } = useIntl(); const extraConfig = disabled ? { @@ -36,6 +31,10 @@ const RichTextEditor = ({ toolbar: 'formatselect | bold italic underline | link blockquote image | numlist bullist outdent indent | strikethrough | code | undo redo', }; + const handleChange = (e) => { + onChange(e.target.getContent()); + }; + return (
setValue(e.target.getContent())} + onChange={handleChange} disabled={disabled} />
@@ -63,8 +62,9 @@ const RichTextEditor = ({ RichTextEditor.defaultProps = { disabled: false, - initialValue: '', + value: '', optional: false, + onChange: () => { }, }; RichTextEditor.propTypes = { @@ -80,8 +80,9 @@ RichTextEditor.propTypes = { // error: PropTypes.string, // }).isRequired, disabled: PropTypes.bool, - initialValue: PropTypes.string, + value: PropTypes.string, optional: PropTypes.bool, + onChange: PropTypes.func, }; export default RichTextEditor; diff --git a/src/components/TextResponse/TextEditor.jsx b/src/components/TextResponse/TextEditor.jsx index 6a0f613e..e9441aa4 100644 --- a/src/components/TextResponse/TextEditor.jsx +++ b/src/components/TextResponse/TextEditor.jsx @@ -1,23 +1,18 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { TextArea } from '@edx/paragon'; -import { StrictDict } from '@edx/react-unit-test-utils'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -export const stateKeys = StrictDict({ - value: 'value', -}); - const TextEditor = ({ // id, - initialValue, + value, disabled, optional, + onChange, }) => { const { formatMessage } = useIntl(); - const [value, setValue] = useState(initialValue); return (