diff --git a/public/assets/empty-state.svg b/public/assets/empty-state.svg
new file mode 100644
index 0000000..dbe0d77
--- /dev/null
+++ b/public/assets/empty-state.svg
@@ -0,0 +1,44 @@
+
diff --git a/src/containers/ListView/EmptySubmission.jsx b/src/containers/ListView/EmptySubmission.jsx
new file mode 100644
index 0000000..f01ca51
--- /dev/null
+++ b/src/containers/ListView/EmptySubmission.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { Hyperlink, Button } from '@edx/paragon';
+
+import urls from 'data/services/lms/urls';
+
+import messages from './messages';
+
+const EmptySubmission = ({ courseId }) => (
+
+

+
+
+
+
+
+
+
+
+
+
+);
+
+EmptySubmission.defaultProps = {
+};
+EmptySubmission.propTypes = {
+ courseId: PropTypes.string.isRequired,
+};
+
+export default EmptySubmission;
diff --git a/src/containers/ListView/EmptySubmission.test.jsx b/src/containers/ListView/EmptySubmission.test.jsx
new file mode 100644
index 0000000..2aa21e6
--- /dev/null
+++ b/src/containers/ListView/EmptySubmission.test.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Hyperlink } from '@edx/paragon';
+
+import urls from 'data/services/lms/urls';
+
+import EmptySubmission from './EmptySubmission';
+
+jest.mock('data/services/lms/urls', () => ({
+ openResponse: (courseId) => `openResponseUrl(${courseId})`,
+}));
+
+let el;
+
+describe('EmptySubmission component', () => {
+ describe('component', () => {
+ const props = { courseId: 'test-course-id' };
+ beforeEach(() => {
+ el = shallow();
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('openResponse destination', () => {
+ expect(
+ el.find(Hyperlink).at(0).props().destination,
+ ).toEqual(urls.openResponse(props.courseId));
+ });
+ });
+});
diff --git a/src/containers/ListView/ListView.scss b/src/containers/ListView/ListView.scss
index 9278823..4d53bae 100644
--- a/src/containers/ListView/ListView.scss
+++ b/src/containers/ListView/ListView.scss
@@ -1,4 +1,21 @@
+@import "@edx/paragon/scss/core/core";
+
span.pgn__icon.breadcrumb-arrow {
width: 16px !important;
height: 16px !important;
};
+
+.empty-submission {
+ width: map-get($container-max-widths, "sm");
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ min-height: 75vh;
+ margin: auto;
+
+ > img {
+ padding: map-get($spacers, 5);
+ }
+}
+
diff --git a/src/containers/ListView/SubmissionsTable.jsx b/src/containers/ListView/SubmissionsTable.jsx
index e970402..a3b126d 100644
--- a/src/containers/ListView/SubmissionsTable.jsx
+++ b/src/containers/ListView/SubmissionsTable.jsx
@@ -139,7 +139,6 @@ export class SubmissionsTable extends React.Component {
>
-
);
diff --git a/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap b/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap
new file mode 100644
index 0000000..67585bf
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EmptySubmission component component snapshot 1`] = `
+
+

+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
index f2de01a..1d30142 100644
--- a/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
+++ b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
@@ -12,7 +12,7 @@ exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1
/>
diff --git a/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
index c6818f6..33e2456 100644
--- a/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
+++ b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
@@ -116,9 +116,6 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
>
-
`;
@@ -237,9 +234,6 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: t
>
-
`;
diff --git a/src/containers/ListView/__snapshots__/index.test.jsx.snap b/src/containers/ListView/__snapshots__/index.test.jsx.snap
index e671e52..1bb21ad 100644
--- a/src/containers/ListView/__snapshots__/index.test.jsx.snap
+++ b/src/containers/ListView/__snapshots__/index.test.jsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ListView component component render tests snapshots snapshot: error 1`] = `
+exports[`ListView component component snapshots error 1`] = `
@@ -9,17 +9,30 @@ exports[`ListView component component render tests snapshots snapshot: error 1`]
`;
-exports[`ListView component component render tests snapshots snapshot: loaded 1`] = `
+exports[`ListView component component snapshots loaded has data 1`] = `
-
-
+
+
+
+
`;
-exports[`ListView component component render tests snapshots snapshot: loading 1`] = `
+exports[`ListView component component snapshots loaded with no data 1`] = `
+
+
+
+
+`;
+
+exports[`ListView component component snapshots loading 1`] = `
diff --git a/src/containers/ListView/index.jsx b/src/containers/ListView/index.jsx
index b17802a..bbed627 100644
--- a/src/containers/ListView/index.jsx
+++ b/src/containers/ListView/index.jsx
@@ -2,10 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import {
- Container,
- Spinner,
-} from '@edx/paragon';
+import { Container, Spinner } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors, thunkActions } from 'data/redux';
@@ -16,6 +13,7 @@ import ReviewModal from 'containers/ReviewModal';
import ListError from './ListError';
import ListViewBreadcrumb from './ListViewBreadcrumb';
import SubmissionsTable from './SubmissionsTable';
+import EmptySubmission from './EmptySubmission';
import messages from './messages';
import './ListView.scss';
@@ -29,16 +27,27 @@ export class ListView extends React.Component {
}
render() {
- const { isLoaded, hasError } = this.props;
+ const {
+ isLoaded, hasError, courseId, isEmptySubmissionData,
+ } = this.props;
return (
- { isLoaded && }
- { isLoaded && }
- { hasError && }
- { (!isLoaded && !hasError) && (
+ {isLoaded
+ && (isEmptySubmissionData ? (
+
+ ) : (
+ <>
+
+
+ >
+ ))}
+ {hasError && }
+ {!isLoaded && !hasError && (
-
+
+
+
)}
@@ -46,22 +55,25 @@ export class ListView extends React.Component {
);
}
}
-ListView.defaultProps = {
-};
+ListView.defaultProps = {};
ListView.propTypes = {
// redux
courseId: PropTypes.string.isRequired,
initializeApp: PropTypes.func.isRequired,
isLoaded: PropTypes.bool.isRequired,
- isPending: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
+ isEmptySubmissionData: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
courseId: selectors.app.courseId(state),
- isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.initialize }),
- isPending: selectors.requests.isPending(state, { requestKey: RequestKeys.initialize }),
- hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.initialize }),
+ isLoaded: selectors.requests.isCompleted(state, {
+ requestKey: RequestKeys.initialize,
+ }),
+ hasError: selectors.requests.isFailed(state, {
+ requestKey: RequestKeys.initialize,
+ }),
+ isEmptySubmissionData: selectors.submissions.isEmptySubmissionData(state),
});
export const mapDispatchToProps = {
diff --git a/src/containers/ListView/index.test.jsx b/src/containers/ListView/index.test.jsx
index eca4530..3604dea 100644
--- a/src/containers/ListView/index.test.jsx
+++ b/src/containers/ListView/index.test.jsx
@@ -5,17 +5,14 @@ import { selectors, thunkActions } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import { formatMessage } from 'testUtils';
-import {
- ListView,
- mapStateToProps,
- mapDispatchToProps,
-} from '.';
+import { ListView, mapStateToProps, mapDispatchToProps } from '.';
jest.mock('components/StatusBadge', () => 'StatusBadge');
jest.mock('containers/ReviewModal', () => 'ReviewModal');
jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb');
jest.mock('./ListError', () => 'ListError');
jest.mock('./SubmissionsTable', () => 'SubmissionsTable');
+jest.mock('./EmptySubmission', () => 'EmptySubmission');
jest.mock('data/redux', () => ({
selectors: {
@@ -24,11 +21,10 @@ jest.mock('data/redux', () => ({
},
requests: {
isCompleted: (...args) => ({ isCompleted: args }),
- isPending: (...args) => ({ isPending: args }),
isFailed: (...args) => ({ isFailed: args }),
},
submissions: {
- listData: (...args) => ({ listData: args }),
+ isEmptySubmissionData: (...args) => ({ isEmptySubmissionData: args }),
},
},
thunkActions: {
@@ -51,29 +47,32 @@ describe('ListView component', () => {
const props = {
courseId: 'test-course-id',
isLoaded: false,
- isPending: false,
hasError: false,
+ isEmptySubmissionData: false,
};
beforeEach(() => {
props.initializeApp = jest.fn();
props.intl = { formatMessage };
});
- describe('render tests', () => {
+ describe('snapshots', () => {
beforeEach(() => {
el = shallow();
});
- describe('snapshots', () => {
- test('snapshot: loading', () => {
- expect(el).toMatchSnapshot();
- });
- test('snapshot: loaded', () => {
- el.setProps({ isLoaded: true });
- expect(el.instance().render()).toMatchSnapshot();
- });
- test('snapshot: error', () => {
- el.setProps({ hasError: true });
- expect(el.instance().render()).toMatchSnapshot();
- });
+ test('loading', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('loaded has data', () => {
+ el.setProps({ isLoaded: true });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+
+ test('loaded with no data', () => {
+ el.setProps({ isLoaded: true, isEmptySubmissionData: true });
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ test('error', () => {
+ el.setProps({ hasError: true });
+ expect(el.instance().render()).toMatchSnapshot();
});
});
describe('behavior', () => {
@@ -94,18 +93,26 @@ describe('ListView component', () => {
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
});
test('isLoaded loads from requests.isCompleted', () => {
- expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
- });
- test('isPending loads from requests.isPending', () => {
- expect(mapped.isPending).toEqual(selectors.requests.isPending(testState, { requestKey }));
+ expect(mapped.isLoaded).toEqual(
+ selectors.requests.isCompleted(testState, { requestKey }),
+ );
});
test('hasError loads from requests.isFailed', () => {
- expect(mapped.hasError).toEqual(selectors.requests.isFailed(testState, { requestKey }));
+ expect(mapped.hasError).toEqual(
+ selectors.requests.isFailed(testState, { requestKey }),
+ );
+ });
+ test('isEmptySubmissionData loads from submissions.isEmptySubmissionData', () => {
+ expect(mapped.isEmptySubmissionData).toEqual(
+ selectors.submissions.isEmptySubmissionData(testState),
+ );
});
});
describe('mapDispatchToProps', () => {
it('loads initializeApp from thunkActions.app.initialize', () => {
- expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
+ expect(mapDispatchToProps.initializeApp).toEqual(
+ thunkActions.app.initialize,
+ );
});
});
});
diff --git a/src/containers/ListView/messages.js b/src/containers/ListView/messages.js
index 71c9f33..cc99ffb 100644
--- a/src/containers/ListView/messages.js
+++ b/src/containers/ListView/messages.js
@@ -4,12 +4,17 @@ const messages = defineMessages({
backToResponses: {
id: 'ora-grading.ListView.ListViewBreadcrumbs.backToResponses',
defaultMessage: 'Back to all open responses',
- description: 'Breadcrumbs link text to return to ORA list in LMS.',
+ description: 'Breadcrumbs link text to return to ORA list in LMS',
},
- noResultsFound: {
- id: 'ora-grading.ListView.noResultsFound',
- defaultMessage: 'No results found',
- description: 'Empty table content for submissions list',
+ noResultsFoundTitle: {
+ id: 'ora-grading.ListView.noResultsFoundTitle',
+ defaultMessage: 'Nothing here yet',
+ description: 'Empty table for the submission table title',
+ },
+ noResultsFoundBody: {
+ id: 'ora-grading.ListView.noResultsFoundBody',
+ defaultMessage: 'When learners submit responses, they will appear here',
+ description: 'Empty table messages',
},
viewAllResponses: {
id: 'ora-grading.ListView.viewAllResponses',
diff --git a/src/data/redux/submissions/selectors.js b/src/data/redux/submissions/selectors.js
index 8d56b0e..ac11082 100644
--- a/src/data/redux/submissions/selectors.js
+++ b/src/data/redux/submissions/selectors.js
@@ -4,6 +4,8 @@ import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import { lockStatuses } from 'data/services/lms/constants';
+import * as module from './selectors';
+
export const simpleSelectors = {
allSubmissions: state => state.submissions.allSubmissions,
};
@@ -27,7 +29,12 @@ export const listData = createSelector(
},
);
+export const isEmptySubmissionData = createSelector(
+ [module.listData], (data) => data.length === 0,
+);
+
export default StrictDict({
...simpleSelectors,
listData,
+ isEmptySubmissionData,
});
diff --git a/src/data/redux/submissions/selectors.test.js b/src/data/redux/submissions/selectors.test.js
index a9dcae9..3341d0e 100644
--- a/src/data/redux/submissions/selectors.test.js
+++ b/src/data/redux/submissions/selectors.test.js
@@ -17,7 +17,7 @@ const testState = {
};
describe('submission selectors unit tests', () => {
- const { simpleSelectors, listData } = selectors;
+ const { simpleSelectors, listData, isEmptySubmissionData } = selectors;
describe('allSubmissions', () => {
it('returns allSubmissions entry from submissions data', () => {
expect(simpleSelectors.allSubmissions(testState)).toEqual(
@@ -25,6 +25,7 @@ describe('submission selectors unit tests', () => {
);
});
});
+
describe('listData selector', () => {
let cb;
let preSelectors;
@@ -81,4 +82,22 @@ describe('submission selectors unit tests', () => {
});
});
});
+
+ describe('isEmptySubmissionData', () => {
+ const { cb, preSelectors } = isEmptySubmissionData;
+ const emptySubmission = [];
+ const noneEmptySubmission = ['some submission'];
+
+ it('is a emmoized selector based on submissions.listData', () => {
+ expect(preSelectors).toEqual([listData]);
+ });
+
+ it('returns true on empty submission', () => {
+ expect(cb(emptySubmission)).toEqual(true);
+ });
+
+ it('return false if submission is not empty', () => {
+ expect(cb(noneEmptySubmission)).toEqual(false);
+ });
+ });
});