Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
25 changes: 24 additions & 1 deletion src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { history } from '@edx/frontend-platform';

// eslint-disable-next-line import/prefer-default-export
Expand Down Expand Up @@ -32,3 +32,26 @@ export const useEscapeClick = ({ onEscape, dependency }) => {
};
}, [dependency]);
};

/**
* Custom hook to debounce a string value.
*
* @param {string} value - The string value to be debounced.
* @param {number} [delay=500] - The delay in milliseconds before updating the debounced value.
* @returns {string} The debounced string value.
*/
export const useDebounce = (value, delay = 500) => {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
};
5 changes: 3 additions & 2 deletions src/studio-home/StudioHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const StudioHome = ({ intl }) => {
showNewCourseContainer,
isShowOrganizationDropdown,
hasAbilityToCreateNewCourse,
isFiltered,
setShowNewCourseContainer,
dispatch,
} = useStudioHome();
Expand Down Expand Up @@ -98,7 +99,7 @@ const StudioHome = ({ intl }) => {
}

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

Expand Down Expand Up @@ -137,7 +138,7 @@ const StudioHome = ({ intl }) => {
tabsData={studioHomeData}
showNewCourseContainer={showNewCourseContainer}
onClickNewCourse={() => setShowNewCourseContainer(true)}
isShowProcessing={isShowProcessing}
isShowProcessing={isShowProcessing && !isFiltered}
dispatch={dispatch}
/>
</section>
Expand Down
2 changes: 2 additions & 0 deletions src/studio-home/__mocks__/studioHomeMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
rerunLink: '/course_rerun/course-v1:MachineLearning+123+2023',
run: '2023',
url: '/course/course-v1:MachineLearning+123+2023',
cmsLink: '//localhost:18010/courses/course-v1:MachineLearning+123+2023',
},
{
courseKey: 'course-v1:Design+123+e.g.2025',
Expand All @@ -22,6 +23,7 @@ module.exports = {
rerunLink: '/course_rerun/course-v1:Design+123+e.g.2025',
run: 'e.g.2025',
url: '/course/course-v1:Design+123+e.g.2025',
cmsLink: '//localhost:18010/courses/course-v1:Design+123+e.g.2025',
},
],
canCreateOrganizations: true,
Expand Down
8 changes: 6 additions & 2 deletions src/studio-home/card-item/CardItem.test.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { render } from '@testing-library/react';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
Expand Down Expand Up @@ -45,13 +45,17 @@ describe('<CardItem />', () => {
});
it('should render correct links for non-library course', () => {
const props = studioHomeMock.archivedCourses[0];
const { getByText } = render(<RootWrapper {...props} />);
const { getByText, getByTestId } = render(<RootWrapper {...props} />);
const courseTitleLink = getByText(props.displayName);
expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
const dropDownMenu = getByTestId('toggle-dropdown');
fireEvent.click(dropDownMenu);
const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
expect(btnReRunCourse).toHaveAttribute('href', props.rerunLink);
const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
const editInStudioLink = getByText(messages.editStudioBtnText.defaultMessage);
expect(editInStudioLink).toHaveAttribute('href', props.cmsLink);
});
it('should render course details for library course', () => {
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
Expand Down
53 changes: 41 additions & 12 deletions src/studio-home/card-item/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ActionRow, Card, Hyperlink } from '@openedx/paragon';
import {
Card,
Hyperlink,
Dropdown,
IconButton,
} from '@openedx/paragon';
import { MoreHoriz } from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';

Expand All @@ -10,7 +16,16 @@ import { getStudioHomeData } from '../data/selectors';
import messages from '../messages';

const CardItem = ({
intl, displayName, lmsLink, rerunLink, org, number, run, isLibraries, url,
intl,
displayName,
lmsLink,
rerunLink,
org,
number,
run,
isLibraries,
url,
cmsLink,
}) => {
const {
allowCourseReruns,
Expand Down Expand Up @@ -41,16 +56,28 @@ const CardItem = ({
)}
subtitle={subtitle}
actions={showActions && (
<ActionRow>
{isShowRerunLink && (
<Hyperlink className="small" destination={rerunLink}>
{intl.formatMessage(messages.btnReRunText)}
</Hyperlink>
)}
<Hyperlink className="small ml-3" destination={lmsLink}>
{intl.formatMessage(messages.viewLiveBtnText)}
</Hyperlink>
</ActionRow>
<Dropdown>
<Dropdown.Toggle
as={IconButton}
iconAs={MoreHoriz}
variant="primary"
data-testid="toggle-dropdown"
/>
<Dropdown.Menu>
{isShowRerunLink && (
<Dropdown.Item href={rerunLink}>
{messages.btnReRunText.defaultMessage}
</Dropdown.Item>
)}
<Dropdown.Item href={lmsLink}>
{intl.formatMessage(messages.viewLiveBtnText)}
</Dropdown.Item>
<Dropdown.Item href={cmsLink}>
{intl.formatMessage(messages.editStudioBtnText)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>

)}
/>
</Card>
Expand All @@ -62,12 +89,14 @@ CardItem.defaultProps = {
rerunLink: '',
lmsLink: '',
run: '',
cmsLink: '',
};

CardItem.propTypes = {
intl: intlShape.isRequired,
displayName: PropTypes.string.isRequired,
lmsLink: PropTypes.string,
cmsLink: PropTypes.string,
rerunLink: PropTypes.string,
org: PropTypes.string.isRequired,
run: PropTypes.string,
Expand Down
16 changes: 13 additions & 3 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 All @@ -16,8 +16,18 @@ export async function getStudioHomeData() {
return camelCaseObject(data);
}

export async function getStudioHomeCourses(search) {
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
/**
* Get's studio home courses.
* @param {string} search - Query string parameters for filtering the courses.
* @param {object} customParams - Additional custom parameters for the API request.
* @returns {Promise<Object>} - A Promise that resolves to the response data containing the studio home courses.
* Note: We are changing /api/contentstore/v1 to /api/contentstore/v2 due to upcoming breaking changes.
* Features such as pagination, filtering, and ordering are better handled in the new version.
* Please refer to this PR for further details: https://github.com/openedx/edx-platform/pull/34173
*/
export async function getStudioHomeCourses(search, customParams) {
const customParamsFormat = snakeCaseObject(customParams);
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParamsFormat });
return camelCaseObject(data);
}

Expand Down
2 changes: 1 addition & 1 deletion src/studio-home/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('studio-home api calls', () => {
});

fit('should get studio courses data', async () => {
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
const result = await getStudioHomeCourses('');
const expected = generateGetStudioCoursesApiResponse();
Expand Down
1 change: 1 addition & 0 deletions src/studio-home/data/selectors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const getStudioHomeData = state => state.studioHome.studioHomeData;
export const getLoadingStatuses = (state) => state.studioHome.loadingStatuses;
export const getSavingStatuses = (state) => state.studioHome.savingStatuses;
export const getStudioHomeCoursesParams = (state) => state.studioHome.studioHomeCoursesCustomParams;
18 changes: 17 additions & 1 deletion src/studio-home/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ const slice = createSlice({
deleteNotificationSavingStatus: '',
},
studioHomeData: {},
studioHomeCoursesCustomParams: {
currentPage: 1,
search: undefined,
order: 'display_name',
archivedOnly: undefined,
activeOnly: undefined,
isFiltered: false,
cleanFilters: false,
},
},
reducers: {
updateLoadingStatuses: (state, { payload }) => {
Expand All @@ -29,15 +38,21 @@ const slice = createSlice({
Object.assign(state.studioHomeData, payload);
},
fetchCourseDataSuccess: (state, { payload }) => {
const { courses, archivedCourses, inProcessCourseActions } = payload;
const { courses, archivedCourses = [], inProcessCourseActions } = payload.results;
const { numPages, count } = payload;
state.studioHomeData.courses = courses;
state.studioHomeData.archivedCourses = archivedCourses;
state.studioHomeData.inProcessCourseActions = inProcessCourseActions;
state.studioHomeData.numPages = numPages;
state.studioHomeData.coursesCount = count;
},
fetchLibraryDataSuccess: (state, { payload }) => {
const { libraries } = payload;
state.studioHomeData.libraries = libraries;
},
updateStudioHomeCoursesCustomParams: (state, { payload }) => {
state.studioHomeCoursesCustomParams = { ...payload };
},
},
});

Expand All @@ -47,6 +62,7 @@ export const {
fetchStudioHomeDataSuccess,
fetchCourseDataSuccess,
fetchLibraryDataSuccess,
updateStudioHomeCoursesCustomParams,
} = slice.actions;

export const {
Expand Down
61 changes: 61 additions & 0 deletions src/studio-home/data/slice.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { reducer, updateStudioHomeCoursesCustomParams } from './slice';

import { RequestStatus } from '../../data/constants';

describe('updateStudioHomeCoursesCustomParams action', () => {
const initialState = {
loadingStatuses: {
studioHomeLoadingStatus: RequestStatus.IN_PROGRESS,
courseNotificationLoadingStatus: RequestStatus.IN_PROGRESS,
courseLoadingStatus: RequestStatus.IN_PROGRESS,
libraryLoadingStatus: RequestStatus.IN_PROGRESS,
},
savingStatuses: {
courseCreatorSavingStatus: '',
deleteNotificationSavingStatus: '',
},
studioHomeData: {},
studioHomeCoursesCustomParams: {
currentPage: 1,
search: undefined,
order: 'display_name',
archivedOnly: undefined,
activeOnly: undefined,
isFiltered: false,
cleanFilters: false,
},
};

it('should return the initial state', () => {
const result = reducer(undefined, { type: undefined });
expect(result).toEqual(initialState);
});

it('should update the payload passed in studioHomeCoursesCustomParams', () => {
const newState = {
...initialState,
studioHomeCoursesCustomParams: {
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));
expect(result).toEqual(newState);
});
});
4 changes: 2 additions & 2 deletions src/studio-home/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
fetchLibraryDataSuccess,
} from './slice';

function fetchStudioHomeData(search, hasHomeData) {
function fetchStudioHomeData(search, hasHomeData, customParams = {}) {
return async (dispatch) => {
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
Expand All @@ -30,7 +30,7 @@ function fetchStudioHomeData(search, hasHomeData) {
}
}
try {
const coursesData = await getStudioHomeCourses(search || '');
const coursesData = await getStudioHomeCourses(search || '', customParams);
dispatch(fetchCourseDataSuccess(coursesData));
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
} catch (error) {
Expand Down
Loading