Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: home studio search filters #846

Merged
merged 25 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94be906
feat: pagination studio home for courses
johnvente Feb 5, 2024
458e219
chore: addressing some comments
johnvente Feb 8, 2024
d6e2e91
refactor: addressing pr comments
johnvente Feb 8, 2024
1f6d30f
test: adding test for studio home slice
johnvente Feb 8, 2024
87cdc56
feat: search input and filters for course home
johnvente Feb 20, 2024
fac7365
fix: solve conflicts
johnvente Feb 20, 2024
2f45f27
fix: using open edx paragon
johnvente Feb 20, 2024
de45775
feat: usedebounce hook for searching courses
johnvente Feb 20, 2024
d01912a
fix: filters params for searching coruses
johnvente Feb 21, 2024
4a7f54d
feat: adding coursekey when course name is empty
johnvente Feb 23, 2024
ea28671
chore: remove edit in studio button
johnvente Feb 23, 2024
49c3413
fix: message changed when courses were not found
johnvente Mar 20, 2024
c2a84ef
fix: solve conflicts
johnvente Apr 4, 2024
6588463
refactor: support courses tab filters and pagination
johnvente Apr 4, 2024
eba3e74
fix: solve conflicts
johnvente Apr 4, 2024
5f13e42
test: more cases for course filters component
johnvente Apr 4, 2024
81257a7
refactor: coverage for onsubmit search field
johnvente Apr 4, 2024
160f0ba
test: unit test for courses tab component
johnvente Apr 4, 2024
8fca777
feat: loading for search input and layout of course tab
johnvente Apr 5, 2024
7130c0e
fix: linter problems
johnvente Apr 5, 2024
a13068d
test: adding more tests for courses tab
johnvente Apr 5, 2024
9eb9bfe
refactor: don't ignore empty string as a case for searching
mariajgrimaldi Apr 8, 2024
ca0dacc
refactor: manage empty search bar as special case for searching
mariajgrimaldi Apr 9, 2024
a1e6c9e
fix: remove expected dispatch mock for clear button
mariajgrimaldi Apr 9, 2024
ef793fe
Merge branch 'master' into jv/feat-home-studio-filters
mariajgrimaldi Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading