Skip to content
Open
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
46 changes: 25 additions & 21 deletions src/courseware/course/sequence/Sequence.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import SequenceBottomNavigationSlot from '@src/plugin-slots/SequenceBottomNavigationSlot';
import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';

Expand Down Expand Up @@ -91,27 +92,27 @@ const Sequence = ({
};

/* istanbul ignore next */
const nextHandler = () => {
logEvent('edx.ui.lms.sequence.next_selected', 'top');
const nextHandler = (placement) => () => {
logEvent('edx.ui.lms.sequence.next_selected', placement);
handleNext();
};

/* istanbul ignore next */
const previousHandler = () => {
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
const previousHandler = (placement) => () => {
logEvent('edx.ui.lms.sequence.previous_selected', placement);
handlePrevious();
};

/* istanbul ignore next */
const onNavigate = (destinationUnitId) => {
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
const onNavigate = (placement) => (destinationUnitId) => {
logEvent('edx.ui.lms.sequence.tab_selected', placement, destinationUnitId);
handleNavigate(destinationUnitId);
};

const sequenceNavProps = {
nextHandler,
previousHandler,
onNavigate,
nextHandler: nextHandler('top'),
previousHandler: previousHandler('top'),
onNavigate: onNavigate('top'),
};

useSequenceBannerTextAlert(sequenceId);
Expand Down Expand Up @@ -166,20 +167,18 @@ const Sequence = ({

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

const unitNavigationProps = {
courseId,
sequenceId,
unitId,
onClickPrevious: previousHandler('bottom'),
onClickNext: nextHandler('bottom'),
};

const renderUnitNavigation = (isAtTop) => (
<UnitNavigation
courseId={courseId}
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();
}}
{...unitNavigationProps}
/>
);

Expand Down Expand Up @@ -224,7 +223,12 @@ const Sequence = ({
isOriginalUserStaff={originalUserIsStaff}
renderUnitNavigation={renderUnitNavigation}
/>
{unitHasLoaded && renderUnitNavigation(false)}
{unitHasLoaded && (
<SequenceBottomNavigationSlot
{...unitNavigationProps}
onNavigate={onNavigate('bottom')}
/>
)}
Comment on lines +226 to +231
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debated quite a few things when thinking through how to make this more DRY.

The first thing that came to mind was moving this logic into renderUnitNavigation, and I also considered removing renderUnitNavigation's isAtTop param, but the fact that it's a pluginProp in UnitTitleSlot

makes that risky.

I do still think we can make this more DRY without changing the existing slot prop API by doing something like:

  // before renderUnitNavigation
  const unitNavigationProps = {
    courseId,
    sequenceId,
    unitId,
    onClickPrevious: () => {
      logEvent('edx.ui.lms.sequence.previous_selected', 'bottom');
      handlePrevious();
    },
    onClickNext: () => {
      logEvent('edx.ui.lms.sequence.next_selected', 'bottom');
      handleNext();
    },
  };

  const renderUnitNavigation = (isAtTop) => (
    <UnitNavigation {...unitNavigationProps} isAtTop={isAtTop} />
  );

  {unitHasLoaded && (
    <SequenceBottomNavigationSlot
      {...unitNavigationProps}
      onNavigate={onNavigate}
    />
  )}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely the cleaner way to do things. I will try to apply the changes tomorrow.

</div>
</div>
<RightSidebarSlot courseId={courseId} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';

import { NextUnitTopNavTriggerSlot } from '@src/plugin-slots/NextUnitTopNavTriggerSlot';
import { GetCourseExitNavigation } from '../../course-exit';

import { useSequenceNavigationMetadata } from './hooks';
import messages from './messages';
import PreviousButton from './generic/PreviousButton';
import NextButton from './generic/NextButton';
import { NextUnitTopNavTriggerSlot } from '../../../../plugin-slots/NextUnitTopNavTriggerSlot';

const UnitNavigation = ({
sequenceId,
Expand Down
66 changes: 66 additions & 0 deletions src/plugin-slots/SequenceBottomNavigationSlot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Sequence Bottom Navigation Slot

### Slot ID: `org.openedx.frontend.learning.sequence_bottom_navigation.v1`

### Props:
* `sequenceId` (string) — Current sequence identifier
* `unitId` (string) — Current unit identifier
* `nextHandler` (function) — Handler for next navigation action
* `onNavigate` (function) — Handler for direct unit navigation
* `previousHandler` (function) — Handler for previous navigation action

## Description

This slot is used to replace/modify/hide the sequence navigation component that controls navigation between units within a course sequence at the bottom of the page.

## Example

### Default Navigation
![Default Navigation](./default-navigation.png)

### Replaced with top naviation arrows
![Top navigation at bottom](./arrow-navigation.png)

```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';

const config = {
pluginSlots: {
'org.openedx.frontend.learning.sequence_bottom_navigation.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_sequence_navigation',
type: DIRECT_PLUGIN,
RenderWidget: ({
sequenceId,
unitId,
nextHandler,
onNavigate,
previousHandler
Comment on lines +40 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should match the prop names being spread in from unitNavigationProps

Suggested change
nextHandler,
onNavigate,
previousHandler
onClickPrevious,
onNavigate,
onClickNext

}) => {
const { courseId } = useParams();
return (
<div className="d-flex justify-content-center align-items-center">
<UnitNavigation
courseId={courseId}
sequenceId={sequenceId}
unitId={unitId}
isAtTop={true}
onClickPrevious={previousHandler}
onClickNext={nextHandler}
/>
</div>
);
},
},
},
],
},
},
};

export default config;
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions src/plugin-slots/SequenceBottomNavigationSlot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { UnitNavigation } from '@src/courseware/course/sequence/sequence-navigation';
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';

export interface SequenceBottomNavigationSlotProps {
courseId: string;
sequenceId: string;
unitId: string;
nextHandler: () => void;
onNavigate: (unitId: string) => void;
previousHandler: () => void;
}

const SequenceBottomNavigationSlot = ({
courseId,
sequenceId,
unitId,
nextHandler,
onNavigate,
previousHandler,
}: SequenceBottomNavigationSlotProps) => (
<PluginSlot
id="org.openedx.frontend.learning.sequence_bottom_navigation.v1"
slotOptions={{
mergeProps: true,
}}
pluginProps={{
courseId,
sequenceId,
unitId,
nextHandler,
onNavigate,
previousHandler,
}}
>
<UnitNavigation
courseId={courseId}
sequenceId={sequenceId}
unitId={unitId}
isAtTop={false}
onClickPrevious={previousHandler}
onClickNext={nextHandler}
/>
</PluginSlot>
);

export default SequenceBottomNavigationSlot;
Loading