diff --git a/src/course-outline/CourseOutline.scss b/src/course-outline/CourseOutline.scss
index 9f65f6e605..e2ce6d950a 100644
--- a/src/course-outline/CourseOutline.scss
+++ b/src/course-outline/CourseOutline.scss
@@ -12,3 +12,7 @@
.border-dashed {
border: dashed;
}
+
+.bg-draft-status {
+ background-color: #F4B57B;
+}
diff --git a/src/course-outline/CourseOutline.test.tsx b/src/course-outline/CourseOutline.test.tsx
index 27ca700667..a576c4e4a1 100644
--- a/src/course-outline/CourseOutline.test.tsx
+++ b/src/course-outline/CourseOutline.test.tsx
@@ -2498,7 +2498,7 @@ describe('', () => {
expect(btn).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'View live' })).toBeInTheDocument();
expect((await screen.findAllByRole('button', { name: 'Add' })).length).toEqual(2);
- expect(await screen.findByRole('button', { name: 'More actions' })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'Course info' })).toBeInTheDocument();
const user = userEvent.setup();
await user.click(btn);
expect(await screen.findByRole('button', { name: 'Expand all' })).toBeInTheDocument();
diff --git a/src/course-outline/data/slice.ts b/src/course-outline/data/slice.ts
index 9052a4539f..c56d66470d 100644
--- a/src/course-outline/data/slice.ts
+++ b/src/course-outline/data/slice.ts
@@ -33,6 +33,7 @@ const initialState = {
},
videoSharingEnabled: false,
videoSharingOptions: VIDEO_SHARING_OPTIONS.perVideo,
+ hasChanges: false,
},
sectionsList: [],
isCustomRelativeDatesActive: false,
diff --git a/src/course-outline/data/thunk.ts b/src/course-outline/data/thunk.ts
index 26c7c460ad..5c975fe247 100644
--- a/src/course-outline/data/thunk.ts
+++ b/src/course-outline/data/thunk.ts
@@ -71,6 +71,7 @@ export function fetchCourseOutlineIndexQuery(courseId: string): (dispatch: any)
videoSharingOptions,
actions,
end,
+ hasChanges,
},
} = outlineIndex;
dispatch(fetchOutlineIndexSuccess(outlineIndex));
@@ -80,6 +81,7 @@ export function fetchCourseOutlineIndexQuery(courseId: string): (dispatch: any)
videoSharingOptions,
videoSharingEnabled,
endDate: end,
+ hasChanges,
}));
dispatch(updateCourseActions(actions));
diff --git a/src/course-outline/data/types.ts b/src/course-outline/data/types.ts
index 572c03caaa..e3f37a99f5 100644
--- a/src/course-outline/data/types.ts
+++ b/src/course-outline/data/types.ts
@@ -7,6 +7,7 @@ export interface CourseStructure {
start: string,
end: string,
actions: XBlockActions,
+ hasChanges: boolean,
}
// TODO: Create interface for all `Object` fields in courseOutline
@@ -35,19 +36,22 @@ export interface CourseDetails {
description?: string;
}
+export interface ChecklistType {
+ totalCourseLaunchChecks: number;
+ completedCourseLaunchChecks: number;
+ totalCourseBestPracticesChecks: number;
+ completedCourseBestPracticesChecks: number;
+}
+
export interface CourseOutlineStatusBar {
courseReleaseDate: string;
endDate: string;
highlightsEnabledForMessaging: boolean;
isSelfPaced: boolean;
- checklist: {
- totalCourseLaunchChecks: number;
- completedCourseLaunchChecks: number;
- totalCourseBestPracticesChecks: number;
- completedCourseBestPracticesChecks: number;
- };
+ checklist: ChecklistType;
videoSharingEnabled: boolean;
videoSharingOptions: string;
+ hasChanges: boolean;
}
export interface CourseOutlineState {
diff --git a/src/course-outline/header-navigations/HeaderActions.test.tsx b/src/course-outline/header-navigations/HeaderActions.test.tsx
index a65e5d1510..c992548c34 100644
--- a/src/course-outline/header-navigations/HeaderActions.test.tsx
+++ b/src/course-outline/header-navigations/HeaderActions.test.tsx
@@ -47,7 +47,6 @@ describe('', () => {
expect(await screen.findByRole('button', { name: messages.addButton.defaultMessage })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: messages.viewLiveButton.defaultMessage })).toBeInTheDocument();
- expect(await screen.findByRole('button', { name: messages.moreActionsButtonAriaLabel.defaultMessage })).toBeInTheDocument();
});
it('calls the correct handlers when clicking buttons', async () => {
@@ -67,17 +66,13 @@ describe('', () => {
expect(await screen.findByRole('button', { name: messages.addButton.defaultMessage })).toBeDisabled();
});
- it('should change pages using the dropdown button', async () => {
+ it('should show course info on click', async () => {
renderComponent();
// Click on the dropdown button
- await userEvent.click(screen.getByRole('button', { name: 'More actions' }));
-
- // Select the Help option
- const helpButton = screen.getByRole('button', { name: 'Help' });
- await userEvent.click(helpButton);
+ await userEvent.click(screen.getByRole('button', { name: 'Course info' }));
// Check if the current page change is called
- expect(setCurrentPageKeyMock).toHaveBeenCalledWith('help');
+ expect(setCurrentPageKeyMock).toHaveBeenCalledWith('info');
});
});
diff --git a/src/course-outline/header-navigations/HeaderActions.tsx b/src/course-outline/header-navigations/HeaderActions.tsx
index 7bfc2a4f54..bddd876e94 100644
--- a/src/course-outline/header-navigations/HeaderActions.tsx
+++ b/src/course-outline/header-navigations/HeaderActions.tsx
@@ -1,18 +1,16 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import {
- Button, Dropdown, Icon, OverlayTrigger, Stack, Tooltip,
+ Button, OverlayTrigger, Stack, Tooltip,
} from '@openedx/paragon';
import {
- Add as IconAdd, FindInPage, ViewSidebar,
+ Add as IconAdd, FindInPage, InfoOutline,
} from '@openedx/paragon/icons';
import { OutlinePageErrors, XBlockActions } from '@src/data/types';
-import type { SidebarPage } from '@src/generic/sidebar';
-import { type OutlineSidebarPageKeys, useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
+import { useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
import messages from './messages';
-import { getOutlineSidebarPages } from '../outline-sidebar/sidebarPages';
export interface HeaderActionsProps {
actions: {
@@ -29,12 +27,27 @@ const HeaderActions = ({
}: HeaderActionsProps) => {
const intl = useIntl();
const { lmsLink } = actions;
- const sidebarPages = getOutlineSidebarPages();
const { setCurrentPageKey } = useOutlineSidebarContext();
return (
+
+ {intl.formatMessage(messages.courseInfoButtonTooltip)}
+
+ )}
+ >
+
+
{courseActions.childAddable && (
-
-
-
-
-
- {Object.entries(sidebarPages).filter(([, page]) => !page.hideFromActionMenu)
- .map(([key, page]: [OutlineSidebarPageKeys, SidebarPage]) => (
- setCurrentPageKey(key)}
- >
-
-
- {intl.formatMessage(page.title)}
-
-
- ))}
-
-
-
);
};
diff --git a/src/course-outline/header-navigations/messages.ts b/src/course-outline/header-navigations/messages.ts
index b03c8c6ce1..94127ce282 100644
--- a/src/course-outline/header-navigations/messages.ts
+++ b/src/course-outline/header-navigations/messages.ts
@@ -34,14 +34,20 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.header-navigations.button.view-live',
defaultMessage: 'View live',
},
- moreActionsButtonAriaLabel: {
- id: 'course-authoring.course-outline.header-navigations.button.more-actions.aria-label',
- defaultMessage: 'More actions',
- description: 'More actions button aria label in course outline',
+ courseInfoButtonTooltip: {
+ id: 'course-authoring.course-outline.header-navigations.button.course.info.tooltip',
+ defaultMessage: 'Click to open course info in sidebar',
+ description: 'Tooltip text of course info button',
+ },
+ courseInfoButton: {
+ id: 'course-authoring.course-outline.header-navigations.button.course.info',
+ defaultMessage: 'Course info',
+ description: 'Course info button in course outline header',
},
viewLiveButtonTooltip: {
id: 'course-authoring.course-outline.header-navigations.button.view-live.tooltip',
defaultMessage: 'Click to open the courseware in the LMS in a new tab',
+ description: 'Tooltip text of view live button',
},
});
diff --git a/src/course-outline/outline-sidebar/sidebarPages.ts b/src/course-outline/outline-sidebar/sidebarPages.ts
index e70d8d6403..18f83b9151 100644
--- a/src/course-outline/outline-sidebar/sidebarPages.ts
+++ b/src/course-outline/outline-sidebar/sidebarPages.ts
@@ -40,7 +40,6 @@ export const getOutlineSidebarPages = (): OutlineSidebarPages => {
component: AddSidebar,
icon: Plus,
title: messages.sidebarButtonAdd,
- hideFromActionMenu: true,
},
} satisfies OutlineSidebarPages;
};
diff --git a/src/course-outline/status-bar/LegacyStatusBar.test.tsx b/src/course-outline/status-bar/LegacyStatusBar.test.tsx
index e6669d7f31..1aa12d7a77 100644
--- a/src/course-outline/status-bar/LegacyStatusBar.test.tsx
+++ b/src/course-outline/status-bar/LegacyStatusBar.test.tsx
@@ -50,6 +50,7 @@ const statusBarData: CourseOutlineStatusBar = {
highlightsEnabledForMessaging: true,
videoSharingEnabled: true,
videoSharingOptions: VIDEO_SHARING_OPTIONS.allOn,
+ hasChanges: true,
};
const queryClient = new QueryClient();
diff --git a/src/course-outline/status-bar/StatusBar.test.tsx b/src/course-outline/status-bar/StatusBar.test.tsx
index 6aaff5bd38..4bd9672226 100644
--- a/src/course-outline/status-bar/StatusBar.test.tsx
+++ b/src/course-outline/status-bar/StatusBar.test.tsx
@@ -20,8 +20,16 @@ const statusBarData: CourseOutlineStatusBar = {
highlightsEnabledForMessaging: true,
videoSharingEnabled: true,
videoSharingOptions: VIDEO_SHARING_OPTIONS.allOn,
+ hasChanges: false,
};
+jest.mock('@src/course-libraries/data/apiHooks', () => ({
+ useEntityLinksSummaryByDownstreamContext: () => ({
+ data: [{ readyToSyncCount: 2 }],
+ isLoading: false,
+ }),
+}));
+
const renderComponent = (props?: Partial) => render(
', () => {
expect(await screen.findByTestId('redux-provider')).toBeEmptyDOMElement();
});
+
+ it('renders unpublished badge', async () => {
+ renderComponent({
+ statusBarData: {
+ ...statusBarData,
+ hasChanges: true,
+ },
+ });
+ expect(await screen.findByText('Unpublished Changes')).toBeInTheDocument();
+ });
+
+ it('renders library updates', async () => {
+ renderComponent();
+ expect(await screen.findByText('2 Library Updates')).toBeInTheDocument();
+ });
+
+ it('hides checklist if completed', async () => {
+ renderComponent({
+ statusBarData: {
+ ...statusBarData,
+ checklist: {
+ totalCourseLaunchChecks: 5,
+ completedCourseLaunchChecks: 5,
+ totalCourseBestPracticesChecks: 4,
+ completedCourseBestPracticesChecks: 4,
+ },
+ },
+ });
+ // wait for render
+ expect(await screen.findByText('Feb 05, 2013 - Apr 09, 2013')).toBeInTheDocument();
+ expect(screen.queryByText(`9/9 ${messages.checklistCompleted.defaultMessage}`)).toBeNull();
+ });
});
diff --git a/src/course-outline/status-bar/StatusBar.tsx b/src/course-outline/status-bar/StatusBar.tsx
index b1fd74d5a6..215cfdff18 100644
--- a/src/course-outline/status-bar/StatusBar.tsx
+++ b/src/course-outline/status-bar/StatusBar.tsx
@@ -1,12 +1,15 @@
import moment, { Moment } from 'moment/moment';
-import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import { FormattedDate, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform/config';
import { Badge, Icon, Stack } from '@openedx/paragon';
import { Link } from 'react-router-dom';
-import { CourseOutlineStatusBar } from '@src/course-outline/data/types';
-import { ChecklistRtl } from '@openedx/paragon/icons';
+import type { ChecklistType, CourseOutlineStatusBar } from '@src/course-outline/data/types';
+import {
+ Cached, ChecklistRtl, Description, Event,
+} from '@openedx/paragon/icons';
import { useWaffleFlags } from '@src/data/apiHooks';
+import { useEntityLinksSummaryByDownstreamContext } from '@src/course-libraries/data/apiHooks';
import messages from './messages';
import { NotificationStatusIcon } from './NotificationStatusIcon';
@@ -23,13 +26,13 @@ const CourseBadge = ({ startDate, endDate }: { startDate: Moment, endDate: Momen
);
case now.isBefore(startDate):
return (
-
+
);
case endDate.isValid() && endDate.isBefore(now):
return (
-
+
);
@@ -39,6 +42,43 @@ const CourseBadge = ({ startDate, endDate }: { startDate: Moment, endDate: Momen
}
};
+const UnpublishedBadgeStatus = () => (
+
+
+
+
+
+
+);
+
+const LibraryUpdates = ({ courseId }: { courseId: string }) => {
+ const { data } = useEntityLinksSummaryByDownstreamContext(courseId);
+ const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0);
+ const url = `/course/${courseId}/libraries?tab=review`;
+
+ if (!outOfSyncCount || outOfSyncCount === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
+
const CourseDates = ({
startDate, endDate, startDateRaw, datesLink,
}: {
@@ -54,7 +94,10 @@ const CourseDates = ({
className="small"
to={datesLink}
>
- {startDateRaw}
+
+
+ {startDateRaw}
+
);
}
@@ -64,23 +107,56 @@ const CourseDates = ({
className="small text-gray-700"
to={datesLink}
>
-
- {endDate.isValid() && (
- <>
- {' - '}
-
- >
- )}
+
+
+
+ {endDate.isValid() && (
+ <>
+ {' - '}
+
+ >
+ )}
+
+
+ );
+};
+
+const Checklists = ({ courseId, checklist }: {
+ courseId: string;
+ checklist: ChecklistType;
+}) => {
+ const {
+ completedCourseLaunchChecks,
+ completedCourseBestPracticesChecks,
+ totalCourseLaunchChecks,
+ totalCourseBestPracticesChecks,
+ } = checklist;
+
+ const completed = completedCourseLaunchChecks + completedCourseBestPracticesChecks;
+ const total = totalCourseLaunchChecks + totalCourseBestPracticesChecks;
+
+ if (completed === total) {
+ return null;
+ }
+
+ const checkListTitle = `${completed}/${total}`;
+ return (
+
+
+ {checkListTitle}
);
};
@@ -96,25 +172,17 @@ export const StatusBar = ({
isLoading,
courseId,
}: StatusBarProps) => {
- const intl = useIntl();
const waffleFlags = useWaffleFlags(courseId);
const {
endDate,
courseReleaseDate,
checklist,
+ hasChanges,
} = statusBarData;
- const {
- completedCourseLaunchChecks,
- completedCourseBestPracticesChecks,
- totalCourseLaunchChecks,
- totalCourseBestPracticesChecks,
- } = checklist;
-
const courseReleaseDateObj = moment.utc(courseReleaseDate, 'MMM DD, YYYY [at] HH:mm UTC', true);
const endDateObj = moment.utc(endDate);
- const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`;
const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, getConfig().STUDIO_BASE_URL).href;
if (isLoading) {
@@ -124,20 +192,16 @@ export const StatusBar = ({
return (
+ {hasChanges && }
+
+
-
-
- {checkListTitle} {intl.formatMessage(messages.checklistCompleted)}
-
);
};
diff --git a/src/course-outline/status-bar/messages.ts b/src/course-outline/status-bar/messages.ts
index f13f70131d..0cc393ae5d 100644
--- a/src/course-outline/status-bar/messages.ts
+++ b/src/course-outline/status-bar/messages.ts
@@ -76,6 +76,11 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.status-bar.video-sharing.allOn.text',
defaultMessage: 'All Videos',
},
+ unpublishedBadgeText: {
+ id: 'course-authoring.course-outline.status-bar.unpublished.badge.text',
+ defaultMessage: 'Unpublished Changes',
+ description: 'Text in badge displayed in course outline if course has unpublished changes.',
+ },
activeBadgeText: {
id: 'course-authoring.course-outline.status-bar.active.badge.text',
defaultMessage: 'Active',
@@ -91,6 +96,11 @@ const messages = defineMessages({
defaultMessage: 'Upcoming',
description: 'Upcoming Badge shown in course outline when the course has not started yet.',
},
+ libraryUpdatesText: {
+ id: 'course-authoring.course-outline.status-bar.library.updates.text',
+ defaultMessage: '{count, plural, one {{count} Library Update} other {{count} Library Updates}}',
+ description: 'Status text displaying count of library updates',
+ },
});
export default messages;
diff --git a/src/generic/sidebar/Sidebar.tsx b/src/generic/sidebar/Sidebar.tsx
index 7db1964bcb..9ca5c29994 100644
--- a/src/generic/sidebar/Sidebar.tsx
+++ b/src/generic/sidebar/Sidebar.tsx
@@ -19,7 +19,6 @@ export interface SidebarPage {
component: React.ComponentType;
icon: React.ComponentType;
title: MessageDescriptor;
- hideFromActionMenu?: boolean;
}
type SidebarPages = Record;