Merge branch 'openedx:master' into master
This commit is contained in:
@@ -17,8 +17,9 @@
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"start:with-theme": "paragon install-theme && npm start && npm install",
|
||||
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests",
|
||||
"test": "NODE_ENV=test fedx-scripts jest --coverage --passWithNoTests",
|
||||
"test:watch": "fedx-scripts jest --watch --passWithNoTests",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
@@ -97,4 +98,4 @@
|
||||
],
|
||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ const DiscussionsNotificationsSidebar = () => {
|
||||
showTitleBar={false}
|
||||
showBorder={false}
|
||||
>
|
||||
<DiscussionsSidebar />
|
||||
{!hideNotificationbar && <div className="my-1.5" />}
|
||||
<NotificationTray />
|
||||
{!hideNotificationbar && <div className="my-1.5" />}
|
||||
<DiscussionsSidebar />
|
||||
</SidebarBase>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#course-sidebar {
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "lg")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "lg")) {
|
||||
overflow-y: scroll;
|
||||
padding: 0 .625rem !important;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
border-radius: 0;
|
||||
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5);
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
padding-left: map-get($spacers, 4);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
ol li > a {
|
||||
padding: map-get($spacers, 3\.5) map-get($spacers, 4) map-get($spacers, 3\.5) map-get($spacers, 5\.5);
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
padding-left: map-get($spacers, 4\.5);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,21 @@ describe('<CourseOutlineTray />', () => {
|
||||
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('collapses sidebar correctly when screen is resized', async () => {
|
||||
const mockToggleSidebar = jest.fn();
|
||||
await initTestStore();
|
||||
renderWithProvider({ toggleSidebar: mockToggleSidebar });
|
||||
|
||||
const collapseBtn = screen.getByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage });
|
||||
expect(collapseBtn).toBeInTheDocument();
|
||||
|
||||
// Simulate screen resize
|
||||
window.innerWidth = 500;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('navigates to section or sequence level correctly on click by back/section button', async () => {
|
||||
const user = userEvent.setup();
|
||||
await initTestStore();
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
|
||||
import { DashedCircleIcon } from '../icons';
|
||||
|
||||
const CompletionIcon = ({ completionStat: { completed = 0, total = 0 } }) => {
|
||||
const CompletionIcon = ({ completionStat: { completed = 0, total = 0 }, enabled }) => {
|
||||
const percentage = total !== 0 ? Math.min((completed / total) * 100, 100) : 0;
|
||||
const remainder = 100 - percentage;
|
||||
|
||||
switch (true) {
|
||||
case !completed:
|
||||
case !completed || !enabled:
|
||||
return <LmsCompletionSolidIcon className="text-gray-300" data-testid="completion-solid-icon" />;
|
||||
case completed === total:
|
||||
return <CheckCircleIcon className="text-success" data-testid="check-circle-icon" />;
|
||||
@@ -25,6 +25,7 @@ CompletionIcon.propTypes = {
|
||||
completed: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}).isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CompletionIcon;
|
||||
|
||||
@@ -3,21 +3,33 @@ import { render, screen } from '@testing-library/react';
|
||||
import CompletionIcon from './CompletionIcon';
|
||||
|
||||
describe('CompletionIcon', () => {
|
||||
it('renders check circle icon when completion is equal to total', () => {
|
||||
it('renders check circle icon when completion is equal to total and completion tracking is enabled', () => {
|
||||
const completionStat = { completed: 5, total: 5 };
|
||||
render(<CompletionIcon completionStat={completionStat} />);
|
||||
render(<CompletionIcon completionStat={completionStat} enabled />);
|
||||
expect(screen.getByTestId('check-circle-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dashed circle icon when completion is between 0 and total', () => {
|
||||
it('renders dashed circle icon when completion is between 0 and total and completion tracking is enabled', () => {
|
||||
const completionStat = { completed: 2, total: 5 };
|
||||
render(<CompletionIcon completionStat={completionStat} />);
|
||||
render(<CompletionIcon completionStat={completionStat} enabled />);
|
||||
expect(screen.getByTestId('dashed-circle-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders completion solid icon when completion is 0', () => {
|
||||
it('renders completion solid icon when completion is between 0 and total and completion tracking is not enabled', () => {
|
||||
const completionStat = { completed: 2, total: 5 };
|
||||
render(<CompletionIcon completionStat={completionStat} enabled={false} />);
|
||||
expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders completion solid icon when completion is 0 and enabled', () => {
|
||||
const completionStat = { completed: 0, total: 5 };
|
||||
render(<CompletionIcon completionStat={completionStat} />);
|
||||
render(<CompletionIcon completionStat={completionStat} enabled />);
|
||||
expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders completion solid icon when completion is at any value and not enabled', () => {
|
||||
const completionStat = { completed: 0, total: 5 };
|
||||
render(<CompletionIcon completionStat={completionStat} enabled={false} />);
|
||||
expect(screen.getByTestId('completion-solid-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,21 +18,24 @@ const SidebarSection = ({ section, handleSelectSection }) => {
|
||||
completionStat,
|
||||
} = section;
|
||||
|
||||
const { activeSequenceId } = useCourseOutlineSidebar();
|
||||
const { activeSequenceId, isEnabledCompletionTracking } = useCourseOutlineSidebar();
|
||||
const isActiveSection = sequenceIds.includes(activeSequenceId);
|
||||
|
||||
const sectionTitle = (
|
||||
<>
|
||||
<div className="col-auto p-0">
|
||||
<CompletionIcon completionStat={completionStat} />
|
||||
<CompletionIcon completionStat={completionStat} enabled={isEnabledCompletionTracking} />
|
||||
</div>
|
||||
<div className="col-10 ml-3 p-0 flex-grow-1 text-dark-500 text-left text-break">
|
||||
{title}
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete
|
||||
? courseOutlineMessages.completedSection
|
||||
: courseOutlineMessages.incompleteSection)}
|
||||
</span>
|
||||
{isEnabledCompletionTracking && (
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete
|
||||
? courseOutlineMessages.completedSection
|
||||
: courseOutlineMessages.incompleteSection)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,22 +28,24 @@ const SidebarSequence = ({
|
||||
} = sequence;
|
||||
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const { activeSequenceId, units } = useCourseOutlineSidebar();
|
||||
const { activeSequenceId, units, isEnabledCompletionTracking } = useCourseOutlineSidebar();
|
||||
const isActiveSequence = id === activeSequenceId;
|
||||
|
||||
const sectionTitle = (
|
||||
<>
|
||||
<div className="col-auto p-0" style={{ fontSize: '1.1rem' }}>
|
||||
<CompletionIcon completionStat={completionStat} />
|
||||
<CompletionIcon completionStat={completionStat} enabled={isEnabledCompletionTracking} />
|
||||
</div>
|
||||
<div className="col-9 d-flex flex-column flex-grow-1 ml-3 mr-auto p-0 text-left">
|
||||
<span className="align-middle text-dark-500">{title}</span>
|
||||
{specialExamInfo && <span className="align-middle small text-muted">{specialExamInfo}</span>}
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete
|
||||
? courseOutlineMessages.completedAssignment
|
||||
: courseOutlineMessages.incompleteAssignment)}
|
||||
</span>
|
||||
{isEnabledCompletionTracking && (
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete
|
||||
? courseOutlineMessages.completedAssignment
|
||||
: courseOutlineMessages.incompleteAssignment)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -69,6 +71,7 @@ const SidebarSequence = ({
|
||||
activeUnitId={activeUnitId}
|
||||
isFirst={index === 0}
|
||||
isLocked={type === UNIT_ICON_TYPES.lock}
|
||||
isCompletionTrackingEnabled={isEnabledCompletionTracking}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('<SidebarSequence />', () => {
|
||||
expect(screen.queryByText(unit.title)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly when sequence is not collapsed and complete', async () => {
|
||||
it('renders correctly when sequence is not collapsed and complete and completion tracking enabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
await initTestStore();
|
||||
renderWithProvider({
|
||||
|
||||
@@ -15,6 +15,7 @@ const SidebarUnit = ({
|
||||
isActive,
|
||||
isLocked,
|
||||
activeUnitId,
|
||||
isCompletionTrackingEnabled,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
@@ -24,6 +25,7 @@ const SidebarUnit = ({
|
||||
} = unit;
|
||||
|
||||
const iconType = isLocked ? UNIT_ICON_TYPES.lock : icon;
|
||||
const completeAndEnabled = complete && isCompletionTrackingEnabled;
|
||||
|
||||
return (
|
||||
<li className={classNames({ 'bg-info-100': isActive, 'border-top border-light': !isFirst })}>
|
||||
@@ -36,15 +38,17 @@ const SidebarUnit = ({
|
||||
}}
|
||||
>
|
||||
<div className="col-auto p-0">
|
||||
<UnitIcon type={iconType} isCompleted={complete} />
|
||||
<UnitIcon type={iconType} isCompleted={completeAndEnabled} />
|
||||
</div>
|
||||
<div className="col-10 p-0 ml-3 text-break">
|
||||
<span className="align-middle">
|
||||
{title}
|
||||
</span>
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
|
||||
</span>
|
||||
{isCompletionTrackingEnabled && (
|
||||
<span className="sr-only">
|
||||
, {intl.formatMessage(complete ? messages.completedUnit : messages.incompleteUnit)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</UnitLinkWrapper>
|
||||
</li>
|
||||
@@ -66,6 +70,7 @@ SidebarUnit.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
sequenceId: PropTypes.string.isRequired,
|
||||
activeUnitId: PropTypes.string.isRequired,
|
||||
isCompletionTrackingEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SidebarUnit;
|
||||
|
||||
@@ -50,6 +50,7 @@ describe('<SidebarUnit />', () => {
|
||||
unit={{ ...unit, icon: 'video', isLocked: false }}
|
||||
isActive={false}
|
||||
activeUnitId={unit.id}
|
||||
isCompletionTrackingEnabled
|
||||
{...props}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@@ -68,7 +69,7 @@ describe('<SidebarUnit />', () => {
|
||||
expect(container.querySelector('.text-success')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly when unit is complete', async () => {
|
||||
it('renders correctly when unit is complete and tracking enabled', async () => {
|
||||
await initTestStore();
|
||||
const container = renderWithProvider({ unit: { ...unit, complete: true } });
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
useContext, useEffect, useLayoutEffect, useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { LOADED } from '@src/constants';
|
||||
@@ -22,7 +25,10 @@ import { ID } from './constants';
|
||||
export const useCourseOutlineSidebar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isCollapsedOutlineSidebar = window.sessionStorage.getItem('hideCourseOutlineSidebar');
|
||||
const { enableNavigationSidebar: isEnabledSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const {
|
||||
enableNavigationSidebar: isEnabledSidebar,
|
||||
enableCompletionTracking: isEnabledCompletionTracking,
|
||||
} = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate);
|
||||
const courseOutlineStatus = useSelector(getCourseOutlineStatus);
|
||||
const sequenceStatus = useSelector(getSequenceStatus);
|
||||
@@ -51,10 +57,14 @@ export const useCourseOutlineSidebar = () => {
|
||||
} = course.entranceExamData || {};
|
||||
const isActiveEntranceExam = entranceExamEnabled && !entranceExamPassed;
|
||||
|
||||
const collapseSidebar = () => {
|
||||
toggleSidebar(null);
|
||||
window.sessionStorage.setItem('hideCourseOutlineSidebar', 'true');
|
||||
};
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
if (currentSidebar === ID) {
|
||||
toggleSidebar(null);
|
||||
window.sessionStorage.setItem('hideCourseOutlineSidebar', 'true');
|
||||
collapseSidebar();
|
||||
} else {
|
||||
toggleSidebar(ID);
|
||||
window.sessionStorage.removeItem('hideCourseOutlineSidebar');
|
||||
@@ -104,12 +114,28 @@ export const useCourseOutlineSidebar = () => {
|
||||
}
|
||||
}, [courseId, isEnabledSidebar, courseOutlineShouldUpdate]);
|
||||
|
||||
// Collapse sidebar if screen resized to a width that displays the sidebar automatically
|
||||
useLayoutEffect(() => {
|
||||
const handleResize = () => {
|
||||
// breakpoints.large.maxWidth is 1200px and currently the breakpoint for showing the sidebar
|
||||
if (currentSidebar === ID && global.innerWidth < breakpoints.large.maxWidth) {
|
||||
collapseSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
global.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
global.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return {
|
||||
courseId,
|
||||
unitId,
|
||||
currentSidebar,
|
||||
shouldDisplayFullScreen,
|
||||
isEnabledSidebar,
|
||||
isEnabledCompletionTracking,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
handleToggleCollapse,
|
||||
@@ -1,5 +1,5 @@
|
||||
.discussions-sidebar-frame {
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "xl")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "xl")) {
|
||||
max-height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,5 +115,6 @@ export async function getCoursewareOutlineSidebarToggles(courseId) {
|
||||
return {
|
||||
enable_navigation_sidebar: data.enable_navigation_sidebar || false,
|
||||
always_open_auxiliary_sidebar: data.always_open_auxiliary_sidebar || false,
|
||||
enable_completion_tracking: data.enable_completion_tracking || false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ describe('Data layer integration tests', () => {
|
||||
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
|
||||
enable_navigation_sidebar: true,
|
||||
always_open_auxiliary_sidebar: true,
|
||||
enable_completion_tracking: true,
|
||||
});
|
||||
|
||||
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
|
||||
@@ -126,6 +127,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
|
||||
enableNavigationSidebar: true,
|
||||
alwaysOpenAuxiliarySidebar: true,
|
||||
enableCompletionTracking: true,
|
||||
});
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
@@ -154,6 +156,7 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
|
||||
enableNavigationSidebar: false,
|
||||
alwaysOpenAuxiliarySidebar: false,
|
||||
enableCompletionTracking: false,
|
||||
});
|
||||
|
||||
// check that at least one key camel cased, thus course data normalized
|
||||
|
||||
@@ -90,8 +90,11 @@ export function fetchCourse(courseId) {
|
||||
const {
|
||||
enable_navigation_sidebar: enableNavigationSidebar,
|
||||
always_open_auxiliary_sidebar: alwaysOpenAuxiliarySidebar,
|
||||
enable_completion_tracking: enableCompletionTracking,
|
||||
} = coursewareOutlineSidebarTogglesResult.value;
|
||||
dispatch(setCoursewareOutlineSidebarToggles({ enableNavigationSidebar, alwaysOpenAuxiliarySidebar }));
|
||||
dispatch(setCoursewareOutlineSidebarToggles(
|
||||
{ enableNavigationSidebar, alwaysOpenAuxiliarySidebar, enableCompletionTracking },
|
||||
));
|
||||
}
|
||||
|
||||
// Log errors for each request if needed. Outline failures may occur
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
margin: -1px -1px 0;
|
||||
}
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
@@ -332,7 +332,7 @@
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
flex-direction: column;
|
||||
gap: $spacer;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
|
||||
const provider = options?.provider || 'legacy';
|
||||
const enableNavigationSidebar = options.enableNavigationSidebar || { enable_navigation_sidebar: true };
|
||||
const alwaysOpenAuxiliarySidebar = options.alwaysOpenAuxiliarySidebar || { always_open_auxiliary_sidebar: true };
|
||||
const enableCompletionTracking = options.enableCompletionTracking || { enable_completion_tracking: true };
|
||||
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||
axiosMock.onGet(courseHomeMetadataUrl).reply(200, courseHomeMetadata);
|
||||
@@ -187,6 +188,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
|
||||
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
|
||||
...enableNavigationSidebar,
|
||||
...alwaysOpenAuxiliarySidebar,
|
||||
...enableCompletionTracking,
|
||||
});
|
||||
|
||||
axiosMock.onGet(outlineSidebarUrl).reply(200, {
|
||||
|
||||
Reference in New Issue
Block a user