Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=false
INVITE_STUDENTS_EMAIL_TO=''
AI_TRANSLATIONS_BASE_URL=''
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ HOTJAR_VERSION=6
HOTJAR_DEBUG=true
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ initialize({
ENABLE_ASSETS_PAGE: process.env.ENABLE_ASSETS_PAGE || 'false',
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
ENABLE_HOME_PAGE_COURSE_API_V2: process.env.ENABLE_HOME_PAGE_COURSE_API_V2 === 'true',
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
}, 'CourseAuthoringConfig');
},
Expand Down
4 changes: 3 additions & 1 deletion src/studio-home/StudioHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useStudioHome } from './hooks';
import AlertMessage from '../generic/alert-message';

const StudioHome = ({ intl }) => {
const isPaginationCoursesEnabled = getConfig().ENABLE_HOME_PAGE_COURSE_API_V2;
const {
isLoadingPage,
isFailedLoadingPage,
Expand All @@ -39,7 +40,7 @@ const StudioHome = ({ intl }) => {
hasAbilityToCreateNewCourse,
setShowNewCourseContainer,
dispatch,
} = useStudioHome();
} = useStudioHome(isPaginationCoursesEnabled);

const {
userIsActive,
Expand Down Expand Up @@ -139,6 +140,7 @@ const StudioHome = ({ intl }) => {
onClickNewCourse={() => setShowNewCourseContainer(true)}
isShowProcessing={isShowProcessing}
dispatch={dispatch}
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
/>
</section>
</Layout.Element>
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
16 changes: 15 additions & 1 deletion 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 @@ -43,6 +43,7 @@ describe('<CardItem />', () => {
const { getByText } = render(<RootWrapper {...props} />);
expect(getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
});

it('should render correct links for non-library course', () => {
const props = studioHomeMock.archivedCourses[0];
const { getByText } = render(<RootWrapper {...props} />);
Expand All @@ -53,6 +54,19 @@ describe('<CardItem />', () => {
const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
});

it('should render correct links for non-library course pagination', () => {
const props = studioHomeMock.archivedCourses[0];
const { getByText, getByTestId } = render(<RootWrapper {...props} isPaginated />);
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);
});
it('should render course details for library course', () => {
const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
const { getByText } = render(<RootWrapper {...props} />);
Expand Down
72 changes: 61 additions & 11 deletions src/studio-home/card-item/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
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,
ActionRow,
} 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 +17,17 @@ 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,
courseKey,
isPaginated,
url,
}) => {
const {
allowCourseReruns,
Expand Down Expand Up @@ -41,16 +58,45 @@ const CardItem = ({
)}
subtitle={subtitle}
actions={showActions && (
<ActionRow>
{isShowRerunLink && (
<Hyperlink className="small" destination={rerunLink}>
{intl.formatMessage(messages.btnReRunText)}
isPaginated ? (
<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.Menu>
</Dropdown>
) : (
<ActionRow>
{isShowRerunLink && (
<Hyperlink
className="small"
destination={rerunLink}
key={`action-row-rerunLink-${courseKey}`}
>
{intl.formatMessage(messages.btnReRunText)}
</Hyperlink>
)}
<Hyperlink
className="small ml-3"
destination={lmsLink}
key={`action-row-lmsLink-${courseKey}`}
>
{intl.formatMessage(messages.viewLiveBtnText)}
</Hyperlink>
)}
<Hyperlink className="small ml-3" destination={lmsLink}>
{intl.formatMessage(messages.viewLiveBtnText)}
</Hyperlink>
</ActionRow>
</ActionRow>
)
)}
/>
</Card>
Expand All @@ -59,6 +105,8 @@ const CardItem = ({

CardItem.defaultProps = {
isLibraries: false,
isPaginated: false,
courseKey: '',
rerunLink: '',
lmsLink: '',
run: '',
Expand All @@ -74,6 +122,8 @@ CardItem.propTypes = {
number: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
isLibraries: PropTypes.bool,
courseKey: PropTypes.string,
isPaginated: PropTypes.bool,
};

export default injectIntl(CardItem);
13 changes: 13 additions & 0 deletions src/studio-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ export async function getStudioHomeCourses(search) {
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
return camelCaseObject(data);
}
/**
* 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 getStudioHomeCoursesV2(search, customParams) {
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v2/home/courses${search}`, { params: customParams });
return camelCaseObject(data);
}

export async function getStudioHomeLibraries() {
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/libraries`);
Expand Down
13 changes: 12 additions & 1 deletion src/studio-home/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
sendRequestForCourseCreator,
getApiBaseUrl,
getStudioHomeCourses,
getStudioHomeCoursesV2,
getStudioHomeLibraries,
} from './api';
import { generateGetStudioCoursesApiResponse, generateGetStudioHomeDataApiResponse, generateGetStuioHomeLibrariesApiResponse } from '../factories/mockApiResponses';
Expand Down Expand Up @@ -43,7 +44,7 @@ describe('studio-home api calls', () => {
expect(result).toEqual(expected);
});

fit('should get studio courses data', async () => {
it('should get studio courses data', async () => {
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
const result = await getStudioHomeCourses('');
Expand All @@ -53,6 +54,16 @@ describe('studio-home api calls', () => {
expect(result).toEqual(expected);
});

it('should get studio courses data v2', async () => {
const apiLink = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
const result = await getStudioHomeCoursesV2('');
const expected = generateGetStudioCoursesApiResponse();

expect(axiosMock.history.get[0].url).toEqual(apiLink);
expect(result).toEqual(expected);
});

it('should get studio libraries data', async () => {
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
axiosMock.onGet(apiLink).reply(200, generateGetStuioHomeLibrariesApiResponse());
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.studioHomeCoursesRequestParams;
18 changes: 18 additions & 0 deletions src/studio-home/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const slice = createSlice({
deleteNotificationSavingStatus: '',
},
studioHomeData: {},
studioHomeCoursesRequestParams: {
currentPage: 1,
},
},
reducers: {
updateLoadingStatuses: (state, { payload }) => {
Expand All @@ -34,10 +37,23 @@ const slice = createSlice({
state.studioHomeData.archivedCourses = archivedCourses;
state.studioHomeData.inProcessCourseActions = inProcessCourseActions;
},
fetchCourseDataSuccessV2: (state, { 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 }) => {
const { currentPage } = payload;
state.studioHomeCoursesRequestParams.currentPage = currentPage;
},
},
});

Expand All @@ -46,7 +62,9 @@ export const {
updateLoadingStatuses,
fetchStudioHomeDataSuccess,
fetchCourseDataSuccess,
fetchCourseDataSuccessV2,
fetchLibraryDataSuccess,
updateStudioHomeCoursesCustomParams,
} = slice.actions;

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

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: {},
studioHomeCoursesRequestParams: {
currentPage: 1,
},
};

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

it('should update the currentPage in studioHomeCoursesRequestParams', () => {
const newState = {
...initialState,
studioHomeCoursesRequestParams: {
currentPage: 2,
},
};
const payload = {
currentPage: 2,
};

const result = reducer(initialState, updateStudioHomeCoursesCustomParams(payload));
expect(result).toEqual(newState);
});
});
14 changes: 11 additions & 3 deletions src/studio-home/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import {
handleCourseNotification,
getStudioHomeCourses,
getStudioHomeLibraries,
getStudioHomeCoursesV2,
} from './api';
import {
fetchStudioHomeDataSuccess,
fetchCourseDataSuccess,
updateLoadingStatuses,
updateSavingStatuses,
fetchLibraryDataSuccess,
fetchCourseDataSuccessV2,
} from './slice';

function fetchStudioHomeData(search, hasHomeData) {
function fetchStudioHomeData(search, hasHomeData, requestParams = {}, isPaginationEnabled = false) {
return async (dispatch) => {
dispatch(updateLoadingStatuses({ studioHomeLoadingStatus: RequestStatus.IN_PROGRESS }));
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.IN_PROGRESS }));
Expand All @@ -30,8 +32,14 @@ function fetchStudioHomeData(search, hasHomeData) {
}
}
try {
const coursesData = await getStudioHomeCourses(search || '');
dispatch(fetchCourseDataSuccess(coursesData));
if (isPaginationEnabled) {
const coursesData = await getStudioHomeCoursesV2(search || '', requestParams);
dispatch(fetchCourseDataSuccessV2(coursesData));
} else {
const coursesData = await getStudioHomeCourses(search || '');
dispatch(fetchCourseDataSuccess(coursesData));
}

dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.SUCCESSFUL }));
} catch (error) {
dispatch(updateLoadingStatuses({ courseLoadingStatus: RequestStatus.FAILED }));
Expand Down
Loading