diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx
deleted file mode 100644
index d4c996c3..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/PDFRenderer.test.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-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/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx
deleted file mode 100644
index 43f82247..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/TXTRenderer.test.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-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/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap
deleted file mode 100644
index c9503872..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Image Renderer Component snapshot 1`] = `
-
-`;
diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap
deleted file mode 100644
index 5d7b3536..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap
+++ /dev/null
@@ -1,139 +0,0 @@
-// 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/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap b/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap
deleted file mode 100644
index 7675f907..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap
+++ /dev/null
@@ -1,9 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`TXT Renderer Component snapshot 1`] = `
-
- test-content
-
-`;
diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.js
index 01447e8f..073a32c7 100644
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.js
+++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.js
@@ -20,7 +20,7 @@ export const safeSetPageNumber = ({ numPages, rawSetPageNumber }) => (pageNumber
}
};
-export const rendererHooks = ({
+export const usePDFRendererData = ({
onError,
onSuccess,
}) => {
diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js
deleted file mode 100644
index 74272be0..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/pdfHooks.test.js
+++ /dev/null
@@ -1,151 +0,0 @@
-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/components/FileRenderer/BaseRenderers/textHooks.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js
index 5e9477e2..82a1395a 100644
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js
+++ b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.js
@@ -21,14 +21,12 @@ export const fetchFile = async ({
export const useTextRendererData = ({ url, onError, onSuccess }) => {
const [content, setContent] = useKeyedState(stateKeys.content, '');
- useEffect(() => {
- return fetchFile({
- setContent,
- url,
- onError,
- onSuccess,
- });
- }, [onError, onSuccess, setContent, url]);
+ useEffect(() => fetchFile({
+ setContent,
+ url,
+ onError,
+ onSuccess,
+ }), [onError, onSuccess, setContent, url]);
return { content };
};
diff --git a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js b/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js
deleted file mode 100644
index b73e4dc4..00000000
--- a/src/components/FilePreview/components/FileRenderer/BaseRenderers/textHooks.test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/* 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/components/FileRenderer/FileCard/index.test.jsx b/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx
deleted file mode 100644
index ea680a23..00000000
--- a/src/components/FilePreview/components/FileRenderer/FileCard/index.test.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import { Collapsible } from '@edx/paragon';
-
-import FileCard from '.';
-
-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/components/FileRenderer/FileRenderer.test.jsx b/src/components/FilePreview/components/FileRenderer/FileRenderer.test.jsx
deleted file mode 100644
index 703f8644..00000000
--- a/src/components/FilePreview/components/FileRenderer/FileRenderer.test.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-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/components/__snapshots__/FileCard.test.jsx.snap b/src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap
deleted file mode 100644
index 2cf08071..00000000
--- a/src/components/FilePreview/components/__snapshots__/FileCard.test.jsx.snap
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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/components/__snapshots__/FileRenderer.test.jsx.snap b/src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap
deleted file mode 100644
index d8af97d8..00000000
--- a/src/components/FilePreview/components/__snapshots__/FileRenderer.test.jsx.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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/components/utils.js b/src/components/FilePreview/components/utils.js
index 92d51b00..515777cf 100644
--- a/src/components/FilePreview/components/utils.js
+++ b/src/components/FilePreview/components/utils.js
@@ -1,9 +1,3 @@
-import {
- PDFRenderer,
- ImageRenderer,
- TXTRenderer,
-} from './FileRenderer/BaseRenderers';
-
import { supportedTypes } from './constants';
/**
diff --git a/src/components/FilePreview/index.jsx b/src/components/FilePreview/index.jsx
index 393d4816..ec8ce7dd 100644
--- a/src/components/FilePreview/index.jsx
+++ b/src/components/FilePreview/index.jsx
@@ -1,11 +1,12 @@
import React from 'react';
+import PropTypes from 'prop-types';
import { useResponseData } from 'hooks/app';
import { FileRenderer, isSupported } from './components';
const FilePreview = ({ defaultCollapsePreview }) => {
const { uploadedFiles } = useResponseData();
- console.log({ files: uploadedFiles.filter(isSupported) });
+ // console.log({ files: uploadedFiles.filter(isSupported) });
return (
{uploadedFiles.filter(isSupported).map((file) => (
@@ -14,5 +15,8 @@ const FilePreview = ({ defaultCollapsePreview }) => {
);
};
+FilePreview.propTypes = {
+ defaultCollapsePreview: PropTypes.bool.isRequired,
+};
export default FilePreview;
diff --git a/src/components/FileUpload/ActionCell.jsx b/src/components/FileUpload/ActionCell.jsx
index 2e800a6c..43bcfebb 100644
--- a/src/components/FileUpload/ActionCell.jsx
+++ b/src/components/FileUpload/ActionCell.jsx
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { IconButton, Icon } from '@edx/paragon';
-import { Delete, Preview } from '@edx/paragon/icons';
+import { Delete } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
@@ -13,28 +13,17 @@ const ActionCell = ({
row,
}) => {
const { formatMessage } = useIntl();
- const deleteFile = useCallback(async () => {
- console.log('deleteFile', row.index);
- await onDeletedFile(row.index);
- }, [onDeletedFile, row.index]);
- return (
- <>
- {!disabled && (
-
- )}
-
- >
+ const deleteFile = useCallback(() => {
+ onDeletedFile(row.original.fileIndex);
+ }, [onDeletedFile, row.original.fileIndex]);
+ return !disabled && (
+
);
};
@@ -46,7 +35,9 @@ ActionCell.propTypes = {
onDeletedFile: PropTypes.func,
disabled: PropTypes.bool.isRequired,
row: PropTypes.shape({
- index: PropTypes.number.isRequired,
+ original: PropTypes.shape({
+ fileIndex: PropTypes.number,
+ }),
}).isRequired,
};
diff --git a/src/components/FileUpload/ActionCell.test.jsx b/src/components/FileUpload/ActionCell.test.jsx
deleted file mode 100644
index 6ffbd72e..00000000
--- a/src/components/FileUpload/ActionCell.test.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-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(
);
- expect(wrapper.snapshot).toMatchSnapshot();
- });
-});
diff --git a/src/components/FileUpload/FileDownload.jsx b/src/components/FileUpload/FileDownload.jsx
index a719531c..9c793588 100644
--- a/src/components/FileUpload/FileDownload.jsx
+++ b/src/components/FileUpload/FileDownload.jsx
@@ -19,6 +19,9 @@ const FileDownload = ({ files }) => {
files,
zipFileName: xblockId,
});
+ if (!files.length) {
+ return null;
+ }
return (
({
- useUploadConfirmModalHooks: jest.fn(),
-}));
-
-describe(' ', () => {
- const props = {
- open: true,
- file: { name: 'file1' },
- closeHandler: jest.fn().mockName('closeHandler'),
- uploadHandler: jest.fn().mockName('uploadHandler'),
- };
-
- const mockHooks = (overrides) => {
- useUploadConfirmModalHooks.mockReturnValueOnce({
- shouldShowError: false,
- exitHandler: jest.fn().mockName('exitHandler'),
- confirmUploadClickHandler: jest.fn().mockName('confirmUploadClickHandler'),
- onFileDescriptionChange: jest.fn().mockName('onFileDescriptionChange'),
- ...overrides,
- });
- };
- describe('renders', () => {
- test('without error', () => {
- mockHooks(
- { errors: new Array(2) },
- );
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('Form.Group').length).toBe(1);
- expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(0);
- });
-
- test('with errors', () => {
- mockHooks({ shouldShowError: true });
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('Form.Group').length).toBe(1);
- expect(wrapper.instance.findByType('Form.Control.Feedback').length).toBe(1);
- });
- });
-});
diff --git a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap b/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap
deleted file mode 100644
index dd66fe44..00000000
--- a/src/components/FileUpload/__snapshots__/ActionCell.test.jsx.snap
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` renders 1`] = `
-
-
-
-
-`;
diff --git a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap b/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap
deleted file mode 100644
index dccdbae1..00000000
--- a/src/components/FileUpload/__snapshots__/UploadConfirmModal.test.jsx.snap
+++ /dev/null
@@ -1,112 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` renders with errors 1`] = `
-
-
-
- Add a text description to your file
-
-
-
-
-
-
-
- Description for:
-
-
- file1
-
-
-
-
- formatMessage(messages.fileDescriptionMissingError)
-
-
-
-
-
-
-
- Cancel upload
-
-
- Upload files
-
-
-
-
-`;
-
-exports[` renders without error 1`] = `
-
-
-
- Add a text description to your file
-
-
-
-
-
-
-
- Description for:
-
-
- file1
-
-
-
-
-
-
-
-
-
- Cancel upload
-
-
- Upload files
-
-
-
-
-`;
diff --git a/src/components/FileUpload/__snapshots__/index.test.jsx.snap b/src/components/FileUpload/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index cecf480a..00000000
--- a/src/components/FileUpload/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -1,139 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` render default 1`] = `
-
-
- File Upload
-
-
-
- Uploaded Files
-
-
-
-
-
-
-
-
-`;
-
-exports[` render no uploaded files 1`] = `
-
-
- File Upload
-
-
-
-
-
-
-`;
-
-exports[` render read only 1`] = `
-
-
- File Upload
-
-
-
- Uploaded Files
-
-
-
-
-`;
diff --git a/src/components/FileUpload/hooks.js b/src/components/FileUpload/hooks.js
index 9c06adb4..fd461aa1 100644
--- a/src/components/FileUpload/hooks.js
+++ b/src/components/FileUpload/hooks.js
@@ -22,7 +22,7 @@ export const useUploadConfirmModalHooks = ({
);
const confirmUploadClickHandler = () => {
- console.log({ confirmUploadClick: { description } });
+ // console.log({ confirmUploadClick: { description } });
if (description !== '') {
uploadHandler(file, description);
} else {
@@ -56,10 +56,10 @@ export const useFileUploadHooks = ({ onFileUploaded }) => {
);
const confirmUpload = useCallback(async (file, description) => {
- console.log({ confirmUpload: { file, description } });
+ // console.log({ confirmUpload: { file, description } });
setIsModalOpen(false);
if (onFileUploaded) {
- console.log({ uploadArgs });
+ // console.log({ uploadArgs });
await onFileUploaded({ ...uploadArgs, description });
}
setUploadArgs({});
@@ -89,11 +89,10 @@ export const useFileUploadHooks = ({ onFileUploaded }) => {
export const useFileDownloadHooks = ({ files, zipFileName }) => {
const downloadFileMation = useDownloadFiles();
- const downloadFiles = () =>
- downloadFileMation.mutate({
- files,
- zipFileName,
- });
+ const downloadFiles = () => downloadFileMation.mutate({
+ files,
+ zipFileName,
+ });
return {
downloadFiles,
status: downloadFileMation.status,
diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx
index 5113dda9..53e3d14f 100644
--- a/src/components/FileUpload/index.jsx
+++ b/src/components/FileUpload/index.jsx
@@ -46,42 +46,38 @@ const FileUpload = ({
return (
-
File Upload
- {isReadOnly &&
}
- {uploadedFiles.length > 0 && (
- <>
- Uploaded Files
- ({
- ...file,
- size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown',
- }))}
- tableActions={[
- ,
- ]}
- columns={[
- {
- Header: formatMessage(messages.fileNameTitle),
- accessor: 'fileName',
- },
- {
- Header: formatMessage(messages.fileDescriptionTitle),
- accessor: 'fileDescription',
- },
- {
- Header: formatMessage(messages.fileSizeTitle),
- accessor: 'fileSize',
- },
- {
- Header: formatMessage(messages.fileActionsTitle),
- accessor: 'actions',
- Cell: createFileActionCell({ onDeletedFile, isReadOnly }),
- },
- ]}
- />
- >
- )}
+ {formatMessage(messages.fileUploadTitle)}
+ {isReadOnly && }
+ {formatMessage(messages.uploadedFilesTitle)}
+ ({
+ ...file,
+ size: typeof file.size === 'number' ? filesize(file.size) : 'Unknown',
+ }))}
+ tableActions={[
+ ,
+ ]}
+ columns={[
+ {
+ Header: formatMessage(messages.fileNameTitle),
+ accessor: 'fileName',
+ },
+ {
+ Header: formatMessage(messages.fileDescriptionTitle),
+ accessor: 'fileDescription',
+ },
+ {
+ Header: formatMessage(messages.fileSizeTitle),
+ accessor: 'fileSize',
+ },
+ {
+ Header: formatMessage(messages.fileActionsTitle),
+ accessor: 'actions',
+ Cell: createFileActionCell({ onDeletedFile, isReadOnly }),
+ },
+ ]}
+ />
{!isReadOnly && (
)}
diff --git a/src/components/FileUpload/index.test.jsx b/src/components/FileUpload/index.test.jsx
deleted file mode 100644
index 81076b3a..00000000
--- a/src/components/FileUpload/index.test.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { shallow } from '@edx/react-unit-test-utils';
-import FileUpload from '.';
-
-import { useFileUploadHooks } from './hooks';
-
-jest.mock('./hooks', () => ({
- useFileUploadHooks: jest.fn(),
-}));
-
-jest.mock('./UploadConfirmModal', () => 'UploadConfirmModal');
-jest.mock('./ActionCell', () => 'ActionCell');
-
-describe(' ', () => {
- const props = {
- isReadOnly: false,
- uploadedFiles: [
- {
- fileName: 'file1',
- fileDescription: 'file1 desc',
- fileSize: 100,
- },
- {
- fileName: 'file2',
- fileDescription: 'file2 desc',
- fileSize: 200,
- },
- ],
- onFileUploaded: jest.fn().mockName('props.onFileUploaded'),
- };
-
- const mockHooks = (overrides) => {
- useFileUploadHooks.mockReturnValueOnce({
- isModalOpen: false,
- uploadArgs: {},
- confirmUpload: jest.fn().mockName('confirmUpload'),
- closeUploadModal: jest.fn().mockName('closeUploadModal'),
- onProcessUpload: jest.fn().mockName('onProcessUpload'),
- ...overrides,
- });
- };
- describe('behavior', () => {
- it('initializes data from hook', () => {
- mockHooks();
- shallow( );
- expect(useFileUploadHooks).toHaveBeenCalledWith({ onFileUploaded: props.onFileUploaded });
- });
- });
- describe('render', () => {
- // TODO: examine files in the table
- // TODO: examine dropzone args
- test('default', () => {
- mockHooks();
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
- expect(wrapper.instance.findByType('Dropzone')).toHaveLength(1);
- expect(wrapper.instance.findByType('DataTable')).toHaveLength(1);
- });
-
- test('read only', () => {
- mockHooks({ isReadOnly: true });
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('Dropzone')).toHaveLength(0);
- });
-
- test('no uploaded files', () => {
- mockHooks();
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('DataTable')).toHaveLength(0);
- });
- });
-});
diff --git a/src/components/FileUpload/messages.js b/src/components/FileUpload/messages.js
index 7aed091a..9cc7f6c2 100644
--- a/src/components/FileUpload/messages.js
+++ b/src/components/FileUpload/messages.js
@@ -16,6 +16,11 @@ const messages = defineMessages({
defaultMessage: 'File Size',
description: ' title for file size',
},
+ fileUploadTitle: {
+ id: 'frontend-app-ora.FileCellContent.fileUploadTitle',
+ defaultMessage: 'File Upload',
+ description: ' title for file upload',
+ },
fileActionsTitle: {
id: 'frontend-app-ora.FileCellContent.fileActionsTitle',
defaultMessage: 'Actions',
@@ -41,6 +46,11 @@ const messages = defineMessages({
defaultMessage: 'Description for: ',
description: 'Label for file description field',
},
+ uploadedFilesTitle: {
+ id: 'frontend-app-ora.FileCellContent.uploadedFilesTitle',
+ defaultMessage: 'Uploaded Files',
+ description: 'Title for uploaded files',
+ },
cancelUploadFileButton: {
id: 'frontend-app-ora.FileCellContent.cancelUploadFileButton',
defaultMessage: 'Cancel upload',
diff --git a/src/components/Instructions/messages.js b/src/components/Instructions/messages.js
index e53b1312..39a18c4b 100644
--- a/src/components/Instructions/messages.js
+++ b/src/components/Instructions/messages.js
@@ -6,7 +6,7 @@ const messages = defineMessages({
[stepNames.submission]: {
defaultMessage: 'Enter your response to the prompt. Your work will save automatically and you can return to complete your response at any time before the due date. After you submit your response, you cannot edit it.',
description: 'Submission step instructions',
- id: 'frontend-app-ora.instructions.submisison',
+ id: 'frontend-app-ora.instructions.submission',
},
[stepNames.studentTraining]: {
defaultMessage: 'Before you begin to assess your peers\' responses, you\'ll learn how to complete peer assessments by reviewing responses that instructors have already assessed. If you select the same options for the response that the instructor selected, you\'ll move to the next step. If you don\'t select the same options, you\'ll review the response and try again.',
diff --git a/src/components/ModalActions/hooks/messages.js b/src/components/ModalActions/hooks/messages.js
index d86a4a5a..7a3f9920 100644
--- a/src/components/ModalActions/hooks/messages.js
+++ b/src/components/ModalActions/hooks/messages.js
@@ -3,8 +3,8 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
finishLater: {
id: 'ora-mfe.ModalActions.simpleAction.finishLater',
- defaultMessage: 'Finish later',
- description: 'Finish later (close) button text',
+ defaultMessage: 'Save for later',
+ description: 'Save for later (close) button text',
},
submitResponse: {
id: 'ora-mfe.ModalActions.submitResponse',
diff --git a/src/components/ModalActions/hooks/useFinishedStateActions.js b/src/components/ModalActions/hooks/useFinishedStateActions.js
index d9e17e77..a4da8367 100644
--- a/src/components/ModalActions/hooks/useFinishedStateActions.js
+++ b/src/components/ModalActions/hooks/useFinishedStateActions.js
@@ -1,4 +1,4 @@
-import { useGlobalState } from 'hooks/app';
+import { useGlobalState, useTrainingStepIsCompleted } from 'hooks/app';
import {
useHasSubmitted,
useSubmittedAssessment,
@@ -19,6 +19,7 @@ const useFinishedStateActions = () => {
const startStepAction = useStartStepAction(step);
const submittedAssessment = useSubmittedAssessment();
const loadNextAction = useLoadNextAction();
+ const trainingStepIsCompleted = useTrainingStepIsCompleted();
const stepState = globalState.activeStepState;
@@ -26,47 +27,40 @@ const useFinishedStateActions = () => {
const exitAction = useExitAction();
if (!hasSubmitted) {
+ if (step === stepNames.studentTraining && trainingStepIsCompleted) {
+ return { primary: startStepAction, secondary: finishLaterAction };
+ }
return null;
}
- // console.log({ step, submittedAssessment, startStepAction });
// assessment finished state
if (submittedAssessment) {
const { activeStepName } = globalState;
- console.log({ activeStepName });
if (activeStepName === stepNames.staff) {
return { primary: exitAction };
}
// finished and moved to next step
if ([stepNames.submission || stepNames.self].includes(step)) {
- console.log("self or submission");
return { primary: startStepAction, secondary: finishLaterAction };
}
if (step !== activeStepName) {
// next step is available
- console.log("next step");
if (stepState === stepStates.inProgress) {
- console.log({ startStepAction });
return { primary: startStepAction, secondary: finishLaterAction };
}
- console.log("next step not available");
// next step is not available
return null;
}
- console.log({ step, activeStepName });
// finished current assessment but not current step
if (stepState === stepStates.inProgress) {
- console.log("finished intermediate");
return { primary: loadNextAction, secondary: finishLaterAction };
}
// finished current assessment, but not step
// and there are no more assessments available for the current step
return { primary: exitAction };
}
- console.log("?");
// submission finished state
- console.log({ startStepAction });
return { primary: startStepAction, secondary: finishLaterAction };
};
diff --git a/src/components/ModalActions/index.jsx b/src/components/ModalActions/index.jsx
index 1f8025fc..953f3a3b 100644
--- a/src/components/ModalActions/index.jsx
+++ b/src/components/ModalActions/index.jsx
@@ -1,24 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, StatefulButton } from '@edx/paragon';
+import { Skeleton } from '@edx/paragon';
-import { MutationStatus } from 'constants';
+import ActionButton from 'components/ActionButton';
+import { useIsPageDataLoading } from 'hooks/app';
import useModalActionConfig from './hooks/useModalActionConfig';
-const className = 'w-100';
-const disabledStates = [MutationStatus.loading];
+const className = 'w-100 mt-3';
const ModalActions = ({ options }) => {
const actions = useModalActionConfig({ options });
+ const isPageDataLoading = useIsPageDataLoading();
const { primary, secondary } = actions || {};
- const actionButton = (variant, btnProps) => (btnProps.state
- ?
- : );
+ const actionButton = (variant, btnProps) => (
+
+ );
+
+ const customWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ if (isPageDataLoading) {
+ return ( );
+ }
return (
-
+
{secondary && actionButton('outline-primary', secondary)}
{primary && actionButton('primary', primary)}
diff --git a/src/components/ModalActions/messages.js b/src/components/ModalActions/messages.js
index 3c30aba4..a5a40429 100644
--- a/src/components/ModalActions/messages.js
+++ b/src/components/ModalActions/messages.js
@@ -29,12 +29,12 @@ const messages = defineMessages({
},
startTraining: {
id: 'ora-mfe.ModalActions.startTraining',
- defaultMessage: 'Begin practice grading',
+ defaultMessage: 'Go to practice grading',
description: 'Action button to begin studentTraining step',
},
startSelf: {
- id: 'ora-mfe.ModalActions.startTraining',
- defaultMessage: 'Begin self grading',
+ id: 'ora-mfe.ModalActions.startSelf',
+ defaultMessage: 'Go to self grading',
description: 'Action button to begin self assessment step',
},
startPeer: {
diff --git a/src/components/ModalContainer.jsx b/src/components/ModalContainer.jsx
index dfb984ae..2934e3cc 100644
--- a/src/components/ModalContainer.jsx
+++ b/src/components/ModalContainer.jsx
@@ -1,30 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { FullscreenModal } from '@edx/paragon';
-
-import { nullMethod } from 'utils';
+import { useORAConfigData } from 'hooks/app';
import ProgressBar from 'components/ProgressBar';
/* The purpose of this component is to wrap views with a header/footer for situations
- * where we need to run non-embedded
+ * where we need to run non-embedded. It is a replicated style of FullScreenModal from
+ * paragon. The reason we use this instead of FullScreenModal is because FullScreenModal
+ * is very opinionated on other components that it uses on top of it. Since we are not
+ * using the modality of the component, it would be better to just replicate the style
+ * instead of using the component.
*/
-const ModalContainer = ({ title, children }) => (
-
}
- >
- {children}
-
-);
+const ModalContainer = ({ children }) => {
+ const { title } = useORAConfigData();
+ return (
+
+ );
+};
ModalContainer.propTypes = {
children: PropTypes.node.isRequired,
- title: PropTypes.string.isRequired,
};
export default ModalContainer;
diff --git a/src/components/ProgressBar/ProgressStep.jsx b/src/components/ProgressBar/ProgressStep.jsx
index 2370dd81..2ea16374 100644
--- a/src/components/ProgressBar/ProgressStep.jsx
+++ b/src/components/ProgressBar/ProgressStep.jsx
@@ -29,6 +29,7 @@ const ProgressStep = ({
label,
}) => {
const {
+ onClick,
href,
isActive,
isEnabled,
@@ -51,7 +52,7 @@ const ProgressStep = ({
}
return (
{
const { xblockId, courseId } = useParams();
const isEmbedded = useIsEmbedded();
const viewStep = useViewStep();
- const { effectiveGrade, stepState } = useGlobalState({ step });
+ const { effectiveGrade, stepState, activeStepName } = useGlobalState({ step });
+ const stepInfo = useStepInfo();
+ const openModal = useOpenModal();
+
+ const href = `/${stepRoutes[step]}${
+ isEmbedded ? '/embedded' : ''
+ }/${courseId}/${xblockId}`;
+ const onClick = () => openModal({ view: step, title: step });
+ const isActive = viewStep === stepNames.xblock ? activeStepName === step : viewStep === step;
+ let isEnabled = isActive || stepState === stepStates.inProgress || (canRevisit && stepState === stepStates.done);
+
+ if (step === stepNames.peer) {
+ const isPeerComplete = stepInfo.peer?.numberOfReceivedAssessments > 0;
+ const isWaitingForSubmissions = stepInfo.peer?.isWaitingForSubmissions;
+ isEnabled = !isWaitingForSubmissions && (isEnabled || isPeerComplete);
+ }
- const href = `/${stepRoutes[step]}${isEmbedded ? '/embedded' : ''}/${courseId}/${xblockId}`;
- const isActive = viewStep === step;
- const isEnabled = (
- isActive
- || (stepState === stepStates.inProgress)
- || (canRevisit && stepState === stepStates.done)
- );
return {
- href,
+ ...(viewStep === stepNames.xblock ? { onClick } : { href }),
isEnabled,
isActive,
isComplete: stepState === stepStates.done,
diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx
index eaed274b..e17bb89c 100644
--- a/src/components/ProgressBar/index.jsx
+++ b/src/components/ProgressBar/index.jsx
@@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { Navbar } from '@edx/paragon';
+import { Navbar, Icon } from '@edx/paragon';
+import { ArrowDropUpDown, ArrowForwardIos } from '@edx/paragon/icons';
import {
useAssessmentStepOrder,
+ useGlobalState,
useHasReceivedFinalGrade,
useIsPageDataLoaded,
- useStepInfo,
} from 'hooks/app';
import { stepNames } from 'constants';
+import { useViewStep } from 'hooks/routing';
import ProgressStep from './ProgressStep';
import messages from './messages';
@@ -36,9 +38,11 @@ export const stepCanRevisit = {
export const ProgressBar = ({ className }) => {
const isLoaded = useIsPageDataLoaded();
- const stepInfo = useStepInfo();
const hasReceivedFinalGrade = useHasReceivedFinalGrade();
+ const activeStep = useViewStep();
+ const { activeStepName } = useGlobalState();
+
const stepOrders = [
stepNames.submission,
...useAssessmentStepOrder(),
@@ -50,18 +54,31 @@ export const ProgressBar = ({ className }) => {
return null;
}
- const stepEl = (curStep) => stepLabels[curStep]
- ? (
-
- ) : null;
+ const stepEl = (curStep) => (stepLabels[curStep] ? (
+
+ ) : null);
+
+ const activeStepTitle = activeStep === stepNames.xblock ? activeStepName : activeStep;
return (
-
+
+
+
+
+
+ {formatMessage(stepLabels[activeStepTitle])}
+
+
+
+
{stepOrders.map(stepEl)}
diff --git a/src/components/ProgressBar/index.scss b/src/components/ProgressBar/index.scss
index c527f87b..90e4493a 100644
--- a/src/components/ProgressBar/index.scss
+++ b/src/components/ProgressBar/index.scss
@@ -1,6 +1,9 @@
+@import '@edx/paragon/scss/core/core';
+
.ora-progress-nav-group {
width: 100%;
display: flex;
+ flex-direction: row;
justify-content: space-between;
.ora-progress-divider {
position: absolute;
@@ -31,3 +34,21 @@
border-bottom: 2px solid black;
}
}
+
+@include media-breakpoint-down(sm) {
+ .ora-progress-nav-group {
+ flex-direction: column;
+ align-items: start;
+ border-top: 1px solid black;
+ padding: 0 map-get($spacers, 3);
+
+ .ora-progress-divider {
+ display: none;
+ }
+
+ .ora-progress-nav {
+ width: 100%;
+ margin: 0 0.25rem;
+ }
+ }
+}
diff --git a/src/components/ProgressBar/messages.js b/src/components/ProgressBar/messages.js
index 2e2bccb1..b0485779 100644
--- a/src/components/ProgressBar/messages.js
+++ b/src/components/ProgressBar/messages.js
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
createSubmission: {
id: 'frontend-app-ora.ProgressBar.createSubmission',
- defaultMessage: 'Create your response',
+ defaultMessage: 'Create response',
description: 'Create response progress indicator',
},
studentTraining: {
@@ -13,7 +13,7 @@ const messages = defineMessages({
},
selfAssess: {
id: 'frontend-app-ora.ProgressBar.selfAssess',
- defaultMessage: 'Grade yourself',
+ defaultMessage: 'Self grading',
description: 'Self assessment step progress indicator',
},
peerAssess: {
diff --git a/src/components/Prompt/__snapshots__/index.test.jsx.snap b/src/components/Prompt/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index 7c4dfd9a..00000000
--- a/src/components/Prompt/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` renders 1`] = `
-
- prompt",
- }
- }
- />
-
-`;
-
-exports[`
renders closed 1`] = `
-
- prompt",
- }
- }
- />
-
-`;
diff --git a/src/components/Prompt/index.jsx b/src/components/Prompt/index.jsx
index 4931cba4..b0316215 100644
--- a/src/components/Prompt/index.jsx
+++ b/src/components/Prompt/index.jsx
@@ -1,14 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@edx/paragon';
+import { useActiveStepName } from 'hooks/app';
+
+import messages from './messages';
import usePromptHooks from './hooks';
const Prompt = ({ prompt, defaultOpen }) => {
const { open, toggleOpen } = usePromptHooks({ defaultOpen });
+ const { formatMessage } = useIntl();
+ const message = messages[useActiveStepName()];
+ const title = message ? formatMessage(message) : '';
return (
-
+ {title})} open={open} onToggle={toggleOpen}>
);
diff --git a/src/components/Prompt/index.test.jsx b/src/components/Prompt/index.test.jsx
deleted file mode 100644
index 627e22f4..00000000
--- a/src/components/Prompt/index.test.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { shallow } from '@edx/react-unit-test-utils';
-import Prompt from '.';
-
-import usePromptHooks from './hooks';
-
-jest.mock('./hooks', () => jest.fn());
-
-describe(' ', () => {
- const props = {
- prompt: 'prompt
',
- };
- const mockHooks = (overrides) => {
- usePromptHooks.mockReturnValueOnce({
- open: true,
- toggleOpen: jest.fn().mockName('toggleOpen'),
- ...overrides,
- });
- };
- it('renders', () => {
- mockHooks();
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('Collapsible')[0].props.title).toEqual('');
- });
-
- it('renders closed', () => {
- mockHooks({ open: false });
- const wrapper = shallow( );
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('Collapsible')[0].props.title).not.toEqual('');
- });
-});
diff --git a/src/components/Prompt/messages.js b/src/components/Prompt/messages.js
new file mode 100644
index 00000000..e8025084
--- /dev/null
+++ b/src/components/Prompt/messages.js
@@ -0,0 +1,27 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+import { stepNames } from 'constants';
+
+const messages = defineMessages({
+ [stepNames.submission]: {
+ defaultMessage: 'Create a response to the prompt below',
+ description: 'Submission step prompt header',
+ id: 'frontend-app-ora.Prompt.header.submission',
+ },
+ [stepNames.studentTraining]: {
+ defaultMessage: 'Practice grading a response to the prompt below',
+ description: 'Practice step prompt header',
+ id: 'frontend-app-ora.Prompt.header.studentTraining',
+ },
+ [stepNames.self]: {
+ defaultMessage: 'Grade your own response to the prompt below',
+ description: 'Self step prompt header',
+ id: 'frontend-app-ora.Prompt.header.self',
+ },
+ [stepNames.peer]: {
+ defaultMessage: "Grade your peers' responses to the prompt below",
+ description: 'Peer step prompt header',
+ id: 'frontend-app-ora.Prompt.header.peer',
+ },
+});
+
+export default messages;
diff --git a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx
index bde9997c..730dcc6a 100644
--- a/src/components/Rubric/CriterionContainer/RadioCriterion.jsx
+++ b/src/components/Rubric/CriterionContainer/RadioCriterion.jsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
-import messages from './messages';
+import messages from '../messages';
/**
*
diff --git a/src/components/Rubric/index.jsx b/src/components/Rubric/index.jsx
index f58c8a9d..e7ebf4e4 100644
--- a/src/components/Rubric/index.jsx
+++ b/src/components/Rubric/index.jsx
@@ -22,8 +22,8 @@ export const Rubric = ({ isCollapsible }) => {
const Layout = isCollapsible ? Collapsible : Card;
return (
{formatMessage(messages.rubric)}}
- className="rubric-card"
+ title={{formatMessage(messages.header)} }
+ className="rubric-card my-3"
defaultOpen
>
@@ -44,11 +44,9 @@ export const Rubric = ({ isCollapsible }) => {
);
};
-
+Rubric.defaultProps = {};
Rubric.propTypes = {
- isCollapsible: PropTypes.bool,
+ isCollapsible: PropTypes.bool.isRequired,
};
-Rubric.defaultProps = {};
-
export default Rubric;
diff --git a/src/components/Rubric/messages.js b/src/components/Rubric/messages.js
index 64dd134f..ce64aef7 100644
--- a/src/components/Rubric/messages.js
+++ b/src/components/Rubric/messages.js
@@ -24,7 +24,7 @@ const messages = defineMessages({
overallComments: {
id: 'frontend-app-ora.Rubric.overallComments',
defaultMessage: 'Overall comments',
- description: 'Rubric overall commnents label',
+ description: 'Rubric overall comments label',
},
addComments: {
id: 'frontend-app-ora.Rubric.addComments',
@@ -41,6 +41,11 @@ const messages = defineMessages({
defaultMessage: 'The overall feedback is required',
description: 'Error message when feedback input is required',
},
+ header: {
+ id: 'frontend-app-ora.Rubric.header',
+ defaultMessage: 'Grading criteria',
+ description: 'Rubric header text',
+ },
});
export default messages;
diff --git a/src/components/Rubric/types.ts b/src/components/Rubric/types.ts
index 48bf3aa5..de70fd82 100644
--- a/src/components/Rubric/types.ts
+++ b/src/components/Rubric/types.ts
@@ -1,4 +1,4 @@
-import { CriterionConfig, MutationStatus, RubricData } from "data/services/lms/types";
+import { CriterionConfig, MutationStatus } from "data/services/lms/types";
export type Criterion = {
optionsValue: string | null;
@@ -11,8 +11,6 @@ export type Criterion = {
} & CriterionConfig;
export type RubricHookData = {
- rubricData: RubricData;
- setRubricData: (data: RubricData) => void;
criteria: Criterion[];
onSubmit: () => void;
submitStatus: MutationStatus;
diff --git a/src/components/StatusAlert/hooks/constants.js b/src/components/StatusAlert/hooks/constants.js
new file mode 100644
index 00000000..7dfb6ec8
--- /dev/null
+++ b/src/components/StatusAlert/hooks/constants.js
@@ -0,0 +1,23 @@
+import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons';
+
+import { stepStates } from 'constants';
+
+export const alertTypes = {
+ success: { variant: 'success', icon: CheckCircle },
+ danger: { variant: 'danger', icon: Info },
+ warning: { variant: 'warning', icon: WarningFilled },
+ light: { variant: 'light', icon: null },
+ dark: { variant: 'dark', icon: null },
+};
+
+export const alertMap = {
+ [stepStates.done]: alertTypes.success,
+ [stepStates.submitted]: alertTypes.success,
+ [stepStates.closed]: alertTypes.danger,
+ [stepStates.teamAlreadySubmitted]: alertTypes.warning,
+ [stepStates.needTeam]: alertTypes.warning,
+ [stepStates.waiting]: alertTypes.warning,
+ [stepStates.cancelled]: alertTypes.warning,
+ [stepStates.notAvailable]: alertTypes.light,
+ [stepStates.inProgress]: alertTypes.dark,
+};
diff --git a/src/components/StatusAlert/hooks/simpleAlerts.js b/src/components/StatusAlert/hooks/simpleAlerts.js
new file mode 100644
index 00000000..425bed6c
--- /dev/null
+++ b/src/components/StatusAlert/hooks/simpleAlerts.js
@@ -0,0 +1,47 @@
+import { useActiveStepName } from 'hooks/app';
+import { useExitAction } from 'hooks/actions';
+
+import { stepNames, stepStates } from 'constants';
+
+import useCreateAlert from './useCreateAlert';
+import messages from '../messages';
+
+export const useGradedAlerts = ({ step }) => ([
+ useCreateAlert({ step })({
+ message: messages.alerts.done.status,
+ heading: messages.headings.done.status,
+ }),
+]);
+
+export const useTrainingErrorAlerts = ({ step }) => ([
+ useCreateAlert({ step })({
+ message: messages.alerts.studentTraining[stepStates.trainingValidation],
+ variant: 'warning',
+ }),
+]);
+
+export const useStaffAlerts = ({ step }) => ([
+ useCreateAlert({ step })({
+ message: messages.alerts[stepNames.staff],
+ heading: messages.headings[stepNames.staff],
+ }),
+]);
+
+export const useCreateFinishedAlert = ({ step }) => {
+ const createAlert = useCreateAlert({ step });
+ return (target) => createAlert({
+ message: messages.alerts[target].finished,
+ heading: messages.headings[target].finished,
+ });
+};
+
+export const useCreateExitAlert = ({ step }) => {
+ const createAlert = useCreateAlert({ step });
+ const exitAction = useExitAction();
+ const activeStepName = useActiveStepName();
+ return (target) => createAlert({
+ message: messages.alerts[activeStepName][target],
+ heading: messages.headings[activeStepName][target],
+ actions: [exitAction],
+ });
+};
diff --git a/src/components/StatusAlert/hooks/useCancelledAlerts.js b/src/components/StatusAlert/hooks/useCancelledAlerts.js
new file mode 100644
index 00000000..5cc65b48
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useCancelledAlerts.js
@@ -0,0 +1,28 @@
+import messages from '../messages';
+import useCreateAlert from './useCreateAlert';
+
+const useCancelledAlerts = ({ step }) => {
+ const createAlert = useCreateAlert({ step });
+ return (cancellationInfo) => {
+ let out = [];
+ const {
+ hasCancelled,
+ cancelledBy,
+ cancelledAt,
+ } = cancellationInfo;
+ const alertMessages = messages.alerts.submission;
+ const headingMessages = messages.headings.submission;
+ if (hasCancelled) {
+ out = [
+ createAlert({
+ message: cancelledBy ? alertMessages.cancelledBy : alertMessages.cancelledAt,
+ heading: cancelledBy ? headingMessages.cancelledBy : headingMessages.cancelledAt,
+ messageVals: { cancelledAt, cancelledBy },
+ }),
+ ];
+ }
+ return { cancelledAlerts: out, hasCancelled };
+ };
+};
+
+export default useCancelledAlerts;
diff --git a/src/components/StatusAlert/hooks/useCreateAlert.js b/src/components/StatusAlert/hooks/useCreateAlert.js
new file mode 100644
index 00000000..36b48e3d
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useCreateAlert.js
@@ -0,0 +1,28 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { useStepState } from 'hooks/app';
+
+import { alertMap } from './constants';
+
+export const useAlertConfig = ({ step }) => alertMap[useStepState({ step })];
+
+const useCreateAlert = ({ step }) => {
+ const { formatMessage } = useIntl();
+ const alertConfig = useAlertConfig({ step });
+ return ({
+ heading,
+ message,
+ actions,
+ headingVals = {},
+ messageVals = {},
+ ...overrides
+ }) => ({
+ ...alertConfig,
+ message: formatMessage(message, messageVals),
+ heading: heading && formatMessage(heading, headingVals),
+ actions,
+ ...overrides,
+ });
+};
+
+export default useCreateAlert;
diff --git a/src/components/StatusAlert/hooks/useModalAlerts.js b/src/components/StatusAlert/hooks/useModalAlerts.js
new file mode 100644
index 00000000..9d2b7ddd
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useModalAlerts.js
@@ -0,0 +1,47 @@
+import { useViewStep } from 'hooks/routing';
+import { useGlobalState, useHasReceivedFinalGrade } from 'hooks/app';
+import { useHasSubmitted } from 'hooks/assessment';
+
+import { stepNames, stepStates } from 'constants';
+
+import { useTrainingErrorAlerts } from './simpleAlerts';
+
+import useRevisitAlerts from './useRevisitAlerts';
+import useSuccessAlerts from './useSuccessAlerts';
+
+const useModalAlerts = ({ step, showTrainingError }) => {
+ const { stepState } = useGlobalState({ step });
+ const isDone = useHasReceivedFinalGrade();
+ const viewStep = useViewStep();
+ const hasSubmitted = useHasSubmitted();
+ const { revisitAlerts, isRevisit } = useRevisitAlerts({ step });
+ const trainingErrorAlerts = useTrainingErrorAlerts({ step });
+ const successAlerts = useSuccessAlerts({ step });
+
+ // Do nothing if in xblock view
+ if (viewStep === stepNames.xblock) {
+ return [];
+ }
+
+ // No in-progress messages unless for submitted step
+ if (stepState === stepStates.inProgress && !hasSubmitted) {
+ return [];
+ }
+ // No modal alerts for graded state
+ if (isDone) {
+ return [];
+ }
+
+ if (isRevisit) {
+ return revisitAlerts;
+ }
+ if (showTrainingError) {
+ return trainingErrorAlerts;
+ }
+ if (hasSubmitted) {
+ return successAlerts;
+ }
+ return [];
+};
+
+export default useModalAlerts;
diff --git a/src/components/StatusAlert/hooks/useRevisitAlerts.js b/src/components/StatusAlert/hooks/useRevisitAlerts.js
new file mode 100644
index 00000000..8cdf8fdf
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useRevisitAlerts.js
@@ -0,0 +1,25 @@
+import { useViewStep } from 'hooks/routing';
+import { useGlobalState } from 'hooks/app';
+
+import { stepNames, stepStates } from 'constants';
+
+import { useCreateFinishedAlert } from './simpleAlerts';
+
+const useRevisitAlerts = ({ step }) => {
+ const { activeStepName, stepState } = useGlobalState({ step });
+ const viewStep = useViewStep();
+ const stepName = step || activeStepName;
+ const finishedAlert = useCreateFinishedAlert({ step });
+ const isRevisit = viewStep !== stepNames.xblock && stepName !== activeStepName;
+ let out = [];
+ if (isRevisit) {
+ if (stepName === stepNames.submission) {
+ out = [finishedAlert(stepNames.submission)];
+ } else if (stepName === stepNames.peer && stepState !== stepStates.waiting) {
+ out = [finishedAlert(stepNames.peer)];
+ }
+ }
+ return { revisitAlerts: out, isRevisit };
+};
+
+export default useRevisitAlerts;
diff --git a/src/components/StatusAlert/hooks/useStatusAlertData.js b/src/components/StatusAlert/hooks/useStatusAlertData.js
new file mode 100644
index 00000000..2d204964
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useStatusAlertData.js
@@ -0,0 +1,58 @@
+import { useViewStep } from 'hooks/routing';
+import { useGlobalState, useHasReceivedFinalGrade } from 'hooks/app';
+
+import { stepNames, stepStates } from 'constants';
+
+import messages from '../messages';
+import useCreateAlert from './useCreateAlert';
+import {
+ useGradedAlerts,
+ useStaffAlerts,
+} from './simpleAlerts';
+
+import useModalAlerts from './useModalAlerts';
+import useCancelledAlerts from './useCancelledAlerts';
+
+const useStatusAlertData = ({
+ step = null,
+ showTrainingError,
+}) => {
+ const {
+ activeStepName,
+ stepState,
+ } = useGlobalState({ step });
+ const isDone = useHasReceivedFinalGrade();
+ const viewStep = useViewStep();
+
+ const createAlert = useCreateAlert({ step });
+ const modalAlerts = useModalAlerts({ step, showTrainingError });
+ const gradedAlerts = useGradedAlerts({ step });
+ const { hasCancelled, cancelledAlerts } = useCancelledAlerts({ step });
+ const staffAlerts = useStaffAlerts({ step });
+
+ const stepName = step || activeStepName;
+
+ if (isDone) {
+ return gradedAlerts;
+ }
+ if (viewStep !== stepNames.xblock) {
+ return modalAlerts;
+ }
+ if (hasCancelled) {
+ return cancelledAlerts;
+ }
+
+ if (stepName === stepNames.staff) {
+ return staffAlerts;
+ }
+ if (stepState === stepStates.inProgress) {
+ return [];
+ }
+
+ return [createAlert({
+ message: messages.alerts[stepName][stepState],
+ heading: messages.headings[stepName][stepState],
+ })];
+};
+
+export default useStatusAlertData;
diff --git a/src/components/StatusAlert/hooks/useSuccessAlerts.js b/src/components/StatusAlert/hooks/useSuccessAlerts.js
new file mode 100644
index 00000000..d17fa5c9
--- /dev/null
+++ b/src/components/StatusAlert/hooks/useSuccessAlerts.js
@@ -0,0 +1,44 @@
+import { useViewStep } from 'hooks/routing';
+import { useGlobalState } from 'hooks/app';
+import { useHasSubmitted } from 'hooks/assessment';
+import { useStartStepAction } from 'hooks/actions';
+
+import { stepNames, stepStates } from 'constants';
+
+import messages from '../messages';
+import { alertTypes } from './constants';
+import useCreateAlert from './useCreateAlert';
+import { useCreateExitAlert } from './simpleAlerts';
+
+const useSuccessAlerts = ({ step }) => {
+ const { activeStepName, activeStepState } = useGlobalState({ step });
+ const viewStep = useViewStep();
+ const hasSubmitted = useHasSubmitted();
+ const startStepAction = useStartStepAction();
+ const exitAlert = useCreateExitAlert({ step });
+
+ const createAlert = useCreateAlert({ step });
+
+ const out = [];
+ if (hasSubmitted) {
+ const successAlert = {
+ message: messages.alerts[viewStep].submitted,
+ heading: messages.headings[viewStep].submitted,
+ ...alertTypes.success,
+ };
+ if (activeStepState === stepStates.inProgress && activeStepName !== viewStep) {
+ successAlert.actions = [startStepAction];
+ }
+ out.push(createAlert(successAlert));
+
+ if (activeStepState !== stepStates.inProgress) {
+ out.push(exitAlert(activeStepState));
+ }
+ if (activeStepName === stepNames.staff) {
+ out.push(exitAlert(stepNames.staff));
+ }
+ }
+ return out;
+};
+
+export default useSuccessAlerts;
diff --git a/src/components/StatusAlert/index.jsx b/src/components/StatusAlert/index.jsx
index 16fba768..bf4f42ea 100644
--- a/src/components/StatusAlert/index.jsx
+++ b/src/components/StatusAlert/index.jsx
@@ -2,8 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
-import { Alert } from '@edx/paragon';
-import useStatusAlertData from './useStatusAlertData';
+import { Alert, Skeleton } from '@edx/paragon';
+import ActionButton from 'components/ActionButton';
+import { useIsPageDataLoading } from 'hooks/app';
+import useStatusAlertData from './hooks/useStatusAlertData';
import './index.scss';
@@ -12,20 +14,31 @@ const StatusAlert = ({
step,
showTrainingError,
}) => {
+ const isPageDataLoading = useIsPageDataLoading();
const alerts = useStatusAlertData({ hasSubmitted, step, showTrainingError });
+ const customWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ if (isPageDataLoading) {
+ return ( );
+ }
+
return alerts.map(({
variant,
icon,
heading,
message,
- actions,
+ actions = [],
}) => (
)}
>
{heading}
{message}
diff --git a/src/components/StatusAlert/messages.js b/src/components/StatusAlert/messages.js
index 21cfffb5..5cf1eefe 100644
--- a/src/components/StatusAlert/messages.js
+++ b/src/components/StatusAlert/messages.js
@@ -2,11 +2,6 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
import { stepNames, stepStates } from 'constants';
const submissionAlerts = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.submission.inProgress',
- defaultMessage: "This assignment has several steps. In the first step you'll provide a response to the prompt.",
- description: 'Submission in-progress status alert',
- },
[stepStates.submitted]: {
id: 'frontend-app-ora.StatusAlert.submission.submitted',
defaultMessage: 'Your response has been submitted. You will receive your grade after all steps are complete and your response is fully assessed.',
@@ -15,7 +10,7 @@ const submissionAlerts = defineMessages({
[stepStates.notAvailable]: {
id: 'frontend-app-ora.StatusAlert.submission.notAvailable',
defaultMessage: 'This task is not available yet. Check back to complete the assignment once this section has opened',
- description: 'Submission not avilable status alert',
+ description: 'Submission not available status alert',
},
[stepStates.cancelled]: {
id: 'frontend-app-ora.StatusAlert.submission.cancelled',
@@ -44,11 +39,6 @@ const submissionAlerts = defineMessages({
},
});
const submissionHeadings = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.Heading.submission.inProgress',
- defaultMessage: 'Submission In Progress',
- description: 'Submission in-progress status alert heading',
- },
[stepStates.submitted]: {
id: 'frontend-app-ora.StatusAlert.Heading.submission.submitted',
defaultMessage: 'Submission Completed',
@@ -57,7 +47,7 @@ const submissionHeadings = defineMessages({
[stepStates.notAvailable]: {
id: 'frontend-app-ora.StatusAlert.Heading.submission.notAvailable',
defaultMessage: 'Submission Not Available',
- description: 'Submission not avilable status alert heading',
+ description: 'Submission not available status alert heading',
},
[stepStates.cancelled]: {
id: 'frontend-app-ora.StatusAlert.Heading.submission.cancelled',
@@ -87,11 +77,6 @@ const submissionHeadings = defineMessages({
});
const studentTrainingAlerts = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.studentTraining.inProgress',
- defaultMessage: 'This assignment is in progress. Complete the learner training step to move on.',
- description: 'Student Training in progress status alert',
- },
[stepStates.submitted]: {
id: 'frontend-app-ora.StatusAlert.studentTraining.submitted',
defaultMessage: 'You have completed this practice grading example. Continue to the next example, or if you have completed all examples, continue to the next step.',
@@ -104,11 +89,6 @@ const studentTrainingAlerts = defineMessages({
},
});
const studentTrainingHeadings = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.inProgress',
- defaultMessage: 'Student Training: In Progress',
- description: 'Student Training in progress status alert heading',
- },
[stepStates.submitted]: {
id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.submitted',
defaultMessage: 'Student Training: Submitted',
@@ -117,14 +97,9 @@ const studentTrainingHeadings = defineMessages({
});
const selfAlerts = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.self.inProgress',
- defaultMessage: 'This assignment is in progress. You still need to complete the self assessment step.',
- description: 'Student Training in progress status alert',
- },
[stepStates.closed]: {
id: 'frontend-app-ora.StatusAlert.self.closed',
- defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this asseignment, and you will receive a grade of inccomplete',
+ defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this assignment, and you will receive a grade of incomplete',
description: 'Student Training closed status alert',
},
[stepStates.submitted]: {
@@ -139,11 +114,6 @@ const selfHeadings = defineMessages({
defaultMessage: 'Self Assessment Completed',
description: 'Self Assessment submitted status alert heading',
},
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.Heading.self.inProgress',
- defaultMessage: 'Self Assessment In Progress',
- description: 'Student Training in progress status alert heading',
- },
[stepStates.closed]: {
id: 'frontend-app-ora.StatusAlert.Heading.self.closed',
defaultMessage: 'Self Assessment: Closed',
@@ -152,11 +122,6 @@ const selfHeadings = defineMessages({
});
const peerAlerts = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.peer.inProgress',
- defaultMessage: 'This assignment is in progress. You still need to complete the peer assessment step.',
- description: 'Peer Assessment closed status alert',
- },
[stepStates.waiting]: {
id: 'frontend-app-ora.StatusAlert.peer.waiting',
defaultMessage: 'All submitted responses have been assessed. Check back later to see if more learners have submitted responses.',
@@ -179,16 +144,11 @@ const peerAlerts = defineMessages({
},
[stepStates.submitted]: {
id: 'frontend-app-ora.StatusAlert.peer.submitted',
- defaultMessage: 'Continue to submite peer assessments until you have completed the required number.',
+ defaultMessage: 'Continue to submit peer assessments until you have completed the required number.',
description: 'Peer Assessment submitted status alert',
},
});
const peerHeadings = defineMessages({
- [stepStates.inProgress]: {
- id: 'frontend-app-ora.StatusAlert.Heading.peer.inProgress',
- defaultMessage: 'Peer Assessment In Progress',
- description: 'Peer Assessment closed status alert heading',
- },
[stepStates.waiting]: {
id: 'frontend-app-ora.StatusAlert.Heading.peer.waiting',
defaultMessage: 'Waiting for peers to submit',
@@ -207,10 +167,10 @@ const peerHeadings = defineMessages({
[stepStates.notAvailable]: {
id: 'frontend-app-ora.StatusAlert.Heading.peer.notAvailable',
defaultMessage: 'Peer Assessment not available',
- description: 'Peer Assessment not avilable status alert heading',
+ description: 'Peer Assessment not available status alert heading',
},
[stepStates.submitted]: {
- id: 'frontend-app-ora.StatusAlert.Heading.studentTraining.submitted',
+ id: 'frontend-app-ora.StatusAlert.Heading.peer.submitted',
defaultMessage: 'Peer Assessment Successfully Submitted',
description: 'Peer Assessment submitted status alert',
},
@@ -226,7 +186,7 @@ const doneAlerts = defineMessages({
const doneHeadings = defineMessages({
status: {
id: 'frontend-app-ora.StatusAlert.Heading.done',
- defaultMessage: 'Assignment Ccomplete and Graded',
+ defaultMessage: 'Assignment Complete and Graded',
description: 'Done status alert heading',
},
});
@@ -260,7 +220,7 @@ export default {
[stepNames.self]: selfHeadings,
[stepNames.peer]: peerHeadings,
[stepNames.done]: doneHeadings,
- [stepNames.staff]: staffHeadings,
+ [stepNames.staff]: staffHeadings.staffAssessment,
},
alerts: {
[stepNames.submission]: submissionAlerts,
@@ -268,7 +228,7 @@ export default {
[stepNames.self]: selfAlerts,
[stepNames.peer]: peerAlerts,
[stepNames.done]: doneAlerts,
- [stepNames.staff]: staffAlerts,
+ [stepNames.staff]: staffAlerts.staffAssessment,
},
...messages,
};
diff --git a/src/components/StatusAlert/useStatusAlertData.jsx b/src/components/StatusAlert/useStatusAlertData.jsx
deleted file mode 100644
index 36e726ae..00000000
--- a/src/components/StatusAlert/useStatusAlertData.jsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { useIntl } from '@edx/frontend-platform/i18n';
-import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons';
-import { Button } from '@edx/paragon';
-
-import { useCloseModal } from 'hooks/modal';
-import { useViewStep } from 'hooks/routing';
-import {
- useHasReceivedFinalGrade,
- useGlobalState,
-} from 'hooks/app';
-import {
- useHasSubmitted,
-} from 'hooks/assessment';
-
-import {
- stepNames,
- stepStates,
-} from 'constants';
-
-import messages from './messages';
-
-const alertTypes = {
- success: { variant: 'success', icon: CheckCircle },
- danger: { variant: 'danger', icon: Info },
- warning: { variant: 'warning', icon: WarningFilled },
- light: {
- variant: 'light',
- icon: null,
- },
- dark: {
- variant: 'dark',
- icon: null,
- },
-};
-
-export const alertMap = {
- [stepStates.done]: alertTypes.success,
- [stepStates.submitted]: alertTypes.success,
- [stepStates.closed]: alertTypes.danger,
- [stepStates.teamAlreadySubmitted]: alertTypes.warning,
- [stepStates.needTeam]: alertTypes.warning,
- [stepStates.waiting]: alertTypes.warning,
- [stepStates.cancelled]: alertTypes.warning,
- [stepStates.inProgress]: alertTypes.dark,
- [stepStates.notAvailable]: alertTypes.light,
-};
-
-const useStatusAlertData = ({
- step = null,
- showTrainingError,
-}) => {
- const { formatMessage } = useIntl();
- const {
- activeStepName,
- activeStepState,
- cancellationInfo,
- stepState,
- } = useGlobalState({ step });
- const closeModal = useCloseModal();
- const isDone = useHasReceivedFinalGrade();
- const viewStep = useViewStep();
- const hasSubmitted = useHasSubmitted();
-
- const stepName = step || activeStepName;
- const isRevisit = stepName !== activeStepName;
-
- const { variant, icon } = alertMap[stepState];
-
- const alertConfig = ({
- heading,
- message,
- actions,
- headingVals = {},
- messageVals = {},
- ...overrides
- }) => ({
- variant,
- icon,
- message: formatMessage(message, messageVals),
- heading: heading && formatMessage(heading, headingVals),
- actions,
- ...overrides,
- });
-
- if (isDone) {
- return [alertConfig({
- message: messages.alerts.done.status,
- heading: messages.headings.done.status,
- })];
- }
-
- if (viewStep !== stepNames.xblock) {
- if (showTrainingError) {
- return [alertConfig({
- message: messages.alerts.studentTraining[stepStates.trainingValidation],
- variant: 'warning',
- })];
- }
- const out = [];
- if (hasSubmitted) {
- out.push(alertConfig({
- message: messages.alerts[viewStep].submitted,
- heading: messages.headings[viewStep].submitted,
- ...alertTypes.success,
- }));
- if (activeStepState !== stepStates.inProgress) {
- out.push(alertConfig({
- message: messages.alerts[activeStepName][activeStepState],
- heading: messages.headings[activeStepName][activeStepState],
- actions: [
- {formatMessage(messages.exit)} ,
- ],
- }));
- }
- if (activeStepName === stepNames.staff) {
- out.push(alertConfig({
- message: messages.alerts[activeStepName].staffAssessment,
- heading: messages.headings[activeStepName].staffAssessment,
- actions: [
- {formatMessage(messages.exit)} ,
- ],
- }));
- }
- return out;
- }
- }
- if (cancellationInfo.hasCancelled) {
- const { cancelledBy, cancelledAt } = cancellationInfo;
- if (cancelledBy) {
- return [alertConfig({
- message: messages.alerts.submission.cancelledBy,
- messageVals: { cancelledBy, cancelledAt },
- heading: messages.headings.submission.cancelledBy,
- })];
- }
- return [alertConfig({
- message: messages.alerts.submission.cancelledAt,
- messageVals: { cancelledAt },
- heading: messages.headings.submission.cancelledAt,
- })];
- }
- if (stepName === stepNames.submission && isRevisit) {
- return [alertConfig({
- message: messages.alerts.submission.finished,
- heading: messages.headings.submission.finished,
- })];
- }
- if (stepName === stepNames.peer && isRevisit && stepState !== stepStates.waiting) {
- return [alertConfig({
- message: messages.alerts.peer.finished,
- heading: messages.headings.peer.finished,
- })];
- }
- if (stepName === stepNames.staff) {
- return [alertConfig({
- message: messages.alerts[activeStepName].staffAssessment,
- heading: messages.headings[activeStepName].staffAssessment,
- })];
- }
- return [alertConfig({
- message: messages.alerts[stepName][stepState],
- heading: messages.headings[stepName][stepState],
- })];
-};
-
-export default useStatusAlertData;
diff --git a/src/components/StepProgressIndicator/index.jsx b/src/components/StepProgressIndicator/index.jsx
index d20a55b4..a60be0b8 100644
--- a/src/components/StepProgressIndicator/index.jsx
+++ b/src/components/StepProgressIndicator/index.jsx
@@ -1,19 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { StatefulButton } from '@edx/paragon';
+import { Skeleton, StatefulButton } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useLoadNextAction } from 'hooks/actions';
import { stepNames } from 'constants';
-import { useViewStep } from 'hooks/routing';
import {
useAssessmentStepConfig,
useGlobalState,
useStepInfo,
useHasSubmitted,
+ useIsPageDataLoading,
} from 'hooks/app';
import messages from './messages';
@@ -27,6 +27,20 @@ const StepProgressIndicator = ({ step }) => {
const hasSubmitted = useHasSubmitted();
const { activeStepName } = globalState;
const loadNextAction = useLoadNextAction();
+
+ const isPageDataLoading = useIsPageDataLoading();
+ const className = 'step-progress-indicator';
+
+ const customWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ if (isPageDataLoading) {
+ return (
);
+ }
+
if (![stepNames.peer, stepNames.studentTraining].includes(step)) {
return null;
}
@@ -37,12 +51,12 @@ const StepProgressIndicator = ({ step }) => {
const done = activeStepName === step
? stepInfo[step].numberOfAssessmentsCompleted
: needed;
- const showAction = hasSubmitted && (
- (step === stepNames.peer && !stepInfo[step].isWaitingForSubmissions)
- || (needed !== done)
- );
+ const showAction = hasSubmitted
+ && !(step === stepNames.peer && stepInfo[step].isWaitingForSubmissions)
+ && (needed !== done);
+
return (
-
+
{formatMessage(messages.progress, { needed, done })}
{showAction && (
diff --git a/src/components/TextResponse/index.jsx b/src/components/TextResponse/index.jsx
index ecee0ed6..3b86d1c6 100644
--- a/src/components/TextResponse/index.jsx
+++ b/src/components/TextResponse/index.jsx
@@ -1,13 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { useSubmissionConfig } from 'hooks/app';
+
import './index.scss';
-const TextResponse = ({ response }) => (
-
-);
+const TextResponse = ({ response }) => {
+ const { textResponseConfig } = useSubmissionConfig();
+
+ return (
+
+ {textResponseConfig.editorType === 'text' ? (
+
{response}
+ ) : (
+
+ )}
+
+ );
+};
TextResponse.propTypes = {
response: PropTypes.string.isRequired,
diff --git a/src/components/TextResponse/index.scss b/src/components/TextResponse/index.scss
index b4477cfd..25fd713c 100644
--- a/src/components/TextResponse/index.scss
+++ b/src/components/TextResponse/index.scss
@@ -1,5 +1,9 @@
-.textarea-response {
- min-height: 200px;
- max-height: 300px;
- overflow-y: scroll;
+.pre-like-textarea {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ word-break: break-all;
+ overflow-wrap: break-word;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
}
\ No newline at end of file
diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js
deleted file mode 100644
index 3bbec275..00000000
--- a/src/data/redux/app/reducer.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-
-import { StrictDict } from 'utils';
-
-const initialState = {
- assessment: {
- submittedAssessment: null,
- showTrainingError: false,
- },
- response: null,
- formFields: {
- criteria: [],
- overallFeedback: '',
- },
- hasSubmitted: false,
- showValidation: false,
-
- testDirty: false,
- testProgressKey: null,
- testDataPath: undefined,
-};
-
-// eslint-disable-next-line no-unused-vars
-const app = createSlice({
- name: 'app',
- initialState,
- reducers: {
- loadAssessment: (state, { payload }) => ({
- ...state,
- assessment: { ...initialState.assessment, submittedAssessment: payload.data },
- }),
- loadResponse: (state, { payload }) => ({ ...state, response: payload }),
- setHasSubmitted: (state, { payload }) => ({
- ...state,
- hasSubmitted: payload,
- testDirty: payload, // test
- }),
- setShowValidation: (state, { payload }) => ({ ...state, showValidation: payload }),
- setShowTrainingError: (state, { payload }) => ({
- ...state,
- assessment: { ...state.assessment, showTrainingError: payload },
- }),
- resetAssessment: (state) => ({
- ...state,
- formFields: initialState.formFields,
- assessment: initialState.assessment,
- hasSubmitted: false,
- showValidation: false,
- }),
- setFormFields: (state, { payload }) => ({
- ...state,
- formFields: { ...state.formFields, ...payload },
- }),
- setCriterionOption: (state, { payload }) => {
- const { criterionIndex, option } = payload;
- // eslint-disable-next-line
- state.formFields.criteria[criterionIndex].selectedOption = option;
- return state;
- },
- setCriterionFeedback: (state, { payload }) => {
- const { criterionIndex, feedback } = payload;
- // eslint-disable-next-line
- state.formFields.criteria[criterionIndex].feedback = feedback;
- return state;
- },
- setOverallFeedback: (state, { payload }) => {
- // eslint-disable-next-line
- state.formFields.overallFeedback = payload;
- return state;
- },
- setTestProgressKey: (state, { payload }) => ({
- ...state,
- testProgressKey: payload,
- testDirty: false,
- }),
- setTestDataPath: (state, { payload }) => ({ ...state, testDataPath: payload }),
- },
-});
-
-const actions = StrictDict(app.actions);
-
-const { reducer } = app;
-
-export {
- actions,
- initialState,
- reducer,
-};
diff --git a/src/data/redux/app/reducer.test.ts b/src/data/redux/app/reducer.test.ts
new file mode 100644
index 00000000..c604d8ed
--- /dev/null
+++ b/src/data/redux/app/reducer.test.ts
@@ -0,0 +1,163 @@
+import { initialState, actions, reducer } from './reducer';
+
+const testState = {
+ assessment: {
+ submittedAssessment: {
+ criteria: [
+ { selectedOption: 1, feedback: 'test-criterion-feedback1' },
+ { selectedOption: 2, feedback: 'test-criterion-feedback2' },
+ ],
+ overallFeedback: 'test-overall-feedback',
+ },
+ showTrainingError: true,
+ },
+ response: ['test-response'],
+ tempResponse: ['test-temp-response'],
+ formFields: {
+ criteria: [
+ { selectedOption: 1, feedback: 'test-formFields-criterion-feedback1' },
+ { selectedOption: 3, feedback: 'test-formFields-criterion-feedback2' },
+ ],
+ overallFeedback: 'formFields-overall-feedback',
+ },
+ hasSubmitted: true,
+ showValidation: true,
+ testDirty: true,
+ testProgressKey: 'test-progress-key',
+ testDataPath: 'test-data-path',
+};
+
+const testValue = 'test-value';
+describe('app reducer', () => {
+ it('returns initial state', () => {
+ expect(reducer(undefined, { type: undefined })).toEqual(initialState);
+ });
+ describe('actions', () => {
+ const testAction = (action, expected) => {
+ expect(reducer(testState, action)).toEqual({ ...testState, ...expected });
+ };
+ describe('loadAssessment', () => {
+ it('overrides assessment.submittedAssessment with action data', () => {
+ testAction(
+ actions.loadAssessment({ data: testValue }),
+ { assessment: { ...initialState.assessment, submittedAssessment: testValue } },
+ );
+ });
+ });
+ describe('loadResponse', () => {
+ it('overrides response if not submitted', () => {
+ const action = actions.loadResponse(testValue);
+ expect(reducer(
+ { ...testState, hasSubmitted: false },
+ action,
+ )).toEqual({ ...testState, hasSubmitted: false, response: testValue });
+ });
+ it('overrides tempResponse if submitted', () => {
+ testAction(actions.loadResponse(testValue), { tempResponse: testValue });
+ });
+ });
+ describe('setHasSubmitted', () => {
+ it('overrides hasSubmitted and testDirty', () => {
+ testAction(
+ actions.setHasSubmitted(testValue),
+ { hasSubmitted: testValue, testDirty: testValue },
+ );
+ });
+ });
+ describe('setShowValidation', () => {
+ it('overrides showValidation', () => {
+ testAction(actions.setShowValidation(testValue), { showValidation: testValue });
+ });
+ });
+ describe('setShowTrainingError', () => {
+ it('overrides assessment.showTrainingError', () => {
+ testAction(
+ actions.setShowTrainingError(testValue),
+ { assessment: { ...testState.assessment, showTrainingError: testValue } },
+ );
+ });
+ });
+ describe('resetAssessment', () => {
+ it('resets formFields, assessment, showValidation, and hasSubmitted', () => {
+ testAction(
+ actions.resetAssessment(),
+ {
+ assessment: initialState.assessment,
+ formFields: initialState.formFields,
+ hasSubmitted: initialState.hasSubmitted,
+ response: testState.tempResponse,
+ showValidation: initialState.showValidation,
+ tempResponse: initialState.tempResponse,
+ },
+ );
+ });
+ });
+ describe('setFormFields', () => {
+ it('partially overrides formFields', () => {
+ testAction(
+ actions.setFormFields({ overallFeedback: testValue }),
+ { formFields: { ...testState.formFields, overallFeedback: testValue } },
+ );
+ const testCriteria = [{ selectedOption: 1, feedback: 'test-feedback' }];
+ testAction(
+ actions.setFormFields({ criteria: testCriteria }),
+ { formFields: { ...testState.formFields, criteria: testCriteria } },
+ );
+ });
+ });
+ describe('setCriterionOption', () => {
+ it('overrides the selectedOption for the criterion with the given index', () => {
+ const criterionIndex = 1;
+ const option = 23;
+ const expectedCriteria = [...testState.formFields.criteria];
+ expectedCriteria[criterionIndex] = {
+ ...expectedCriteria[criterionIndex],
+ selectedOption: option,
+ };
+ testAction(
+ actions.setCriterionOption({ criterionIndex, option }),
+ { formFields: { ...testState.formFields, criteria: expectedCriteria } },
+ );
+ });
+ });
+ describe('setCriterionFeedback', () => {
+ it('overrides the feedback for the criterion with the given index', () => {
+ const criterionIndex = 1;
+ const feedback = 'expected-feedback';
+ const expectedCriteria = [...testState.formFields.criteria];
+ expectedCriteria[criterionIndex] = {
+ ...expectedCriteria[criterionIndex],
+ feedback,
+ };
+ testAction(
+ actions.setCriterionFeedback({ criterionIndex, feedback }),
+ { formFields: { ...testState.formFields, criteria: expectedCriteria } },
+ );
+ });
+ });
+ describe('setOverallFeedback', () => {
+ it('overrides formFields.overallFeedback', () => {
+ testAction(
+ actions.setOverallFeedback(testValue),
+ { formFields: { ...testState.formFields, overallFeedback: testValue } },
+ );
+ });
+ });
+ describe('setTestProgressKey', () => {
+ it('overrides formFields.overallFeedback', () => {
+ testAction(
+ actions.setTestProgressKey(testValue),
+ { testProgressKey: testValue, testDirty: false },
+ );
+ });
+ });
+ describe('setTestDataPath', () => {
+ it('overrides testDataPath', () => {
+ testAction(
+ actions.setTestDataPath(testValue),
+ { testDataPath: testValue },
+ );
+ });
+ });
+ });
+});
diff --git a/src/data/redux/app/reducer.ts b/src/data/redux/app/reducer.ts
new file mode 100644
index 00000000..46b3ec5f
--- /dev/null
+++ b/src/data/redux/app/reducer.ts
@@ -0,0 +1,112 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+import { StrictDict } from '@edx/react-unit-test-utils';
+
+import * as types from './types';
+
+const initialState = {
+ assessment: {
+ submittedAssessment: null,
+ showTrainingError: false,
+ },
+ response: null,
+ tempResponse: null,
+ formFields: {
+ criteria: [],
+ overallFeedback: '',
+ },
+ hasSubmitted: false,
+ showValidation: false,
+
+ testDirty: false,
+ testProgressKey: null,
+ testDataPath: undefined,
+};
+
+// eslint-disable-next-line no-unused-vars
+const app = createSlice({
+ name: 'app',
+ initialState: initialState as types.AppState,
+ reducers: {
+ loadAssessment: (state: types.AppState, action: PayloadAction
) => ({
+ ...state,
+ assessment: { ...initialState.assessment, submittedAssessment: action.payload.data },
+ }),
+ loadResponse: (state: types.AppState, action: PayloadAction) => (
+ state.hasSubmitted
+ ? { ...state, tempResponse: action.payload }
+ : { ...state, response: action.payload }
+ ),
+ setHasSubmitted: (state: types.AppState, action: PayloadAction) => {
+ const out = {
+ ...state,
+ hasSubmitted: action.payload,
+ testDirty: action.payload, // test
+ };
+ if (!action.payload) {
+ out.response = out.tempResponse;
+ }
+ return out;
+ },
+ setShowValidation: (state: types.AppState, action: PayloadAction) => ({
+ ...state,
+ showValidation: action.payload,
+ }),
+ setShowTrainingError: (state: types.AppState, action: PayloadAction) => ({
+ ...state,
+ assessment: { ...state.assessment, showTrainingError: action.payload },
+ }),
+ resetAssessment: (state: types.AppState) => ({
+ ...state,
+ formFields: initialState.formFields,
+ assessment: initialState.assessment,
+ hasSubmitted: initialState.hasSubmitted,
+ showValidation: initialState.showValidation,
+ response: state.tempResponse,
+ tempResponse: initialState.tempResponse,
+ }),
+ setFormFields: (state: types.AppState, action: PayloadAction) => ({
+ ...state,
+ formFields: { ...state.formFields, ...action.payload },
+ }),
+ setCriterionOption: (state: types.AppState, action: PayloadAction) => {
+ const { criterionIndex, option } = action.payload;
+ // eslint-disable-next-line
+ state.formFields.criteria[criterionIndex].selectedOption = option;
+ return state;
+ },
+ setCriterionFeedback: (
+ state: types.AppState,
+ action: PayloadAction,
+ ) => {
+ const { criterionIndex, feedback } = action.payload;
+ // eslint-disable-next-line
+ state.formFields.criteria[criterionIndex].feedback = feedback;
+ return state;
+ },
+ setOverallFeedback: (state: types.AppState, action: PayloadAction) => {
+ // eslint-disable-next-line
+ state.formFields.overallFeedback = action.payload;
+ return state;
+ },
+ setTestProgressKey: (state: types.AppState, action: PayloadAction) => ({
+ ...state,
+ testProgressKey: action.payload,
+ testDirty: false,
+ }),
+ setTestDataPath: (state: types.AppState, action: PayloadAction) => ({
+ ...state,
+ testDataPath: action.payload,
+ }),
+ },
+});
+
+const actions = StrictDict(app.actions);
+
+const { reducer } = app;
+
+export {
+ actions,
+ initialState,
+ reducer,
+};
diff --git a/src/data/redux/app/selectors.test.ts b/src/data/redux/app/selectors.test.ts
new file mode 100644
index 00000000..400cd3fe
--- /dev/null
+++ b/src/data/redux/app/selectors.test.ts
@@ -0,0 +1,138 @@
+import { keyStore } from '@edx/react-unit-test-utils';
+
+import selectors from './selectors';
+
+const testState = {
+ app: {
+ assessment: {
+ submittedAssessment: {
+ criteria: [
+ { selectedOption: 1, feedback: 'test-criterion-feedback1' },
+ { selectedOption: 2, feedback: 'test-criterion-feedback2' },
+ ],
+ overallFeedback: 'test-overall-feedback',
+ },
+ showTrainingError: true,
+ },
+ response: ['test-response'],
+ formFields: {
+ criteria: [
+ { selectedOption: 1, feedback: 'test-formFields-criterion-feedback1' },
+ { selectedOption: 3, feedback: 'test-formFields-criterion-feedback2' },
+ ],
+ overallFeedback: 'formFields-overall-feedback',
+ },
+ hasSubmitted: true,
+ showValidation: true,
+ testDirty: true,
+ testProgressKey: 'test-progress-key',
+ testDataPath: 'test-data-path',
+ },
+};
+
+let selector;
+const appKeys = keyStore(testState.app);
+describe('redux app selectors', () => {
+ const testSimpleAppSelector = (key) => {
+ selector = selectors[key];
+ expect(selector.dependencies).toEqual([selectors.root]);
+ expect(selector.resultFunc(testState.app)).toEqual(testState.app[key]);
+ };
+ describe('root', () => {
+ it('returns app object from top-level state', () => {
+ expect(selectors.root(testState)).toEqual(testState.app);
+ });
+ });
+ describe('assessment', () => {
+ it('returns assessment object from root selector', () => {
+ testSimpleAppSelector(appKeys.assessment);
+ });
+ });
+ describe('formFields', () => {
+ it('returns formFields object from root selector', () => {
+ testSimpleAppSelector(appKeys.formFields);
+ });
+ });
+ describe('criterionFeedback', () => {
+ beforeEach(() => {
+ selector = selectors.criterionFeedback;
+ });
+ it('returns criterion feedback if it exists', () => {
+ expect(selector(testState, 0))
+ .toEqual(testState.app.formFields.criteria[0].feedback);
+ });
+ it('returns undefined if criterion does not exist', () => {
+ expect(selector(testState, 4)).toEqual(undefined);
+ });
+ });
+ describe('criterionOption', () => {
+ beforeEach(() => {
+ selector = selectors.criterionOption;
+ });
+ it('returns criterion option if it exists', () => {
+ expect(selector(testState, 0))
+ .toEqual(testState.app.formFields.criteria[0].selectedOption);
+ });
+ it('returns undefined if criterion does not exist', () => {
+ expect(selector(testState, 4)).toEqual(undefined);
+ });
+ });
+ describe('hasSubmitted', () => {
+ it('returns hasSubmitted from root selector', () => {
+ testSimpleAppSelector(appKeys.hasSubmitted);
+ });
+ });
+ describe('overallFeedback', () => {
+ it('returns overallFeedback from formFields', () => {
+ selector = selectors.overallFeedback;
+ expect(selector(testState)).toEqual(testState.app.formFields.overallFeedback);
+ });
+ });
+ describe('response', () => {
+ it('returns the response from the root selector', () => {
+ testSimpleAppSelector(appKeys.response);
+ });
+ });
+ describe('showTrainingError', () => {
+ beforeEach(() => {
+ selector = selectors.showTrainingError;
+ });
+ it('is memoized based on the assessment', () => {
+ expect(selector.dependencies).toEqual([selectors.assessment]);
+ });
+ it('returns showTrainingError from the assessment if there is one', () => {
+ expect(selector.resultFunc(testState.app.assessment)).toEqual(testState.app.assessment.showTrainingError);
+ });
+ it('returns undefined if there is no assessment', () => {
+ expect(selector.resultFunc(null)).toEqual(undefined);
+ });
+ });
+ describe('showValidation', () => {
+ it('returns showValidation from the root selector', () => {
+ testSimpleAppSelector(appKeys.showValidation);
+ });
+ });
+ describe('submittedAssessment', () => {
+ it('returns submittedAssessment from assessment selector', () => {
+ selector = selectors.submittedAssessment;
+ expect(selector.dependencies).toEqual([selectors.assessment]);
+ expect(selector.resultFunc(testState.app.assessment))
+ .toEqual(testState.app.assessment.submittedAssessment);
+ });
+ });
+ describe('testDataPath', () => {
+ it('returns testDataPath from root selector', () => {
+ testSimpleAppSelector(appKeys.testDataPath);
+ });
+ });
+ describe('testDirty', () => {
+ it('returns testDirty from root selector', () => {
+ testSimpleAppSelector(appKeys.testDirty);
+ });
+ });
+ describe('testProgressKey', () => {
+ it('returns testProgressKey from root selector', () => {
+ testSimpleAppSelector(appKeys.testProgressKey);
+ });
+ });
+});
diff --git a/src/data/redux/app/selectors.ts b/src/data/redux/app/selectors.ts
new file mode 100644
index 00000000..1528e67f
--- /dev/null
+++ b/src/data/redux/app/selectors.ts
@@ -0,0 +1,68 @@
+import { createSelector } from '@reduxjs/toolkit';
+import * as types from './types';
+
+type RootState = { app: types.AppState };
+
+const selectors: { [k: string]: any } = {
+ root: (state: RootState): types.AppState => state.app,
+};
+selectors.assessment = createSelector(
+ selectors.root,
+ (app: types.AppState): types.Assessment => app.assessment,
+);
+selectors.formFields = createSelector(
+ selectors.root,
+ (app: types.AppState): types.FormFields => app.formFields,
+);
+selectors.criterionFeedback = (state: RootState, criterionIndex: number): string | undefined => (
+ selectors.formFields(state).criteria[criterionIndex]?.feedback
+);
+
+selectors.criterionOption = (state: RootState, criterionIndex: number): number | undefined => (
+ selectors.formFields(state).criteria[criterionIndex]?.selectedOption
+);
+
+selectors.overallFeedback = (state: RootState): string => (
+ selectors.formFields(state).overallFeedback
+);
+
+selectors.hasSubmitted = createSelector(
+ selectors.root,
+ (app: types.AppState) => app.hasSubmitted,
+);
+
+selectors.response = createSelector(
+ selectors.root,
+ (app: types.AppState): types.Response => app.response,
+);
+
+selectors.showTrainingError = createSelector(
+ selectors.assessment,
+ (assessment: types.Assessment): boolean => assessment?.showTrainingError,
+);
+
+selectors.showValidation = createSelector(
+ selectors.root,
+ (app: types.AppState) => app.showValidation,
+);
+
+selectors.submittedAssessment = createSelector(
+ selectors.assessment,
+ (assessment: types.Assessment) => assessment.submittedAssessment,
+);
+
+// test
+selectors.testDataPath = createSelector(
+ selectors.root,
+ (app: types.AppState): string | null | undefined => app.testDataPath,
+);
+selectors.testDirty = createSelector(
+ selectors.root,
+ (app: types.AppState): boolean => app.testDirty,
+);
+selectors.testProgressKey = createSelector(
+ selectors.root,
+ (app: types.AppState): string | null | undefined => app.testProgressKey,
+);
+
+export default selectors;
diff --git a/src/data/redux/app/selectors/index.js b/src/data/redux/app/selectors/index.js
deleted file mode 100644
index 69c4493c..00000000
--- a/src/data/redux/app/selectors/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-
-const rootSelector = ({ app }) => app;
-const assessmentData = createSelector(rootSelector, ({ assessment }) => assessment);
-const formFieldsData = createSelector(rootSelector, ({ formFields }) => formFields);
-const selectors = {
- criterionFeedback: (state, criterionIndex) => (
- formFieldsData(state).criteria[criterionIndex]?.feedback
- ),
-
- criterionOption: (state, criterionIndex) => (
- formFieldsData(state).criteria[criterionIndex]?.selectedOption
- ),
-
- response: createSelector(rootSelector, ({ response }) => response),
-
- hasSubmitted: createSelector(rootSelector, ({ hasSubmitted }) => hasSubmitted),
-
- overallFeedback: (state) => formFieldsData(state).overallFeedback,
-
- showTrainingError: createSelector(assessmentData, (assessment) => assessment?.showTrainingError),
-
- showValidation: createSelector(rootSelector, ({ showValidation }) => showValidation),
-
- submittedAssessment:
- createSelector(assessmentData, ({ submittedAssessment }) => submittedAssessment),
-
- // test
- testProgressKey: createSelector(rootSelector, ({ testProgressKey }) => testProgressKey),
- testDirty: createSelector(rootSelector, ({ testDirty }) => testDirty),
- testDataPath: createSelector(rootSelector, ({ testDataPath }) => testDataPath),
-};
-
-export default {
- assessment: assessmentData,
- formFields: formFieldsData,
- ...selectors,
-};
diff --git a/src/data/redux/app/types.ts b/src/data/redux/app/types.ts
new file mode 100644
index 00000000..c22ca840
--- /dev/null
+++ b/src/data/redux/app/types.ts
@@ -0,0 +1,38 @@
+import * as apiTypes from 'data/services/lms/types';
+
+export interface Assessment {
+ submittedAssessment: apiTypes.AssessmentData | null,
+ showTrainingError: boolean,
+}
+
+export interface Criterion {
+ feedback?: string,
+ selectedOption?: number,
+}
+
+export interface FormFields {
+ criteria: Criterion[],
+ overallFeedback: string,
+}
+
+export interface AssessmentAction { data: apiTypes.AssessmentData }
+
+export type Response = string[] | null;
+
+export interface CriterionAction {
+ criterionIndex: number,
+ option?: number,
+ feedback?: string,
+}
+
+export interface AppState {
+ assessment: Assessment,
+ response: Response,
+ tempResponse: Response,
+ formFields: FormFields,
+ hasSubmitted: boolean,
+ showValidation: boolean,
+ testDirty: boolean,
+ testProgressKey?: string | null,
+ testDataPath?: string | null,
+}
diff --git a/src/data/services/lms/api.test.ts b/src/data/services/lms/api.test.ts
new file mode 100644
index 00000000..600b8bfb
--- /dev/null
+++ b/src/data/services/lms/api.test.ts
@@ -0,0 +1,194 @@
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { when } from 'jest-when';
+
+import * as api from './api';
+import * as urls from './urls';
+
+jest.mock('@edx/frontend-platform/auth', () => ({
+ getAuthenticatedHttpClient: jest.fn(),
+}));
+jest.mock('./urls', () => ({
+ useSubmitAssessmentUrl: jest.fn(),
+ useSubmitUrl: jest.fn(),
+ useSaveDraftUrl: jest.fn(),
+ useAddFileUrl: jest.fn(),
+ useUploadResponseUrl: jest.fn(),
+ useDeleteFileUrl: jest.fn(),
+}));
+
+const testUrls = {
+ submitAssessment: 'test-submit-assessment-url',
+ submit: 'test-submit-url',
+ saveDraft: 'test-save-draft-url',
+ addFile: 'test-add-file-url',
+ uploadResponse: 'test-upload-response-url',
+ deleteFile: 'test-delete-file-url',
+};
+when(urls.useSubmitAssessmentUrl).calledWith().mockReturnValue(testUrls.submitAssessment);
+when(urls.useSubmitUrl).calledWith().mockReturnValue(testUrls.submit);
+when(urls.useSaveDraftUrl).calledWith().mockReturnValue(testUrls.saveDraft);
+when(urls.useAddFileUrl).calledWith().mockReturnValue(testUrls.addFile);
+when(urls.useUploadResponseUrl).calledWith().mockReturnValue(testUrls.uploadResponse);
+when(urls.useDeleteFileUrl).calledWith().mockReturnValue(testUrls.deleteFile);
+
+const authClient = { post: jest.fn((...args) => ({ post: args })) };
+when(getAuthenticatedHttpClient).calledWith().mockReturnValue(authClient);
+
+const testData = 'test-data';
+global.fetch = jest.fn();
+
+let hook;
+describe('lms api', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ const testLoadsClient = () => {
+ it('loads authenticated http client', () => {
+ expect(getAuthenticatedHttpClient).toHaveBeenCalledWith();
+ });
+ };
+ describe('useSubmitAssessment', () => {
+ beforeEach(() => {
+ hook = api.useSubmitAssessment();
+ });
+ describe('behavior', () => {
+ it('loads url from hook', () => {
+ expect(urls.useSubmitAssessmentUrl).toHaveBeenCalledWith();
+ });
+ testLoadsClient();
+ });
+ describe('output', () => {
+ it('returns a method that posts data to submitAssessment url', () => {
+ expect(hook(testData)).toEqual(
+ authClient.post(testUrls.submitAssessment, testData),
+ );
+ });
+ });
+ });
+ describe('useSubmitResponse', () => {
+ beforeEach(() => {
+ hook = api.useSubmitResponse();
+ });
+ describe('behavior', () => {
+ it('loads url from hook', () => {
+ expect(urls.useSubmitUrl).toHaveBeenCalledWith();
+ });
+ testLoadsClient();
+ });
+ describe('output', () => {
+ it('returns a method that posts data as submission to submit url', () => {
+ expect(hook(testData)).toEqual(
+ authClient.post(testUrls.submit, { submission: testData }),
+ );
+ });
+ });
+ });
+ describe('useSaveDraft', () => {
+ beforeEach(() => {
+ hook = api.useSaveDraft();
+ });
+ describe('behavior', () => {
+ it('loads url from hook', () => {
+ expect(urls.useSaveDraftUrl).toHaveBeenCalledWith();
+ });
+ testLoadsClient();
+ });
+ describe('output', () => {
+ it('returns a method that posts data as submission to submit url', () => {
+ expect(hook(testData)).toEqual(
+ authClient.post(testUrls.saveDraft, { response: testData }),
+ );
+ });
+ });
+ });
+ describe('encode', () => {
+ it('escapes invalid characters', () => {
+ const testString = '()*^`' + "'"; // eslint-disable-line no-useless-concat
+ expect(api.encode(testString)).toEqual('%28%29%2A^`%27');
+ });
+ });
+ describe('fileHeader', () => {
+ it('returns object with content disposition', () => {
+ const encodedName = 'encoded-name';
+ const encode = jest.spyOn(api, 'encode');
+ when(encode).calledWith(testData).mockReturnValue(encodedName);
+ const keys = api.uploadKeys;
+ expect(api.fileHeader(testData)).toEqual({
+ [keys.contentDisposition]: `${keys.attachmentPrefix}${encodedName}`,
+ });
+ });
+ });
+ describe('uploadFile', () => {
+ it('PUTS data to fileUrl with fileHeaders for file name', () => {
+ });
+ });
+ describe('useAddFile', () => {
+ const fileData = {
+ name: 'test-name',
+ size: 'test-size',
+ type: 'test-type',
+ };
+ const description = 'test-description';
+ const file = {
+ fileDescription: description,
+ fileName: fileData.name,
+ fileSize: fileData.size,
+ contentType: fileData.type,
+ };
+ const fileIndex = 23;
+ const fileUrl = 'test-file-url';
+ const downloadUrl = 'test-download-url';
+ const addFileResponse = { data: { fileIndex, fileUrl } };
+ const uploadFile = jest.spyOn(api, 'uploadFile');
+ beforeEach(() => {
+ when(authClient.post)
+ .calledWith(testUrls.addFile, expect.anything())
+ .mockResolvedValue(addFileResponse);
+ when(authClient.post)
+ .calledWith(fileUrl, expect.anything())
+ .mockResolvedValue();
+ when(authClient.post)
+ .calledWith(testUrls.uploadResponse, expect.anything())
+ .mockResolvedValue({ data: { downloadUrl } });
+ when(uploadFile)
+ .calledWith(fileData, fileUrl)
+ .mockResolvedValue();
+ hook = api.useAddFile();
+ });
+ describe('behavior', () => {
+ it('loads url from hook', () => {
+ expect(urls.useAddFileUrl).toHaveBeenCalledWith();
+ expect(urls.useUploadResponseUrl).toHaveBeenCalledWith();
+ });
+ testLoadsClient();
+ });
+ describe('output', () => {
+ it('returns callback that takes file and description, adds and uploads file', async () => {
+ await expect(hook(fileData, description)).resolves.toStrictEqual({
+ ...file,
+ fileIndex,
+ fileUrl: downloadUrl,
+ });
+ });
+ });
+ });
+ describe('useDeleteFile', () => {
+ beforeEach(() => {
+ hook = api.useDeleteFile();
+ });
+ describe('behavior', () => {
+ it('loads url from hook', () => {
+ expect(urls.useDeleteFileUrl).toHaveBeenCalledWith();
+ });
+ testLoadsClient();
+ });
+ describe('output', () => {
+ it('returns a method that posts data as submission to submit url', () => {
+ const fileIndex = 3;
+ expect(hook(fileIndex)).toEqual(
+ authClient.post(testUrls.deleteFile, { fileIndex }),
+ );
+ });
+ });
+ });
+});
diff --git a/src/data/services/lms/api.ts b/src/data/services/lms/api.ts
index 860e8dec..28fa7fac 100644
--- a/src/data/services/lms/api.ts
+++ b/src/data/services/lms/api.ts
@@ -1,87 +1,79 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-// import { queryKeys } from './constants';
-import { AssessmentData } from './types';
+import * as types from './types';
import * as urls from './urls';
export const useSubmitAssessment = () => {
const url = urls.useSubmitAssessmentUrl();
- return (data: AssessmentData) => {
+ const client = getAuthenticatedHttpClient();
+ return (data: types.AssessmentData) => {
console.log({ submitAssessment: data });
- return getAuthenticatedHttpClient().post(url, data);
+ return client.post(url, data);
};
};
export const useSubmitResponse = () => {
const url = urls.useSubmitUrl();
- return (data: any) => {
+ const client = getAuthenticatedHttpClient();
+ return (data: types.ResponseData) => {
console.log({ submitResponse: data });
- return getAuthenticatedHttpClient().post(url, { submission: data });
+ return client.post(url, { submission: data });
};
};
export const useSaveDraft = () => {
const url = urls.useSaveDraftUrl();
- return (data: any) => {
- console.log({ save: data });
- return getAuthenticatedHttpClient().post(url, { response: data });
- };
+ const client = getAuthenticatedHttpClient();
+ return (data: string[]) => client.post(url, { response: data });
+};
+
+export const encode = (str) => encodeURIComponent(str)
+ // Note that although RFC3986 reserves "!", RFC5987 does not,
+ // so we do not need to escape it
+ .replace(/['()]/g, escape) // i.e., %27 %28 %29
+ .replace(/\*/g, '%2A')
+ // The following are not required for percent-encoding per RFC5987,
+ // so we can allow for a little better readability over the wire: |`^
+ .replace(/%(?:7C|60|5E)/g, unescape);
+
+export const uploadKeys = {
+ contentDisposition: 'Content-Disposition',
+ attachmentPrefix: 'attachment; filename*=UTF-8\'\'',
};
+export const fileHeader = (name) => ({
+ [uploadKeys.contentDisposition]: `${uploadKeys.attachmentPrefix}${encode(name)}`,
+});
+export const uploadFile = (data, fileUrl) => fetch(
+ fileUrl,
+ {
+ method: 'PUT',
+ body: data,
+ headers: fileHeader(data.name),
+ },
+);
export const useAddFile = () => {
const url = urls.useAddFileUrl();
+ const client = getAuthenticatedHttpClient();
const responseUrl = urls.useUploadResponseUrl();
- return (data: any, description: string) => {
- const { post } = getAuthenticatedHttpClient();
+ return (data: Blob, description: string) => {
const file = {
fileDescription: description,
fileName: data.name,
fileSize: data.size,
contentType: data.type,
};
- console.log({ addFile: { data, description, file } });
- return post(url, file)
- .then(response => post(responseUrl, { fileIndex: response.data.fileIndex, success: true }))
+
+ return client.post(url, file).then(response => {
+ const { fileIndex, fileUrl } = response.data;
+ return uploadFile(data, fileUrl)
+ .then(() => client.post(responseUrl, { fileIndex, success: true }))
+ .then(({ data }) => ({ ...file, fileIndex, fileUrl: data.downloadUrl }));
+ });
};
};
export const useDeleteFile = () => {
const url = urls.useDeleteFileUrl();
- return (fileIndex) => {
- return getAuthenticatedHttpClient().post(url, { fileIndex });
- };
-};
-
-export const fakeProgress = async (requestConfig) => {
- 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 });
- }
-};
-
-export const uploadFiles = (data: any) => {
- const { fileData, requestConfig } = data;
- console.log({ uploadFiles: { fileData, requestConfig } });
- // TODO: upload files
- /*
- * const files = fileData.getAll('file');
- * const addFileResponse = await post(`{xblock_id}/handler/file/add`, file);
- * const uploadResponse = await(post(response.fileUrl, file));
- * post(`${xblock_id}/handler/download_url', (response));
- */
- return fakeProgress(data.requestConfig).then(() => {
- Promise.resolve();
- });
-};
-
-export const deleteFile = (fileIndex) => {
- console.log({ deleteFile: fileIndex });
- return new Promise((resolve) => {
- setTimeout(() => {
- console.log('deleted file');
- resolve(null);
- }, 1000);
- });
+ const client = getAuthenticatedHttpClient();
+ return (fileIndex: number) => client.post(url, { fileIndex });
};
diff --git a/src/data/services/lms/dataLoaders.js b/src/data/services/lms/dataLoaders.js
deleted file mode 100644
index 1d623ecc..00000000
--- a/src/data/services/lms/dataLoaders.js
+++ /dev/null
@@ -1,127 +0,0 @@
-// ORA Config loaders
-export const loadAssessmentConfig = ({
- assessmentSteps: {
- order,
- settings: {
- peer,
- self,
- studentTraining,
- staff,
- },
- },
-}) => ({
- order,
- peer: peer && {
- startTime: peer.startTime,
- endTime: peer.endTime,
- required: peer.required,
- data: {
- minNumberToGrade: peer.data.minNumberToGrade,
- minNumberToBeGradedBy: peer.data.minNumberToBeGradedBy,
- enableFlexibleGrading: peer.data.enableFlexibleGrading,
- },
- },
- self: self && {
- startTime: self.startTime,
- endTime: self.endTime,
- required: self.required,
- },
- studentTraining: studentTraining && {
- required: studentTraining.required,
- data: { examples: studentTraining.data.examples },
- },
- staff: staff && { required: staff.required },
-});
-
-export const loadSubmissionConfig = ({
- submissionConfig: {
- textResponseConfig: text,
- fileResponseConfig: file,
- teamsConfig,
- ...config
- },
-}) => ({
- startDatetime: config.startDatetime,
- endDatetime: config.endDatetime,
- textResponseConfig: text && {
- enabled: text.enabled,
- optional: text.optional,
- editorType: text.editorType,
- allowLatexPreview: text.allowLatexPreview,
- },
- fileResponseConfig: file && {
- enabled: file.enabled,
- optional: file.optional,
- fileUploadType: file.fileUploadType,
- allowedExtensions: file.allowedExtensions,
- blockedExtensions: file.blockedExtensions,
- fileTypeDescription: file.fileTypeDescription,
- },
- teamsConfig: teamsConfig && {
- enabled: teamsConfig.enabled,
- teamsetName: teamsConfig.teamsetName,
- },
-});
-
-export const loadRubricConfig = ({ rubric }) => ({
- showDuringResponse: rubric.showDuringResponse,
- feedbackConfig: {
- description: rubric.feedbackConfig.description,
- defaultText: rubric.feedbackConfig.defaultText,
- },
- criteria: rubric.criteria.map(criterion => ({
- name: criterion.name,
- description: criterion.description,
- feedbackEnabled: criterion.feedbackEnabled,
- feedbackRequired: criterion.feedbackRequired,
- options: criterion.options.map(option => ({
- name: option.name,
- points: option.points,
- description: option.description,
- })),
- })),
-});
-
-export const loadORAConfigData = (data) => ({
- title: data.title,
- prompts: data.prompts,
- baseAssetUrl: data.baseAssetUrl,
- submissionConfig: loadSubmissionConfig(data),
- assessmentSteps: loadAssessmentConfig(data),
- rubric: loadRubricConfig(data),
- leaderboardConfig: {
- enabled: data.leaderboardConfig.enabled,
- numberOfEntries: data.leaderboardConfig.numberOfEntries,
- },
-});
-
-// Submission loaders
-export const loadFile = (file) => {
- console.log({ loadFile: file });
- return {
- url: file.fileUrl,
- description: file.fileDescription,
- name: file.fileName,
- size: file.fileSize,
- uploadedBy: file.uploadedBy,
- };
-};
-
-export const loadSubmissionData = ({ teamInfo, submissionStatus, submission }) => ({
- teamInfo: {
- teamName: teamInfo.teamName,
- teamUsernames: teamInfo.teamUsernames,
- previousTeamName: teamInfo.previousTeamName,
- hasSubmitted: teamInfo.hasSubmitted,
- uploadedFiles: teamInfo.teamUploadedFiles.map(loadFile),
- },
- submissionStatus: {
- hasSubmitted: submissionStatus.hasSubmitted,
- hasCancelled: submissionStatus.hasCancelled,
- hasReceivedGrade: submissionStatus.hasReceivedGrade,
- },
- submission: {
- textResponses: submission.textResponses,
- uploadedFiles: submission.uploadedFiles.map(loadFile),
- },
-});
diff --git a/src/data/services/lms/fakeData/dataStates.js b/src/data/services/lms/fakeData/dataStates.js
index 087fe27f..59cf8e6d 100644
--- a/src/data/services/lms/fakeData/dataStates.js
+++ b/src/data/services/lms/fakeData/dataStates.js
@@ -1,12 +1,8 @@
-import { StrictDict } from '@edx/react-unit-test-utils';
-
-import { routeSteps } from 'constants';
+import { routeSteps } from 'constants/index';
import {
defaultViewProgressKeys,
- progressKeys,
stepConfigs,
teamStates,
- viewKeys,
stateStepConfigs,
} from 'constants/mockData';
@@ -28,3 +24,5 @@ export const loadState = (opts) => {
};
return state;
};
+
+export default { loadState };
diff --git a/src/data/services/lms/fakeData/pageData/assessments.js b/src/data/services/lms/fakeData/pageData/assessments.js
index c2e03e7b..0f1d30dc 100644
--- a/src/data/services/lms/fakeData/pageData/assessments.js
+++ b/src/data/services/lms/fakeData/pageData/assessments.js
@@ -13,7 +13,7 @@ const gradedState = createAssessmentState({
criteria: new Array(4).fill(0).map((_, i) => ({
feedback: `feedback ${i + 1}`,
// random 0-3
- selectedOption: Math.floor(Math.random() * 4)
+ selectedOption: Math.floor(Math.random() * 4),
})),
overall_feedback: 'nice job',
});
diff --git a/src/data/services/lms/fakeData/pageData/progress.js b/src/data/services/lms/fakeData/pageData/progress.js
index 763a2fa0..11992e55 100644
--- a/src/data/services/lms/fakeData/pageData/progress.js
+++ b/src/data/services/lms/fakeData/pageData/progress.js
@@ -1,6 +1,6 @@
import { StrictDict } from '@edx/react-unit-test-utils';
-import { stepNames } from 'constants';
+import { stepNames } from 'constants/index';
import { closedStates, progressKeys } from 'constants/mockData';
import { assessmentSteps } from '../oraConfig';
/* eslint-disable camelcase */
@@ -87,7 +87,6 @@ const peerStatuses = StrictDict({
numReceived: assessmentSteps.settings.peer.min_number_to_be_graded_by,
}),
});
-console.log({ peerStatuses });
export const createTrainingStepInfo = ({
closedState = closedStates.open,
@@ -103,7 +102,6 @@ export const createTrainingStepInfo = ({
},
});
-console.log({ assessmentSteps });
const trainingStatuses = {
unsubmitted: createTrainingStepInfo(),
partial: createTrainingStepInfo({ numCompleted: 1 }),
@@ -120,12 +118,6 @@ const finishedStates = StrictDict({
[stepNames.peer]: peerStatuses.finished,
});
-const staffStates = {
- afterSubmission: { step: stepNames.staff },
- afterSelf: { step: stepNames.staff, self: closedStates.open },
- afterPeer: { step: stepNames.staff },
-};
-
const nullStepInfo = { student_training: null, self: null, peer: null };
export const getProgressState = ({ viewStep, progressKey, stepConfig }) => {
@@ -235,7 +227,7 @@ export const getProgressState = ({ viewStep, progressKey, stepConfig }) => {
[progressKeys.graded]:
createProgressData(stepConfig[stepConfig.length - 1], { isGraded: true }),
[progressKeys.gradedSubmittedOnPreviousTeam]:
- createProgressData(stepConfig[stepConfig.length - 1], { isGrdaed: true }),
+ createProgressData(stepConfig[stepConfig.length - 1], { isGraded: true }),
});
return mapping[progressKey];
};
diff --git a/src/data/services/lms/fakeData/pageData/response.js b/src/data/services/lms/fakeData/pageData/response.js
index a3e76cdf..049e71c7 100644
--- a/src/data/services/lms/fakeData/pageData/response.js
+++ b/src/data/services/lms/fakeData/pageData/response.js
@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import { StrictDict } from '@edx/react-unit-test-utils';
-import { closedStates, progressKeys } from 'constants/mockData';
+import { progressKeys } from 'constants/mockData';
const files = [
{
diff --git a/src/data/services/lms/hooks/actions/files.ts b/src/data/services/lms/hooks/actions/files.ts
index 307194eb..27bc6311 100644
--- a/src/data/services/lms/hooks/actions/files.ts
+++ b/src/data/services/lms/hooks/actions/files.ts
@@ -1,30 +1,14 @@
import * as zip from '@zip.js/zip.js';
import FileSaver from 'file-saver';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform';
-import { useMutation } from '@tanstack/react-query';
-
-import { queryKeys } from 'constants';
+import { useQueryClient, useMutation } from '@tanstack/react-query';
+import { queryKeys } from 'constants/index';
import * as api from 'data/services/lms/api';
-import { useTestDataPath } from 'hooks/test';
-
-import fakeData from '../../fakeData';
-import { UploadedFile } from '../../types';
-
-import { useCreateMutationAction } from './utils';
+import { PageData, UploadedFile } from '../../types';
-export const fakeProgress = async (requestConfig) => {
- 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 });
- }
-};
-
-export const DownloadException = (errors: string[]) => ({
- errors,
- name: 'DownloadException',
-});
+export const DownloadException = (errors: string[]) => new Error(
+ `DownloadException: ${errors.join(', ')}`
+);
export const FetchSubmissionFilesException = () => ({
name: 'FetchSubmissionFilesException',
@@ -33,38 +17,35 @@ export const FetchSubmissionFilesException = () => ({
/**
* Generate a manifest file content based on files object
*/
-export const genManifest = (files: UploadedFile[]) =>
- files
- .map(
- (file, i) =>
- `Filename: ${i}-${file.fileName}\nDescription: ${file.fileDescription}\nSize: ${file.fileSize}`
- )
- .join('\n\n');
+export const manifestString = ({ fileName, fileDescription, fileSize }, index) => (
+ `Filename: ${index}-${fileName}\nDescription: ${fileDescription}\nSize: ${fileSize}`
+);
+export const genManifest = (files: UploadedFile[]) => files.map(manifestString).join('\n\n');
/**
* Zip the blob output of a set of files with a manifest file.
*/
+export const zipSubFileName = ({ fileName }, index) => `${index}-${fileName}`;
export const zipFiles = async (
files: UploadedFile[],
blobs: Blob[],
- zipFileName: string
+ zipFileName: string,
) => {
const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));
await zipWriter.add('manifest.txt', new zip.TextReader(genManifest(files)));
// forEach or map will create additional thread. It is less readable if we create more
// promise or async function just to circumvent that.
+ const promises: Promise[] = [];
for (let i = 0; i < blobs.length; i++) {
- // eslint-disable-next-line no-await-in-loop
- await zipWriter.add(
- `${i}-${files[i].fileName}`,
- new zip.BlobReader(blobs[i]),
- {
- bufferedWrite: true,
- }
- );
+ const blob = new zip.BlobReader(blobs[i]);
+ promises.push(zipWriter.add(
+ zipSubFileName(files[i], i),
+ blob,
+ { bufferedWrite: true },
+ ));
}
-
+ await Promise.all(promises);
const zipFile = await zipWriter.close();
const zipName = `${zipFileName}.zip`;
FileSaver.saveAs(zipFile, zipName);
@@ -73,15 +54,14 @@ export const zipFiles = async (
/**
* Download a file and return its blob is successful, or null if not.
*/
-export const downloadFile = (file: UploadedFile) =>
- fetch(file.fileUrl).then((response) => {
- if (!response.ok) {
- // This is necessary because some of the error such as 404 does not throw.
- // Due to that inconsistency, I have decide to share catch statement like this.
- throw new Error(response.statusText);
- }
- return response.blob();
- });
+export const downloadFile = (file: UploadedFile) => fetch(file.fileUrl).then((response) => {
+ if (!response.ok) {
+ // This is necessary because some of the error such as 404 does not throw.
+ // Due to that inconsistency, I have decide to share catch statement like this.
+ throw new Error(response.statusText);
+ }
+ return response.blob();
+});
/**
* Download blobs given file objects. Returns a promise map.
@@ -90,62 +70,76 @@ export const downloadBlobs = async (files: UploadedFile[]) => {
const blobs: Blob[] = [];
const errors: string[] = [];
+ const promises: Promise[] = [];
// eslint-disable-next-line no-restricted-syntax
for (const file of files) {
try {
- // eslint-disable-next-line no-await-in-loop
- blobs.push(await downloadFile(file));
+ promises.push(downloadFile(file).then(blobs.push));
} catch (error) {
errors.push(file.fileName);
}
}
+ await Promise.all(promises);
if (errors.length) {
throw DownloadException(errors);
}
return { blobs, files };
};
+export const transforms: ({ [k: string]: any }) = {
+ loadResponse: (oldData, response) => ({ ...oldData, response }),
+};
+transforms.loadFiles = (oldData, uploadedFiles) => transforms.loadResponse(
+ oldData,
+ { ...oldData.response, uploadedFiles },
+);
+transforms.deleteFile = (oldData, index) => {
+ const { uploadedFiles } = oldData.response;
+ return uploadedFiles
+ ? transforms.loadFiles(oldData, uploadedFiles.filter(f => f.fileIndex !== index))
+ : oldData;
+};
+transforms.addFile = (oldData, addedFile) => {
+ const { uploadedFiles } = oldData.response;
+ return uploadedFiles
+ ? transforms.loadFiles(oldData, [...uploadedFiles, addedFile])
+ : oldData;
+};
+
export const useUploadFiles = () => {
- const testDataPath = useTestDataPath();
const addFile = api.useAddFile();
+ const queryClient = useQueryClient();
const apiFn = (data) => {
- const { fileData, requestConfig, description } = data;
+ const { fileData, description } = data;
const file = fileData.getAll('file')[0];
- console.log({ file });
- return addFile(file, description);
- };
- const mockFn = (data, description) => {
- const { fileData, requestConfig } = data;
- return fakeProgress(requestConfig);
+ return addFile(file, description).then(addedFile => {
+ queryClient.setQueryData(
+ [queryKeys.pageData],
+ (oldData: PageData) => transforms.addFile(oldData, addedFile),
+ );
+ });
};
- return useMutation({
- mutationFn: testDataPath ? mockFn : apiFn,
- });
+ return useMutation({ mutationFn: apiFn });
};
export const useDeleteFile = () => {
- const testDataPath = useTestDataPath();
const deleteFile = api.useDeleteFile();
+ const queryClient = useQueryClient();
const apiFn = (index) => {
console.log({ deleteFile: index });
- return deleteFile(index);
+ return deleteFile(index).then(() => {
+ queryClient.setQueryData(
+ [queryKeys.pageData],
+ (oldData: PageData) => transforms.deleteFile(oldData, index),
+ );
+ });
};
- const mockFn = (data) => Promise.resolve(data);
- return useMutation({
- mutationFn: testDataPath ? mockFn : apiFn,
- });
+ return useMutation({ mutationFn: apiFn });
};
-export const useDownloadFiles = () =>
- useCreateMutationAction(
- async ({
- files,
- zipFileName,
- }: {
- files: UploadedFile[];
- zipFileName: string;
- }) => {
- const { blobs } = await downloadBlobs(files);
- return zipFiles(files, blobs, zipFileName);
- }
- );
+export const useDownloadFiles = () => useMutation({
+ mutationFn: async ({ files, zipFileName }: { files: UploadedFile[], zipFileName: string }) => {
+ const { blobs } = await downloadBlobs(files);
+ return zipFiles(files, blobs, zipFileName);
+ },
+});
diff --git a/src/data/services/lms/hooks/actions/index.ts b/src/data/services/lms/hooks/actions/index.ts
index 4ab01fe9..86fb1dd0 100644
--- a/src/data/services/lms/hooks/actions/index.ts
+++ b/src/data/services/lms/hooks/actions/index.ts
@@ -9,7 +9,7 @@ import { progressKeys } from 'constants/mockData';
import * as api from 'data/services/lms/api';
// import { AssessmentData } from 'data/services/lms/types';
import { loadState } from 'data/services/lms/fakeData/dataStates';
-import { useTestDataPath } from 'hooks/test';
+import { useTestDataPath } from 'hooks/testHooks';
import { useViewStep } from 'hooks/routing';
diff --git a/src/data/services/lms/hooks/actions/utils.test.ts b/src/data/services/lms/hooks/actions/utils.test.ts
index f7909423..c61ca755 100644
--- a/src/data/services/lms/hooks/actions/utils.test.ts
+++ b/src/data/services/lms/hooks/actions/utils.test.ts
@@ -19,11 +19,11 @@ describe.skip('actions', () => {
describe('createMutationAction', () => {
it('returns a mutation function', () => {
- const aribtraryMutationFn = jest.fn();
- const mutation = useCreateMutationAction(aribtraryMutationFn) as any;
+ const arbitraryMutationFn = jest.fn();
+ const mutation = useCreateMutationAction(arbitraryMutationFn) as any;
mutation.mutate('foo', 'bar');
- expect(aribtraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient);
+ expect(arbitraryMutationFn).toHaveBeenCalledWith('foo', 'bar', queryClient);
});
});
});
diff --git a/src/data/services/lms/hooks/data.test.ts b/src/data/services/lms/hooks/data.test.ts
index c6bed049..1655af6e 100644
--- a/src/data/services/lms/hooks/data.test.ts
+++ b/src/data/services/lms/hooks/data.test.ts
@@ -1,144 +1,158 @@
-import { useQuery } from '@tanstack/react-query';
-import { useMatch } from 'react-router-dom';
-import { camelCaseObject } from '@edx/frontend-platform';
import { when } from 'jest-when';
+import { useQuery } from '@tanstack/react-query';
+import { useViewStep } from 'hooks/routing';
+import { useTestDataPath } from 'hooks/testHooks';
-import routes from 'routes';
-import * as types from '../types';
-import { queryKeys } from '../constants';
+import { queryKeys } from 'constants/index';
import fakeData from '../fakeData';
+import { loadState } from '../fakeData/dataStates';
+import { useORAConfigUrl, usePageDataUrl } from '../urls';
+import { loadData, logPageData, post } from './utils';
+import { useMockORAConfig, useMockPageData } from './mockData';
import { useORAConfig, usePageData } from './data';
jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn() }));
+jest.mock('hooks/routing', () => ({ useViewStep: jest.fn() }));
+jest.mock('hooks/testHooks', () => ({
+ useTestDataPath: jest.fn(() => null),
+}));
+jest.mock('./utils', () => ({
+ loadData: jest.fn(),
+ logPageData: jest.fn(),
+ post: jest.fn(),
+}));
+jest.mock('../urls', () => ({
+ useORAConfigUrl: jest.fn(),
+ usePageDataUrl: jest.fn(),
+}));
+jest.mock('./mockData', () => ({
+ useMockORAConfig: jest.fn(),
+ useMockPageData: jest.fn(),
+}));
-jest.mock('react-router-dom', () => ({ useMatch: jest.fn() }));
+when(useQuery)
+ .calledWith(expect.anything())
+ .mockImplementation(args => ({ useQuery: args }));
-interface QueryFn { (): string }
-interface QueryArgs { queryKey: string, queryFn: QueryFn }
+const viewStep = 'view-step';
+when(useViewStep).calledWith().mockReturnValue(viewStep);
+const oraConfigUrl = 'test-url';
+when(useORAConfigUrl).calledWith().mockReturnValue(oraConfigUrl);
+const pageDataUrl = (step) => ({ pageDataUrl: step });
+when(usePageDataUrl).calledWith().mockReturnValue(pageDataUrl);
+const mockORAConfig = fakeData.oraConfig.assessmentTinyMCE;
+when(useMockORAConfig).calledWith().mockReturnValue(mockORAConfig);
+when(loadData).calledWith(expect.anything()).mockImplementation(
+ data => ({ loadData: data }),
+);
+when(logPageData).calledWith(expect.anything()).mockImplementation(data => data);
+const postObj = (url, data) => ({ data: { post: { url, data } } });
+when(post).calledWith(expect.anything(), expect.anything())
+ .mockImplementation((url, data) => Promise.resolve(postObj(url, data)));
+const mockPageData = loadState({ view: 'submission', progressKey: 'submission_saved' });
+when(useMockPageData).calledWith().mockReturnValue(mockPageData);
-interface MockORAQuery extends QueryArgs { data: types.ORAConfig }
-interface MockUseORAQuery { (QueryArgs): MockORAQuery }
-interface MockORAUseConfigHook { (): MockORAQuery }
+const testDataPath = 'test-data-path';
-interface MockPageDataQuery extends QueryArgs { data: types.PageData }
-interface MockUsePageDataQuery { (QueryArgs): MockPageDataQuery }
-interface MockPageDataUseConfigHook { (): MockPageDataQuery }
-
-let out;
-describe.skip('lms data hooks', () => {
+let hook;
+describe('lms service top-level data hooks', () => {
describe('useORAConfig', () => {
- const mockUseQuery = (hasData: boolean): MockUseORAQuery => ({ queryKey, queryFn }) => ({
- data: hasData ? camelCaseObject(fakeData.oraConfig.assessmentText) : undefined,
- queryKey,
- queryFn,
- });
-
- const mockUseQueryForORA = (hasData) => {
- when(useQuery)
- .calledWith(expect.objectContaining({ queryKey: [queryKeys.oraConfig] }))
- .mockImplementationOnce(mockUseQuery(hasData));
- };
-
- const testUseORAConfig = useORAConfig as unknown as MockORAUseConfigHook;
-
- beforeEach(() => {
- mockUseQueryForORA(true);
- out = testUseORAConfig();
- });
- it('initializes query with oraConfig queryKey', () => {
- expect(out.queryKey).toEqual([queryKeys.oraConfig]);
- });
- it('initializes query with promise pointing to assessment text', async () => {
- const old = window.location;
- Object.defineProperty(window, 'location', {
- value: new URL('http://dummy.com/text'),
- writable: true,
+ describe('behavior', () => {
+ beforeEach(() => {
+ hook = useORAConfig();
+ });
+ it('loads url from hook', () => {
+ expect(useORAConfigUrl).toHaveBeenCalledWith();
+ });
+ it('loads testDataPath and mockORAConfig from hooks', () => {
+ expect(useTestDataPath).toHaveBeenCalledWith();
+ expect(useMockORAConfig).toHaveBeenCalledWith();
});
- const response = await out.queryFn();
- expect(response).toEqual(fakeData.oraConfig.assessmentText);
- window.location = old;
- });
- it('initializes query with promise pointing to assessment tinyMCE', async () => {
- const response = await out.queryFn();
- expect(response).toEqual(fakeData.oraConfig.assessmentTinyMCE);
- });
- it('returns camelCase object from data if data has been returned', () => {
- expect(out.data).toEqual(camelCaseObject(fakeData.oraConfig.assessmentText));
});
- it('returns empty object from data if data has not been returned', () => {
- mockUseQueryForORA(false);
- out = testUseORAConfig();
- expect(out.data).toEqual({});
+ describe('output', () => {
+ describe('if testDataPath is set', () => {
+ beforeEach(() => {
+ when(useTestDataPath).calledWith().mockReturnValueOnce('test-data-path');
+ hook = useORAConfig();
+ });
+ it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => {
+ expect(hook.useQuery.queryKey).toEqual([queryKeys.oraConfig]);
+ expect(hook.useQuery.staleTime).toEqual(Infinity);
+ });
+ it('returns mockORAConfig for queryFn', () => {
+ expect(hook.useQuery.queryFn).toEqual(mockORAConfig);
+ });
+ });
+ describe('if testDataPath is not set', () => {
+ beforeEach(() => {
+ hook = useORAConfig();
+ });
+ it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => {
+ expect(hook.useQuery.queryKey).toEqual([queryKeys.oraConfig]);
+ expect(hook.useQuery.staleTime).toEqual(Infinity);
+ });
+ describe('queryFn', () => {
+ it('returns a callback based on oraConfigUrl', () => {
+ expect(hook.useQuery.queryFn.useCallback.prereqs).toEqual([oraConfigUrl]);
+ });
+ it('posts empty object to oraConfigUrl, then calls loadData with result', async () => {
+ await expect(hook.useQuery.queryFn.useCallback.cb())
+ .resolves.toStrictEqual(loadData(postObj(oraConfigUrl, {})));
+ });
+ });
+ });
});
});
describe('usePageData', () => {
- const pageDataCamelCase = (data: any) => ({
- ...camelCaseObject(data),
- rubric: {
- optionsSelected: {...data.rubric.options_selected},
- criterionFeedback: {...data.rubric.criterion_feedback},
- overallFeedback: data.rubric.overall_feedback,
- },
- });
- const mockUseQuery = (data?: types.PageData): MockUsePageDataQuery => ({ queryKey, queryFn }) => ({
- data: data ? pageDataCamelCase(data) : {},
- queryKey,
- queryFn,
- });
-
- const mockUseQueryForPageData = (data, isAssessment) => {
- when(useQuery)
- .calledWith(expect.objectContaining({ queryKey: [queryKeys.pageData, isAssessment] }))
- .mockImplementationOnce(mockUseQuery(data));
- };
-
- const mockUseMatch = (path) => {
- when(useMatch)
- .calledWith(path)
- .mockReturnValueOnce({ pattern: { path } });
- };
-
- const testUsePageData = usePageData as unknown as MockPageDataUseConfigHook;
- describe('submission', () => {
+ describe('behavior', () => {
beforeEach(() => {
- mockUseMatch(routes.submission);
- mockUseQueryForPageData(fakeData.pageData.shapes.emptySubmission, false);
- out = testUsePageData();
- });
- it('initializes query with pageData queryKey and isAssessment: false', () => {
- expect(out.queryKey).toEqual([queryKeys.pageData, false]);
+ hook = usePageData();
});
- it('initializes query with promise pointing to empty submission page data', async () => {
- const response = await out.queryFn();
- expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission));
+ it('loads url from hook', () => {
+ expect(usePageDataUrl).toHaveBeenCalledWith();
});
- it('returns camelCase object from data if data has been returned', () => {
- expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.emptySubmission));
+ it('loads testDataPath and mockORAConfig from hooks', () => {
+ expect(useTestDataPath).toHaveBeenCalledWith();
+ expect(useMockPageData).toHaveBeenCalledWith();
});
});
- describe('assessment', () => {
- beforeEach(() => {
- mockUseMatch(routes.peerAssessment);
- mockUseQueryForPageData(fakeData.pageData.shapes.peerAssessment, true);
- out = testUsePageData();
+ describe('output', () => {
+ describe('if testDataPath is set', () => {
+ beforeEach(() => {
+ when(useTestDataPath).calledWith().mockReturnValueOnce(testDataPath);
+ hook = usePageData();
+ });
+ it('returns a useQuery call with inifite staleTime and oraConfig queryKey', () => {
+ expect(hook.useQuery.queryKey).toEqual([queryKeys.pageData, testDataPath]);
+ expect(hook.useQuery.staleTime).toEqual(Infinity);
+ });
+ it('returns mockORAConfig for queryFn', () => {
+ expect(hook.useQuery.queryFn).toEqual(mockPageData);
+ });
});
- it('initializes query with pageData queryKey and isAssessment: true', () => {
- expect(out.queryKey).toEqual([queryKeys.pageData, true]);
+ describe('if testDataPath is not set', () => {
+ let url;
+ let callback;
+ beforeEach(() => {
+ hook = usePageData();
+ url = pageDataUrl(viewStep);
+ callback = hook.useQuery.queryFn.useCallback;
+ });
+ it('returns a useQuery call with inifite staleTime and pageData queryKey', () => {
+ expect(hook.useQuery.queryKey).toEqual([queryKeys.pageData, null]);
+ expect(hook.useQuery.staleTime).toEqual(Infinity);
+ });
+ describe('queryFn', () => {
+ it('returns a callback based on pageDataUrl', () => {
+ expect(callback.prereqs).toEqual([pageDataUrl, viewStep]);
+ });
+ it('posts empty object to pageDataUrl, then calls loadData with result', async () => {
+ await expect(callback.cb())
+ .resolves.toStrictEqual(loadData(postObj(url, {})));
+ });
+ });
});
- it('initializes query with promise pointing to peer assessment page data', async () => {
- const response = await out.queryFn();
- expect(response).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment));
- });
- it('returns camelCase object from data if data has been returned', () => {
- expect(out.data).toEqual(pageDataCamelCase(fakeData.pageData.shapes.peerAssessment));
- });
- });
- it('returns empty object from data if data has not been returned', () => {
- mockUseMatch(routes.submission);
- mockUseQueryForPageData(undefined, false);
- out = testUsePageData();
- expect(out.data).toEqual({});
});
});
});
diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts
index d112beec..73a62a52 100644
--- a/src/data/services/lms/hooks/data.ts
+++ b/src/data/services/lms/hooks/data.ts
@@ -1,113 +1,47 @@
import React from 'react';
-import { useParams, useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'
-import { camelCaseObject } from '@edx/frontend-platform';
+import { queryKeys } from 'constants/index';
-import { useHasSubmitted } from 'data/redux/hooks'; // for test data
-import {
- useTestProgressKey,
- useTestDataPath,
-} from 'hooks/test';
-
-import {
- routeSteps,
- queryKeys,
- stepNames,
- stepRoutes,
-} from 'constants';
-import { defaultViewProgressKeys, progressKeys } from 'constants/mockData';
+import { useViewStep } from 'hooks/routing';
+import { useTestDataPath } from 'hooks/testHooks';
import * as types from '../types';
import { useORAConfigUrl, usePageDataUrl } from '../urls';
-import fakeData from '../fakeData';
-
-import { loadState } from '../fakeData/dataStates';
+import { loadData, logPageData, post } from './utils';
+import { useMockORAConfig, useMockPageData } from './mockData';
-export const useORAConfig = (): types.QueryData => {
+export const useORAConfig = (): types.QueryData => {
const oraConfigUrl = useORAConfigUrl();
const testDataPath = useTestDataPath();
- const { progressKey } = useParams();
-
+ const mockORAConfig = useMockORAConfig();
+ const apiMethod = React.useCallback(
+ () => post(oraConfigUrl, {}).then(loadData),
+ [oraConfigUrl],
+ );
return useQuery({
queryKey: [queryKeys.oraConfig],
- queryFn: () => {
- if (testDataPath) {
- console.log("ora config fake data");
- if (progressKey === progressKeys.staffAfterSubmission) {
- return Promise.resolve(
- camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSubmission)
- );
- }
- if (progressKey === progressKeys.staffAfterSelf) {
- return Promise.resolve(
- camelCaseObject(fakeData.oraConfig.assessmentStaffAfterSelf)
- );
- }
- return Promise.resolve(
- camelCaseObject(fakeData.oraConfig.assessmentTinyMCE)
- );
- }
- console.log("ora config real data");
- return getAuthenticatedHttpClient().post(oraConfigUrl, {}).then(
- ({ data }) => camelCaseObject(data)
- );
- },
+ queryFn: testDataPath ? mockORAConfig : apiMethod,
staleTime: Infinity,
});
};
-export const usePageData = () => {
- const location = useLocation();
- const view = location.pathname.split('/')[1];
- const hasSubmitted = useHasSubmitted();
- const viewStep = routeSteps[view];
+export const usePageData = (): types.QueryData => {
+ const viewStep = useViewStep();
+ const pageDataUrl = usePageDataUrl();
const testDataPath = useTestDataPath();
+ const mockPageData = useMockPageData();
- const pageDataUrl = usePageDataUrl();
- const loadMockData = (key) => Promise.resolve(
- camelCaseObject(loadState({ view, progressKey: key })),
+ const apiMethod = React.useCallback(
+ () => post(pageDataUrl(viewStep), {}).then(loadData).then(logPageData),
+ [pageDataUrl, viewStep],
);
- // test
- const testProgressKey = useTestProgressKey();
- const params = useParams();
- const viewKey = stepRoutes[viewStep];
- const progressKey = testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey];
-
- const queryFn = React.useCallback(() => {
- if (testDataPath) {
- console.log("page data fake data");
- return Promise.resolve(camelCaseObject(loadState({ view, progressKey })));
- }
- const url = (hasSubmitted || view === stepNames.xblock)
- ? pageDataUrl()
- : pageDataUrl(viewStep);
- console.log({ url, hasSubmitted, view });
- console.log("page data real data");
- console.log({ pageDataUrl: url });
- return getAuthenticatedHttpClient().post(url, {})
- .then(({ data }) => camelCaseObject(data))
- .then(data => {
- console.log({ pageData: data });
- return data;
- });
- }, [testDataPath, view, progressKey, testProgressKey, hasSubmitted]);
-
return useQuery({
queryKey: [queryKeys.pageData, testDataPath],
- queryFn,
+ queryFn: testDataPath ? mockPageData : apiMethod,
staleTime: Infinity,
});
};
-
-export const useSubmitResponse = () =>
- useMutation({
- mutationFn: (response) => {
- // console.log({ submit: response });
- return Promise.resolve();
- },
- });
diff --git a/src/data/services/lms/hooks/mockData.test.ts b/src/data/services/lms/hooks/mockData.test.ts
new file mode 100644
index 00000000..449ed1e0
--- /dev/null
+++ b/src/data/services/lms/hooks/mockData.test.ts
@@ -0,0 +1,129 @@
+import { when } from 'jest-when';
+import { useParams } from 'react-router-dom';
+import { useActiveView, useViewStep } from 'hooks/routing';
+import { useTestProgressKey } from 'hooks/testHooks';
+import {
+ defaultViewProgressKeys,
+ progressKeys,
+ viewKeys,
+} from 'constants/mockData';
+import { stepNames } from 'constants/index';
+import { loadState } from '../fakeData/dataStates';
+import { fakeResponse } from './utils';
+
+import * as mockData from './mockData';
+
+jest.mock('react-router-dom', () => ({
+ useParams: jest.fn(),
+}));
+jest.mock('hooks/routing', () => ({
+ useActiveView: jest.fn(),
+ useViewStep: jest.fn(),
+}));
+jest.mock('hooks/testHooks', () => ({
+ useTestProgressKey: jest.fn(),
+}));
+jest.mock('../fakeData', () => ({
+ oraConfig: {
+ assessmentStaffAfterSubmission: 'staff-after-submission',
+ assessmentStaffAfterSelf: 'staff-after-self',
+ assessmentTinyMCE: 'tiny-mce',
+ },
+}));
+jest.mock('../fakeData/dataStates', () => ({
+ loadState: jest.fn(),
+}));
+jest.mock('./utils', () => ({
+ fakeResponse: jest.fn(),
+}));
+
+when(useViewStep).calledWith().mockReturnValue(stepNames.self);
+when(fakeResponse).calledWith(expect.anything())
+ .mockImplementation(data => ({ fakeResponse: data }));
+
+let testValue;
+let out;
+describe('lms mock data hooks', () => {
+ describe('useProgressKey', () => {
+ it('returns testProgressKey if there is one', () => {
+ testValue = progressKeys.submissionFinished;
+ when(useTestProgressKey).calledWith().mockReturnValueOnce(testValue);
+ expect(mockData.useProgressKey()).toEqual(testValue);
+ });
+ describe('if testProgressKey is not truthy', () => {
+ it('returns params.progressKey if truthy', () => {
+ testValue = progressKeys.peerAssessment;
+ when(useParams).calledWith().mockReturnValueOnce({ progressKey: testValue });
+ expect(mockData.useProgressKey()).toEqual(testValue);
+ });
+ describe('if params.progressKey is not set', () => {
+ it('returns the defaultViewProgressKey for the view', () => {
+ when(useParams).calledWith().mockReturnValueOnce({});
+ testValue = progressKeys.peerAssessment;
+ expect(mockData.useProgressKey())
+ .toEqual(defaultViewProgressKeys[viewKeys.self]);
+ });
+ });
+ });
+ });
+ describe('mock configs', () => {
+ let progressKey;
+ let progressKeySpy;
+ beforeEach(() => {
+ progressKeySpy = jest.spyOn(mockData, 'useProgressKey');
+ });
+ describe('useMockORAConfig', () => {
+ describe('behavior', () => {
+ it('loads progressKey from hook', () => {
+ progressKey = progressKeys.selfAssessment;
+ when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey);
+ mockData.useMockORAConfig();
+ expect(progressKeySpy).toHaveBeenCalled();
+ });
+ });
+ describe('output - useCallback hook', () => {
+ it('returns fakeResponse of oraConfig for progressKey, based on config', () => {
+ progressKey = progressKeys.selfAssessment;
+ when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey);
+ out = mockData.useMockORAConfig();
+ const config = mockData.oraConfigs.default;
+ expect(out.useCallback.prereqs).toEqual([config]);
+ expect(out.useCallback.cb()).toEqual(fakeResponse(config));
+ });
+ it('returns fakeResponse of default oraConfig based on config if not config', () => {
+ progressKey = progressKeys.staffAfterSubmission;
+ when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey);
+ out = mockData.useMockORAConfig();
+ const config = mockData.oraConfigs[progressKey];
+ expect(out.useCallback.prereqs).toEqual([config]);
+ expect(out.useCallback.cb()).toEqual(fakeResponse(config));
+ });
+ });
+ });
+ describe('useMockPageData', () => {
+ describe('behavior', () => {
+ it('loads progressKey from hook', () => {
+ progressKey = progressKeys.selfAssessment;
+ when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey);
+ const testView = 'testView';
+ when(useActiveView).calledWith().mockReturnValue(testView);
+ mockData.useMockPageData();
+ expect(progressKeySpy).toHaveBeenCalled();
+ expect(useActiveView).toHaveBeenCalled();
+ });
+ });
+ describe('output - useCallback hook', () => {
+ it('returns fakeResponse of loadState({ view, progressKey })', () => {
+ const testView = 'testView';
+ when(useActiveView).calledWith().mockReturnValue(testView);
+ progressKey = progressKeys.selfAssessment;
+ when(progressKeySpy).calledWith().mockReturnValueOnce(progressKey);
+ out = mockData.useMockPageData();
+ expect(out.useCallback.prereqs).toEqual([testView, progressKey]);
+ expect(out.useCallback.cb())
+ .toEqual(fakeResponse(loadState({ view: testView, progressKey })));
+ });
+ });
+ });
+ });
+});
diff --git a/src/data/services/lms/hooks/mockData.ts b/src/data/services/lms/hooks/mockData.ts
new file mode 100644
index 00000000..9464de1d
--- /dev/null
+++ b/src/data/services/lms/hooks/mockData.ts
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useParams } from 'react-router-dom';
+
+import { useActiveView, useViewStep } from 'hooks/routing';
+import { useTestProgressKey } from 'hooks/testHooks';
+import { defaultViewProgressKeys, progressKeys } from 'constants/mockData';
+import { stepRoutes } from 'constants/index';
+
+import * as types from '../types';
+import fakeData from '../fakeData';
+import { loadState } from '../fakeData/dataStates';
+import { fakeResponse } from './utils';
+
+export const useProgressKey = (): string => {
+ const params = useParams();
+ const viewStep = useViewStep();
+ const testProgressKey = useTestProgressKey();
+ const viewKey = stepRoutes[viewStep];
+ return testProgressKey || params.progressKey || defaultViewProgressKeys[viewKey];
+};
+
+export const oraConfigs = {
+ [progressKeys.staffAfterSubmission]: fakeData.oraConfig.assessmentStaffAfterSubmission,
+ [progressKeys.staffAfterSelf]: fakeData.oraConfig.assessmentStaffAfterSelf,
+ default: fakeData.oraConfig.assessmentTinyMCE,
+};
+
+type ORAConfigEvent = () => Promise;
+export const useMockORAConfig = (): ORAConfigEvent => {
+ const progressKey = useProgressKey();
+ const config = progressKey in oraConfigs ? oraConfigs[progressKey] : oraConfigs.default;
+ return React.useCallback(() => fakeResponse(config), [config]);
+};
+
+type PageDataEvent = () => Promise;
+export const useMockPageData = (): PageDataEvent => {
+ const view = useActiveView();
+ const progressKey = useProgressKey();
+ return React.useCallback(
+ () => fakeResponse(loadState({ view, progressKey })),
+ [view, progressKey],
+ );
+};
diff --git a/src/data/services/lms/hooks/selectors/index.ts b/src/data/services/lms/hooks/selectors/index.ts
index 5add47fc..61b2f667 100644
--- a/src/data/services/lms/hooks/selectors/index.ts
+++ b/src/data/services/lms/hooks/selectors/index.ts
@@ -4,9 +4,7 @@ import {
stepNames,
closedReasons,
stepStates,
- globalStates,
-} from 'constants';
-import { useViewStep } from 'hooks/routing';
+} from 'constants/index';
import * as oraConfigSelectors from './oraConfig';
import * as pageDataSelectors from './pageData';
@@ -29,6 +27,7 @@ export const useStepState = ({ step = null } = {}) => {
const activeStepIndex = selectors.useStepIndex({ step: activeStepName });
const stepIndex = selectors.useStepIndex({ step: stepName });
const subState = selectors.useSubmissionState();
+ const trainingStepIsCompleted = selectors.useTrainingStepIsCompleted();
if (hasReceivedFinalGrade) {
return stepStates.done;
}
@@ -44,6 +43,10 @@ export const useStepState = ({ step = null } = {}) => {
return hasReceivedFinalGrade ? stepStates.done : stepStates.notAvailable;
}
+ if (stepName === stepNames.studentTraining && trainingStepIsCompleted) {
+ return stepStates.done;
+ }
+
if (activeStepName === stepNames.peer && stepInfo?.peer?.isWaitingForSubmissions) {
return stepStates.waiting;
}
diff --git a/src/data/services/lms/hooks/selectors/oraConfig.ts b/src/data/services/lms/hooks/selectors/oraConfig.ts
index 81142209..2a000fec 100644
--- a/src/data/services/lms/hooks/selectors/oraConfig.ts
+++ b/src/data/services/lms/hooks/selectors/oraConfig.ts
@@ -1,7 +1,7 @@
import React from 'react';
import * as data from 'data/services/lms/hooks/data';
import * as types from 'data/services/lms/types';
-import { stepNames } from 'constants';
+import { stepNames } from 'constants/index';
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ORA Config Data
diff --git a/src/data/services/lms/hooks/selectors/pageData.ts b/src/data/services/lms/hooks/selectors/pageData.ts
index 7f3959cc..0126c42f 100644
--- a/src/data/services/lms/hooks/selectors/pageData.ts
+++ b/src/data/services/lms/hooks/selectors/pageData.ts
@@ -4,6 +4,7 @@ import {
} from 'constants';
import * as data from 'data/services/lms/hooks/data';
import * as types from 'data/services/lms/types';
+import { useAssessmentStepConfig } from './oraConfig';
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Page Data
@@ -22,14 +23,19 @@ export const usePageDataStatus = () => {
};
export const useIsPageDataLoaded = (): boolean => {
const pageData = data.usePageData();
- console.log({ rawPageData: pageData });
- const { isRefetching, isStale, status } = pageData;
- console.log({ isStale, isRefetching });
- return status === 'success' && !isRefetching;
+ const { status } = pageData;
+ return status === 'success';
};
+
+export const useIsPageDataLoading = (): boolean => {
+ const pageData = data.usePageData();
+ return pageData.isFetching || pageData.isRefetching;
+};
+
export const usePageData = (): types.PageData => {
const pageData = data.usePageData()?.data;
if (process.env.NODE_ENV === 'development') {
+ // @ts-ignore
window.pageData = pageData;
}
return data.usePageData()?.data;
@@ -68,7 +74,7 @@ export const useSubmissionState = () => {
if (subStatus.hasSubmitted) {
return stepStates.done;
}
- if (subStatus.isClosed) {
+ if (subStatus.closed) {
if (subStatus.closedReason === closedReasons.pastDue) {
return stepStates.closed;
}
@@ -87,3 +93,5 @@ export const useEffectiveGrade = () => {
const assessment = useAssessmentData();
return assessment ? assessment[assessment.effectiveAssessmentType] : null;
};
+
+export const useTrainingStepIsCompleted = () => useStepInfo().studentTraining?.numberOfAssessmentsCompleted === useAssessmentStepConfig().settings.studentTraining.numberOfExamples;
diff --git a/src/data/services/lms/hooks/utils.js b/src/data/services/lms/hooks/utils.js
new file mode 100644
index 00000000..f2758e29
--- /dev/null
+++ b/src/data/services/lms/hooks/utils.js
@@ -0,0 +1,13 @@
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { camelCaseObject } from '@edx/frontend-platform';
+
+export const loadData = ({ data }) => camelCaseObject(data);
+
+export const post = (...args) => getAuthenticatedHttpClient().post(...args);
+
+export const fakeResponse = (data) => Promise.resolve(camelCaseObject(data));
+
+export const logPageData = (data) => {
+ console.log({ pageData: data });
+ return data;
+};
diff --git a/src/data/services/lms/hooks/utils.test.js b/src/data/services/lms/hooks/utils.test.js
new file mode 100644
index 00000000..f0848874
--- /dev/null
+++ b/src/data/services/lms/hooks/utils.test.js
@@ -0,0 +1,44 @@
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { camelCaseObject } from '@edx/frontend-platform';
+import * as utils from './utils';
+
+jest.mock('@edx/frontend-platform/auth', () => ({
+ getAuthenticatedHttpClient: jest.fn(),
+}));
+jest.mock('@edx/frontend-platform', () => ({
+ camelCaseObject: jest.fn(obj => ({ camelCaseObject: obj })),
+}));
+const post = jest.fn((...args) => ({ post: args }));
+getAuthenticatedHttpClient.mockReturnValue({ post });
+
+const testValue = 'test-value';
+const testValue2 = 'test-value-2';
+const testObject = {
+ testKey: 'test-value',
+ testKey1: 'test-value-1',
+};
+describe('lms service hook utils', () => {
+ describe('loadData', () => {
+ it('returns camel-cased data object from input arg', () => {
+ expect(utils.loadData({ data: testObject }))
+ .toEqual(camelCaseObject(testObject));
+ });
+ });
+ describe('post', () => {
+ it('forwards the arguments to authenticated post request', () => {
+ expect(utils.post(testValue, testValue2))
+ .toEqual(getAuthenticatedHttpClient().post(testValue, testValue2));
+ });
+ });
+ describe('fakeResponse', () => {
+ it('returns a promise that resolves to camel-cased input', async () => {
+ await expect(utils.fakeResponse(testObject))
+ .resolves.toStrictEqual(camelCaseObject(testObject));
+ });
+ });
+ describe('logPageData', () => {
+ it('returns input data', () => {
+ expect(utils.logPageData(testObject)).toEqual(testObject);
+ });
+ });
+});
diff --git a/src/data/services/lms/types/blockInfo.ts b/src/data/services/lms/types/blockInfo.ts
index 0ee93851..2b8010b5 100644
--- a/src/data/services/lms/types/blockInfo.ts
+++ b/src/data/services/lms/types/blockInfo.ts
@@ -61,12 +61,7 @@ export interface SelfStepSettings {
export interface TrainingStepSettings {
required: boolean,
- data: {
- examples: {
- response: string,
- criteria: { name: string, selection: string }[],
- }[],
- },
+ numberOfExamples: number,
}
export interface AssessmentStepConfig {
diff --git a/src/data/services/lms/types/pageData.ts b/src/data/services/lms/types/pageData.ts
index d81a82d1..0116825f 100644
--- a/src/data/services/lms/types/pageData.ts
+++ b/src/data/services/lms/types/pageData.ts
@@ -15,7 +15,7 @@ export interface RubricSelection {
}
export interface StepClosedInfo {
- isClosed: boolean,
+ closed: boolean,
closedReason?: 'notAvailable' | 'pastDue',
}
@@ -34,7 +34,7 @@ export interface SubmissionStepInfo extends StepClosedInfo {
teamInfo: SubmissionTeamInfo | null,
}
-export interface LearnerTrainingStepInfo extends StepClosedInfo {
+export interface StudentTrainingStepInfo extends StepClosedInfo {
numberOfAssessmentsCompleted: number,
expectedRubricSelctions: RubricSelection[],
}
@@ -48,7 +48,7 @@ export interface PeerStepInfo extends StepClosedInfo {
export interface StepInfo {
submission: SubmissionStepInfo,
peer: PeerStepInfo | null,
- learnerTraining: LearnerTrainingStepInfo | null,
+ studentTraining: StudentTrainingStepInfo | null,
self: StepClosedInfo | null,
}
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index a446c3fd..39b857c4 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -1,9 +1,10 @@
+import React from 'react';
import { useParams } from 'react-router-dom';
import { StrictDict } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
-import { stepRoutes, stepNames } from 'constants';
+import { stepNames, stepRoutes } from 'constants';
const useBaseUrl = () => {
const { xblockId, courseId } = useParams();
@@ -24,11 +25,12 @@ export const useViewUrl = () => {
return ({ view }) => `${getConfig().BASE_URL}/${stepRoutes[view]}/${courseId}/${xblockId}`;
};
-export const usePageDataUrl = () => {
+export const usePageDataUrl = (hasSubmitted) => {
const baseUrl = useBaseUrl();
- return (step) => (step
- ? `${baseUrl}/get_learner_data/${step}`
- : `${baseUrl}/get_learner_data/`);
+ const url = `${baseUrl}/get_learner_data/`;
+ return React.useCallback((step) => (
+ ((step === stepNames.xblock) || hasSubmitted) ? url : `${url}${step}`
+ ), [hasSubmitted, url]);
};
export default StrictDict({
diff --git a/src/hooks/actions/index.js b/src/hooks/actions/index.js
index 41421bfe..44404c50 100644
--- a/src/hooks/actions/index.js
+++ b/src/hooks/actions/index.js
@@ -14,4 +14,4 @@ export default {
useExitAction,
useLoadNextAction,
useStartStepAction,
-}
+};
diff --git a/src/hooks/actions/messages.js b/src/hooks/actions/messages.js
index 92340d8f..eabaf1ec 100644
--- a/src/hooks/actions/messages.js
+++ b/src/hooks/actions/messages.js
@@ -9,12 +9,12 @@ const messages = defineMessages({
},
startTraining: {
id: 'ora-mfe.ModalActions.startTraining',
- defaultMessage: 'Begin practice grading',
+ defaultMessage: 'Go to practice grading',
description: 'Action button to begin studentTraining step',
},
startSelf: {
- id: 'ora-mfe.ModalActions.startTraining',
- defaultMessage: 'Begin self grading',
+ id: 'ora-mfe.ModalActions.startSelf',
+ defaultMessage: 'Go to self grading',
description: 'Action button to begin self assessment step',
},
startPeer: {
@@ -34,7 +34,7 @@ const messages = defineMessages({
},
loadNext: {
id: 'ora-mfe.ModalActions.loadNext',
- defaultMessage: 'Load next',
+ defaultMessage: 'Grade next',
description: 'Action button to load next peer response',
},
loadingNext: {
diff --git a/src/hooks/actions/useStartStepAction.js b/src/hooks/actions/useStartStepAction.js
index 63cf5f1b..fe6ce02d 100644
--- a/src/hooks/actions/useStartStepAction.js
+++ b/src/hooks/actions/useStartStepAction.js
@@ -1,20 +1,16 @@
-import { useNavigate, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { stepNames, stepRoutes } from 'constants';
-import { useRefreshPageData, useActiveStepName, useSetResponse } from 'hooks/app';
-import { useSetHasSubmitted, useSetShowValidation } from 'hooks/assessment';
+import {
+ useActiveStepName,
+} from 'hooks/app';
import messages from './messages';
const useStartStepAction = (viewStep) => {
const { formatMessage } = useIntl();
- const navigate = useNavigate();
const { courseId, xblockId } = useParams();
- const refreshPageData = useRefreshPageData();
- const setHasSubmitted = useSetHasSubmitted();
- const setShowValidation = useSetShowValidation();
- const setResponse = useSetResponse();
const stepName = useActiveStepName();
@@ -22,15 +18,7 @@ const useStartStepAction = (viewStep) => {
|| [stepNames.submission, stepNames.staff].includes(stepName)) {
return null;
}
-
- const onClick = () => {
- console.log("Load next page");
- setHasSubmitted(false);
- setShowValidation(false);
- setResponse(null);
- navigate(`/${stepRoutes[stepName]}/${courseId}/${xblockId}`);
- refreshPageData();
- };
+ const url = `/${stepRoutes[stepName]}/${courseId}/${xblockId}`;
const startMessages = {
[stepNames.studentTraining]: messages.startTraining,
@@ -38,7 +26,7 @@ const useStartStepAction = (viewStep) => {
[stepNames.peer]: messages.startPeer,
[stepNames.done]: messages.viewGrades,
};
- return { children: formatMessage(startMessages[stepName]), onClick };
+ return { children: formatMessage(startMessages[stepName]), href: url };
};
export default useStartStepAction;
diff --git a/src/hooks/app.js b/src/hooks/app.js
index 3c41c58f..c5d45aff 100644
--- a/src/hooks/app.js
+++ b/src/hooks/app.js
@@ -21,6 +21,8 @@ export const {
useHasReceivedFinalGrade,
useIsORAConfigLoaded,
useIsPageDataLoaded,
+ useIsPageDataLoading,
+ useORAConfigData,
usePageDataStatus,
usePrompts,
useResponseData,
@@ -30,6 +32,7 @@ export const {
useSubmissionConfig,
useTextResponses,
useFileUploadEnabled,
+ useTrainingStepIsCompleted,
} = lmsSelectors;
export const {
diff --git a/src/hooks/assessment.js b/src/hooks/assessment.js
index bacac0ce..1b2aed34 100644
--- a/src/hooks/assessment.js
+++ b/src/hooks/assessment.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { feedbackRequirement, stepNames } from 'constants';
+import { stepNames } from 'constants';
import * as reduxHooks from 'data/redux/hooks';
import * as lmsSelectors from 'data/services/lms/hooks/selectors';
@@ -14,7 +14,7 @@ const useIsCriterionFeedbackInvalid = () => {
const config = criteriaConfig[criterionIndex];
return viewStep !== stepNames.studentTraining
&& value === ''
- && config.feedbackRequired === feedbackRequirement.required;
+ && config.feedbackRequired;
};
};
@@ -76,11 +76,11 @@ export const useInitializeAssessment = () => {
const response = lmsSelectors.useResponseData();
React.useEffect(() => {
setResponse(response);
- }, []);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
return React.useCallback(() => {
setFormFields(emptyRubric);
- }, []);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
};
export const useOnSubmit = () => {
@@ -88,7 +88,6 @@ export const useOnSubmit = () => {
const setShowValidation = reduxHooks.useSetShowValidation();
const setShowTrainingError = reduxHooks.useSetShowTrainingError();
const setHasSubmitted = reduxHooks.useSetHasSubmitted();
- const refreshPageData = lmsActions.useRefreshPageData();
const isInvalid = useIsAssessmentInvalid();
const checkTrainingSelection = useCheckTrainingSelection();
@@ -97,22 +96,18 @@ export const useOnSubmit = () => {
const formFields = reduxHooks.useFormFields();
const submitAssessmentMutation = lmsActions.useSubmitAssessment({ onSuccess: setAssessment });
+
return {
onSubmit: React.useCallback(() => {
- console.log({ onSubmit: { isInvalid, activeStepName, checkTrainingSelection } });
if (isInvalid) {
- console.log("is invalid");
return setShowValidation(true);
}
if (activeStepName === stepNames.studentTraining && !checkTrainingSelection) {
- console.log("training validation");
return setShowTrainingError(true);
}
- console.log("is valid");
return submitAssessmentMutation.mutateAsync(formFields).then((data) => {
setAssessment(data);
setHasSubmitted(true);
- refreshPageData();
});
}, [
formFields,
diff --git a/src/hooks/modal.js b/src/hooks/modal.js
index 7fb35148..11844b0a 100644
--- a/src/hooks/modal.js
+++ b/src/hooks/modal.js
@@ -1,17 +1,14 @@
-import { useLocation } from 'react-router-dom';
import { useViewUrl } from 'data/services/lms/urls';
-import { routeSteps } from 'constants';
export const useRefreshUpstream = () => {
if (document.referrer !== '') {
const postMessage = (data) => window.parent.postMessage(data, process.env.BASE_URL);
return () => {
- console.log("Send refresh upstream");
postMessage({ type: 'ora-refresh' });
};
}
return () => {
- console.log("refresh upstream");
+ console.log('refresh upstream');
};
};
@@ -24,7 +21,7 @@ export const useCloseModal = () => {
};
}
return () => {
- console.log("CLose Modal");
+ console.log('Close Modal');
};
};
@@ -36,6 +33,7 @@ export const useOpenModal = () => {
type: 'plugin.modal',
payload: {
url: viewUrl({ view }),
+ isFullscreen: true,
title,
height: '83vh',
},
diff --git a/src/hooks/test.js b/src/hooks/testHooks.js
similarity index 92%
rename from src/hooks/test.js
rename to src/hooks/testHooks.js
index 379eebe6..6a531011 100644
--- a/src/hooks/test.js
+++ b/src/hooks/testHooks.js
@@ -32,7 +32,7 @@ export const useUpdateTestProgressKey = () => {
const testDirty = useTestDirty();
console.log({ setTestDataPath });
- React.useEffect(() => {
+ React.useEffect(() => {
window.useTestData = () => setTestDataPath(true);
}, [setTestDataPath]);
@@ -63,7 +63,7 @@ export const useUpdateTestProgressKey = () => {
}
}
}
- }, [
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
hasSubmitted,
viewStep,
testDataPath,
@@ -72,9 +72,9 @@ export const useUpdateTestProgressKey = () => {
if (testDataPath && !testDirty) {
console.log({ testDirty, testProgressKey });
queryClient.invalidateQueries({ queryKey: [queryKeys.pageData] });
- console.log("invalidated");
+ console.log('invalidated');
}
- }, [testDirty]);
+ }, [testDirty]); // eslint-disable-line react-hooks/exhaustive-deps
};
export default {
diff --git a/src/i18n/index.js b/src/i18n/index.js
index 8331894b..8a7be528 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -1,6 +1,3 @@
-import { messages as headerMessages } from '@edx/frontend-component-header';
-import { messages as footerMessages } from '@edx/frontend-component-footer';
-
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
@@ -33,7 +30,5 @@ const appMessages = {
};
export default [
- headerMessages,
- footerMessages,
appMessages,
];
diff --git a/src/index.jsx b/src/index.jsx
index 58ac232b..313bdbb5 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -25,13 +25,6 @@ const queryClient = new QueryClient({
subscribe(APP_READY, () => {
const isDev = process.env.NODE_ENV === 'development';
const rootEl = document.getElementById('root');
- if (isDev) {
- setTimeout(() => {
- // This is a hack to prevent the Paragon Modal overlay stop query devtools from clickable
- rootEl.removeAttribute('data-focus-on-hidden');
- rootEl.removeAttribute('aria-hidden');
- }, 3000);
- }
ReactDOM.render(
diff --git a/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss b/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss
index 01920d70..4481dae6 100644
--- a/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss
+++ b/src/views/AssessmentView/BaseAssessmentView/BaseAssessmentView.scss
@@ -12,9 +12,12 @@
height: 100%;
.content-wrapper {
- min-width: min-content;
+ max-width: $max-width-lg;
}
+ .assessment-col {
+ min-width: 300px;
+ }
}
@include media-breakpoint-down(sm) {
@@ -22,6 +25,14 @@
.content-wrapper {
width: 100%;
}
+
+ .content-body {
+ flex-direction: column;
+
+ .assessment-col {
+ max-width: 100%;
+ }
+ }
}
}
diff --git a/src/views/AssessmentView/BaseAssessmentView/index.jsx b/src/views/AssessmentView/BaseAssessmentView/index.jsx
index ac671166..b80b49e1 100644
--- a/src/views/AssessmentView/BaseAssessmentView/index.jsx
+++ b/src/views/AssessmentView/BaseAssessmentView/index.jsx
@@ -8,12 +8,15 @@ import { useShowTrainingError } from 'hooks/assessment';
import { useViewStep } from 'hooks/routing';
import Assessment from 'components/Assessment';
+import Instructions from 'components/Instructions';
import ModalActions from 'components/ModalActions';
import StatusAlert from 'components/StatusAlert';
import StepProgressIndicator from 'components/StepProgressIndicator';
import messages from '../messages';
+import './BaseAssessmentView.scss';
+
const BaseAssessmentView = ({
children,
}) => {
@@ -28,12 +31,15 @@ const BaseAssessmentView = ({
{formatMessage(messages[step])}
-
+
+
{children}
-
+
+
+
diff --git a/src/views/AssessmentView/PeerAssessmentView/index.jsx b/src/views/AssessmentView/PeerAssessmentView/index.jsx
deleted file mode 100644
index ea3ca4b6..00000000
--- a/src/views/AssessmentView/PeerAssessmentView/index.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-
-import {
- useIsORAConfigLoaded,
- usePrompts,
- useResponse,
- useSetResponse,
- useResponseData,
-} from 'hooks/app';
-
-import Prompt from 'components/Prompt';
-import TextResponse from 'components/TextResponse';
-import FileUpload from 'components/FileUpload';
-import BaseAssessmentView from '../BaseAssessmentView';
-import useAssessmentData from './useAssessmentData';
-
-export const PeerAssessmentView = () => {
- const { prompts, response, isLoaded } = useAssessmentData();
- if (!isLoaded || !response) {
- return null;
- }
-
- return (
- {}}>
-
- {React.Children.toArray(
- prompts.map((prompt, index) => (
-
- )),
- )}
-
-
-
- );
-};
-
-export default PeerAssessmentView;
diff --git a/src/views/AssessmentView/SelfAssessmentView/index.jsx b/src/views/AssessmentView/SelfAssessmentView/index.jsx
deleted file mode 100644
index d509883d..00000000
--- a/src/views/AssessmentView/SelfAssessmentView/index.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-import {
- useIsORAConfigLoaded,
- usePrompts,
- useResponse,
-} from 'hooks/app';
-
-import FileUpload from 'components/FileUpload';
-import ModalActions from 'components/ModalActions';
-import Prompt from 'components/Prompt';
-import TextResponse from 'components/TextResponse';
-
-import BaseAssessmentView from '../BaseAssessmentView';
-
-export const SelfAssessmentView = () => {
- const prompts = usePrompts();
- const response = useResponse();
- if (!useIsORAConfigLoaded()) {
- return null;
- }
- return (
- {}}>
-
- {React.Children.toArray(
- prompts.map((prompt, index) => (
-
- )),
- )}
-
-
-
- );
-};
-
-export default SelfAssessmentView;
diff --git a/src/views/AssessmentView/StudentTrainingView/index.jsx b/src/views/AssessmentView/StudentTrainingView/index.jsx
deleted file mode 100644
index 6a2b230f..00000000
--- a/src/views/AssessmentView/StudentTrainingView/index.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-import {
- useIsORAConfigLoaded,
- usePrompts,
- useResponse,
-} from 'hooks/app';
-
-import Prompt from 'components/Prompt';
-import TextResponse from 'components/TextResponse';
-import FileUpload from 'components/FileUpload';
-import ModalActions from 'components/ModalActions';
-
-import BaseAssessmentView from '../BaseAssessmentView';
-
-export const StudentTrainingView = () => {
- const prompts = usePrompts();
- const response = useResponse();
- console.log("StudentTrainingView");
- if (!useIsORAConfigLoaded()) {
- return null;
- }
- return (
- {}}>
-
- {React.Children.toArray(
- prompts.map((prompt, index) => (
-
- )),
- )}
-
-
-
- );
-};
-export default StudentTrainingView;
diff --git a/src/views/AssessmentView/index.jsx b/src/views/AssessmentView/index.jsx
index ff40ee38..aa17b107 100644
--- a/src/views/AssessmentView/index.jsx
+++ b/src/views/AssessmentView/index.jsx
@@ -1,18 +1,26 @@
import React from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
import Prompt from 'components/Prompt';
import TextResponse from 'components/TextResponse';
import FileUpload from 'components/FileUpload';
+import { useViewStep } from 'hooks/routing';
import BaseAssessmentView from './BaseAssessmentView';
import useAssessmentData from './useAssessmentData';
+import messages from './messages';
export const AssessmentView = () => {
const { prompts, response, isLoaded } = useAssessmentData();
- if (!isLoaded || !response) {
+ const { formatMessage } = useIntl();
+ const step = useViewStep();
+ if (!isLoaded) {
return null;
}
+ const responseIsEmpty = !response?.textResponses?.length;
+
return (
{}}>
@@ -20,11 +28,16 @@ export const AssessmentView = () => {
prompts.map((prompt, index) => (
-
+ {!responseIsEmpty && (
+ <>
+
{formatMessage(messages.responseMessages[step])}
+
+ >
+ )}
)),
)}
-
+ {!responseIsEmpty &&
}
);
diff --git a/src/views/AssessmentView/messages.js b/src/views/AssessmentView/messages.js
index 3deba73e..0c339ba3 100644
--- a/src/views/AssessmentView/messages.js
+++ b/src/views/AssessmentView/messages.js
@@ -3,7 +3,7 @@ import { stepNames } from 'constants';
const messages = defineMessages({
[stepNames.self]: {
- defaultMessage: 'Grade yourself',
+ defaultMessage: 'Self grading',
description: 'Self assessment view header text',
id: 'frontend-app-ora.selfAssessmentView.header',
},
@@ -19,4 +19,25 @@ const messages = defineMessages({
},
});
-export default messages;
+const responseMessages = defineMessages({
+ [stepNames.self]: {
+ defaultMessage: 'Your response',
+ description: 'Self assessment view response header text',
+ id: 'frontend-app-ora.selfAssessmentView.responseHeader',
+ },
+ [stepNames.peer]: {
+ defaultMessage: 'Peer response',
+ description: 'Peer assessment view response header text',
+ id: 'frontend-app-ora.peerAssessmentView.responseHeader',
+ },
+ [stepNames.studentTraining]: {
+ defaultMessage: 'Example response',
+ description: 'Student training view response header text',
+ id: 'frontend-app-ora.studentTrainingView.responseHeader',
+ },
+});
+
+export default {
+ ...messages,
+ responseMessages,
+};
diff --git a/src/views/AssessmentView/useAssessmentData.js b/src/views/AssessmentView/useAssessmentData.js
index 17cf1822..47fec368 100644
--- a/src/views/AssessmentView/useAssessmentData.js
+++ b/src/views/AssessmentView/useAssessmentData.js
@@ -3,7 +3,6 @@ import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import {
useIsORAConfigLoaded,
useIsPageDataLoaded,
- usePageDataStatus,
usePrompts,
useResponse,
useSetResponse,
@@ -22,9 +21,7 @@ const useAssessmentData = () => {
const setResponse = useSetResponse();
const isLoaded = useIsORAConfigLoaded();
const isPageDataLoaded = useIsPageDataLoaded();
- console.log({ pageDataStatus: usePageDataStatus() });
React.useEffect(() => {
- console.log("useAssessmentView useEffect");
if (!initialized && isLoaded && isPageDataLoaded) {
setResponse(responseData);
setInitialized(true);
@@ -32,7 +29,7 @@ const useAssessmentData = () => {
if (initialized && responseData && response !== responseData) {
setResponse(responseData);
}
- }, [responseData, initialized]);
+ }, [responseData, initialized]); // eslint-disable-line react-hooks/exhaustive-deps
return {
isLoaded,
response,
diff --git a/src/views/GradeView/Content.jsx b/src/views/GradeView/Content.jsx
index b21b3623..5cb176d7 100644
--- a/src/views/GradeView/Content.jsx
+++ b/src/views/GradeView/Content.jsx
@@ -39,7 +39,7 @@ const Content = () => {
prompts.map((prompt, index) => (
-
+ {response.textResponses[index] &&
}
))
}
diff --git a/src/views/GradeView/index.jsx b/src/views/GradeView/index.jsx
index c7947903..8e63e7c6 100644
--- a/src/views/GradeView/index.jsx
+++ b/src/views/GradeView/index.jsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { Layout, Row } from '@edx/paragon';
+import { Layout } from '@edx/paragon';
import { stepNames } from 'constants';
@@ -12,24 +12,26 @@ import Content from './Content';
import './index.scss';
const GradeView = () => (
-
-
-
-
-
-
-
-
-
-
+
);
GradeView.defaultProps = {};
diff --git a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
index beecfc4b..9ea3db27 100644
--- a/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
+++ b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx
@@ -9,7 +9,6 @@ import 'tinymce/plugins/lists';
import 'tinymce/plugins/code';
import 'tinymce/plugins/image';
import 'tinymce/themes/silver';
-import 'tinymce/skins/ui/oxide/skin.min.css';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
@@ -32,18 +31,23 @@ const RichTextEditor = ({
};
return (
-
+
{formatMessage(messages.yourResponse)} ({formatMessage(optional ? messages.optional : messages.required)})
render disabled 1`] = `
render disabled 1`] = `
)
render disabled 1`] = `
exports[`
render optional 1`] = `
render optional 1`] = `
)
render optional 1`] = `
exports[`
render required 1`] = `
render required 1`] = `
)
render Rich Text Editor 1`] = `
-
-
-
-`;
-
-exports[`
render Text Editor 1`] = `
-
-
-
-`;
diff --git a/src/views/SubmissionView/TextResponseEditor/index.jsx b/src/views/SubmissionView/TextResponseEditor/index.jsx
index c64d8edf..8e480047 100644
--- a/src/views/SubmissionView/TextResponseEditor/index.jsx
+++ b/src/views/SubmissionView/TextResponseEditor/index.jsx
@@ -6,8 +6,6 @@ import { useSubmissionConfig } from 'hooks/app';
import TextEditor from './TextEditor';
import RichTextEditor from './RichTextEditor';
-import './index.scss';
-
const TextResponseEditor = ({ value, onChange }) => {
const { textResponseConfig } = useSubmissionConfig();
const {
diff --git a/src/views/SubmissionView/TextResponseEditor/index.scss b/src/views/SubmissionView/TextResponseEditor/index.scss
deleted file mode 100644
index b4477cfd..00000000
--- a/src/views/SubmissionView/TextResponseEditor/index.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.textarea-response {
- min-height: 200px;
- max-height: 300px;
- overflow-y: scroll;
-}
\ No newline at end of file
diff --git a/src/views/SubmissionView/TextResponseEditor/index.test.jsx b/src/views/SubmissionView/TextResponseEditor/index.test.jsx
deleted file mode 100644
index 8f278295..00000000
--- a/src/views/SubmissionView/TextResponseEditor/index.test.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { shallow } from '@edx/react-unit-test-utils';
-import TextResponseEditor from '.';
-
-jest.mock('./TextEditor', () => 'TextEditor');
-jest.mock('./RichTextEditor', () => 'RichTextEditor');
-
-describe('
', () => {
- const props = {
- submissionConfig: {
- textResponseConfig: {
- optional: false,
- enabled: true,
- editorType: 'text',
- },
- },
- value: 'value',
- onChange: jest.fn().mockName('onChange'),
- };
-
- it('render Text Editor ', () => {
- const wrapper = shallow(
);
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('TextEditor').length).toEqual(1);
- expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(0);
- });
-
- it('render Rich Text Editor ', () => {
- const wrapper = shallow(
);
- expect(wrapper.snapshot).toMatchSnapshot();
-
- expect(wrapper.instance.findByType('TextEditor').length).toEqual(0);
- expect(wrapper.instance.findByType('RichTextEditor').length).toEqual(1);
- });
-});
diff --git a/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap
deleted file mode 100644
index e3cb598b..00000000
--- a/src/views/SubmissionView/__snapshots__/SubmissionActions.test.jsx.snap
+++ /dev/null
@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
renders 1`] = `
-
-
-
-
-`;
diff --git a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap
deleted file mode 100644
index 7b07e027..00000000
--- a/src/views/SubmissionView/__snapshots__/SubmissionContent.test.jsx.snap
+++ /dev/null
@@ -1,101 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
render default 1`] = `
-
-
-
- Your response
-
-
-
- Draft saved
-
-
-
-
- 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
-
-
-
- Draft saved
-
-
-
-
- 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/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap b/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap
deleted file mode 100644
index 86180b15..00000000
--- a/src/views/SubmissionView/__snapshots__/SubmissionContentLayout.test.jsx.snap
+++ /dev/null
@@ -1,66 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
hide rubric 1`] = `
-
-`;
-
-exports[`
show rubric 1`] = `
-
-`;
diff --git a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index 8eaaa21d..00000000
--- a/src/views/SubmissionView/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -1,25 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
renders 1`] = `
-
-
-
-
-
-`;
diff --git a/src/views/SubmissionView/hooks/index.js b/src/views/SubmissionView/hooks/index.js
index 2b42cdd8..d7b9af06 100644
--- a/src/views/SubmissionView/hooks/index.js
+++ b/src/views/SubmissionView/hooks/index.js
@@ -1,2 +1,3 @@
import useSubmissionViewData from './useSubmissionViewData';
+
export default useSubmissionViewData;
diff --git a/src/views/SubmissionView/hooks/useSubmissionViewData.js b/src/views/SubmissionView/hooks/useSubmissionViewData.js
index b1248a1e..95f53958 100644
--- a/src/views/SubmissionView/hooks/useSubmissionViewData.js
+++ b/src/views/SubmissionView/hooks/useSubmissionViewData.js
@@ -1,4 +1,4 @@
-import { useCallback, useEffect } from 'react';
+import React from 'react';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
@@ -44,37 +44,30 @@ const useSubmissionViewData = () => {
saveResponseStatus,
finishLater,
finishLaterStatus,
- } = useTextResponsesData();
+ } = useTextResponsesData({ setHasSavedDraft });
+
const {
uploadedFiles,
onFileUploaded,
onDeletedFile,
} = useUploadedFilesData();
- const submitResponseHandler = useCallback(() => {
+ const submitResponseHandler = React.useCallback(() => {
submitResponseMutation.mutateAsync({
textResponses,
uploadedFiles,
}).then(() => {
- console.log("submitResponseMutation.then");
setResponse({ textResponses, uploadedFiles });
setHasSubmitted(true);
refreshPageData();
refreshUpstream();
});
- }, [setHasSubmitted, submitResponseMutation, textResponses, uploadedFiles]);
-
- useEffect(() => {
- if (!hasSubmitted) {
- const timer = setTimeout(() => {
- saveResponse();
- if (!hasSavedDraft) {
- setHasSavedDraft(true);
- }
- }, 5000);
- return () => clearTimeout(timer);
- }
- }, [saveResponse, hasSubmitted]);
+ }, [ // eslint-disable-line react-hooks/exhaustive-deps
+ setHasSubmitted,
+ submitResponseMutation,
+ textResponses,
+ uploadedFiles,
+ ]);
return {
actionOptions: {
diff --git a/src/views/SubmissionView/hooks/useTextResponsesData.js b/src/views/SubmissionView/hooks/useTextResponsesData.js
index 6b85046d..569481c2 100644
--- a/src/views/SubmissionView/hooks/useTextResponsesData.js
+++ b/src/views/SubmissionView/hooks/useTextResponsesData.js
@@ -1,17 +1,26 @@
-import { useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
-import { useFinishLater, useSaveDraftResponse, useTextResponses } from 'hooks/app';
+import {
+ useFinishLater,
+ useHasSubmitted,
+ useSaveDraftResponse,
+ useTextResponses,
+} from 'hooks/app';
import { MutationStatus } from 'constants';
export const stateKeys = StrictDict({
textResponses: 'textResponses',
isDirty: 'isDirty',
+ hasSaved: 'hasSaved',
+ lastChanged: 'lastChanged',
});
-const useTextResponsesData = () => {
+const useTextResponsesData = ({ setHasSavedDraft }) => {
const textResponses = useTextResponses();
-
+ const hasSubmitted = useHasSubmitted();
+ const [hasSaved, setHasSaved] = useKeyedState(stateKeys.hasSaved, false);
+ const [lastChanged, setLastChanged] = useKeyedState(stateKeys.lastChanged, null);
const [isDirty, setIsDirty] = useKeyedState(stateKeys.isDirty, false);
const [value, setValue] = useKeyedState(stateKeys.textResponses, textResponses);
@@ -20,8 +29,15 @@ const useTextResponsesData = () => {
const saveResponse = useCallback(() => {
setIsDirty(false);
- return saveResponseMutation.mutateAsync({ textResponses: value });
- }, [setIsDirty, saveResponseMutation, value]);
+ return saveResponseMutation.mutateAsync({ textResponses: value }).then(() => {
+ setHasSavedDraft(true);
+ });
+ }, [
+ saveResponseMutation,
+ value,
+ setIsDirty,
+ setHasSavedDraft,
+ ]);
const finishLater = useCallback(() => {
setIsDirty(false);
@@ -35,7 +51,35 @@ const useTextResponsesData = () => {
return out;
});
setIsDirty(true);
- }, [setValue, setIsDirty]);
+ setLastChanged(Date.now());
+ }, [
+ setValue,
+ setIsDirty,
+ setLastChanged,
+ ]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (!lastChanged) {
+ return;
+ }
+ const timeSinceChange = Date.now() - lastChanged;
+ if (isDirty && timeSinceChange > 2000) {
+ saveResponse();
+ if (!hasSaved) {
+ setHasSaved(true);
+ }
+ }
+ }, 2000);
+ return () => clearInterval(interval);
+ }, [
+ isDirty,
+ hasSubmitted,
+ hasSaved,
+ setHasSaved,
+ lastChanged,
+ saveResponse,
+ ]);
return {
textResponses: value,
diff --git a/src/views/SubmissionView/hooks/useUploadedFilesData.js b/src/views/SubmissionView/hooks/useUploadedFilesData.js
index 163362c2..743d733d 100644
--- a/src/views/SubmissionView/hooks/useUploadedFilesData.js
+++ b/src/views/SubmissionView/hooks/useUploadedFilesData.js
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import React from 'react';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
@@ -21,15 +21,11 @@ const useUploadedFilesData = () => {
response ? response.uploadedFiles : [],
);
- const onFileUploaded = useCallback(async (data) => {
- console.log({ onFileUploaded: { data } });
- // const { fileData, queryClient } = data;
- const uploadResponse = await uploadFilesMutation.mutateAsync(data);
- if (uploadResponse) {
- setValue((oldFiles) => [...oldFiles, uploadResponse.uploadedFiles[0]]);
- }
- }, [uploadFilesMutation, setValue]);
+ React.useEffect(() => {
+ setValue(response.uploadedFiles);
+ }, [setValue, response.uploadedFiles]);
+ const onFileUploaded = uploadFilesMutation.mutateAsync;
const onDeletedFile = deleteFileMutation.mutateAsync;
return {
diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx
index 168cbfb0..2f949bca 100644
--- a/src/views/SubmissionView/index.jsx
+++ b/src/views/SubmissionView/index.jsx
@@ -4,7 +4,6 @@ import { Col, Icon, Row } from '@edx/paragon';
import { CheckCircle } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { useStepState } from 'hooks/app';
import { stepNames } from 'constants';
import Rubric from 'components/Rubric';
diff --git a/src/views/SubmissionView/index.scss b/src/views/SubmissionView/index.scss
index f078d74f..106fe5ee 100644
--- a/src/views/SubmissionView/index.scss
+++ b/src/views/SubmissionView/index.scss
@@ -12,10 +12,14 @@
height: 100%;
.content-wrapper {
- min-width: min-content;
+ max-width: $max-width-lg;
}
}
+.ora-tinymce .tox-tinymce {
+ background-color: $white;
+}
+
@include media-breakpoint-down(sm) {
.assessment-content-layout {
.content-wrapper {
diff --git a/src/views/SubmissionView/index.test.jsx b/src/views/SubmissionView/index.test.jsx
deleted file mode 100644
index 8311e215..00000000
--- a/src/views/SubmissionView/index.test.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { shallow } from '@edx/react-unit-test-utils';
-import { SubmissionView } from '.';
-
-jest.mock('components/Rubric', () => 'Rubric');
-jest.mock('components/ProgressBar', () => 'ProgressBar');
-jest.mock('./SubmissionContent', () => 'SubmissionContent');
-jest.mock('./SubmissionActions', () => 'SubmissionActions');
-
-jest.mock('./hooks', () => jest.fn().mockReturnValue({
- submission: 'submission',
- oraConfigData: 'oraConfigData',
- onFileUploaded: jest.fn().mockName('onFileUploaded'),
- onTextResponseChange: jest.fn().mockName('onTextResponseChange'),
- submitResponseHandler: jest.fn().mockName('submitResponseHandler'),
- submitResponseStatus: 'submitResponseStatus',
- saveResponseHandler: jest.fn().mockName('saveResponseHandler'),
- saveResponseStatus: 'saveResponseStatus',
- draftSaved: true,
-}));
-jest.mock('data/services/lms/hooks/selectors', () => ({
- useIsPageDataLoaded: jest.fn(() => true),
-}));
-
-describe('
', () => {
- it('renders', () => {
- const wrapper = shallow(
);
- expect(wrapper.snapshot).toMatchSnapshot();
- });
-});
diff --git a/src/views/SubmissionView/messages.js b/src/views/SubmissionView/messages.js
index eb1f8d8d..97195051 100644
--- a/src/views/SubmissionView/messages.js
+++ b/src/views/SubmissionView/messages.js
@@ -54,7 +54,7 @@ export const submitActionMessages = defineMessages({
export const saveActionMessages = defineMessages({
[MutationStatus.idle]: {
id: 'ora-grading.SaveAction.save',
- defaultMessage: 'Finish later',
+ defaultMessage: 'Save for later',
description: 'Save for later button text',
},
[MutationStatus.loading]: {
diff --git a/src/views/XBlockView/Actions/index.jsx b/src/views/XBlockView/Actions/index.jsx
index 6410e372..3037c090 100644
--- a/src/views/XBlockView/Actions/index.jsx
+++ b/src/views/XBlockView/Actions/index.jsx
@@ -28,13 +28,13 @@ const SubmissionActions = () => {
) {
const onClick = () => openModal({ view: activeStepName, title: activeStepName });
action = (
-
+
{formatMessage(messages[activeStepName])}
);
}
return (
-
+
{action}
);
diff --git a/src/views/XBlockView/Actions/messages.js b/src/views/XBlockView/Actions/messages.js
index ad57e4ae..8fdcd8c2 100644
--- a/src/views/XBlockView/Actions/messages.js
+++ b/src/views/XBlockView/Actions/messages.js
@@ -4,17 +4,17 @@ import { stepNames } from 'constants';
const messages = defineMessages({
[stepNames.submission]: {
- defaultMessage: 'Create your response',
+ defaultMessage: 'Create response',
description: 'Xblock view action button for submission step to work on response',
id: 'frontend-app-ora.XBlockView.Actions.submission',
},
[stepNames.studentTraining]: {
- defaultMessage: 'Begin practice grading',
+ defaultMessage: 'Go to practice grading',
description: 'Xblock view action button for studentTraining step to practice grading',
id: 'frontend-app-ora.XBlockView.Actions.studentTraining',
},
[stepNames.self]: {
- defaultMessage: 'Begin self grading',
+ defaultMessage: 'Go to self grading',
description: 'Xblock view action button for self step to self-grade',
id: 'frontend-app-ora.XBlockView.Actions.self',
},
diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/messages.js b/src/views/XBlockView/StatusRow/DueDateMessage/messages.js
index 372598c7..8bafbdeb 100644
--- a/src/views/XBlockView/StatusRow/DueDateMessage/messages.js
+++ b/src/views/XBlockView/StatusRow/DueDateMessage/messages.js
@@ -19,7 +19,7 @@ const messages = defineMessages({
yourSelfAssessment: {
defaultMessage: 'Your self assessment',
description: 'Self assessment label string for sentence noun context',
- id: 'frontend-app-ora.XBlockView.DueDateMessage.selfAssessment',
+ id: 'frontend-app-ora.XBlockView.DueDateMessage.yourSelfAssessment',
},
peerGrading: {
defaultMessage: 'Peer grading',
diff --git a/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js
index 30b39654..1a65c958 100644
--- a/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js
+++ b/src/views/XBlockView/StatusRow/DueDateMessage/useDueDateMessage.js
@@ -9,7 +9,7 @@ import { stepNames, stepStates } from 'constants';
import messages from './messages';
-export const useDueDateMessage = () => {
+const useDueDateMessage = () => {
const { formatMessage } = useIntl();
const { activeStepName, stepState } = useGlobalState();
const stepConfig = useActiveStepConfig();
diff --git a/src/views/XBlockView/StatusRow/index.jsx b/src/views/XBlockView/StatusRow/index.jsx
index f7581f72..177d250e 100644
--- a/src/views/XBlockView/StatusRow/index.jsx
+++ b/src/views/XBlockView/StatusRow/index.jsx
@@ -1,5 +1,4 @@
import React from 'react';
-import { Badge } from '@edx/paragon';
import StatusBadge from './StatusBadge';
import DueDateMessage from './DueDateMessage';
diff --git a/src/views/XBlockView/index.jsx b/src/views/XBlockView/index.jsx
index 8bab2ae0..4458c58f 100644
--- a/src/views/XBlockView/index.jsx
+++ b/src/views/XBlockView/index.jsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useEffect } from 'react';
-import { usePrompts } from 'hooks/app';
+import { usePrompts, useRubricConfig } from 'hooks/app';
import ProgressBar from 'components/ProgressBar';
import Prompt from 'components/Prompt';
@@ -15,6 +15,16 @@ import './index.scss';
export const XBlockView = () => {
const prompts = usePrompts();
+ const rubricConfig = useRubricConfig();
+
+ useEffect(() => {
+ if (window.parent.length > 0) {
+ new ResizeObserver(() => {
+ window.parent.postMessage({ type: 'plugin.resize', payload: { height: document.body.scrollHeight } }, document.referrer);
+ }).observe(document.body);
+ }
+ }, []);
+
return (
Open Response Assessment
@@ -24,8 +34,7 @@ export const XBlockView = () => {
{prompts.map(prompt =>
)}
-
-
+ {rubricConfig.showDuringResponse &&
}
);
};
diff --git a/src/views/XBlockView/index.scss b/src/views/XBlockView/index.scss
index 8fe83a09..fedcb49f 100644
--- a/src/views/XBlockView/index.scss
+++ b/src/views/XBlockView/index.scss
@@ -1,6 +1,8 @@
+@import "@edx/paragon/scss/core/core";
+
#ora-xblock-view {
max-width: 1024px;
- @media (min-width: 830px) {
+ @include media-breakpoint-down(md) {
padding-left: 40px;
padding-right: 40px;
}
diff --git a/tsconfig.json b/tsconfig.json
index 173f94f6..4c65ac2f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,11 +3,15 @@
"compilerOptions": {
"rootDir": "./src",
"outDir": "dist",
- "baseUrl": "./src",
+ "baseUrl": "src",
"paths": {
- "*": ["*"]
+ "data": ["data/*"],
+ "components": ["components/*"],
+ "constants": ["constants/*"],
+ "views": ["views/*"],
+ "utils": ["utils/*"]
}
},
- "include": ["src/**/*"],
+ "include": ["src"],
"exclude": ["dist", "node_modules"]
}