Skip to content

Commit

Permalink
feat: [FC-0070] Remove backend redirects (use SPA functionality) (#1372)
Browse files Browse the repository at this point in the history
Introduces the ability to utilize SPA functionality when the relevant waffle flags are enabled for current MFE pages. When any new MFE page is loaded, a request is made to retrieve the waffle flags. This includes both global waffle flags related to MFE Authoring pages, as well as waffle flags specific to the current course.
  • Loading branch information
PKulkoRaccoonGang authored Nov 8, 2024
1 parent 979c69b commit f9ef00e
Show file tree
Hide file tree
Showing 47 changed files with 983 additions and 356 deletions.
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.1.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-component-header": "^5.7.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-platform": "^8.0.3",
"@edx/openedx-atlas": "^0.6.0",
Expand Down
3 changes: 2 additions & 1 deletion src/CourseAuthoringPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'react-router-dom';
import { StudioFooter } from '@edx/frontend-component-footer';
import Header from './header';
import { fetchCourseDetail } from './data/thunks';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { useModel } from './generic/model-store';
import NotFoundAlert from './generic/NotFoundAlert';
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
Expand All @@ -21,6 +21,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {

useEffect(() => {
dispatch(fetchCourseDetail(courseId));
dispatch(fetchWaffleFlags(courseId));
}, [courseId]);

useEffect(() => {
Expand Down
9 changes: 7 additions & 2 deletions src/CourseAuthoringPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import CourseAuthoringPage from './CourseAuthoringPage';
import PagesAndResources from './pages-and-resources/PagesAndResources';
import { executeThunk } from './utils';
import { fetchCourseApps } from './pages-and-resources/data/thunks';
import { fetchCourseDetail } from './data/thunks';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { getApiWaffleFlagsUrl } from './data/api';

const courseId = 'course-v1:edX+TestX+Test_Course';
let mockPathname = '/evilguy/';
Expand All @@ -25,7 +26,7 @@ jest.mock('react-router-dom', () => ({
let axiosMock;
let store;

beforeEach(() => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
Expand All @@ -36,6 +37,10 @@ beforeEach(() => {
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
});

describe('Editor Pages Load no header', () => {
Expand Down
13 changes: 12 additions & 1 deletion src/CourseAuthoringRoutes.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import initializeStore from './store';
import { executeThunk } from './utils';
import { getApiWaffleFlagsUrl } from './data/api';
import { fetchWaffleFlags } from './data/thunks';

const courseId = 'course-v1:edX+TestX+Test_Course';
const pagesAndResourcesMockText = 'Pages And Resources';
const editorContainerMockText = 'Editor Container';
const videoSelectorContainerMockText = 'Video Selector Container';
const customPagesMockText = 'Custom Pages';
let store;
let axiosMock;
const mockComponentFn = jest.fn();

jest.mock('react-router-dom', () => ({
Expand Down Expand Up @@ -50,7 +56,7 @@ jest.mock('./custom-pages/CustomPages', () => (props) => {
});

describe('<CourseAuthoringRoutes>', () => {
beforeEach(() => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
Expand All @@ -60,6 +66,11 @@ describe('<CourseAuthoringRoutes>', () => {
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {});
await executeThunk(fetchWaffleFlags(courseId), store.dispatch);
});

fit('renders the PagesAndResources component when the pages and resources route is active', () => {
Expand Down
125 changes: 75 additions & 50 deletions src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Hyperlink,
Icon,
} from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { ActionRow, Button, Icon } from '@openedx/paragon';
import { useSelector } from 'react-redux';
import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';

import { getWaffleFlags } from '../../data/selectors';
import messages from './messages';

const getUpdateLinks = (courseId, waffleFlags) => {
const baseUrl = getConfig().STUDIO_BASE_URL;
const isLegacyGradingUrl = !waffleFlags.useNewGradingPage;
const isLegacyCertificateUrl = !waffleFlags.useNewCertificatesPage;
const isLegacyCourseDatesUrl = !waffleFlags.useNewScheduleDetailsPage;
const isLegacyOutlineUrl = !waffleFlags.useNewCourseOutlinePage;

return {
welcomeMessage: `/course/${courseId}/course_info`,
gradingPolicy: isLegacyGradingUrl
? `${baseUrl}/settings/grading/${courseId}` : `/course/${courseId}/settings/grading`,
certificate: isLegacyCertificateUrl
? `${baseUrl}/certificates/${courseId}` : `/course/${courseId}/certificates`,
courseDates: isLegacyCourseDatesUrl
? `${baseUrl}/settings/details/${courseId}#schedule` : `/course/${courseId}/settings/details/#schedule`,
proctoringEmail: `${baseUrl}/pages-and-resources/proctoring/settings`,
outline: isLegacyOutlineUrl ? `${baseUrl}/course/${courseId}` : `/course/${courseId}`,
};
};

const ChecklistItemBody = ({
courseId,
checkId,
isCompleted,
updateLink,
// injected
intl,
}) => (
<ActionRow>
<div className="mr-3" id={`icon-${checkId}`} data-testid={`icon-${checkId}`}>
{isCompleted ? (
<Icon
data-testid="completed-icon"
src={CheckCircle}
className="text-success"
style={{ height: '32px', width: '32px' }}
screenReaderText={intl.formatMessage(messages.completedItemLabel)}
/>
) : (
<Icon
data-testid="uncompleted-icon"
src={RadioButtonUnchecked}
style={{ height: '32px', width: '32px' }}
screenReaderText={intl.formatMessage(messages.uncompletedItemLabel)}
/>
)}
</div>
<div>
<div>
<FormattedMessage {...messages[`${checkId}ShortDescription`]} />
}) => {
const intl = useIntl();
const waffleFlags = useSelector(getWaffleFlags);
const updateLinks = getUpdateLinks(courseId, waffleFlags);

return (
<ActionRow>
<div className="mr-3" id={`icon-${checkId}`} data-testid={`icon-${checkId}`}>
{isCompleted ? (
<Icon
data-testid="completed-icon"
src={CheckCircle}
className="text-success"
style={{ height: '32px', width: '32px' }}
screenReaderText={intl.formatMessage(messages.completedItemLabel)}
/>
) : (
<Icon
data-testid="uncompleted-icon"
src={RadioButtonUnchecked}
style={{ height: '32px', width: '32px' }}
screenReaderText={intl.formatMessage(messages.uncompletedItemLabel)}
/>
)}
</div>
<div className="small">
<FormattedMessage {...messages[`${checkId}LongDescription`]} />
<div>
<div>
<FormattedMessage {...messages[`${checkId}ShortDescription`]} />
</div>
<div className="small">
<FormattedMessage {...messages[`${checkId}LongDescription`]} />
</div>
</div>
</div>
<ActionRow.Spacer />
{updateLink && (
<Hyperlink destination={updateLink} data-testid="update-hyperlink">
<Button size="sm">
<FormattedMessage {...messages.updateLinkLabel} />
</Button>
</Hyperlink>
)}
</ActionRow>
);
<ActionRow.Spacer />
{updateLinks?.[checkId] && (
<Link
to={updateLinks[checkId]}
data-testid="update-link"
>
<Button size="sm">
<FormattedMessage {...messages.updateLinkLabel} />
</Button>
</Link>
)}
</ActionRow>
);
};

ChecklistItemBody.defaultProps = {
updateLink: null,
};

ChecklistItemBody.propTypes = {
courseId: PropTypes.string.isRequired,
checkId: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired,
updateLink: PropTypes.string,
// injected
intl: intlShape.isRequired,
};

export default injectIntl(ChecklistItemBody);
export default ChecklistItemBody;
19 changes: 14 additions & 5 deletions src/course-checklist/ChecklistSection/ChecklistItemComment.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { injectIntl, FormattedMessage, FormattedNumber } from '@edx/frontend-platform/i18n';
import { Hyperlink, Icon } from '@openedx/paragon';
import { Icon } from '@openedx/paragon';
import { Link } from 'react-router-dom';
import { ModeComment } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
import { getWaffleFlags } from '../../data/selectors';
import messages from './messages';

const ChecklistItemComment = ({
courseId,
checkId,
outlineUrl,
data,
}) => {
const waffleFlags = useSelector(getWaffleFlags);

const getPathToCourseOutlinePage = (assignmentId) => (waffleFlags.useNewCourseOutlinePage
? `/course/${courseId}#${assignmentId}` : `${getConfig().STUDIO_BASE_URL}/course/${courseId}#${assignmentId}`);

const commentWrapper = (comment) => (
<div className="row m-0 mt-3 pt-3 border-top align-items-center" data-identifier="comment">
<div className="mr-4">
Expand Down Expand Up @@ -79,9 +87,9 @@ const ChecklistItemComment = ({
<ul className="assignment-list">
{gradedAssignmentsOutsideDateRange.map(assignment => (
<li className="assignment-list-item" key={assignment.id}>
<Hyperlink destination={`${outlineUrl}#${assignment.id}`}>
<Link to={getPathToCourseOutlinePage(assignment.id)}>
{assignment.displayName}
</Hyperlink>
</Link>
</li>
))}
</ul>
Expand All @@ -96,6 +104,7 @@ const ChecklistItemComment = ({
};

ChecklistItemComment.propTypes = {
courseId: PropTypes.string.isRequired,
checkId: PropTypes.string.isRequired,
outlineUrl: PropTypes.string.isRequired,
data: PropTypes.oneOfType([
Expand Down
18 changes: 4 additions & 14 deletions src/course-checklist/ChecklistSection/ChecklistSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import ChecklistItemComment from './ChecklistItemComment';
import { checklistItems } from './utils/courseChecklistData';

const ChecklistSection = ({
courseId,
dataHeading,
data,
idPrefix,
isLoading,
updateLinks,
}) => {
const dataList = checklistItems[idPrefix];
const getCompletionCountID = () => (`${idPrefix}-completion-count`);
Expand All @@ -37,18 +37,16 @@ const ChecklistSection = ({
{checks.map(check => {
const checkId = check.id;
const isCompleted = values[checkId];
const updateLink = updateLinks?.[checkId];
const outlineUrl = updateLinks.outline;
return (
<div
className={`bg-white border py-3 px-4 ${isCompleted && 'checklist-item-complete'}`}
id={`checklist-item-${checkId}`}
data-testid={`checklist-item-${checkId}`}
key={checkId}
>
<ChecklistItemBody {...{ checkId, isCompleted, updateLink }} />
<ChecklistItemBody courseId={courseId} {...{ checkId, isCompleted }} />
<div data-testid={`comment-section-${checkId}`}>
<ChecklistItemComment {...{ checkId, outlineUrl, data }} />
<ChecklistItemComment {...{ courseId, checkId, data }} />
</div>
</div>
);
Expand All @@ -61,11 +59,11 @@ const ChecklistSection = ({
};

ChecklistSection.defaultProps = {
updateLinks: {},
data: {},
};

ChecklistSection.propTypes = {
courseId: PropTypes.string.isRequired,
dataHeading: PropTypes.string.isRequired,
data: PropTypes.oneOfType([
PropTypes.shape({
Expand Down Expand Up @@ -129,14 +127,6 @@ ChecklistSection.propTypes = {
]),
idPrefix: PropTypes.string.isRequired,
isLoading: PropTypes.bool.isRequired,
updateLinks: PropTypes.shape({
welcomeMessage: PropTypes.string,
gradingPolicy: PropTypes.string,
certificate: PropTypes.string,
courseDates: PropTypes.string,
proctoringEmail: PropTypes.string,
outline: PropTypes.string,
}),
};

export default injectIntl(ChecklistSection);
Loading

0 comments on commit f9ef00e

Please sign in to comment.