diff --git a/src/containers/CourseFilterControls/CourseFilterControls.jsx b/src/containers/CourseFilterControls/CourseFilterControls.jsx
index 59d00cd..e145b32 100644
--- a/src/containers/CourseFilterControls/CourseFilterControls.jsx
+++ b/src/containers/CourseFilterControls/CourseFilterControls.jsx
@@ -92,7 +92,7 @@ export const CourseFilterControls = ({
{
});
const handleRemoveFilter = (filter) => () => setFilters.remove(filter);
const setPageNumber = (value) => dispatch(actions.app.setPageNumber(value));
+ const initIsPending = appHooks.useIsPendingRequest(RequestKeys.initialize);
+
return {
pageNumber,
numPages,
@@ -40,6 +43,7 @@ export const useCourseListData = () => {
handleRemoveFilter,
},
showFilters: filters.length > 0,
+ initIsPending,
};
};
diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js
index dfc9c22..9ff166c 100644
--- a/src/containers/CourseList/hooks.test.js
+++ b/src/containers/CourseList/hooks.test.js
@@ -15,6 +15,7 @@ jest.mock('data/redux', () => ({
hooks: {
useCurrentCourseList: jest.fn(),
usePageNumber: jest.fn(() => 23),
+ useIsPendingRequest: jest.fn(),
},
}));
@@ -34,6 +35,11 @@ const testCheckboxSetValues = [testFilters, testSetFilters];
describe('CourseList hooks', () => {
let out;
+
+ appHooks.useCurrentCourseList.mockReturnValue(testListData);
+ appHooks.useIsPendingRequest.mockReturnValue(false);
+ paragon.useCheckboxSetValues.mockImplementation(() => testCheckboxSetValues);
+
describe('state values', () => {
state.testGetter(state.keys.sortBy);
jest.clearAllMocks();
@@ -44,8 +50,6 @@ describe('CourseList hooks', () => {
beforeEach(() => {
state.mock();
state.mockVal(state.keys.sortBy, testSortBy);
- paragon.useCheckboxSetValues.mockImplementationOnce(() => testCheckboxSetValues);
- appHooks.useCurrentCourseList.mockReturnValueOnce(testListData);
out = hooks.useCourseListData();
});
describe('behavior', () => {
@@ -72,12 +76,17 @@ describe('CourseList hooks', () => {
test('showFilters is true iff filters is not empty', () => {
expect(out.showFilters).toEqual(true);
state.mockVal(state.keys.sortBy, testSortBy);
- appHooks.useCurrentCourseList.mockReturnValueOnce(testListData);
+ paragon.useCheckboxSetValues.mockReturnValueOnce([[], testSetFilters]);
out = hooks.useCourseListData();
- // checkbox values default to returning a list, and were only overridden once.
- // Thus this time, the values for the list should be empty.
+ // don't show filter when list is empty.
expect(out.showFilters).toEqual(false);
});
+ test('initIsPending loads from useIsPendingRequest', () => {
+ expect(out.initIsPending).toEqual(false);
+ appHooks.useIsPendingRequest.mockReturnValueOnce(true);
+ out = hooks.useCourseListData();
+ expect(out.initIsPending).toEqual(true);
+ });
describe('filterOptions', () => {
test('sortBy and setSortBy are connected to the state value', () => {
expect(out.filterOptions.sortBy).toEqual(testSortBy);
diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx
index d57aae9..88ff7a1 100644
--- a/src/containers/CourseList/index.jsx
+++ b/src/containers/CourseList/index.jsx
@@ -1,9 +1,12 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { Pagination } from '@edx/paragon';
+import { Pagination, Spinner } from '@edx/paragon';
-import { ActiveCourseFilters, CourseFilterControls } from 'containers/CourseFilterControls';
+import {
+ ActiveCourseFilters,
+ CourseFilterControls,
+} from 'containers/CourseFilterControls';
import CourseCard from 'containers/CourseCard';
import { useCourseListData } from './hooks';
@@ -20,21 +23,21 @@ export const CourseList = () => {
numPages,
showFilters,
visibleList,
+ initIsPending,
} = useCourseListData();
- return (
+ return initIsPending ? (
+
+
+
+ ) : (
-
- {formatMessage(messages.myCourses)}
-
-
+
{formatMessage(messages.myCourses)}
+
- { showFilters && (
+ {showFilters && (
@@ -43,7 +46,7 @@ export const CourseList = () => {
{visibleList.map(({ cardId }) => (
))}
- {(numPages > 1) && (
+ {numPages > 1 && (
{
);
};
-CourseList.propTypes = {
-};
+CourseList.propTypes = {};
export default CourseList;
diff --git a/src/containers/CourseList/index.scss b/src/containers/CourseList/index.scss
index 6dd1e29..eeeeec1 100644
--- a/src/containers/CourseList/index.scss
+++ b/src/containers/CourseList/index.scss
@@ -2,3 +2,10 @@
display: flex;
justify-content: space-between;
}
+
+.course-list-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+}
\ No newline at end of file
diff --git a/src/containers/CourseList/index.test.jsx b/src/containers/CourseList/index.test.jsx
index 691a552..b848583 100644
--- a/src/containers/CourseList/index.test.jsx
+++ b/src/containers/CourseList/index.test.jsx
@@ -20,6 +20,7 @@ describe('CourseList', () => {
setPageNumber: jest.fn().mockName('setPageNumber'),
showFilters: false,
visibleList: [],
+ initIsPending: false,
};
const createWrapper = (courseListData) => {
useCourseListData.mockReturnValueOnce({
@@ -30,6 +31,10 @@ describe('CourseList', () => {
};
describe('snapshots', () => {
+ it('renders loading', () => {
+ const wrapper = createWrapper({ initIsPending: true });
+ expect(wrapper).toMatchSnapshot();
+ });
test('with no filters', () => {
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();
diff --git a/src/containers/Dashboard/__snapshots__/index.test.jsx.snap b/src/containers/Dashboard/__snapshots__/index.test.jsx.snap
index fb763df..30cc0f3 100644
--- a/src/containers/Dashboard/__snapshots__/index.test.jsx.snap
+++ b/src/containers/Dashboard/__snapshots__/index.test.jsx.snap
@@ -10,7 +10,7 @@ exports[`Dashboard snapshots there are available dashboards 1`] = `
`;
-exports[`Dashboard snapshots there are courses 1`] = `
+exports[`Dashboard snapshots there are courses, or they are still loading 1`] = `
{
const hasCourses = appHooks.useHasCourses();
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
+ const initIsPending = appHooks.useIsPendingRequest(RequestKeys.initialize);
+
return (
{hasAvailableDashboards && }
- {hasCourses ? (
+ {initIsPending || (!initIsPending && hasCourses) ? (
({
useHasCourses: jest.fn(),
useHasAvailableDashboards: jest.fn(),
useShowSelectSessionModal: jest.fn(),
+ useIsPendingRequest: jest.fn(),
},
}));
@@ -34,21 +35,32 @@ describe('Dashboard', () => {
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
+ initIsPending,
}) => {
hooks.useHasCourses.mockReturnValueOnce(hasCourses);
hooks.useHasAvailableDashboards.mockReturnValueOnce(hasAvailableDashboards);
hooks.useShowSelectSessionModal.mockReturnValueOnce(showSelectSessionModal);
+ hooks.useIsPendingRequest.mockReturnValueOnce(initIsPending);
return shallow();
};
describe('snapshots', () => {
- test('there are courses', () => {
- const wrapper = createWrapper({
+ test('there are courses, or they are still loading', () => {
+ const pendingNoCoursesWrapper = createWrapper({
+ hasCourses: false,
+ hasAvailableDashboards: false,
+ showSelectSessionModal: false,
+ initIsPending: true,
+ });
+ expect(pendingNoCoursesWrapper).toMatchSnapshot();
+
+ const doneLoadingWithCoursesWrapper = createWrapper({
hasCourses: true,
hasAvailableDashboards: false,
showSelectSessionModal: false,
+ initIsPending: false,
});
- expect(wrapper).toMatchSnapshot();
+ expect(doneLoadingWithCoursesWrapper).toEqual(pendingNoCoursesWrapper);
});
test('there are no courses', () => {
@@ -56,6 +68,7 @@ describe('Dashboard', () => {
hasCourses: false,
hasAvailableDashboards: false,
showSelectSessionModal: false,
+ initIsPending: false,
});
expect(wrapper).toMatchSnapshot();
});
@@ -65,6 +78,7 @@ describe('Dashboard', () => {
hasCourses: false,
hasAvailableDashboards: true,
showSelectSessionModal: false,
+ initIsPending: false,
});
expect(wrapper).toMatchSnapshot();
});
@@ -74,6 +88,7 @@ describe('Dashboard', () => {
hasCourses: false,
hasAvailableDashboards: false,
showSelectSessionModal: true,
+ initIsPending: false,
});
expect(wrapper).toMatchSnapshot();
});
@@ -85,6 +100,7 @@ describe('Dashboard', () => {
hasCourses: false,
hasAvailableDashboards: false,
showSelectSessionModal: false,
+ initIsPending: false,
});
expect(wrapper.find(EmptyCourse).length).toEqual(1);
expect(wrapper.find(CourseList).length).toEqual(0);
@@ -97,6 +113,7 @@ describe('Dashboard', () => {
hasCourses: true,
hasAvailableDashboards: true,
showSelectSessionModal: true,
+ initIsPending: false,
});
expect(wrapper.find(EmptyCourse).length).toEqual(0);
expect(wrapper.find(CourseList).length).toEqual(1);
@@ -109,6 +126,7 @@ describe('Dashboard', () => {
hasCourses: true,
hasAvailableDashboards: true,
showSelectSessionModal: false,
+ initIsPending: false,
});
expect(wrapper.find(EmptyCourse).length).toEqual(0);
expect(wrapper.find(CourseList).length).toEqual(1);
diff --git a/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap b/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap
index 6434cb7..b9036e4 100644
--- a/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap
+++ b/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap
@@ -20,12 +20,16 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
value=""
/>
-
+ labels={
+ Object {
+ "default": "Submit",
+ }
+ }
+ state="default"
+ variant="brand"
+ />
`;
@@ -49,12 +53,16 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
value="test"
/>
-
+ labels={
+ Object {
+ "default": "Submit",
+ }
+ }
+ state="default"
+ variant="brand"
+ />
`;
@@ -80,17 +88,55 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
value=""
/>
test-error
-
+
+`;
+
+exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
+
+
- Submit
-
+ View as:
+
+
+
+
+
`;
diff --git a/src/containers/MasqueradeBar/hooks.js b/src/containers/MasqueradeBar/hooks.js
index 54196cf..1142945 100644
--- a/src/containers/MasqueradeBar/hooks.js
+++ b/src/containers/MasqueradeBar/hooks.js
@@ -1,8 +1,8 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
-import { thunkActions, selectors } from 'data/redux';
+import { thunkActions, hooks as appHooks } from 'data/redux';
import { StrictDict } from 'utils';
import * as module from './hooks';
@@ -32,14 +32,16 @@ export const useMasqueradeBarData = ({
const {
isMasquerading,
isMasqueradingFailed,
+ isMasqueradingPending,
masqueradeError,
- } = useSelector(selectors.requests.masquerade);
+ } = appHooks.useMasqueradeData();
const { masqueradeInput, handleMasqueradeInputChange } = module.useMasqueradeInput();
return {
canMasquerade,
isMasquerading,
isMasqueradingFailed,
+ isMasqueradingPending,
masqueradeError,
masqueradeInput,
handleMasqueradeSubmit,
diff --git a/src/containers/MasqueradeBar/hooks.test.js b/src/containers/MasqueradeBar/hooks.test.js
index ab0773a..01ebdd9 100644
--- a/src/containers/MasqueradeBar/hooks.test.js
+++ b/src/containers/MasqueradeBar/hooks.test.js
@@ -1,7 +1,6 @@
import { MockUseState } from 'testUtils';
-import { thunkActions } from 'data/redux';
+import { thunkActions, hooks as appHooks } from 'data/redux';
-import { useSelector } from 'react-redux';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
@@ -11,58 +10,70 @@ jest.mock('data/redux', () => ({
clearMasquerade: jest.fn(),
},
},
- selectors: {
- requests: {
- masquerade: jest.fn(),
- },
+ hooks: {
+ useMasqueradeData: jest.fn(),
},
}));
const state = new MockUseState(hooks);
describe('MasqueradeBar hooks', () => {
- let out;
const authenticatedUser = {
administrator: true,
};
+ const defaultMasqueradeData = {
+ isMasquerading: false,
+ isMasqueradingFailed: false,
+ isMasqueradingPending: false,
+ masqueradeError: null,
+ };
+ const createHook = (masqueradeData = {}, user) => {
+ appHooks.useMasqueradeData.mockReturnValueOnce({
+ ...defaultMasqueradeData,
+ ...masqueradeData,
+ });
+ return hooks.useMasqueradeBarData({ authenticatedUser: user || authenticatedUser });
+ };
+
describe('state values', () => {
state.testGetter(state.keys.masqueradeInput);
});
describe('useMasqueradeBarData', () => {
- beforeEach(() => {
- state.mock();
- out = hooks.useMasqueradeBarData({ authenticatedUser });
- });
+ beforeEach(() => state.mock());
afterEach(state.restore);
test('canMasquerade', () => {
+ const out = createHook();
expect(out.canMasquerade).toEqual(true);
});
test('cannotMasquerade', () => {
- out = hooks.useMasqueradeBarData({
- authenticatedUser: {
- administrator: false,
- },
- });
+ const out = createHook({}, { administrator: false });
expect(out.canMasquerade).toEqual(false);
});
test('masqueradeError', () => {
- expect(out.masqueradeError).toBeUndefined();
- useSelector.mockReturnValueOnce({
- masqueradeError: 'test error',
- });
- out = hooks.useMasqueradeBarData({ authenticatedUser });
+ let out = createHook();
+ expect(out.masqueradeError).toBeNull();
+ out = createHook({ masqueradeError: 'test error' });
expect(out.masqueradeError).toEqual('test error');
});
+ test('isMasqueradePending', () => {
+ let out = createHook();
+ expect(out.isMasqueradingPending).toEqual(false);
+ out = createHook({ isMasqueradingPending: true });
+ expect(out.isMasqueradingPending).toEqual(true);
+ });
test('handleMasqueradeInputChange', () => {
+ const out = createHook();
expect(state.stateVals.masqueradeInput).toEqual('');
out.handleMasqueradeInputChange({ target: { value: 'test' } });
expect(state.setState.masqueradeInput).toHaveBeenCalledWith('test');
});
test('handleMasqueradeSubmit', () => {
+ const out = createHook();
out.handleMasqueradeSubmit('test')();
expect(thunkActions.app.masqueradeAs).toHaveBeenCalledWith('test');
});
test('handleClearMasquerade', () => {
+ const out = createHook();
out.handleClearMasquerade();
expect(thunkActions.app.clearMasquerade).toHaveBeenCalled();
});
diff --git a/src/containers/MasqueradeBar/index.jsx b/src/containers/MasqueradeBar/index.jsx
index 20eb2ab..2cf8fe1 100644
--- a/src/containers/MasqueradeBar/index.jsx
+++ b/src/containers/MasqueradeBar/index.jsx
@@ -3,11 +3,11 @@ import { AppContext } from '@edx/frontend-platform/react';
import {
Chip,
- Button,
FormControl,
FormControlFeedback,
FormLabel,
FormGroup,
+ StatefulButton,
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
@@ -22,6 +22,7 @@ export const MasqueradeBar = () => {
canMasquerade,
isMasquerading,
isMasqueradingFailed,
+ isMasqueradingPending,
masqueradeInput,
masqueradeError,
handleMasqueradeInputChange,
@@ -59,18 +60,20 @@ export const MasqueradeBar = () => {
floatingLabel={formatMessage(messages.StudentNameInput)}
/>
{isMasqueradingFailed && (
-
+
{masqueradeError}
)}
-
+ labels={{
+ default: formatMessage(messages.SubmitButton),
+ }}
+ state={isMasqueradingPending ? 'pending' : 'default'}
+ />
>
)}
diff --git a/src/containers/MasqueradeBar/index.test.jsx b/src/containers/MasqueradeBar/index.test.jsx
index f3704e7..84382e5 100644
--- a/src/containers/MasqueradeBar/index.test.jsx
+++ b/src/containers/MasqueradeBar/index.test.jsx
@@ -14,6 +14,7 @@ describe('MasqueradeBar', () => {
canMasquerade: true,
isMasquerading: false,
isMasqueradingFailed: false,
+ isMasqueradingPending: false,
masqueradeInput: '',
masqueradeError: '',
handleMasqueradeInputChange: jest.fn().mockName('handleMasqueradeInputChange'),
@@ -57,5 +58,12 @@ describe('MasqueradeBar', () => {
});
expect(shallow(