Skip to content

Commit

Permalink
refactor: update redwood with upstream (#19)
Browse files Browse the repository at this point in the history
* feat: [FC-0056] courseware sidebar enhancement (openedx#1398)

- Display section and sequence progress
- Add tracking event to the unit button
- Hide the horizontal unit navigation with enabled sidebar navigation

* fix: optimize scroll position observer after video fullscreen exit (openedx#1405)

* feat!: organize plugin slots as components, add footer slot (openedx#1408)

BREAKING CHANGE: slot ids have been changed for consistency
* `sequence_container_plugin` -> `sequence_container_slot`
* `unit_title_plugin` -> `unit_title_slot`

Co-authored-by: Brian Smith <[email protected]>

* feat(redwood): add css support and cdn compatibility  (#17)

* perf: add runtime theming support to redwood

* fix: outline collapsible icons

* fix: solve issue with overlaid input element

* refactor: add frontend-platform dep from npm

* refactor: rename frontend-build github branch

---------

Co-authored-by: Diana Catalina Olarte <[email protected]>

* fix: some variables weren't css-variables

* fix: recreate package-lock

---------

Co-authored-by: Ihor Romaniuk <[email protected]>
Co-authored-by: Adolfo R. Brandes <[email protected]>
Co-authored-by: Brian Smith <[email protected]>
Co-authored-by: Diana Catalina Olarte <[email protected]>
  • Loading branch information
5 people committed Jul 29, 2024
1 parent 9156a05 commit a81e057
Show file tree
Hide file tree
Showing 40 changed files with 3,864 additions and 6,992 deletions.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ The Learning MFE is similar to all the other Open edX MFEs. Read the Open
edX Developer Guide's section on
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.

Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.

The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.

Environment Variables
======================

Expand Down
10,240 changes: 3,359 additions & 6,881 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/frontend-plugin-framework": "1.0.2",
"@openedx/frontend-plugin-framework": "1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "23.0.0-alpha.1",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1",
Expand Down
85 changes: 43 additions & 42 deletions src/courseware/course/sequence/Sequence.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import SequenceExamWrapper from '@edx/frontend-lib-special-exams';
import { PluginSlot } from '@openedx/frontend-plugin-framework';

import PageLoading from '@src/generic/PageLoading';
import { useModel } from '@src/generic/model-store';
import { useSequenceBannerTextAlert, useSequenceEntranceExamAlert } from '@src/alerts/sequence-alerts/hooks';
import SequenceContainerSlot from '../../../plugin-slots/SequenceContainerSlot';

import { getCoursewareOutlineSidebarSettings } from '../../data/selectors';
import CourseLicense from '../course-license';
import Sidebar from '../sidebar/Sidebar';
import NewSidebar from '../new-sidebar/Sidebar';
Expand Down Expand Up @@ -49,6 +50,7 @@ const Sequence = ({
const unit = useModel('units', unitId);
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
const sequenceMightBeUnit = useSelector(state => state.courseware.sequenceMightBeUnit);
const { enableNavigationSidebar: isEnabledOutlineSidebar } = useSelector(getCoursewareOutlineSidebarSettings);

const handleNext = () => {
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
Expand Down Expand Up @@ -144,65 +146,63 @@ const Sequence = ({

const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;

const renderUnitNavigation = (isAtTop) => (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
isAtTop={isAtTop}
onClickPrevious={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
onClickNext={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
/>
);

const defaultContent = (
<>
<div className="sequence-container d-inline-flex flex-row w-100">
<CourseOutlineTrigger />
<CourseOutlineTray />
<div className="sequence w-100">
<div className="sequence-navigation-container">
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
className="mb-4"
nextHandler={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'top');
handleNext();
}}
onNavigate={(destinationUnitId) => {
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
handleNavigate(destinationUnitId);
}}
previousHandler={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
handlePrevious();
}}
/>
</div>
{!isEnabledOutlineSidebar && (
<div className="sequence-navigation-container">
<SequenceNavigation
sequenceId={sequenceId}
unitId={unitId}
nextHandler={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'top');
handleNext();
}}
onNavigate={(destinationUnitId) => {
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
handleNavigate(destinationUnitId);
}}
previousHandler={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
handlePrevious();
}}
/>
</div>
)}

<div className="unit-container flex-grow-1">
<div className="unit-container flex-grow-1 pt-4">
<SequenceContent
courseId={courseId}
gated={gated}
sequenceId={sequenceId}
unitId={unitId}
unitLoadedHandler={handleUnitLoaded}
/>
{unitHasLoaded && (
<UnitNavigation
sequenceId={sequenceId}
unitId={unitId}
onClickPrevious={() => {
logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
handlePrevious();
}}
onClickNext={() => {
logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
handleNext();
}}
/>
)}
{unitHasLoaded && renderUnitNavigation(false)}
</div>
</div>
{isNewDiscussionSidebarViewEnabled ? <NewSidebar /> : <Sidebar />}
</div>
<PluginSlot
id="sequence_container_plugin"
pluginProps={{
courseId,
unitId,
}}
/>
<SequenceContainerSlot courseId={courseId} unitId={unitId} />
</>
);

Expand All @@ -216,6 +216,7 @@ const Sequence = ({
originalUserIsStaff={originalUserIsStaff}
canAccessProctoredExams={canAccessProctoredExams}
>
{isEnabledOutlineSidebar && renderUnitNavigation(true)}
{defaultContent}
</SequenceExamWrapper>
<CourseLicense license={license || undefined} />
Expand Down
30 changes: 21 additions & 9 deletions src/courseware/course/sequence/Sequence.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Factory } from 'rosie';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
Expand All @@ -25,6 +24,7 @@ describe('Sequence', () => {
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
const enableNavigationSidebar = { enable_navigation_sidebar: false };

beforeAll(async () => {
const store = await initializeTestStore({ courseMetadata, unitBlocks });
Expand Down Expand Up @@ -92,7 +92,11 @@ describe('Sequence', () => {
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
courseMetadata,
unitBlocks,
sequenceBlocks,
sequenceMetadata,
enableNavigationSidebar: { enable_navigation_sidebar: true },
}, false);
const { container } = render(
<SidebarWrapper overrideData={{ sequenceId: sequenceBlocks[0].id }} />,
Expand All @@ -102,8 +106,8 @@ describe('Sequence', () => {
await waitFor(() => expect(screen.queryByText('Loading locked content messaging...')).toBeInTheDocument());
// `Previous`, `Prerequisite` and `Close Tray` buttons.
expect(screen.getAllByRole('button').length).toEqual(3);
// `Active` and `Next` buttons.
expect(screen.getAllByRole('link').length).toEqual(2);
// `Next` button.
expect(screen.getAllByRole('link').length).toEqual(1);

expect(screen.getByText('Content Locked')).toBeInTheDocument();
const unitContainer = container.querySelector('.unit-container');
Expand All @@ -125,7 +129,7 @@ describe('Sequence', () => {
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
)];
const testStore = await initializeTestStore({
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, enableNavigationSidebar,
}, false);
render(
<Sequence {...mockData} {...{ sequenceId: sequenceBlocks[0].id }} />,
Expand Down Expand Up @@ -156,14 +160,16 @@ describe('Sequence', () => {
expect(await screen.findByText('Loading learning sequence...')).toBeInTheDocument();
// `Previous`, `Prerequisite` and `Close Tray` buttons.
expect(screen.getAllByRole('button')).toHaveLength(3);
// Renders `Next` button plus one button for each unit.
expect(screen.getAllByRole('link')).toHaveLength(1 + unitBlocks.length);
// Renders `Next` button.
expect(screen.getAllByRole('link')).toHaveLength(1);

loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
// At this point there will be 2 `Previous` and 2 `Next` buttons.
expect(screen.getAllByRole('button', { name: /previous/i }).length).toEqual(2);
expect(screen.getAllByRole('link', { name: /next/i }).length).toEqual(2);
// Renders two `Next` buttons for top and bottom unit navigations.
expect(screen.getAllByRole('link')).toHaveLength(2);
});

describe('sequence and unit navigation buttons', () => {
Expand All @@ -179,7 +185,9 @@ describe('Sequence', () => {
)];

beforeAll(async () => {
testStore = await initializeTestStore({ courseMetadata, unitBlocks, sequenceBlocks }, false);
testStore = await initializeTestStore({
courseMetadata, unitBlocks, sequenceBlocks, enableNavigationSidebar,
}, false);
});

beforeEach(() => {
Expand Down Expand Up @@ -340,7 +348,11 @@ describe('Sequence', () => {
{ courseId: courseMetadata.id, unitBlocks: block.children.length ? unitBlocks : [], sequenceBlock: block },
));
const innerTestStore = await initializeTestStore({
courseMetadata, unitBlocks, sequenceBlocks: testSequenceBlocks, sequenceMetadata: testSequenceMetadata,
courseMetadata,
unitBlocks,
sequenceBlocks: testSequenceBlocks,
sequenceMetadata: testSequenceMetadata,
enableNavigationSidebar,
}, false);
const testData = {
...mockData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,9 @@ exports[`Unit component output snapshot: not bookmarked, do not show content 1`]
>
unit-title
</h3>
<PluginSlot
id="unit_title_plugin"
pluginProps={
Object {
"courseId": "test-course-id",
"unitId": "test-props-id",
}
}
<UnitTitleSlot
courseId="test-course-id"
unitId="test-props-id"
/>
</div>
<h2
Expand Down
12 changes: 6 additions & 6 deletions src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ const useIFrameBehavior = ({
if (type === messageTypes.resize) {
setIframeHeight(payload.height);

// We observe exit from the video xblock fullscreen mode
// and scroll to the previously saved scroll position
if (windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}

if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
if (onLoaded) {
onLoaded();
}
}
} else if (type === messageTypes.videoFullScreen) {
// We observe exit from the video xblock fullscreen mode
// and scroll to the previously saved scroll position
if (!payload.open && windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}

// We listen for this message from LMS to know when we need to
// save or reset scroll position on toggle video xblock fullscreen mode
setWindowTopOffset(payload.open ? window.scrollY : null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ describe('useIFrameBehavior hook', () => {
const resizeMessage = (height = 23) => ({
data: { type: messageTypes.resize, payload: { height } },
});
const videoFullScreenMessage = (open = false) => ({
data: { type: messageTypes.videoFullScreen, payload: { open } },
});
const testSetIFrameHeight = (height = 23) => {
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage(height));
Expand Down Expand Up @@ -209,7 +212,7 @@ describe('useIFrameBehavior hook', () => {
state.mockVals({ ...defaultStateVals, windowTopOffset });
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
cb(videoFullScreenMessage());
expect(window.scrollTo).toHaveBeenCalledWith(0, windowTopOffset);
});
it('does not scroll if towverticalp offset is not set', () => {
Expand Down
10 changes: 2 additions & 8 deletions src/courseware/course/sequence/Unit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { useModel } from '@src/generic/model-store';
import { usePluginsCallback } from '@src/generic/plugin-store';

import { PluginSlot } from '@openedx/frontend-plugin-framework';
import BookmarkButton from '../../bookmark/BookmarkButton';
import messages from '../messages';
import ContentIFrame from './ContentIFrame';
import UnitSuspense from './UnitSuspense';
import { modelKeys, views } from './constants';
import { useExamAccess, useShouldDisplayHonorCode } from './hooks';
import { getIFrameUrl } from './urls';
import UnitTitleSlot from '../../../../plugin-slots/UnitTitleSlot';

const Unit = ({
courseId,
Expand Down Expand Up @@ -43,13 +43,7 @@ const Unit = ({
<div className="unit">
<div className="mb-0">
<h3 className="h3">{unit.title}</h3>
<PluginSlot
id="unit_title_plugin"
pluginProps={{
courseId,
unitId: id,
}}
/>
<UnitTitleSlot courseId={courseId} unitId={id} />
</div>
<h2 className="sr-only">{formatMessage(messages.headerPlaceholder)}</h2>
<BookmarkButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Button } from '@openedx/paragon';
Expand All @@ -21,6 +21,7 @@ const UnitNavigation = ({
unitId,
onClickPrevious,
onClickNext,
isAtTop,
}) => {
const {
isFirstUnit, isLastUnit, nextLink, previousLink,
Expand All @@ -33,7 +34,7 @@ const UnitNavigation = ({
return (
<Button
variant="outline-secondary"
className="previous-button mr-2 d-flex align-items-center justify-content-center"
className="previous-button mr-sm-2 d-flex align-items-center justify-content-center"
disabled={disabled}
onClick={onClickPrevious}
as={disabled ? undefined : Link}
Expand Down Expand Up @@ -68,7 +69,7 @@ const UnitNavigation = ({
};

return (
<div className="unit-navigation d-flex">
<div className={classNames('unit-navigation d-flex', { 'top-unit-navigation mb-3 w-100': isAtTop })}>
{renderPreviousButton()}
{renderNextButton()}
</div>
Expand All @@ -81,10 +82,12 @@ UnitNavigation.propTypes = {
unitId: PropTypes.string,
onClickPrevious: PropTypes.func.isRequired,
onClickNext: PropTypes.func.isRequired,
isAtTop: PropTypes.bool,
};

UnitNavigation.defaultProps = {
unitId: null,
isAtTop: false,
};

export default injectIntl(UnitNavigation);
2 changes: 1 addition & 1 deletion src/courseware/course/sidebar/SidebarTriggers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const SidebarTriggers = () => {
return (
<div
className={classNames({ 'ml-1': !isMobileView, 'border-primary-700 sidebar-active': isActive })}
style={{ borderBottom: isActive ? '2px solid' : null }}
style={{ borderBottom: '2px solid', borderColor: isActive ? 'inherit' : 'transparent' }}
key={sidebarId}
>
<Trigger onClick={() => toggleSidebar(sidebarId)} key={sidebarId} />
Expand Down
Loading

0 comments on commit a81e057

Please sign in to comment.