diff --git a/src/containers/ResponseDisplay/PromptDisplay.jsx b/src/containers/ResponseDisplay/PromptDisplay.jsx new file mode 100644 index 000000000..e8475c501 --- /dev/null +++ b/src/containers/ResponseDisplay/PromptDisplay.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Collapsible, Card } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import PropTypes from 'prop-types'; +import messages from './messages'; + +const PromptDisplay = ({ + prompt, className, styling, headerTitle, +}) => { + const intl = useIntl(); + const msg = intl.formatMessage(messages.promptCollapsibleHeader); + return ( +
+ {msg} : msg} + > + { prompt } + +
+ ); +}; + +PromptDisplay.propTypes = { + prompt: PropTypes.string.isRequired, + className: PropTypes.string.isRequired, + styling: PropTypes.string.isRequired, + headerTitle: PropTypes.bool.isRequired, +}; + +const SinglePromptDisplay = ({ prompt }) => ( + +); + +SinglePromptDisplay.propTypes = { + prompt: PropTypes.string.isRequired, +}; + +const MultiplePromptDisplay = ({ prompt }) => ( + <> + + + +); + +MultiplePromptDisplay.propTypes = { + prompt: PropTypes.string.isRequired, +}; + +export { SinglePromptDisplay, MultiplePromptDisplay }; diff --git a/src/containers/ResponseDisplay/ResponseDisplay.scss b/src/containers/ResponseDisplay/ResponseDisplay.scss index 5d4e0fe53..16a8e04e9 100644 --- a/src/containers/ResponseDisplay/ResponseDisplay.scss +++ b/src/containers/ResponseDisplay/ResponseDisplay.scss @@ -4,6 +4,14 @@ overflow-y: hidden; height: fit-content; + .prompt-display-single { + padding: var(--pgn-spacing-spacer-3) 0; + } + + .prompt-display-multiple > .collapsible-basic .collapsible-trigger{ + text-decoration: none!important; + } + .submission-files { .submission-files-title { padding: var(--pgn-spacing-spacer-3); @@ -42,6 +50,10 @@ padding: var(--pgn-spacing-spacer-3) 0; } + .response-display-card { + margin: var(--pgn-spacing-spacer-3) 0; + } + .response-display-text-content { white-space: pre-line; overflow: hidden; diff --git a/src/containers/ResponseDisplay/index.jsx b/src/containers/ResponseDisplay/index.jsx index 09cdac4bd..38ef3594a 100644 --- a/src/containers/ResponseDisplay/index.jsx +++ b/src/containers/ResponseDisplay/index.jsx @@ -14,7 +14,7 @@ import { fileUploadResponseOptions } from 'data/services/lms/constants'; import { getConfig } from '@edx/frontend-platform'; import SubmissionFiles from './SubmissionFiles'; import PreviewDisplay from './PreviewDisplay'; - +import { SinglePromptDisplay, MultiplePromptDisplay } from './PromptDisplay'; import './ResponseDisplay.scss'; /** @@ -26,13 +26,13 @@ export class ResponseDisplay extends React.Component { this.purify = createDOMPurify(window); } + get prompts() { + return this.props.prompts.map((item) => this.formattedHtml(item)); + } + get textContents() { const { text } = this.props.response; - - const formattedText = text - .map((item) => item.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`)) - .map((item) => parse(this.purify.sanitize(item))); - + const formattedText = text.map((item) => this.formattedHtml(item)); return formattedText; } @@ -46,15 +46,24 @@ export class ResponseDisplay extends React.Component { ); } + formattedHtml(text) { + const cleanedText = text.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`); + return parse(this.purify.sanitize(cleanedText)); + } + render() { + const { prompts } = this; + const multiPrompt = prompts.length > 1; return (
+ {!multiPrompt && } {this.allowFileUpload && } {this.allowFileUpload && } { /* eslint-disable react/no-array-index-key */ this.textContents.map((textContent, index) => ( - + + {multiPrompt && } {textContent} )) @@ -71,6 +80,7 @@ ResponseDisplay.defaultProps = { }, fileUploadResponseConfig: fileUploadResponseOptions.none, }; + ResponseDisplay.propTypes = { response: PropTypes.shape({ text: PropTypes.arrayOf(PropTypes.string), @@ -83,11 +93,13 @@ ResponseDisplay.propTypes = { fileUploadResponseConfig: PropTypes.oneOf( Object.values(fileUploadResponseOptions), ), + prompts: PropTypes.arrayOf(PropTypes.string).isRequired, }; export const mapStateToProps = (state) => ({ response: selectors.grading.selected.response(state), fileUploadResponseConfig: selectors.app.ora.fileUploadResponseConfig(state), + prompts: selectors.app.ora.prompts(state), }); export const mapDispatchToProps = {}; diff --git a/src/containers/ResponseDisplay/index.test.jsx b/src/containers/ResponseDisplay/index.test.jsx index 892540086..47fe2f184 100644 --- a/src/containers/ResponseDisplay/index.test.jsx +++ b/src/containers/ResponseDisplay/index.test.jsx @@ -13,11 +13,17 @@ jest.mock('data/redux', () => ({ app: { ora: { fileUploadResponseConfig: jest.fn((state) => state.fileUploadResponseConfig || 'optional'), + prompts: jest.fn((state) => state.prompts || ['prompt']), }, }, }, })); +jest.mock('./PromptDisplay', () => ({ + SinglePromptDisplay: jest.fn(({ prompt }) => (
Prompt: {prompt}
)), + MultiplePromptDisplay: jest.fn(({ prompt }) => (
Prompt: {prompt}
)), +})); + jest.mock('./SubmissionFiles', () => jest.fn(({ files }) => (
Files: {files.length}
))); @@ -50,6 +56,7 @@ describe('ResponseDisplay', () => { ], }, fileUploadResponseConfig: 'optional', + prompts: ['prompt one', 'prompt two'], }; beforeAll(() => { @@ -100,6 +107,18 @@ describe('ResponseDisplay', () => { const textContents = container.querySelectorAll('.response-display-text-content'); expect(textContents).toHaveLength(0); }); + + it('displays single prompt when only one prompt', () => { + render(); + expect(screen.queryByTestId('prompt-single')).toBeInTheDocument(); + expect(screen.queryByTestId('prompt-multiple')).not.toBeInTheDocument(); + }); + + it('displays multiple prompts when there are multiple prompts', () => { + render(); + expect(screen.queryByTestId('prompt-single')).not.toBeInTheDocument(); + expect(screen.queryAllByTestId('prompt-multiple')).toHaveLength(2); + }); }); describe('mapStateToProps', () => { @@ -109,6 +128,7 @@ describe('ResponseDisplay', () => { files: ['file1', 'file2'], }, fileUploadResponseConfig: 'required', + prompts: ['prompt'], }; it('maps response from grading.selected.response selector', () => { @@ -120,5 +140,10 @@ describe('ResponseDisplay', () => { const mapped = mapStateToProps(testState); expect(mapped.fileUploadResponseConfig).toEqual(selectors.app.ora.fileUploadResponseConfig(testState)); }); + + it('maps prompts from app.ora.prompts selector', () => { + const mapped = mapStateToProps(testState); + expect(mapped.prompts).toEqual(selectors.app.ora.prompts(testState)); + }); }); }); diff --git a/src/containers/ResponseDisplay/messages.js b/src/containers/ResponseDisplay/messages.js index e7387762d..de66c405b 100644 --- a/src/containers/ResponseDisplay/messages.js +++ b/src/containers/ResponseDisplay/messages.js @@ -46,6 +46,11 @@ const messages = defineMessages({ defaultMessage: 'Exceeded the allow download size', description: 'Exceed the allow download size error message', }, + promptCollapsibleHeader: { + id: 'ora-grading.ResponseDisplay.Prompt.collapsibleHeader', + defaultMessage: 'Prompt', + description: 'Header for a collapsible that displays the assignment prompt', + }, }); export default messages; diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index f466e6071..aea71aa25 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -11,7 +11,7 @@ const initialState = { isEnabled: false, isGrading: false, oraMetadata: { - prompt: '', + prompts: [], name: '', type: '', rubricConfig: null, diff --git a/src/data/redux/app/reducer.test.js b/src/data/redux/app/reducer.test.js index 4302fd2dd..c1384945d 100644 --- a/src/data/redux/app/reducer.test.js +++ b/src/data/redux/app/reducer.test.js @@ -17,7 +17,7 @@ describe('app reducer', () => { }); test('populated, but empty ora metadata', () => { const data = initialState.oraMetadata; - expect(data.prompt).toEqual(''); + expect(data.prompts).toEqual([]); expect(data.name).toEqual(''); expect(data.type).toEqual(''); expect(data.rubricConfig).toEqual(null); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index 815a73d61..4099f7e06 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -33,10 +33,10 @@ export const ora = { */ name: oraMetadataSelector(data => data.name), /** - * Returns the ORA Prompt - * @return {string} - ORA prompt + * Returns the ORA Prompts + * @return {array[string]} - ORA prompt */ - prompt: oraMetadataSelector(data => data.prompt), + prompts: oraMetadataSelector(data => (data.prompts ? data.prompts.map((oraPrompt) => oraPrompt.description) : [])), /** * Returns the ORA type * @return {string} - ORA type (team vs individual) diff --git a/src/data/redux/app/selectors.test.js b/src/data/redux/app/selectors.test.js index cb22fb3cb..36b7f16e6 100644 --- a/src/data/redux/app/selectors.test.js +++ b/src/data/redux/app/selectors.test.js @@ -18,7 +18,10 @@ const testState = { }, oraMetadata: { name: 'test-ora-name', - prompt: 'test-ora-prompt', + prompts: [ + { description: 'test-ora-prompt' }, + { description: 'test-second-prompt' }, + ], type: 'test-ora-type', fileUploadResponseConfig: 'file-upload-response-config', rubricConfig: { @@ -102,8 +105,8 @@ describe('app selectors unit tests', () => { test('ora.name selector returns name from oraMetadata', () => { testOraSelector(selectors.ora.name, oraMetadata.name); }); - test('ora.prompt selector returns prompt from oraMetadata', () => { - testOraSelector(selectors.ora.prompt, oraMetadata.prompt); + test('ora.prompts selector returns prompts from oraMetadata', () => { + testOraSelector(selectors.ora.prompts, ['test-ora-prompt', 'test-second-prompt']); }); test('ora.type selector returns type from oraMetadata', () => { testOraSelector(selectors.ora.type, oraMetadata.type); diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index bc4c41ce3..c2ea60e55 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -16,7 +16,7 @@ import { /** * get('/api/initialize', { oraLocation }) * @return { - * oraMetadata: { name, prompt, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig }, + * oraMetadata: { name, prompts, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig }, * courseMetadata: { courseOrg, courseName, courseNumber, courseId }, * submissions: { * [submissionUUID]: {