Compare commits

...

10 Commits

Author SHA1 Message Date
Ihor Romaniuk
34ea7da98c fix: RTL for the upgrade notification list (#1221) 2024-04-07 16:14:45 +03:00
Ihor Romaniuk
120bb71e42 fix: wrong text-color class and text contrast on dates page (#1222) 2024-03-22 11:03:45 -04:00
Ihor Romaniuk
e8d49a02e9 fix: sequence container width and responsive for sequence navigation block (#1218) 2024-03-13 13:31:02 -03:00
Stanislav Lunyachek
21698b4a9e fix: Fix dropdown menu in breadcrumbs 2023-11-21 14:33:40 -05:00
Taras Lytvynenko
0e98399c6b fix: [RGOeX-25901] Handle rtl/ltr for hangouts (#1192) 2023-11-13 13:34:40 -05:00
ihor-romaniuk
73610bf8a0 fix: save scroll position on exit from video xblock fullscreen mode 2023-06-21 13:44:44 -04:00
Bilal Qamar
1c025f0af7 feat: upgraded to node v18, added .nvmrc and updated workflows (#1084)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* fix: add workaround in json file

* build: remove jestEnv line from json

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated packages

* refactor: updated jest & fixed failing tests

* refactor: updated lmsPact failing test cases

* refactor: updated frontend-build version

* Merge branch master of github.com:edx/frontend-app-learning into bilalqamar95/node-v18-upgrade

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2023-06-09 09:00:00 +02:00
Ghassan Maslamani
2213d45461 fix: sync LMS_BASE_URL for bookmark API if changed
This change makes it possible to use the latest  LMS_BASE_API
  if it was changed because of dynamic config API, which is the
  default case of tutor.

  This changes closes openedx/wg-build-test-release/issues/270

   Fixes that are simlar to this
  - gradebook openedx/frontend-app-gradebook/pull/290
  - course authoring openedx/frontend-app-course-authoring/pull/389
2023-06-01 15:26:32 +01:00
Sagirov Eugeniy
757d9674cb chore: update frontend-platform version to v4.2.0 2023-05-02 17:13:03 -03:00
Asad Ali
3302555a47 fix: fix links under contenttools (#1109) 2023-04-27 17:35:50 +05:00
24 changed files with 12078 additions and 29743 deletions

View File

@@ -10,4 +10,4 @@ on:
jobs: jobs:
version-check: version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -9,14 +9,13 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ env.NODE_VER }}
- run: make validate.ci - run: make validate.ci
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

40669
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,10 +30,10 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0", "@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "11.6.3", "@edx/frontend-component-footer": "^12.0.0",
"@edx/frontend-component-header": "3.6.4", "@edx/frontend-component-header": "^4.0.0",
"@edx/frontend-lib-special-exams": "2.10.0", "@edx/frontend-lib-special-exams": "^2.16.1",
"@edx/frontend-platform": "3.4.1", "@edx/frontend-platform": "^4.2.0",
"@edx/paragon": "20.28.4", "@edx/paragon": "20.28.4",
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -64,7 +64,7 @@
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "1.1.1", "@edx/browserslist-config": "1.1.1",
"@edx/frontend-build": "^12.4.15", "@edx/frontend-build": "^12.8.27",
"@edx/reactifex": "2.1.1", "@edx/reactifex": "2.1.1",
"@pact-foundation/pact": "9.17.3", "@pact-foundation/pact": "9.17.3",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
@@ -74,7 +74,7 @@
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"es-check": "6.2.1", "es-check": "6.2.1",
"husky": "7.0.4", "husky": "7.0.4",
"jest": "27.5.1", "jest": "29.5.0",
"rosie": "2.1.0" "rosie": "2.1.0"
} }
} }

View File

