feat: home studio search filters (#846)
* feat: pagination studio home for courses * chore: addressing some comments * refactor: addressing pr comments * test: adding test for studio home slice * feat: search input and filters for course home * fix: using open edx paragon * feat: usedebounce hook for searching courses * fix: filters params for searching coruses * feat: adding coursekey when course name is empty * chore: remove edit in studio button * fix: message changed when courses were not found * refactor: support courses tab filters and pagination * test: more cases for course filters component * refactor: coverage for onsubmit search field * test: unit test for courses tab component * feat: loading for search input and layout of course tab * fix: linter problems * test: adding more tests for courses tab * refactor: don't ignore empty string as a case for searching * refactor: manage empty search bar as special case for searching * fix: remove expected dispatch mock for clear button --------- Co-authored-by: Maria Grimaldi <maria.grimaldi@edunext.co>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const LoadingSpinner = () => (
|
||||
export const LoadingSpinner = ({ size }) => (
|
||||
<Spinner
|
||||
animation="border"
|
||||
role="status"
|
||||
variant="primary"
|
||||
size={size}
|
||||
screenReaderText={(
|
||||
<FormattedMessage
|
||||
id="authoring.loading"
|
||||
@@ -17,6 +19,14 @@ export const LoadingSpinner = () => (
|
||||
/>
|
||||
);
|
||||
|
||||
LoadingSpinner.defaultProps = {
|
||||
size: undefined,
|
||||
};
|
||||
|
||||
LoadingSpinner.propTypes = {
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
const Loading = () => (
|
||||
<div className="d-flex justify-content-center align-items-center flex-column vh-100">
|
||||
<LoadingSpinner />
|
||||
|
||||
@@ -38,6 +38,7 @@ const StudioHome = ({ intl }) => {
|
||||
showNewCourseContainer,
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
isFiltered,
|
||||
setShowNewCourseContainer,
|
||||
dispatch,
|
||||
} = useStudioHome(isPaginationCoursesEnabled);
|
||||
@@ -99,7 +100,7 @@ const StudioHome = ({ intl }) => {
|
||||
}
|
||||
|
||||
const headerButtons = userIsActive ? getHeaderButtons() : [];
|
||||
if (isLoadingPage) {
|
||||
if (isLoadingPage && !isFiltered) {
|
||||
return (<Loading />);
|
||||
}
|
||||
|
||||
@@ -138,7 +139,7 @@ const StudioHome = ({ intl }) => {
|
||||
tabsData={studioHomeData}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={() => setShowNewCourseContainer(true)}
|
||||
isShowProcessing={isShowProcessing}
|
||||
isShowProcessing={isShowProcessing && !isFiltered}
|
||||
dispatch={dispatch}
|
||||
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
|
||||
/>
|
||||
|
||||
@@ -47,7 +47,7 @@ module.exports = {
|
||||
org: 'org.0',
|
||||
rerunLink: null,
|
||||
run: 'Run_0',
|
||||
url: null,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
inProcessCourseActions: [],
|
||||
|
||||
@@ -87,4 +87,20 @@ describe('<CardItem />', () => {
|
||||
expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render course key if displayname is empty', () => {
|
||||
const props = studioHomeMock.courses[1];
|
||||
const courseKeyTest = 'course-key';
|
||||
const { getByText } = render(
|
||||
<RootWrapper
|
||||
{...props}
|
||||
displayName=""
|
||||
courseKey={courseKeyTest}
|
||||
lmsLink="lmsLink"
|
||||
rerunLink="returnLink"
|
||||
url="url"
|
||||
/>,
|
||||
);
|
||||
expect(getByText(courseKeyTest)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ const CardItem = ({
|
||||
const isShowRerunLink = allowCourseReruns
|
||||
&& rerunCreatorStatus
|
||||
&& courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const hasDisplayName = displayName.trim().length ? displayName : courseKey;
|
||||
|
||||
return (
|
||||
<Card className="card-item">
|
||||
@@ -51,7 +52,7 @@ const CardItem = ({
|
||||
className="card-item-title"
|
||||
destination={courseUrl().toString()}
|
||||
>
|
||||
{displayName}
|
||||
{hasDisplayName}
|
||||
</Hyperlink>
|
||||
) : (
|
||||
<span className="card-item-title">{displayName}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { camelCaseObject, snakeCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
@@ -30,7 +30,8 @@ export async function getStudioHomeCourses(search) {
|
||||
* Please refer to this PR for further details: https://github.com/openedx/edx-platform/pull/34173
|
||||
*/
|
||||
export async function getStudioHomeCoursesV2(search, customParams) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParams });
|
||||
const customParamsFormat = snakeCaseObject(customParams);
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParamsFormat });
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ const slice = createSlice({
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
order: 'display_name',
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
isFiltered: false,
|
||||
cleanFilters: false,
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
@@ -51,8 +57,7 @@ const slice = createSlice({
|
||||
state.studioHomeData.libraries = libraries;
|
||||
},
|
||||
updateStudioHomeCoursesCustomParams: (state, { payload }) => {
|
||||
const { currentPage } = payload;
|
||||
state.studioHomeCoursesRequestParams.currentPage = currentPage;
|
||||
Object.assign(state.studioHomeCoursesRequestParams, payload);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { reducer, updateStudioHomeCoursesCustomParams } from './slice'; // Assuming the file is named slice.js
|
||||
import { reducer, updateStudioHomeCoursesCustomParams } from './slice';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
@@ -17,6 +17,12 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
studioHomeData: {},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
order: 'display_name',
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
isFiltered: false,
|
||||
cleanFilters: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,15 +31,28 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
|
||||
expect(result).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should update the currentPage in studioHomeCoursesRequestParams', () => {
|
||||
it('should update the payload passed in studioHomeCoursesRequestParams', () => {
|
||||
const newState = {
|
||||
...initialState,
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 2,
|
||||
search: 'test',
|
||||
order: 'display_name',
|
||||
archivedOnly: true,
|
||||
activeOnly: true,
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = {
|
||||
currentPage: 2,
|
||||
search: 'test',
|
||||
order: 'display_name',
|
||||
archivedOnly: true,
|
||||
activeOnly: true,
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
};
|
||||
|
||||
const result = reducer(initialState, updateStudioHomeCoursesCustomParams(payload));
|
||||
|
||||
@@ -49,51 +49,35 @@ export const generateGetStudioHomeDataApiResponse = () => ({
|
||||
});
|
||||
|
||||
export const generateGetStudioCoursesApiResponse = () => ({
|
||||
archivedCourses: [
|
||||
{
|
||||
courseKey: 'course-v1:MachineLearning+123+2023',
|
||||
displayName: 'Machine Learning',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:MachineLearning+123+2023/jump_to/block-v1:MachineLearning+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'LSE',
|
||||
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:MachineLearning+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'course-v1:Design+123+e.g.2025',
|
||||
displayName: 'Design',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:Design+123+e.g.2025/jump_to/block-v1:Design+123+e.g.2025+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'University of Cape Town',
|
||||
rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025',
|
||||
run: 'e.g.2025',
|
||||
url: '/course/course-v1:Design+123+e.g.2025',
|
||||
},
|
||||
],
|
||||
courses: [
|
||||
{
|
||||
courseKey: 'course-v1:HarvardX+123+2023',
|
||||
displayName: 'Managing Risk in the Information Age',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'HarvardX',
|
||||
rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:HarvardX+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'org.0/course_0/Run_0',
|
||||
displayName: 'Run 0',
|
||||
lmsLink: null,
|
||||
number: 'course_0',
|
||||
org: 'org.0',
|
||||
rerunLink: null,
|
||||
run: 'Run_0',
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
inProcessCourseActions: [],
|
||||
count: 5,
|
||||
next: null,
|
||||
previous: null,
|
||||
numPages: 2,
|
||||
results: {
|
||||
courses: [
|
||||
{
|
||||
courseKey: 'course-v1:HarvardX+123+2023',
|
||||
displayName: 'Managing Risk in the Information Age',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
|
||||
number: '123',
|
||||
org: 'HarvardX',
|
||||
rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
|
||||
run: '2023',
|
||||
url: '/course/course-v1:HarvardX+123+2023',
|
||||
},
|
||||
{
|
||||
courseKey: 'org.0/course_0/Run_0',
|
||||
displayName: 'Run 0',
|
||||
lmsLink: null,
|
||||
number: 'course_0',
|
||||
org: 'org.0',
|
||||
rerunLink: null,
|
||||
run: 'Run_0',
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
inProcessCourseActions: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const generateGetStudioCoursesApiResponseV2 = () => ({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getLoadingStatuses,
|
||||
getSavingStatuses,
|
||||
getStudioHomeData,
|
||||
getStudioHomeCoursesParams,
|
||||
} from './data/selectors';
|
||||
import { updateSavingStatuses } from './data/slice';
|
||||
|
||||
@@ -17,6 +18,8 @@ const useStudioHome = (isPaginated = false) => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const studioHomeData = useSelector(getStudioHomeData);
|
||||
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
|
||||
const { isFiltered } = studioHomeCoursesParams;
|
||||
const newCourseData = useSelector(getCourseData);
|
||||
const { studioHomeLoadingStatus } = useSelector(getLoadingStatuses);
|
||||
const savingCreateRerunStatus = useSelector(getSavingStatus);
|
||||
@@ -89,6 +92,7 @@ const useStudioHome = (isPaginated = false) => {
|
||||
courseCreatorSavingStatus,
|
||||
isShowOrganizationDropdown,
|
||||
hasAbilityToCreateNewCourse,
|
||||
isFiltered,
|
||||
dispatch,
|
||||
setShowNewCourseContainer,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ const ProcessingCourses = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-gray-500 small">
|
||||
<div className="text-gray-500 small" data-testid="processing-courses-title">
|
||||
{intl.formatMessage(messages.processingTitle)}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
@@ -88,8 +88,9 @@ describe('<TabsSection />', () => {
|
||||
describe('course tab', () => {
|
||||
it('should render specific course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { results: data } = generateGetStudioCoursesApiResponse();
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
expect(screen.getByText(studioHomeMock.courses[0].displayName)).toBeVisible();
|
||||
@@ -101,7 +102,7 @@ describe('<TabsSection />', () => {
|
||||
|
||||
it('should render default sections when courses are empty', async () => {
|
||||
const data = generateGetStudioCoursesApiResponse();
|
||||
data.courses = [];
|
||||
data.results.courses = [];
|
||||
|
||||
render(<RootWrapper />);
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
@@ -187,8 +188,10 @@ describe('<TabsSection />', () => {
|
||||
describe('archived tab', () => {
|
||||
it('should switch to Archived tab and render specific archived course details', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { results: data } = generateGetStudioCoursesApiResponse();
|
||||
data.archivedCourses = studioHomeMock.archivedCourses;
|
||||
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||
axiosMock.onGet(courseApiLink).reply(200, data);
|
||||
await executeThunk(fetchStudioHomeData(), store.dispatch);
|
||||
|
||||
const archivedTab = screen.getByText(tabMessages.archivedTabTitle.defaultMessage);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursesFilters snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="d-flex"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-row"
|
||||
>
|
||||
<div
|
||||
class="pgn__searchfield d-flex mr-4"
|
||||
data-testid="input-filter-courses-search"
|
||||
>
|
||||
<form
|
||||
class="pgn__searchfield-form"
|
||||
role="search"
|
||||
>
|
||||
<label
|
||||
class="m-0"
|
||||
for="pgn-searchfield-input-1"
|
||||
>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
search
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="pgn-searchfield-input-1"
|
||||
name="searchfield-input"
|
||||
placeholder="Search"
|
||||
role="searchbox"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="btn"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5Zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
>
|
||||
submit search
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
id="dropdown-toggle-dropdown-toggle-course-type-menu"
|
||||
>
|
||||
<button
|
||||
alt="dropdown-toggle-menu-items"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle-menu-items dropdown-toggle btn btn-none"
|
||||
data-testid="dropdown-toggle-course-type-menu"
|
||||
id="dropdown-toggle-course-type-menu"
|
||||
type="button"
|
||||
>
|
||||
All courses
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
id="dropdown-toggle-dropdown-toggle-courses-order-menu"
|
||||
>
|
||||
<button
|
||||
alt="dropdown-toggle-menu-items"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle-menu-items dropdown-toggle btn btn-none"
|
||||
data-testid="dropdown-toggle-courses-order-menu"
|
||||
id="dropdown-toggle-courses-order-menu"
|
||||
type="button"
|
||||
>
|
||||
Name A-Z
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursesFilterMenu snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
id="dropdown-toggle-course-filter-menu-toggle"
|
||||
>
|
||||
<button
|
||||
alt="dropdown-toggle-menu-items"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle-menu-items dropdown-toggle btn btn-none"
|
||||
data-testid="course-filter-menu-toggle"
|
||||
id="course-filter-menu-toggle"
|
||||
type="button"
|
||||
>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Icon, Dropdown } from '@openedx/paragon';
|
||||
import { Check } from '@openedx/paragon/icons';
|
||||
import { getStudioHomeCoursesParams } from '../../../../data/selectors';
|
||||
|
||||
const CoursesFilterMenu = ({
|
||||
id: idProp,
|
||||
menuItems,
|
||||
onItemMenuSelected,
|
||||
defaultItemSelectedText,
|
||||
}) => {
|
||||
const [itemMenuSelected, setItemMenuSelected] = useState(defaultItemSelectedText);
|
||||
const { cleanFilters } = useSelector(getStudioHomeCoursesParams);
|
||||
const handleCourseTypeSelected = (name, value) => {
|
||||
setItemMenuSelected(name);
|
||||
onItemMenuSelected(value);
|
||||
};
|
||||
|
||||
const courseTypeSelectedIcon = (itemValue) => (itemValue === itemMenuSelected ? (
|
||||
<Icon src={Check} className="ml-2" data-testid="menu-item-icon" />
|
||||
) : null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cleanFilters) {
|
||||
setItemMenuSelected(defaultItemSelectedText);
|
||||
}
|
||||
}, [cleanFilters]);
|
||||
|
||||
return (
|
||||
<Dropdown id={`dropdown-toggle-${idProp}`}>
|
||||
<Dropdown.Toggle
|
||||
alt="dropdown-toggle-menu-items"
|
||||
id={idProp}
|
||||
variant="none"
|
||||
className="dropdown-toggle-menu-items"
|
||||
data-testid={idProp}
|
||||
>
|
||||
{itemMenuSelected}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{menuItems.map(({ id, name, value }) => (
|
||||
<Dropdown.Item
|
||||
key={id}
|
||||
onClick={() => handleCourseTypeSelected(name, value)}
|
||||
data-testid={`item-menu-${id}`}
|
||||
>
|
||||
{name} {courseTypeSelectedIcon(name)}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
CoursesFilterMenu.defaultProps = {
|
||||
defaultItemSelectedText: '',
|
||||
menuItems: [],
|
||||
};
|
||||
|
||||
CoursesFilterMenu.propTypes = {
|
||||
onItemMenuSelected: PropTypes.func.isRequired,
|
||||
defaultItemSelectedText: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
menuItems: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export default CoursesFilterMenu;
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { screen, fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import CoursesFilterMenu from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CoursesFilterMenu', () => {
|
||||
const onCourseTypeSelectedMock = jest.fn();
|
||||
|
||||
const menuItemsMock = [
|
||||
{
|
||||
id: 'active-courses',
|
||||
name: 'Active',
|
||||
value: 'active-courses',
|
||||
},
|
||||
{
|
||||
id: 'upcoming-courses',
|
||||
name: 'Upcoming',
|
||||
value: 'upcoming-courses',
|
||||
},
|
||||
{
|
||||
id: 'archived-courses',
|
||||
name: 'Archived',
|
||||
value: 'archived-courses',
|
||||
},
|
||||
];
|
||||
|
||||
const renderComponent = (overrideProps = {}) => render(
|
||||
<CoursesFilterMenu
|
||||
menuItems={menuItemsMock}
|
||||
onItemMenuSelected={onCourseTypeSelectedMock}
|
||||
id="course-filter-menu-toggle"
|
||||
{...overrideProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useSelector.mockReturnValue({
|
||||
currentPage: 1,
|
||||
order: 'display_name',
|
||||
search: '',
|
||||
activeOnly: false,
|
||||
archivedOnly: false,
|
||||
cleanFilters: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('snapshot', () => {
|
||||
const { container } = renderComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
renderComponent();
|
||||
const courseFilterMenuToggle = screen.getByTestId('course-filter-menu-toggle');
|
||||
expect(courseFilterMenuToggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the items when the menu is clicked', () => {
|
||||
renderComponent();
|
||||
const courseFilterMenuToggle = screen.getByTestId('course-filter-menu-toggle');
|
||||
expect(courseFilterMenuToggle).toBeInTheDocument();
|
||||
fireEvent.click(courseFilterMenuToggle);
|
||||
const activeCoursesMenuItem = screen.getByText('Active');
|
||||
const upcomingCoursesMenuItem = screen.getByText('Upcoming');
|
||||
const archiveCoursesMenuItem = screen.getByText('Archived');
|
||||
expect(activeCoursesMenuItem).toBeInTheDocument();
|
||||
expect(upcomingCoursesMenuItem).toBeInTheDocument();
|
||||
expect(archiveCoursesMenuItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show an icon when a menu item is selected', () => {
|
||||
renderComponent();
|
||||
const courseFilterMenuToggle = screen.getByTestId('course-filter-menu-toggle');
|
||||
expect(courseFilterMenuToggle).toBeInTheDocument();
|
||||
fireEvent.click(courseFilterMenuToggle);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
fireEvent.click(courseFilterMenuToggle);
|
||||
expect(screen.getByTestId('menu-item-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onCourseTypeSelected function when a menu item is selected ', () => {
|
||||
renderComponent();
|
||||
const courseFilterMenuToggle = screen.getByTestId('course-filter-menu-toggle');
|
||||
expect(courseFilterMenuToggle).toBeInTheDocument();
|
||||
fireEvent.click(courseFilterMenuToggle);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
expect(onCourseTypeSelectedMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursesTypesFilterMenu snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
id="dropdown-toggle-dropdown-toggle-courses-order-menu"
|
||||
>
|
||||
<button
|
||||
alt="dropdown-toggle-menu-items"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle-menu-items dropdown-toggle btn btn-none"
|
||||
data-testid="dropdown-toggle-courses-order-menu"
|
||||
id="dropdown-toggle-courses-order-menu"
|
||||
type="button"
|
||||
>
|
||||
Name A-Z
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import CoursesFilterMenu from '../courses-filter-menu';
|
||||
|
||||
const CoursesOrderFilterMenu = ({ onItemMenuSelected }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const courseOrders = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'az-courses',
|
||||
name: intl.formatMessage(messages.coursesOrderFilterMenuAscendantCurses),
|
||||
value: 'azCourses',
|
||||
},
|
||||
{
|
||||
id: 'za-courses',
|
||||
name: intl.formatMessage(messages.coursesOrderFilterMenuDescendantCurses),
|
||||
value: 'zaCourses',
|
||||
},
|
||||
{
|
||||
id: 'newest-courses',
|
||||
name: intl.formatMessage(messages.coursesOrderFilterMenuNewestCurses),
|
||||
value: 'newestCourses',
|
||||
},
|
||||
{
|
||||
id: 'oldest-courses',
|
||||
name: intl.formatMessage(messages.coursesOrderFilterMenuOldestCurses),
|
||||
value: 'oldestCourses',
|
||||
},
|
||||
],
|
||||
[intl],
|
||||
);
|
||||
|
||||
const handleCourseTypeSelected = (courseOrder) => {
|
||||
onItemMenuSelected(courseOrder);
|
||||
};
|
||||
|
||||
return (
|
||||
<CoursesFilterMenu
|
||||
id="dropdown-toggle-courses-order-menu"
|
||||
menuItems={courseOrders}
|
||||
onItemMenuSelected={handleCourseTypeSelected}
|
||||
defaultItemSelectedText={intl.formatMessage(messages.coursesOrderFilterMenuAscendantCurses)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CoursesOrderFilterMenu.propTypes = {
|
||||
onItemMenuSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CoursesOrderFilterMenu;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { screen, fireEvent, render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import CoursesOrderFilterMenu from '.';
|
||||
import message from './messages';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
describe('CoursesTypesFilterMenu', () => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const IntlProviderWrapper = ({ children }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const onItemMenuSelectedMock = jest.fn();
|
||||
|
||||
const renderComponent = (overrideProps = {}) => render(
|
||||
<IntlProviderWrapper>
|
||||
<CoursesOrderFilterMenu
|
||||
onItemMenuSelected={onItemMenuSelectedMock}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProviderWrapper>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useSelector.mockReturnValue({
|
||||
currentPage: 1,
|
||||
order: 'display_name',
|
||||
search: '',
|
||||
activeOnly: false,
|
||||
archivedOnly: false,
|
||||
cleanFilters: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('snapshot', () => {
|
||||
const { container } = renderComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
renderComponent();
|
||||
const courseOrderMenu = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
expect(courseOrderMenu).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the items when the menu is clicked', () => {
|
||||
renderComponent();
|
||||
const courseOrderMenuFilter = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
fireEvent.click(courseOrderMenuFilter);
|
||||
const { defaultMessage: ascendantCoursesMenuText } = message.coursesOrderFilterMenuAscendantCurses;
|
||||
const { defaultMessage: descendantCoursesMenuText } = message.coursesOrderFilterMenuDescendantCurses;
|
||||
const { defaultMessage: newWestCoursesMenuText } = message.coursesOrderFilterMenuNewestCurses;
|
||||
const { defaultMessage: oldestCoursesMenuText } = message.coursesOrderFilterMenuOldestCurses;
|
||||
const ascendantCoursesMenuItem = screen.getByTestId('item-menu-az-courses');
|
||||
const descendantCoursesMenuItem = screen.getByText(descendantCoursesMenuText);
|
||||
const newestCoursesMenuItem = screen.getByText(newWestCoursesMenuText);
|
||||
const oldestCoursesMenuItem = screen.getByText(oldestCoursesMenuText);
|
||||
expect(ascendantCoursesMenuItem.textContent).toContain(ascendantCoursesMenuText);
|
||||
expect(descendantCoursesMenuItem).toBeInTheDocument();
|
||||
expect(newestCoursesMenuItem).toBeInTheDocument();
|
||||
expect(oldestCoursesMenuItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show an icon when a menu item is selected ', () => {
|
||||
renderComponent();
|
||||
const courseOrderMenu = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
fireEvent.click(courseOrderMenu);
|
||||
const ascendantCoursesMenuItem = screen.getByTestId('item-menu-az-courses');
|
||||
fireEvent.click(ascendantCoursesMenuItem);
|
||||
fireEvent.click(courseOrderMenu);
|
||||
expect(screen.getByTestId('menu-item-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onCourseTypeSelected function when a menu item is selected ', () => {
|
||||
renderComponent();
|
||||
const courseOrderMenu = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
fireEvent.click(courseOrderMenu);
|
||||
const ascendantCoursesMenuItem = screen.getByTestId('item-menu-az-courses');
|
||||
fireEvent.click(ascendantCoursesMenuItem);
|
||||
expect(onItemMenuSelectedMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
coursesOrderFilterMenuAscendantCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.order-filter-menu.ascendant-courses',
|
||||
defaultMessage: 'Name A-Z',
|
||||
},
|
||||
coursesOrderFilterMenuDescendantCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.order-filter-menu.descendant-courses',
|
||||
defaultMessage: 'Name Z-A',
|
||||
},
|
||||
coursesOrderFilterMenuNewestCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.order-filter-menu.newest-courses',
|
||||
defaultMessage: 'Newest',
|
||||
},
|
||||
coursesOrderFilterMenuOldestCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.order-filter-menu.oldest-courses',
|
||||
defaultMessage: 'Oldest',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursesTypesFilterMenu snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="pgn__dropdown pgn__dropdown-light dropdown"
|
||||
data-testid="dropdown"
|
||||
id="dropdown-toggle-dropdown-toggle-course-type-menu"
|
||||
>
|
||||
<button
|
||||
alt="dropdown-toggle-menu-items"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="dropdown-toggle-menu-items dropdown-toggle btn btn-none"
|
||||
data-testid="dropdown-toggle-course-type-menu"
|
||||
id="dropdown-toggle-course-type-menu"
|
||||
type="button"
|
||||
>
|
||||
All courses
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import CoursesFilterMenu from '../courses-filter-menu';
|
||||
|
||||
const CoursesTypesFilterMenu = ({ onItemMenuSelected }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const courseTypes = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'all-courses',
|
||||
name: intl.formatMessage(messages.coursesTypesFilterMenuAllCurses),
|
||||
value: 'allCourses',
|
||||
},
|
||||
{
|
||||
id: 'active-courses',
|
||||
name: intl.formatMessage(messages.coursesTypesFilterMenuActiveCurses),
|
||||
value: 'activeCourses',
|
||||
},
|
||||
{
|
||||
id: 'archived-courses',
|
||||
name: intl.formatMessage(messages.coursesTypesFilterMenuArchivedCurses),
|
||||
value: 'archivedCourses',
|
||||
},
|
||||
],
|
||||
[intl],
|
||||
);
|
||||
|
||||
const handleCourseTypeSelected = (courseType) => {
|
||||
onItemMenuSelected(courseType);
|
||||
};
|
||||
|
||||
return (
|
||||
<CoursesFilterMenu
|
||||
id="dropdown-toggle-course-type-menu"
|
||||
menuItems={courseTypes}
|
||||
onItemMenuSelected={handleCourseTypeSelected}
|
||||
defaultItemSelectedText={intl.formatMessage(messages.coursesTypesFilterMenuAllCurses)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CoursesTypesFilterMenu.propTypes = {
|
||||
onItemMenuSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CoursesTypesFilterMenu;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { screen, fireEvent, render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import CoursesTypesFilterMenu from '.';
|
||||
import message from './messages';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CoursesTypesFilterMenu', () => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const IntlProviderWrapper = ({ children }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const onItemMenuSelectedMock = jest.fn();
|
||||
|
||||
const renderComponent = (overrideProps = {}) => render(
|
||||
<IntlProviderWrapper>
|
||||
<CoursesTypesFilterMenu
|
||||
onItemMenuSelected={onItemMenuSelectedMock}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProviderWrapper>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useSelector.mockReturnValue({
|
||||
currentPage: 1,
|
||||
order: 'display_name',
|
||||
search: '',
|
||||
activeOnly: false,
|
||||
archivedOnly: false,
|
||||
cleanFilters: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('snapshot', () => {
|
||||
const { container } = renderComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
renderComponent();
|
||||
const courseTypesMenu = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
expect(courseTypesMenu).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the items when the menu is clicked', () => {
|
||||
renderComponent();
|
||||
const courseTypeMenuFilter = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
fireEvent.click(courseTypeMenuFilter);
|
||||
const { defaultMessage: activeCoursesMenuText } = message.coursesTypesFilterMenuActiveCurses;
|
||||
const { defaultMessage: allCoursesMenuText } = message.coursesTypesFilterMenuAllCurses;
|
||||
const { defaultMessage: archiveCoursesMenuText } = message.coursesTypesFilterMenuArchivedCurses;
|
||||
const activeCoursesMenuItem = screen.getByText(activeCoursesMenuText);
|
||||
const allCoursesMenuItem = screen.getByTestId('item-menu-all-courses');
|
||||
const archiveCoursesMenuItem = screen.getByText(archiveCoursesMenuText);
|
||||
expect(activeCoursesMenuItem).toBeInTheDocument();
|
||||
expect(allCoursesMenuItem.textContent).toContain(allCoursesMenuText);
|
||||
expect(archiveCoursesMenuItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show an icon when a menu item is selected ', () => {
|
||||
renderComponent();
|
||||
const courseTypesMenu = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
fireEvent.click(courseTypesMenu);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
fireEvent.click(courseTypesMenu);
|
||||
expect(screen.getByTestId('menu-item-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onCourseTypeSelected function when a menu item is selected ', () => {
|
||||
renderComponent();
|
||||
const courseTypesMenu = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
fireEvent.click(courseTypesMenu);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
expect(onItemMenuSelectedMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
coursesTypesFilterMenuAllCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.types-filter-menu.all-courses',
|
||||
defaultMessage: 'All courses',
|
||||
},
|
||||
coursesTypesFilterMenuActiveCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.types-filter-menu.active-courses',
|
||||
defaultMessage: 'Active',
|
||||
},
|
||||
coursesTypesFilterMenuArchivedCurses: {
|
||||
id: 'course-authoring.studio-home.courses.tab.types-filter-menu.archived-courses',
|
||||
defaultMessage: 'Archived',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchField } from '@openedx/paragon';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { getStudioHomeCoursesParams } from '../../../data/selectors';
|
||||
import { updateStudioHomeCoursesCustomParams } from '../../../data/slice';
|
||||
import { fetchStudioHomeData } from '../../../data/thunks';
|
||||
import { LoadingSpinner } from '../../../../generic/Loading';
|
||||
import CoursesTypesFilterMenu from './courses-types-filter-menu';
|
||||
import CoursesOrderFilterMenu from './courses-order-filter-menu';
|
||||
import './index.scss';
|
||||
|
||||
/* regex to check if a string has only whitespace
|
||||
example " "
|
||||
*/
|
||||
const regexOnlyWhiteSpaces = /^\s+$/;
|
||||
|
||||
const CoursesFilters = ({
|
||||
dispatch,
|
||||
locationValue,
|
||||
onSubmitSearchField,
|
||||
isLoading,
|
||||
}) => {
|
||||
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
|
||||
const {
|
||||
order,
|
||||
search,
|
||||
activeOnly,
|
||||
archivedOnly,
|
||||
cleanFilters,
|
||||
} = studioHomeCoursesParams;
|
||||
const [inputSearchValue, setInputSearchValue] = useState('');
|
||||
|
||||
const getFilterTypeData = (baseFilters) => ({
|
||||
archivedCourses: { ...baseFilters, archivedOnly: true, activeOnly: undefined },
|
||||
activeCourses: { ...baseFilters, activeOnly: true, archivedOnly: undefined },
|
||||
allCourses: { ...baseFilters, archivedOnly: undefined, activeOnly: undefined },
|
||||
azCourses: { ...baseFilters, order: 'display_name' },
|
||||
zaCourses: { ...baseFilters, order: '-display_name' },
|
||||
newestCourses: { ...baseFilters, order: '-created' },
|
||||
oldestCourses: { ...baseFilters, order: 'created' },
|
||||
});
|
||||
|
||||
const handleMenuFilterItemSelected = (filterType) => {
|
||||
const baseFilters = {
|
||||
currentPage: 1,
|
||||
search,
|
||||
order,
|
||||
isFiltered: true,
|
||||
archivedOnly,
|
||||
activeOnly,
|
||||
cleanFilters: false,
|
||||
};
|
||||
|
||||
const filterParams = getFilterTypeData(baseFilters);
|
||||
const filterParamsFormat = filterParams[filterType] || baseFilters;
|
||||
const {
|
||||
coursesOrderLabel,
|
||||
coursesTypesLabel,
|
||||
isFiltered,
|
||||
orderTypeLabel,
|
||||
cleanFilters: cleanFilterParams,
|
||||
currentPage,
|
||||
...customParams
|
||||
} = filterParamsFormat;
|
||||
dispatch(updateStudioHomeCoursesCustomParams(filterParamsFormat));
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, ...customParams }, true));
|
||||
};
|
||||
|
||||
const handleSearchCourses = (searchValueDebounced) => {
|
||||
const valueFormatted = searchValueDebounced.trim();
|
||||
const filterParams = {
|
||||
search: valueFormatted.length > 0 ? valueFormatted : undefined,
|
||||
activeOnly,
|
||||
archivedOnly,
|
||||
order,
|
||||
};
|
||||
const hasOnlySpaces = regexOnlyWhiteSpaces.test(searchValueDebounced);
|
||||
|
||||
if (valueFormatted !== search && !hasOnlySpaces && !cleanFilters) {
|
||||
dispatch(updateStudioHomeCoursesCustomParams({
|
||||
currentPage: 1,
|
||||
isFiltered: true,
|
||||
cleanFilters: false,
|
||||
...filterParams,
|
||||
}));
|
||||
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, ...filterParams }, true));
|
||||
}
|
||||
|
||||
setInputSearchValue(searchValueDebounced);
|
||||
};
|
||||
|
||||
const handleSearchCoursesDebounced = useCallback(
|
||||
debounce((value) => handleSearchCourses(value), 400),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="d-flex flex-row">
|
||||
<SearchField
|
||||
onSubmit={onSubmitSearchField}
|
||||
onChange={handleSearchCoursesDebounced}
|
||||
value={cleanFilters ? '' : inputSearchValue}
|
||||
className="mr-4"
|
||||
data-testid="input-filter-courses-search"
|
||||
placeholder="Search"
|
||||
/>
|
||||
{isLoading && (
|
||||
<span className="search-field-loading" data-testid="loading-search-spinner">
|
||||
<LoadingSpinner size="sm" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CoursesTypesFilterMenu onItemMenuSelected={handleMenuFilterItemSelected} />
|
||||
<CoursesOrderFilterMenu onItemMenuSelected={handleMenuFilterItemSelected} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CoursesFilters.defaultProps = {
|
||||
locationValue: '',
|
||||
onSubmitSearchField: () => {},
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
CoursesFilters.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
locationValue: PropTypes.string,
|
||||
onSubmitSearchField: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CoursesFilters;
|
||||
@@ -0,0 +1,4 @@
|
||||
.search-field-loading {
|
||||
margin-left: -25%;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
screen, fireEvent, render, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import CoursesFilters from '.';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CoursesFilters', () => {
|
||||
const dispatchMock = jest.fn();
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const IntlProviderWrapper = ({ children }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const renderComponent = (overrideProps = {}) => render(
|
||||
<IntlProviderWrapper>
|
||||
<CoursesFilters
|
||||
dispatch={dispatchMock}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProviderWrapper>,
|
||||
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useSelector.mockReturnValue({
|
||||
currentPage: 1,
|
||||
order: 'display_name',
|
||||
search: '',
|
||||
activeOnly: false,
|
||||
archivedOnly: false,
|
||||
cleanFilters: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('snapshot', () => {
|
||||
const { container } = renderComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render without crashing', () => {
|
||||
renderComponent();
|
||||
const searchInput = screen.getByTestId('input-filter-courses-search');
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render type courses menu and order curses menu', () => {
|
||||
renderComponent();
|
||||
const courseTypeMenuFilter = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
const courseOrderMenuFilter = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
expect(courseTypeMenuFilter).toBeInTheDocument();
|
||||
expect(courseOrderMenuFilter).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call dispatch when the search input changes', async () => {
|
||||
renderComponent();
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
fireEvent.change(searchInput, { target: { value: 'test' } });
|
||||
await waitFor(() => expect(dispatchMock).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should call dispatch when a menu item of course type menu is selected', () => {
|
||||
renderComponent();
|
||||
const courseTypeMenuFilter = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
fireEvent.click(courseTypeMenuFilter);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call dispatch when a menu item of course order menu is selected', () => {
|
||||
renderComponent();
|
||||
const courseOrderMenuFilter = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
fireEvent.click(courseOrderMenuFilter);
|
||||
const descendantCoursesMenuItem = screen.getByTestId('item-menu-za-courses');
|
||||
fireEvent.click(descendantCoursesMenuItem);
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear the search input when cleanFilters is true', () => {
|
||||
useSelector.mockReturnValue({
|
||||
cleanFilters: true,
|
||||
});
|
||||
renderComponent();
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
expect(searchInput.value).toBe('');
|
||||
});
|
||||
|
||||
it('should call dispatch with the correct parameters when a menu item of course type menu is selected', () => {
|
||||
renderComponent();
|
||||
const courseTypeMenuFilter = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
fireEvent.click(courseTypeMenuFilter);
|
||||
const activeCoursesMenuItem = screen.getByTestId('item-menu-active-courses');
|
||||
fireEvent.click(activeCoursesMenuItem);
|
||||
|
||||
// Check that updateStudioHomeCoursesCustomParams is called with the correct payload
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
payload: {
|
||||
currentPage: 1,
|
||||
search: '',
|
||||
order: 'display_name',
|
||||
isFiltered: true,
|
||||
archivedOnly: undefined,
|
||||
activeOnly: true,
|
||||
cleanFilters: false,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('should handle search input submission', () => {
|
||||
const handleSubmit = jest.fn();
|
||||
renderComponent({ onSubmitSearchField: handleSubmit });
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
userEvent.type(searchInput, 'testing{enter}');
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call dispatch after debounce delay when the search input changes', async () => {
|
||||
renderComponent();
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
fireEvent.change(searchInput, { target: { value: 'test' } });
|
||||
await waitFor(() => expect(dispatchMock).toHaveBeenCalled(), { timeout: 500 });
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expect.anything());
|
||||
});
|
||||
|
||||
it('should not call dispatch when the search input contains only spaces', async () => {
|
||||
renderComponent();
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
fireEvent.change(searchInput, { target: { value: ' ' } });
|
||||
await waitFor(() => expect(dispatchMock).not.toHaveBeenCalled(), { timeout: 500 });
|
||||
expect(dispatchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should display the loading spinner when isLoading is true', () => {
|
||||
renderComponent({ isLoading: true });
|
||||
const spinner = screen.getByTestId('loading-search-spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display the loading spinner when isLoading is false', () => {
|
||||
renderComponent({ isLoading: false });
|
||||
const spinner = screen.queryByTestId('loading-search-spinner');
|
||||
expect(spinner).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should clear the search input and call dispatch when the reset button is clicked', async () => {
|
||||
renderComponent();
|
||||
const searchInput = screen.getByRole('searchbox');
|
||||
fireEvent.change(searchInput, { target: { value: 'test' } });
|
||||
const form = searchInput.closest('form');
|
||||
const resetButton = form.querySelector('button[type="reset"]');
|
||||
fireEvent.click(resetButton);
|
||||
expect(searchInput.value).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,13 @@ import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Row, Pagination } from '@openedx/paragon';
|
||||
import {
|
||||
Icon,
|
||||
Row,
|
||||
Pagination,
|
||||
Alert,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
|
||||
import { COURSE_CREATOR_STATES } from '../../../constants';
|
||||
@@ -12,12 +18,13 @@ import { updateStudioHomeCoursesCustomParams } from '../../data/slice';
|
||||
import { fetchStudioHomeData } from '../../data/thunks';
|
||||
import CardItem from '../../card-item';
|
||||
import CollapsibleStateWithAction from '../../collapsible-state-with-action';
|
||||
import { sortAlphabeticallyArray } from '../utils';
|
||||
import ContactAdministrator from './contact-administrator';
|
||||
import CoursesFilters from './courses-filters';
|
||||
import ProcessingCourses from '../../processing-courses';
|
||||
import { LoadingSpinner } from '../../../generic/Loading';
|
||||
import AlertMessage from '../../../generic/alert-message';
|
||||
import messages from '../messages';
|
||||
import './index.scss';
|
||||
|
||||
const CoursesTab = ({
|
||||
coursesDataItems,
|
||||
@@ -37,21 +44,54 @@ const CoursesTab = ({
|
||||
courseCreatorStatus,
|
||||
optimizationEnabled,
|
||||
} = useSelector(getStudioHomeData);
|
||||
const { currentPage } = useSelector(getStudioHomeCoursesParams);
|
||||
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
|
||||
const { currentPage, isFiltered } = studioHomeCoursesParams;
|
||||
const hasAbilityToCreateCourse = courseCreatorStatus === COURSE_CREATOR_STATES.granted;
|
||||
const showCollapsible = [
|
||||
COURSE_CREATOR_STATES.denied,
|
||||
COURSE_CREATOR_STATES.pending,
|
||||
COURSE_CREATOR_STATES.unrequested,
|
||||
].includes(courseCreatorStatus);
|
||||
const locationValue = location.search ?? '';
|
||||
|
||||
const handlePageSelected = (page) => {
|
||||
dispatch(fetchStudioHomeData(location.search ?? '', false, { page }, true));
|
||||
dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page }));
|
||||
const {
|
||||
search,
|
||||
order,
|
||||
archivedOnly,
|
||||
activeOnly,
|
||||
} = studioHomeCoursesParams;
|
||||
|
||||
const customParams = {
|
||||
search,
|
||||
order,
|
||||
archivedOnly,
|
||||
activeOnly,
|
||||
};
|
||||
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page, ...customParams }, true));
|
||||
dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page, isFiltered: true }));
|
||||
};
|
||||
|
||||
const handleCleanFilters = () => {
|
||||
const customParams = {
|
||||
currentPage: 1,
|
||||
search: undefined,
|
||||
order: 'display_name',
|
||||
isFiltered: true,
|
||||
cleanFilters: true,
|
||||
archivedOnly: undefined,
|
||||
activeOnly: undefined,
|
||||
};
|
||||
|
||||
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, order: 'display_name' }, true));
|
||||
dispatch(updateStudioHomeCoursesCustomParams(customParams));
|
||||
};
|
||||
|
||||
const isNotFilteringCourses = !isFiltered && !isLoading;
|
||||
const hasCourses = coursesDataItems?.length > 0;
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !isFiltered) {
|
||||
return (
|
||||
<Row className="m-0 mt-4 justify-content-center">
|
||||
<LoadingSpinner />
|
||||
@@ -60,21 +100,22 @@ const CoursesTab = ({
|
||||
}
|
||||
|
||||
return (
|
||||
isFailed ? (
|
||||
isFailed && !isFiltered ? (
|
||||
<AlertMessage
|
||||
variant="danger"
|
||||
description={(
|
||||
<Row className="m-0 align-items-center">
|
||||
<Icon src={Error} className="text-danger-500 mr-1" />
|
||||
<span>{intl.formatMessage(messages.courseTabErrorMessage)}</span>
|
||||
<span data-testid="error-failed-message">{intl.formatMessage(messages.courseTabErrorMessage)}</span>
|
||||
</Row>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isShowProcessing && <ProcessingCourses />}
|
||||
{hasCourses && isEnabledPagination && (
|
||||
<div className="d-flex justify-content-end">
|
||||
<div className="courses-tab-container">
|
||||
{isShowProcessing && !isEnabledPagination && <ProcessingCourses />}
|
||||
{isEnabledPagination && (
|
||||
<div className="d-flex flex-row justify-content-between my-4">
|
||||
<CoursesFilters dispatch={dispatch} locationValue={locationValue} isLoading={isLoading} />
|
||||
<p data-testid="pagination-info">
|
||||
{intl.formatMessage(messages.coursesPaginationInfo, {
|
||||
length: coursesDataItems.length,
|
||||
@@ -85,7 +126,7 @@ const CoursesTab = ({
|
||||
)}
|
||||
{hasCourses ? (
|
||||
<>
|
||||
{sortAlphabeticallyArray(coursesDataItems).map(
|
||||
{coursesDataItems.map(
|
||||
({
|
||||
courseKey,
|
||||
displayName,
|
||||
@@ -99,6 +140,7 @@ const CoursesTab = ({
|
||||
}) => (
|
||||
<CardItem
|
||||
key={courseKey}
|
||||
courseKey={courseKey}
|
||||
displayName={displayName}
|
||||
lmsLink={lmsLink}
|
||||
rerunLink={rerunLink}
|
||||
@@ -122,7 +164,7 @@ const CoursesTab = ({
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (!optimizationEnabled && (
|
||||
) : (!optimizationEnabled && isNotFilteringCourses && (
|
||||
<ContactAdministrator
|
||||
hasAbilityToCreateCourse={hasAbilityToCreateCourse}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
@@ -130,13 +172,27 @@ const CoursesTab = ({
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{isFiltered && !hasCourses && !isLoading && (
|
||||
<Alert className="mt-4">
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertTitle)}
|
||||
</Alert.Heading>
|
||||
<p data-testid="courses-not-found-alert">
|
||||
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertMessage)}
|
||||
</p>
|
||||
<Button variant="primary" onClick={handleCleanFilters}>
|
||||
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)}
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
{showCollapsible && (
|
||||
<CollapsibleStateWithAction
|
||||
state={courseCreatorStatus}
|
||||
className="mt-3"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
3
src/studio-home/tabs-section/courses-tab/index.scss
Normal file
3
src/studio-home/tabs-section/courses-tab/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.courses-tab-container {
|
||||
min-height: 80vh;
|
||||
}
|
||||
133
src/studio-home/tabs-section/courses-tab/index.test.jsx
Normal file
133
src/studio-home/tabs-section/courses-tab/index.test.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../../store';
|
||||
import { studioHomeMock } from '../../__mocks__';
|
||||
import { initialState } from '../../factories/mockApiResponses';
|
||||
|
||||
import CoursesTab from '.';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const onClickNewCourse = jest.fn();
|
||||
const isShowProcessing = false;
|
||||
const isLoading = false;
|
||||
const isFailed = false;
|
||||
const numPages = 1;
|
||||
const coursesCount = studioHomeMock.courses.length;
|
||||
const isEnabledPagination = true;
|
||||
const showNewCourseContainer = true;
|
||||
|
||||
const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
|
||||
// Generate a custom initial state based on studioHomeCoursesRequestParams
|
||||
const customInitialState = {
|
||||
...initialState,
|
||||
studioHome: {
|
||||
...initialState.studioHome,
|
||||
...studioHomeState,
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize the store with the custom initial state
|
||||
const store = initializeStore(customInitialState);
|
||||
|
||||
return render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<CoursesTab
|
||||
dispatch={mockDispatch}
|
||||
coursesDataItems={studioHomeMock.courses}
|
||||
showNewCourseContainer={showNewCourseContainer}
|
||||
onClickNewCourse={onClickNewCourse}
|
||||
isShowProcessing={isShowProcessing}
|
||||
isLoading={isLoading}
|
||||
isFailed={isFailed}
|
||||
numPages={numPages}
|
||||
coursesCount={coursesCount}
|
||||
isEnabledPagination={isEnabledPagination}
|
||||
{...overrideProps}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('<CoursesTab />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render correctly', async () => {
|
||||
renderComponent();
|
||||
const coursesPaginationInfo = screen.getByTestId('pagination-info');
|
||||
const coursesTypesMenu = screen.getByTestId('dropdown-toggle-course-type-menu');
|
||||
const coursesOrderMenu = screen.getByTestId('dropdown-toggle-courses-order-menu');
|
||||
const coursesFilterSearchInput = screen.getByTestId('input-filter-courses-search');
|
||||
expect(coursesPaginationInfo).toBeInTheDocument();
|
||||
expect(coursesTypesMenu).toBeInTheDocument();
|
||||
expect(coursesOrderMenu).toBeInTheDocument();
|
||||
expect(coursesFilterSearchInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render pagination and filter elements when isEnabledPagination is false', () => {
|
||||
renderComponent({ isEnabledPagination: false });
|
||||
const coursesPaginationInfo = screen.queryByTestId('pagination-info');
|
||||
const coursesTypesMenu = screen.queryByTestId('dropdown-toggle-course-type-menu');
|
||||
const coursesOrderMenu = screen.queryByTestId('dropdown-toggle-courses-order-menu');
|
||||
const coursesFilterSearchInput = screen.queryByTestId('input-filter-courses-search');
|
||||
expect(coursesPaginationInfo).not.toBeInTheDocument();
|
||||
expect(coursesTypesMenu).not.toBeInTheDocument();
|
||||
expect(coursesOrderMenu).not.toBeInTheDocument();
|
||||
expect(coursesFilterSearchInput).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading spinner when isLoading is true and isFiltered is false', () => {
|
||||
const props = { isLoading: true, coursesDataItems: [] };
|
||||
const customStoreData = { studioHomeCoursesRequestParams: { currentPage: 1, isFiltered: true } };
|
||||
renderComponent(props, customStoreData);
|
||||
const loadingSpinner = screen.getByRole('status');
|
||||
expect(loadingSpinner).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error message when something went wrong', () => {
|
||||
const props = { isFailed: true };
|
||||
const customStoreData = { studioHomeCoursesRequestParams: { currentPage: 1, isFiltered: false } };
|
||||
renderComponent(props, customStoreData);
|
||||
const alertErrorFailed = screen.queryByTestId('error-failed-message');
|
||||
expect(alertErrorFailed).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an alert message when there is not courses found', () => {
|
||||
const props = { isLoading: false, coursesDataItems: [] };
|
||||
const customStoreData = { studioHomeCoursesRequestParams: { currentPage: 1, isFiltered: true } };
|
||||
renderComponent(props, customStoreData);
|
||||
const alertCoursesNotFound = screen.queryByTestId('courses-not-found-alert');
|
||||
expect(alertCoursesNotFound).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render processing courses component when isEnabledPagination is false and isShowProcessing is true', () => {
|
||||
const props = { isShowProcessing: true, isEnabledPagination: false };
|
||||
const customStoreData = {
|
||||
studioHomeData: {
|
||||
inProcessCourseActions: [],
|
||||
},
|
||||
studioHomeCoursesRequestParams: {
|
||||
currentPage: 1,
|
||||
isFiltered: true,
|
||||
},
|
||||
};
|
||||
renderComponent(props, customStoreData);
|
||||
const alertCoursesNotFound = screen.queryByTestId('processing-courses-title');
|
||||
expect(alertCoursesNotFound).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,18 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.studio-home.archived.tab.error.message',
|
||||
defaultMessage: 'Failed to fetch archived courses. Please try again later.',
|
||||
},
|
||||
coursesTabCourseNotFoundAlertTitle: {
|
||||
id: 'course-authoring.studio-home.courses.tab.course.not.found.alert.title',
|
||||
defaultMessage: 'We could not find any result',
|
||||
},
|
||||
coursesTabCourseNotFoundAlertMessage: {
|
||||
id: 'course-authoring.studio-home.courses.tab.course.not.found.alert.message',
|
||||
defaultMessage: 'There are no courses with the current filters.',
|
||||
},
|
||||
coursesTabCourseNotFoundAlertCleanFiltersButton: {
|
||||
id: 'course-authoring.studio-home.courses.tab.course.not.found.alert.clean.filters.button',
|
||||
defaultMessage: 'Clear filters',
|
||||
},
|
||||
taxonomiesTabTitle: {
|
||||
id: 'course-authoring.studio-home.taxonomies.tab.title',
|
||||
defaultMessage: 'Taxonomies',
|
||||
|
||||
Reference in New Issue
Block a user