Skip to content

Commit

Permalink
feat: home studio search filters (#846)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
johnvente and mariajgrimaldi authored Apr 11, 2024
1 parent fc3e38f commit 2641aec
Show file tree
Hide file tree
Showing 31 changed files with 1,307 additions and 75 deletions.
12 changes: 11 additions & 1 deletion src/generic/Loading.jsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 />
Expand Down
5 changes: 3 additions & 2 deletions src/studio-home/StudioHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const StudioHome = ({ intl }) => {
showNewCourseContainer,
isShowOrganizationDropdown,
hasAbilityToCreateNewCourse,
isFiltered,
setShowNewCourseContainer,
dispatch,
} = useStudioHome(isPaginationCoursesEnabled);
Expand Down Expand Up @@ -99,7 +100,7 @@ const StudioHome = ({ intl }) => {
}

const headerButtons = userIsActive ? getHeaderButtons() : [];
if (isLoadingPage) {
if (isLoadingPage && !isFiltered) {
return (<Loading />);
}

Expand Down Expand Up @@ -138,7 +139,7 @@ const StudioHome = ({ intl }) => {
tabsData={studioHomeData}
showNewCourseContainer={showNewCourseContainer}
onClickNewCourse={() => setShowNewCourseContainer(true)}
isShowProcessing={isShowProcessing}
isShowProcessing={isShowProcessing && !isFiltered}
dispatch={dispatch}
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/studio-home/__mocks__/studioHomeMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module.exports = {
org: 'org.0',
rerunLink: null,
run: 'Run_0',
url: null,
url: '',
},
],
inProcessCourseActions: [],
Expand Down
16 changes: 16 additions & 0 deletions src/studio-home/card-item/CardItem.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
3 changes: 2 additions & 1 deletion src/studio-home/card-item/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -51,7 +52,7 @@ const CardItem = ({
className="card-item-title"
destination={courseUrl().toString()}
>
{displayName}
{hasDisplayName}
</Hyperlink>
) : (
<span className="card-item-title">{displayName}</span>
Expand Down
5 changes: 3 additions & 2 deletions src/studio-home/data/api.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
9 changes: 7 additions & 2 deletions src/studio-home/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
},
},
});
Expand Down
23 changes: 21 additions & 2 deletions src/studio-home/data/slice.test.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,6 +17,12 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
studioHomeData: {},
studioHomeCoursesRequestParams: {
currentPage: 1,
search: undefined,
order: 'display_name',
archivedOnly: undefined,
activeOnly: undefined,
isFiltered: false,
cleanFilters: false,
},
};

Expand All @@ -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));
Expand Down
74 changes: 29 additions & 45 deletions src/studio-home/factories/mockApiResponses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => ({
Expand Down
4 changes: 4 additions & 0 deletions src/studio-home/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import {
getLoadingStatuses,
getSavingStatuses,
getStudioHomeData,
getStudioHomeCoursesParams,
} from './data/selectors';
import { updateSavingStatuses } from './data/slice';

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);
Expand Down Expand Up @@ -89,6 +92,7 @@ const useStudioHome = (isPaginated = false) => {
courseCreatorSavingStatus,
isShowOrganizationDropdown,
hasAbilityToCreateNewCourse,
isFiltered,
dispatch,
setShowNewCourseContainer,
};
Expand Down
2 changes: 1 addition & 1 deletion src/studio-home/processing-courses/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />
Expand Down
9 changes: 6 additions & 3 deletions src/studio-home/tabs-section/TabsSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 2641aec

Please sign in to comment.