From e656534a51322dc6c45deca89b31bd7605c0344a Mon Sep 17 00:00:00 2001 From: Leangseu Kim Date: Fri, 25 Feb 2022 10:27:28 -0500 Subject: [PATCH] feat: empty submission components chore: update usage of empty submission in list view chore: remove unused test: add unit testing --- public/assets/empty-state.svg | 44 +++++++++++++ src/containers/ListView/EmptySubmission.jsx | 34 +++++++++++ .../ListView/EmptySubmission.test.jsx | 31 ++++++++++ src/containers/ListView/ListView.scss | 17 ++++++ src/containers/ListView/SubmissionsTable.jsx | 1 - .../EmptySubmission.test.jsx.snap | 40 ++++++++++++ .../ListViewBreadcrumb.test.jsx.snap | 2 +- .../SubmissionsTable.test.jsx.snap | 6 -- .../__snapshots__/index.test.jsx.snap | 23 +++++-- src/containers/ListView/index.jsx | 44 ++++++++----- src/containers/ListView/index.test.jsx | 61 +++++++++++-------- src/containers/ListView/messages.js | 15 +++-- src/data/redux/submissions/selectors.js | 7 +++ src/data/redux/submissions/selectors.test.js | 21 ++++++- 14 files changed, 284 insertions(+), 62 deletions(-) create mode 100644 public/assets/empty-state.svg create mode 100644 src/containers/ListView/EmptySubmission.jsx create mode 100644 src/containers/ListView/EmptySubmission.test.jsx create mode 100644 src/containers/ListView/__snapshots__/EmptySubmission.test.jsx.snap 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 }) => ( +
+ empty state +

+ +

+

+ +

+ + + +
+); + +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`] = ` +
+ empty state +

+ +

+

+ +

+ + + +
+`; 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); + }); + }); });