Files
frontend-app-authoring/src/custom-pages/CustomPages.jsx
Peter Kulko f9ef00e29f feat: [FC-0070] Remove backend redirects (use SPA functionality) (#1372)
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.
2024-11-08 08:19:23 -03:00

285 lines
9.7 KiB
JavaScript

import { useEffect, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import {
Routes, Route, useNavigate, Link,
} from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { AppContext, PageWrap } from '@edx/frontend-platform/react';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Breadcrumb,
Button,
Layout,
Hyperlink,
StatefulButton,
Icon,
useToggle,
Image,
ModalDialog,
Container,
} from '@openedx/paragon';
import { Add, SpinnerSimple } from '@openedx/paragon/icons';
import Placeholder from '../editors/Placeholder';
import DraggableList, { SortableItem } from '../editors/sharedComponents/DraggableList';
import ErrorAlert from '../editors/sharedComponents/ErrorAlerts/ErrorAlert';
import { RequestStatus } from '../data/constants';
import { useModels, useModel } from '../generic/model-store';
import { getLoadingStatus, getSavingStatus } from './data/selectors';
import {
addSingleCustomPage,
fetchCustomPages,
updatePageOrder,
updateSingleCustomPage,
} from './data/thunks';
import previewLmsStaticPages from './data/images/previewLmsStaticPages.png';
import CustomPageCard from './CustomPageCard';
import messages from './messages';
import CustomPagesProvider from './CustomPagesProvider';
import EditModal from './EditModal';
import { getWaffleFlags } from '../data/selectors';
import getPageHeadTitle from '../generic/utils';
import { getPagePath } from '../utils';
const CustomPages = ({
courseId,
// injected
intl,
}) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [orderedPages, setOrderedPages] = useState([]);
const [currentPage, setCurrentPage] = useState();
const [isOpen, open, close] = useToggle(false);
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
const { config } = useContext(AppContext);
const learningCourseURL = `${config.LEARNING_BASE_URL}/course/${courseId}`;
useEffect(() => {
dispatch(fetchCustomPages(courseId));
}, [courseId]);
const customPagesIds = useSelector(state => state.customPages.customPagesIds);
const addPageStatus = useSelector(state => state.customPages.addingStatus);
const deletePageStatus = useSelector(state => state.customPages.deletingStatus);
const savingStatus = useSelector(getSavingStatus);
const loadingStatus = useSelector(getLoadingStatus);
const waffleFlags = useSelector(getWaffleFlags);
const pages = useModels('customPages', customPagesIds);
const handleAddPage = () => { dispatch(addSingleCustomPage(courseId)); };
const handleReorder = () => (newPageOrder) => {
dispatch(updatePageOrder(courseId, newPageOrder));
};
const handleEditClose = () => (content) => {
navigate(`/course/${courseId}/custom-pages`);
if (!content?.metadata) {
setCurrentPage(null);
return;
}
dispatch(updateSingleCustomPage({
blockId: currentPage,
metadata: { displayName: content.metadata.display_name },
setCurrentPage,
}));
};
const addPageStateProps = {
labels: {
default: intl.formatMessage(messages.addPageBodyLabel),
pending: intl.formatMessage(messages.addingPageBodyLabel),
},
icons: {
default: <Icon src={Add} />,
pending: <Icon src={SpinnerSimple} className="icon-spin" />,
},
disabledStates: ['pending'],
};
useEffect(() => { setOrderedPages(pages); }, [customPagesIds, savingStatus]);
if (loadingStatus === RequestStatus.IN_PROGRESS) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return (<></>);
}
if (loadingStatus === RequestStatus.DENIED) {
return (
<div data-testid="under-construction-placeholder" className="row justify-content-center m-6">
<Placeholder />
</div>
);
}
return (
<CustomPagesProvider courseId={courseId}>
<Container size="xl" className="p-4 pt-5">
<div className="small gray-700">
<Breadcrumb
ariaLabel="Custom Page breadcrumbs"
linkAs={Link}
links={[
{
label: 'Content',
to: waffleFlags.useNewCourseOutlinePage ? `/course/${courseId}` : `${config.STUDIO_BASE_URL}/course/${courseId}`,
},
{ label: 'Pages and Resources', to: getPagePath(courseId, 'true', 'tabs') },
]}
/>
</div>
<ActionRow>
<div className="h2">
<FormattedMessage {...messages.heading} />
</div>
<ActionRow.Spacer />
<Button
iconBefore={Add}
onClick={handleAddPage}
data-testid="header-add-button"
>
<FormattedMessage {...messages.addPageHeaderLabel} />
</Button>
<Hyperlink
destination={learningCourseURL}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
data-testid="header-view-live-button"
>
<Button>
<FormattedMessage {...messages.viewLiveLabel} />
</Button>
</Hyperlink>
</ActionRow>
<hr />
<Layout
lg={[{ span: 9, offset: 0 }, { span: 3, offset: 0 }]}
md={[{ span: 9, offset: 0 }, { span: 3, offset: 0 }]}
sm={[{ span: 9, offset: 0 }, { span: 3, offset: 0 }]}
xs={[{ span: 9, offset: 0 }, { span: 3, offset: 0 }]}
xl={[{ span: 9, offset: 0 }, { span: 3, offset: 0 }]}
>
<Layout.Element>
<ErrorAlert hideHeading isError={deletePageStatus === RequestStatus.FAILED}>
{intl.formatMessage(messages.errorAlertMessage, { actionName: 'delete' })}
</ErrorAlert>
<ErrorAlert hideHeading isError={addPageStatus === RequestStatus.FAILED}>
{intl.formatMessage(messages.errorAlertMessage, { actionName: 'add' })}
</ErrorAlert>
<ErrorAlert hideHeading isError={savingStatus === RequestStatus.FAILED}>
{intl.formatMessage(messages.errorAlertMessage, { actionName: 'save' })}
</ErrorAlert>
<div className="small gray-700 mb-4">
<FormattedMessage {...messages.note} />
</div>
<DraggableList itemList={orderedPages} setState={setOrderedPages} updateOrder={handleReorder}>
{orderedPages.map((page) => (
<SortableItem
id={page.id}
key={page.id}
componentStyle={{
background: 'white',
borderRadius: '6px',
padding: '24px',
marginBottom: '16px',
boxShadow: '0px 1px 5px #ADADAD',
}}
>
<CustomPageCard
{...{
page,
dispatch,
deletePageStatus,
courseId,
setCurrentPage,
}}
/>
</SortableItem>
))}
</DraggableList>
<StatefulButton
data-testid="body-add-button"
onClick={handleAddPage}
state={addPageStatus}
{...addPageStateProps}
/>
</Layout.Element>
<Layout.Element>
<div className="h4">
<FormattedMessage {...messages.pageExplanationHeader} />
</div>
<div className="small gray-700">
<FormattedMessage {...messages.pageExplanationBody} />
</div>
<hr />
<div className="h4">
<FormattedMessage {...messages.customPagesExplanationHeader} />
</div>
<div className="small gray-700">
<FormattedMessage {...messages.customPagesExplanationBody} />
</div>
<hr />
<div className="h4">
<FormattedMessage {...messages.studentViewExplanationHeader} />
</div>
<div className="small gray-700">
<FormattedMessage {...messages.studentViewExplanationBody} />
</div>
<Button
data-testid="student-view-example-button"
variant="link"
size="sm"
onClick={open}
className="pl-0"
>
<FormattedMessage {...messages.studentViewExampleButton} />
</Button>
</Layout.Element>
</Layout>
<ModalDialog
isOpen={isOpen}
onClose={close}
size="lg"
title={intl.formatMessage(messages.studentViewModalTitle)}
>
<ModalDialog.Header>
<ModalDialog.Title>
<FormattedMessage {...messages.studentViewModalTitle} />
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<Image src={previewLmsStaticPages} fluid className="mb-3" />
<div className="small">
<FormattedMessage {...messages.studentViewModalBody} />
</div>
</ModalDialog.Body>
</ModalDialog>
<Routes>
<Route
path="/editor"
element={currentPage && (
<PageWrap>
<EditModal
courseId={courseId}
pageId={currentPage}
onClose={handleEditClose}
/>
</PageWrap>
)}
/>
</Routes>
</Container>
</CustomPagesProvider>
);
};
CustomPages.propTypes = {
courseId: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(CustomPages);