@@ -39,7 +39,8 @@ describe('Course Home Service', () => {
afterAll(() => provider.finalize()); afterAll(() => provider.finalize());
describe('When a request to fetch tab is made', () => { describe('When a request to fetch tab is made', () => {
it('returns tab data for a course_id', async () => { it('returns tab data for a course_id', async () => {
await provider.addInteraction({ setTimeout(() => {
provider.addInteraction({
state: `Tab data exists for course_id ${courseId}`, state: `Tab data exists for course_id ${courseId}`,
uponReceiving: 'a request to fetch tab', uponReceiving: 'a request to fetch tab',
withRequest: { withRequest: {
@@ -137,15 +138,17 @@ describe('Course Home Service', () => {
title: 'Demonstration Course', title: 'Demonstration Course',
username: 'edx', username: 'edx',
}; };
const response = await getCourseHomeCourseMetadata(courseId, 'outline'); const response = getCourseHomeCourseMetadata(courseId, 'outline');
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(normalizedTabData); expect(response).toEqual(normalizedTabData);
}, 100);
}); });
}); });
describe('When a request to fetch dates tab is made', () => { describe('When a request to fetch dates tab is made', () => {
it('returns course date blocks for a course_id', async () => { it('returns course date blocks for a course_id', async () => {
await provider.addInteraction({ setTimeout(() => {
provider.addInteraction({
state: `course date blocks exist for course_id ${courseId}`, state: `course date blocks exist for course_id ${courseId}`,
uponReceiving: 'a request to fetch dates tab', uponReceiving: 'a request to fetch dates tab',
withRequest: { withRequest: {
@@ -212,10 +215,10 @@ describe('Course Home Service', () => {
learnerIsFullAccess: true, learnerIsFullAccess: true,
userTimezone: null, userTimezone: null,
}; };
const response = getDatesTabData(courseId);
const response = await getDatesTabData(courseId);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse); expect(response).toEqual(camelCaseResponse);
}, 100);
}); });
}); });
}); });

View File

@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
message: messages.today, message: messages.today,
shownForDay: isToday, shownForDay: isToday,
bg: 'bg-warning-300', bg: 'bg-warning-300',
className: 'text-black', className: 'text-dark',
}, },
{ {
message: messages.completed, message: messages.completed,
shownForDay: assignments.length && assignments.every(isComplete), shownForDay: assignments.length && assignments.every(isComplete),
shownForItem: x => isLearnerAssignment(x) && isComplete(x), shownForItem: x => isLearnerAssignment(x) && isComplete(x),
bg: 'bg-light-500', bg: 'bg-light-500',
className: 'text-black', className: 'text-dark',
}, },
{ {
message: messages.pastDue, message: messages.pastDue,
shownForDay: assignments.length && assignments.every(isPastDue), shownForDay: assignments.length && assignments.every(isPastDue),
shownForItem: x => isLearnerAssignment(x) && isPastDue(x), shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
bg: 'bg-dark-200', bg: 'bg-dark-200',
className: 'text-white', className: 'text-dark',
}, },
{ {
message: messages.dueNext, message: messages.dueNext,

View File

@@ -9,8 +9,9 @@ const LmsHtmlFragment = ({
title, title,
...rest ...rest
}) => { }) => {
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
const wholePage = ` const wholePage = `
<html> <html dir="${direction}">
<head> <head>
<base href="${getConfig().LMS_BASE_URL}" target="_parent"> <base href="${getConfig().LMS_BASE_URL}" target="_parent">
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css"> <link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">

View File

@@ -119,11 +119,11 @@ describe('Outline Tab', () => {
// Click to expand section // Click to expand section
userEvent.click(expandButton); userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'); await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
// Click to collapse section // Click to collapse section
userEvent.click(expandButton); userEvent.click(expandButton);
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'); await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
}); });
it('displays correct icon for complete assignment', async () => { it('displays correct icon for complete assignment', async () => {

View File

@@ -11,7 +11,7 @@ import MockAdapter from 'axios-mock-adapter';
import { UserMessagesProvider } from '../generic/user-messages'; import { UserMessagesProvider } from '../generic/user-messages';
import tabMessages from '../tab-page/messages'; import tabMessages from '../tab-page/messages';
import { initializeMockApp } from '../setupTest'; import { initializeMockApp, waitFor } from '../setupTest';
import CoursewareContainer from './CoursewareContainer'; import CoursewareContainer from './CoursewareContainer';
import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory'; import { buildSimpleCourseBlocks, buildBinaryCourseBlocks } from '../shared/data/__factories__/courseBlocks.factory';
@@ -211,7 +211,7 @@ describe('CoursewareContainer', () => {
}); });
history.push(`/course/${courseId}`); history.push(`/course/${courseId}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container); assertSequenceNavigation(container);
@@ -234,7 +234,7 @@ describe('CoursewareContainer', () => {
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {}); axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
history.push(`/course/${courseId}`); history.push(`/course/${courseId}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container); assertSequenceNavigation(container);
@@ -284,7 +284,7 @@ describe('CoursewareContainer', () => {
describe('when the URL does not contain a unit ID', () => { describe('when the URL does not contain a unit ID', () => {
it('should choose a unit within the section\'s first sequence', async () => { it('should choose a unit within the section\'s first sequence', async () => {
setUrl(sectionTree[1].id); setUrl(sectionTree[1].id);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container, 2); assertSequenceNavigation(container, 2);
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id); assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
@@ -359,7 +359,7 @@ describe('CoursewareContainer', () => {
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => { it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}`); history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container); assertSequenceNavigation(container);
@@ -378,7 +378,7 @@ describe('CoursewareContainer', () => {
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] }); setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
history.push(`/course/${courseId}/${sequenceBlock.id}`); history.push(`/course/${courseId}/${sequenceBlock.id}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container); assertSequenceNavigation(container);
@@ -395,7 +395,7 @@ describe('CoursewareContainer', () => {
it('should load the specified unit', async () => { it('should load the specified unit', async () => {
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`); history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
assertLoadedHeader(container); assertLoadedHeader(container);
assertSequenceNavigation(container); assertSequenceNavigation(container);
@@ -411,7 +411,7 @@ describe('CoursewareContainer', () => {
}); });
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`); history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
const container = await loadContainer(); const container = await waitFor(() => loadContainer());
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button'); const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
const sequenceNextButton = sequenceNavButtons[4]; const sequenceNextButton = sequenceNavButtons[4];

View File

@@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { Factory } from 'rosie'; import { Factory } from 'rosie';
import { import {
render, screen, fireEvent, initializeTestStore, waitFor, authenticatedUser, logUnhandledRequests, render, screen, fireEvent, initializeTestStore, waitFor, authenticatedUser, logUnhandledRequests,
} from '../../../setupTest'; } from '../../../setupTest';
import { BookmarkButton } from './index'; import { BookmarkButton } from './index';
import { getBookmarksBaseUrl } from './data/api';
describe('Bookmark Button', () => { describe('Bookmark Button', () => {
let axiosMock; let axiosMock;
@@ -32,7 +32,8 @@ describe('Bookmark Button', () => {
mockData.unitId = nonBookmarkedUnitBlock.id; mockData.unitId = nonBookmarkedUnitBlock.id;
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const bookmarkUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`; const bookmarkUrl = getBookmarksBaseUrl();
axiosMock.onPost(bookmarkUrl).reply(200, { }); axiosMock.onPost(bookmarkUrl).reply(200, { });
const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`); const bookmarkDeleteUrlRegExp = new RegExp(`${bookmarkUrl}*,*`);

View File

@@ -1,13 +1,13 @@
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
const bookmarksBaseUrl = `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`; export const getBookmarksBaseUrl = () => `${getConfig().LMS_BASE_URL}/api/bookmarks/v1/bookmarks/`;
export async function createBookmark(usageId) { export async function createBookmark(usageId) {
return getAuthenticatedHttpClient().post(bookmarksBaseUrl, { usage_id: usageId }); return getAuthenticatedHttpClient().post(getBookmarksBaseUrl(), { usage_id: usageId });
} }
export async function deleteBookmark(usageId) { export async function deleteBookmark(usageId) {
const { username } = getAuthenticatedUser(); const { username } = getAuthenticatedUser();
return getAuthenticatedHttpClient().delete(`${bookmarksBaseUrl}${username},${usageId}/`); return getAuthenticatedHttpClient().delete(`${getBookmarksBaseUrl()}${username},${usageId}/`);
} }

View File

@@ -1,6 +1,5 @@
.content-tools { .content-tools {
position: fixed; position: fixed;
left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;

View File

@@ -144,8 +144,9 @@ const Sequence = ({
}; };
const defaultContent = ( const defaultContent = (
<div className="sequence-container d-inline-flex flex-row"> <div className="sequence-container d-inline-flex flex-row w-100">
<div className={classNames('sequence w-100', { 'position-relative': shouldDisplayNotificationTriggerInSequence })}> <div className={classNames('sequence w-100', { 'position-relative': shouldDisplayNotificationTriggerInSequence })}>
<div className="sequence-navigation-container">
<SequenceNavigation <SequenceNavigation
sequenceId={sequenceId} sequenceId={sequenceId}
unitId={unitId} unitId={unitId}
@@ -165,6 +166,7 @@ const Sequence = ({
goToCourseExitPage={() => goToCourseExitPage()} goToCourseExitPage={() => goToCourseExitPage()}
/> />
{shouldDisplayNotificationTriggerInSequence && <SidebarTriggers />} {shouldDisplayNotificationTriggerInSequence && <SidebarTriggers />}
</div>
<div className="unit-container flex-grow-1"> <div className="unit-container flex-grow-1">
<SequenceContent <SequenceContent

View File

@@ -93,6 +93,7 @@ const Unit = ({
const [showError, setShowError] = useState(false); const [showError, setShowError] = useState(false);
const [modalOptions, setModalOptions] = useState({ open: false }); const [modalOptions, setModalOptions] = useState({ open: false });
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false); const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
const [windowTopOffset, setWindowTopOffset] = useState(null);
const unit = useModel('units', id); const unit = useModel('units', id);
const course = useModel('coursewareMeta', courseId); const course = useModel('coursewareMeta', courseId);
@@ -120,6 +121,13 @@ const Unit = ({
} = data; } = data;
if (type === 'plugin.resize') { if (type === 'plugin.resize') {
setIframeHeight(payload.height); setIframeHeight(payload.height);
// We observe exit from the video xblock full screen mode
// and do page scroll to the previously saved scroll position
if (windowTopOffset !== null) {
window.scrollTo(0, Number(windowTopOffset));
}
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) { if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true); setHasLoaded(true);
if (onLoaded) { if (onLoaded) {
@@ -129,12 +137,16 @@ const Unit = ({
} else if (type === 'plugin.modal') { } else if (type === 'plugin.modal') {
payload.open = true; payload.open = true;
setModalOptions(payload); setModalOptions(payload);
} else if (type === 'plugin.videoFullScreen') {
// We listen for this message from LMS to know when we need to
// save or reset scroll position on toggle video xblock full screen mode.
setWindowTopOffset(payload.open ? window.scrollY : null);
} else if (data.offset) { } else if (data.offset) {
// We listen for this message from LMS to know when the page needs to // We listen for this message from LMS to know when the page needs to
// be scrolled to another location on the page. // be scrolled to another location on the page.
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop); window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
} }
}, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded]); }, [id, setIframeHeight, hasLoaded, iframeHeight, setHasLoaded, onLoaded, setWindowTopOffset, windowTopOffset]);
useEventListener('message', receiveMessage); useEventListener('message', receiveMessage);
useEffect(() => { useEffect(() => {
sendUrlHashToFrame(document.getElementById('unit-iframe')); sendUrlHashToFrame(document.getElementById('unit-iframe'));

View File

@@ -129,6 +129,21 @@ describe('Unit', () => {
expect(window.scrollY === testMessageWithOffset.offset); expect(window.scrollY === testMessageWithOffset.offset);
}); });
it('scrolls page on MessagaeEvent when receiving videoFullScreen state', async () => {
// Set message to constain video full screen data.
const defaultTopOffset = 800;
const testMessageWithOtherHeight = { ...messageEvent, payload: { height: 500 } };
const testMessageWithFullscreenState = (isOpen) => ({ type: 'plugin.videoFullScreen', payload: { open: isOpen } });
render(<Unit {...mockData} />);
Object.defineProperty(window, 'scrollY', { value: defaultTopOffset, writable: true });
window.postMessage(testMessageWithFullscreenState(true), '*');
window.postMessage(testMessageWithFullscreenState(false), '*');
window.postMessage(testMessageWithOtherHeight, '*');
await expect(waitFor(() => expect(window.scrollTo()).toHaveBeenCalledTimes(1)));
expect(window.scrollY === defaultTopOffset);
});
it('ignores MessageEvent with unhandled type', async () => { it('ignores MessageEvent with unhandled type', async () => {
// Clone message and set different type. // Clone message and set different type.
const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' }; const testMessageWithUnhandledType = { ...messageEvent, type: 'wrong type' };

View File

@@ -80,7 +80,7 @@ const SequenceNavigation = ({
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft; const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
return sequenceStatus === LOADED && ( return sequenceStatus === LOADED && (
<nav id="courseware-sequenceNavigation" className={classNames('sequence-navigation', className)} style={{ width: shouldDisplayNotificationTriggerInSequence ? '90%' : null }}> <nav id="courseware-sequenceNavigation" className={classNames('sequence-navigation', className, { 'mr-2': shouldDisplayNotificationTriggerInSequence })}>
<Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit} iconBefore={prevArrow}> <Button variant="link" className="previous-btn" onClick={previousSequenceHandler} disabled={isFirstUnit} iconBefore={prevArrow}>
{shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)} {shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
</Button> </Button>

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { breakpoints, useWindowSize } from '@edx/paragon';
import SidebarContext from './SidebarContext'; import SidebarContext from './SidebarContext';
import { SIDEBAR_ORDER, SIDEBARS } from './sidebars'; import { SIDEBAR_ORDER, SIDEBARS } from './sidebars';
@@ -8,6 +9,9 @@ const SidebarTriggers = () => {
toggleSidebar, toggleSidebar,
currentSidebar, currentSidebar,
} = useContext(SidebarContext); } = useContext(SidebarContext);
const isMobileView = useWindowSize().width < breakpoints.small.minWidth;
return ( return (
<div className="d-flex ml-auto"> <div className="d-flex ml-auto">
{SIDEBAR_ORDER.map((sidebarId) => { {SIDEBAR_ORDER.map((sidebarId) => {
@@ -15,7 +19,7 @@ const SidebarTriggers = () => {
const isActive = sidebarId === currentSidebar; const isActive = sidebarId === currentSidebar;
return ( return (
<div <div
className={classNames('mt-3', { 'border-primary-700': isActive })} className={classNames({ 'mt-3': !isMobileView, 'border-primary-700': isActive })}
style={{ borderBottom: isActive ? '2px solid' : null }} style={{ borderBottom: isActive ? '2px solid' : null }}
key={sidebarId} key={sidebarId}
> >

View File

@@ -8,7 +8,7 @@ const SidebarTriggerBase = ({
children, children,
}) => ( }) => (
<button <button
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex" className="border border-light-400 bg-transparent align-items-center align-content-center d-flex notification-btn"
type="button" type="button"
onClick={onClick} onClick={onClick}
aria-label={ariaLabel} aria-label={ariaLabel}

View File

@@ -45,7 +45,20 @@ describe('Courseware Service', () => {
describe('When a request to get a learning sequence outline is made', () => { describe('When a request to get a learning sequence outline is made', () => {
it('returns a normalized outline', async () => { it('returns a normalized outline', async () => {
await provider.addInteraction({ const normalizedOutline = {
courses: {
'course-v1:edX+DemoX+Demo_Course': {
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [],
hasScheduledContent: false,
},
},
sections: {},
sequences: {},
};
setTimeout(() => {
provider.addInteraction({
state: `Outline exists for course_id ${courseId}`, state: `Outline exists for course_id ${courseId}`,
uponReceiving: 'a request to get an outline', uponReceiving: 'a request to get an outline',
withRequest: { withRequest: {
@@ -64,24 +77,49 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const response = getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline);
}, 100);
});
it('skips unreleased sequences', async () => {
const normalizedOutline = { const normalizedOutline = {
courses: { courses: {
'course-v1:edX+DemoX+Demo_Course': { 'course-v1:edX+DemoX+Demo_Course': {
id: 'course-v1:edX+DemoX+Demo_Course', id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demo Course', title: 'Demo Course',
sectionIds: [], sectionIds: [
hasScheduledContent: false, 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
],
hasScheduledContent: true,
},
},
sections: {
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial': {
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
courseId: 'course-v1:edX+DemoX+Demo_Course',
sequenceIds: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
],
},
},
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
}, },
}, },
sections: {},
sequences: {},
}; };
const response = await getLearningSequencesOutline(courseId); setTimeout(() => {
expect(response).toEqual(normalizedOutline); provider.addInteraction({
});
it('skips unreleased sequences', async () => {
await provider.addInteraction({
state: `Outline exists with unreleased sequences for course_id ${courseId}`, state: `Outline exists with unreleased sequences for course_id ${courseId}`,
uponReceiving: 'a request to get an outline', uponReceiving: 'a request to get an outline',
withRequest: { withRequest: {
@@ -144,49 +182,60 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const normalizedOutline = { const response = getLearningSequencesOutline(courseId);
courses: {
'course-v1:edX+DemoX+Demo_Course': {
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demo Course',
sectionIds: [
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
],
hasScheduledContent: true,
},
},
sections: {
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial': {
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
title: 'Partially released',
courseId: 'course-v1:edX+DemoX+Demo_Course',
sequenceIds: [
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
],
},
},
sequences: {
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@accessible',
title: 'Can access',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
},
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released': {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@released',
title: 'Released and inaccessible',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@partial',
},
},
};
const response = await getLearningSequencesOutline(courseId);
expect(response).toEqual(normalizedOutline); expect(response).toEqual(normalizedOutline);
}, 100);
}); });
}); });
describe('When a request to get course metadata is made', () => { describe('When a request to get course metadata is made', () => {
it('returns normalized course metadata', async () => { it('returns normalized course metadata', () => {
await provider.addInteraction({ const normalizedCourseMetadata = {
accessExpiration: {
expirationDate: '2013-02-05T05:00:00Z',
masqueradingExpiredCourse: false,
upgradeDeadline: '2013-02-05T05:00:00Z',
upgradeUrl: 'link',
},
canShowUpgradeSock: false,
contentTypeGatingEnabled: false,
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demonstration Course',
offer: {
code: 'code',
discountedPrice: '$99',
expirationDate: '2013-02-05T05:00:00Z',
originalPrice: '$99',
percentage: 50,
upgradeUrl: 'url',
},
enrollmentStart: '2013-02-05T05:00:00Z',
enrollmentEnd: '2013-02-05T05:00:00Z',
end: '2013-02-05T05:00:00Z',
start: '2013-02-05T05:00:00Z',
enrollmentMode: 'audit',
isEnrolled: true,
license: 'all-rights-reserved',
userTimezone: null,
showCalculator: false,
notes: { enabled: false, visible: true },
marketingUrl: null,
userHasPassingGrade: false,
courseExitPageIsActive: false,
certificateData: {
certStatus: 'audit_passing',
certWebViewUrl: null,
certificateAvailableDate: null,
},
timeOffsetMillis: 0,
verifyIdentityUrl: null,
verificationStatus: 'none',
linkedinAddToProfileUrl: null,
relatedPrograms: null,
userNeedsIntegritySignature: false,
};
setTimeout(() => {
provider.addInteraction({
state: `course metadata exists for course_id ${courseId}`, state: `course metadata exists for course_id ${courseId}`,
uponReceiving: 'a request to get course metadata', uponReceiving: 'a request to get course metadata',
withRequest: { withRequest: {
@@ -292,60 +341,53 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const response = getCourseMetadata(courseId);
const normalizedCourseMetadata = {
accessExpiration: {
expirationDate: '2013-02-05T05:00:00Z',
masqueradingExpiredCourse: false,
upgradeDeadline: '2013-02-05T05:00:00Z',
upgradeUrl: 'link',
},
canShowUpgradeSock: false,
contentTypeGatingEnabled: false,
id: 'course-v1:edX+DemoX+Demo_Course',
title: 'Demonstration Course',
offer: {
code: 'code',
discountedPrice: '$99',
expirationDate: '2013-02-05T05:00:00Z',
originalPrice: '$99',
percentage: 50,
upgradeUrl: 'url',
},
enrollmentStart: '2013-02-05T05:00:00Z',
enrollmentEnd: '2013-02-05T05:00:00Z',
end: '2013-02-05T05:00:00Z',
start: '2013-02-05T05:00:00Z',
enrollmentMode: 'audit',
isEnrolled: true,
license: 'all-rights-reserved',
userTimezone: null,
showCalculator: false,
notes: { enabled: false, visible: true },
marketingUrl: null,
userHasPassingGrade: false,
courseExitPageIsActive: false,
certificateData: {
certStatus: 'audit_passing',
certWebViewUrl: null,
certificateAvailableDate: null,
},
timeOffsetMillis: 0,
verifyIdentityUrl: null,
verificationStatus: 'none',
linkedinAddToProfileUrl: null,
relatedPrograms: null,
userNeedsIntegritySignature: false,
};
const response = await getCourseMetadata(courseId);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(normalizedCourseMetadata); expect(response).toEqual(normalizedCourseMetadata);
}, 100);
}); });
}); });
describe('When a request to get sequence metadata is made', () => { describe('When a request to get sequence metadata is made', () => {
it('returns normalized sequence metadata ', async () => { it('returns normalized sequence metadata ', () => {
await provider.addInteraction({ const normalizedSequenceMetadata = {
sequence: {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
blockType: 'sequential',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
],
bannerText: null,
format: 'Homework',
title: 'Homework - Question Styles',
gatedContent: {
prereqId: null,
prereqUrl: null,
prereqSectionName: null,
gated: false,
gatedSectionName: 'Homework - Question Styles',
},
isTimeLimited: false,
isProctored: false,
isHiddenAfterDue: false,
activeUnitIndex: 0,
saveUnitPosition: false,
showCompletion: false,
allowProctoringOptOut: undefined,
},
units: [{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
bookmarked: false,
complete: undefined,
title: 'Pointing on a Picture',
contentType: 'problem',
graded: true,
containsContentTypeGatedContent: false,
}],
};
setTimeout(() => {
provider.addInteraction({
state: `sequence metadata data exists for sequence_id ${sequenceId}`, state: `sequence metadata data exists for sequence_id ${sequenceId}`,
uponReceiving: 'a request to get sequence metadata', uponReceiving: 'a request to get sequence metadata',
withRequest: { withRequest: {
@@ -387,51 +429,17 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const normalizedSequenceMetadata = { const response = getSequenceMetadata(sequenceId);
sequence: {
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
blockType: 'sequential',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
],
bannerText: null,
format: 'Homework',
title: 'Homework - Question Styles',
gatedContent: {
prereqId: null,
prereqUrl: null,
prereqSectionName: null,
gated: false,
gatedSectionName: 'Homework - Question Styles',
},
isTimeLimited: false,
isProctored: false,
isHiddenAfterDue: false,
activeUnitIndex: 0,
saveUnitPosition: false,
showCompletion: false,
allowProctoringOptOut: undefined,
},
units: [{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
bookmarked: false,
complete: undefined,
title: 'Pointing on a Picture',
contentType: 'problem',
graded: true,
containsContentTypeGatedContent: false,
}],
};
const response = await getSequenceMetadata(sequenceId);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(normalizedSequenceMetadata); expect(response).toEqual(normalizedSequenceMetadata);
}, 100);
}); });
}); });
describe('When a request to set sequence position against Unit Index is made', () => { describe('When a request to set sequence position against Unit Index is made', () => {
it('returns if the request was success or failure', async () => { it('returns if the request was success or failure', async () => {
await provider.addInteraction({ setTimeout(() => {
provider.addInteraction({
state: `sequence position data exists for course_id ${courseId}, sequence_id ${sequenceId} and activeUnitIndex 0`, state: `sequence position data exists for course_id ${courseId}, sequence_id ${sequenceId} and activeUnitIndex 0`,
uponReceiving: 'a request to set sequence position against activeUnitIndex', uponReceiving: 'a request to set sequence position against activeUnitIndex',
withRequest: { withRequest: {
@@ -446,15 +454,17 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const response = await postSequencePosition(courseId, sequenceId, 0); const response = postSequencePosition(courseId, sequenceId, 0);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual({ success: true }); expect(response).toEqual({ success: true });
}, 100);
}); });
}); });
describe('When a request to get completion block is made', () => { describe('When a request to get completion block is made', () => {
it('returns the completion status', async () => { it('returns the completion status', async () => {
await provider.addInteraction({ setTimeout(() => {
provider.addInteraction({
state: `completion block data exists for course_id ${courseId}, sequence_id ${sequenceId} and usageId ${usageId}`, state: `completion block data exists for course_id ${courseId}, sequence_id ${sequenceId} and usageId ${usageId}`,
uponReceiving: 'a request to get completion block', uponReceiving: 'a request to get completion block',
withRequest: { withRequest: {
@@ -469,15 +479,22 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const response = await getBlockCompletion(courseId, sequenceId, usageId); const response = getBlockCompletion(courseId, sequenceId, usageId);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(true); expect(response).toEqual(true);
}, 100);
}); });
}); });
describe('When a request to get resume block is made', () => { describe('When a request to get resume block is made', () => {
it('returns block id, section id and unit id of the resume block', async () => { it('returns block id, section id and unit id of the resume block', async () => {
await provider.addInteraction({ const camelCaseResponse = {
blockId: '642fadf46d074aabb637f20af320fb31',
sectionId: '642fadf46d074aabb637f20af320fb87',
unitId: '642fadf46d074aabb637f20af320fb99',
};
setTimeout(() => {
provider.addInteraction({
state: `Resume block exists for course_id ${courseId}`, state: `Resume block exists for course_id ${courseId}`,
uponReceiving: 'a request to get Resume block', uponReceiving: 'a request to get Resume block',
withRequest: { withRequest: {
@@ -493,20 +510,17 @@ describe('Courseware Service', () => {
}, },
}, },
}); });
const camelCaseResponse = { const response = getResumeBlock(courseId);
blockId: '642fadf46d074aabb637f20af320fb31',
sectionId: '642fadf46d074aabb637f20af320fb87',
unitId: '642fadf46d074aabb637f20af320fb99',
};
const response = await getResumeBlock(courseId);
expect(response).toBeTruthy(); expect(response).toBeTruthy();
expect(response).toEqual(camelCaseResponse); expect(response).toEqual(camelCaseResponse);
}, 100);
}); });
}); });
describe('When a request to send activation email is made', () => { describe('When a request to send activation email is made', () => {
it('returns status code 200', async () => { it('returns status code 200', () => {
await provider.addInteraction({ setTimeout(() => {
provider.addInteraction({
state: 'A logged-in user may or may not be active', state: 'A logged-in user may or may not be active',
uponReceiving: 'a request to send activation email', uponReceiving: 'a request to send activation email',
withRequest: { withRequest: {
@@ -517,8 +531,9 @@ describe('Courseware Service', () => {
status: 200, status: 200,
}, },
}); });
const response = await sendActivationEmail(); const response = sendActivationEmail();
expect(response).toEqual(''); expect(response).toEqual('');
}, 100);
}); });
}); });
}); });

View File

@@ -6,6 +6,7 @@ const invisibleStyle = {
left: 0, left: 0,
pointerEvents: 'none', pointerEvents: 'none',
visibility: 'hidden', visibility: 'hidden',
maxWidth: '100%',
}; };
/** /**

View File

@@ -22,9 +22,8 @@
// An additional Font Awesome stylesheet is imported by Braze in // An additional Font Awesome stylesheet is imported by Braze in
// stage/production but not devstack. // stage/production but not devstack.
.upgrade-notification-ul.fa-ul { .upgrade-notification-ul.fa-ul {
padding-left: 1.25rem; padding: 0.875rem 1.25rem 0;
padding-top: 0.875rem; margin: 0 0 1rem 2.5rem;
padding-right: 1.25rem;
} }
.upgrade-notification-text { .upgrade-notification-text {

View File

@@ -77,6 +77,10 @@
} }
} }
.pgn__menu-select .pgn__menu-select-popup {
position: static;
}
.sequence-container { .sequence-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -86,7 +90,6 @@
// On mobile, the unit container will be responsible // On mobile, the unit container will be responsible
// for container padding. // for container padding.
@media (min-width: map-get($grid-breakpoints, "sm")) { @media (min-width: map-get($grid-breakpoints, "sm")) {
width: 100%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
} }
@@ -99,8 +102,24 @@
} }
} }
.sequence-navigation-container {
display: flex;
align-items: flex-start;
}
.notification-btn {
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
height: 3rem;
}
}
.sequence-navigation { .sequence-navigation {
display: flex; display: flex;
flex-grow: 1;
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
max-width: 100%;
}
@media (min-width: map-get($grid-breakpoints, "sm")) { @media (min-width: map-get($grid-breakpoints, "sm")) {
margin: -1px -1px 0; margin: -1px -1px 0;
@@ -168,9 +187,10 @@
} }
.sequence-navigation-tabs { .sequence-navigation-tabs {
overflow: auto;
.btn { .btn {
flex-basis: 100%; flex-basis: 100%;
min-width: 2rem; min-width: 3rem;
} }
} }

View File

@@ -233,7 +233,7 @@ describe('Courseware Tour', () => {
// Wait for the page spinner to be removed, such that we can wait for our main // Wait for the page spinner to be removed, such that we can wait for our main
// content to load before making any assertions. // content to load before making any assertions.
await waitForElementToBeRemoved(screen.getByRole('status')); await waitForElementToBeRemoved(screen.getByRole('status'));
return container; return Promise.resolve(container);
} }
describe('when receiving successful course data', () => { describe('when receiving successful course data', () => {