diff --git a/src/course-outline/section-card/SectionCard.tsx b/src/course-outline/section-card/SectionCard.tsx
index 2e4bfac07b..aaa6e7f018 100644
--- a/src/course-outline/section-card/SectionCard.tsx
+++ b/src/course-outline/section-card/SectionCard.tsx
@@ -6,7 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import {
Bubble, Button, Icon, StandardModal, useToggle,
} from '@openedx/paragon';
-import { Newsstand } from '@openedx/paragon/icons';
+import { LinkOff, Newsstand } from '@openedx/paragon/icons';
import { useSearchParams } from 'react-router-dom';
import classNames from 'classnames';
@@ -23,7 +23,7 @@ import { ContainerType } from '@src/generic/key-utils';
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
import { ContentType } from '@src/library-authoring/routes';
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
-import { XBlock } from '@src/data/types';
+import type { XBlock } from '@src/data/types';
import messages from './messages';
interface SectionCardProps {
@@ -123,6 +123,7 @@ const SectionCard = ({
highlights,
actions: sectionActions,
isHeaderVisible = true,
+ upstreamInfo,
} = section;
useEffect(() => {
@@ -219,14 +220,16 @@ const SectionCard = ({
}
}, [savingStatus]);
+ const upstreamRefOk = !upstreamInfo?.errorMessage;
+
const titleComponent = (
+ prefixIcon={!!upstreamInfo?.upstreamRef && (
+
)}
/>
);
diff --git a/src/course-outline/subsection-card/SubsectionCard.tsx b/src/course-outline/subsection-card/SubsectionCard.tsx
index 2ede79e353..12aebe28f7 100644
--- a/src/course-outline/subsection-card/SubsectionCard.tsx
+++ b/src/course-outline/subsection-card/SubsectionCard.tsx
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, StandardModal, useToggle } from '@openedx/paragon';
-import { Newsstand } from '@openedx/paragon/icons';
+import { LinkOff, Newsstand } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
@@ -24,7 +24,7 @@ import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import { ContainerType } from '@src/generic/key-utils';
import { ContentType } from '@src/library-authoring/routes';
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
-import { XBlock } from '@src/data/types';
+import type { XBlock } from '@src/data/types';
import messages from './messages';
interface SubsectionCardProps {
@@ -105,6 +105,7 @@ const SubsectionCard = ({
isHeaderVisible = true,
enableCopyPasteUnits = false,
proctoringExamConfigurationLink,
+ upstreamInfo,
} = subsection;
// re-create actions object for customizations
@@ -167,14 +168,16 @@ const SubsectionCard = ({
const handleNewButtonClick = () => onNewUnitSubmit(id);
const handlePasteButtonClick = () => onPasteClick(id, section.id);
+ const upstreamRefOk = !upstreamInfo?.errorMessage;
+
const titleComponent = (
+ prefixIcon={!!upstreamInfo?.upstreamRef && (
+
)}
/>
);
diff --git a/src/course-outline/unit-card/UnitCard.tsx b/src/course-outline/unit-card/UnitCard.tsx
index 62a5d307f9..5f0339900f 100644
--- a/src/course-outline/unit-card/UnitCard.tsx
+++ b/src/course-outline/unit-card/UnitCard.tsx
@@ -6,7 +6,7 @@ import {
} from 'react';
import { useDispatch } from 'react-redux';
import { Icon, useToggle } from '@openedx/paragon';
-import { Newsstand } from '@openedx/paragon/icons';
+import { LinkOff, Newsstand } from '@openedx/paragon/icons';
import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router-dom';
@@ -22,7 +22,7 @@ import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
import { useClipboard } from '@src/generic/clipboard';
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
-import { XBlock } from '@src/data/types';
+import type { XBlock } from '@src/data/types';
interface UnitCardProps {
unit: XBlock;
@@ -157,13 +157,15 @@ const UnitCard = ({
dispatch(fetchCourseSectionQuery([section.id]));
}, [dispatch, section]);
+ const upstreamRefOk = !upstreamInfo?.errorMessage;
+
const titleComponent = (
+ prefixIcon={!!upstreamInfo?.upstreamRef && (
+
)}
/>
);
diff --git a/src/course-unit/data/utils.js b/src/course-unit/data/utils.ts
similarity index 55%
rename from src/course-unit/data/utils.js
rename to src/course-unit/data/utils.ts
index 891021debd..94c457455f 100644
--- a/src/course-unit/data/utils.js
+++ b/src/course-unit/data/utils.ts
@@ -1,5 +1,7 @@
import { camelCaseObject } from '@edx/frontend-platform';
+import type { XBlock } from '@src/data/types';
+
import { NOTIFICATION_MESSAGES } from '../../constants';
import { PUBLISH_TYPES } from '../constants';
@@ -27,35 +29,42 @@ export function normalizeCourseSectionVerticalData(metadata) {
/**
* Get the notification message based on the publishing type and visibility.
- * @param {string} type - The publishing type.
- * @param {boolean} isVisible - The visibility status.
- * @param {boolean} isModalView - The modal view status.
- * @returns {string} The corresponding notification message.
+ * @param type - The publishing type.
+ * @param isVisible - The visibility status.
+ * @param isModalView - The modal view status.
+ * @returns The corresponding notification message.
*/
-export const getNotificationMessage = (type, isVisible, isModalView) => {
- let notificationMessage;
-
+export const getNotificationMessage = (type: string, isVisible: boolean, isModalView: boolean): string => {
if (type === PUBLISH_TYPES.discardChanges) {
- notificationMessage = NOTIFICATION_MESSAGES.discardChanges;
- } else if (type === PUBLISH_TYPES.makePublic) {
- notificationMessage = NOTIFICATION_MESSAGES.publishing;
- } else if (type === PUBLISH_TYPES.republish && isModalView) {
- notificationMessage = NOTIFICATION_MESSAGES.saving;
- } else if (type === PUBLISH_TYPES.republish && !isVisible) {
- notificationMessage = NOTIFICATION_MESSAGES.makingVisibleToStudents;
- } else if (type === PUBLISH_TYPES.republish && isVisible) {
- notificationMessage = NOTIFICATION_MESSAGES.hidingFromStudents;
+ return NOTIFICATION_MESSAGES.discardChanges;
+ }
+ if (type === PUBLISH_TYPES.makePublic) {
+ return NOTIFICATION_MESSAGES.publishing;
+ }
+ if (type === PUBLISH_TYPES.republish && isModalView) {
+ return NOTIFICATION_MESSAGES.saving;
+ }
+ // istanbul ignore next: this is not used in the app
+ if (type === PUBLISH_TYPES.republish && !isVisible) {
+ return NOTIFICATION_MESSAGES.makingVisibleToStudents;
+ }
+
+ // istanbul ignore next: this is not used in the app
+ if (type === PUBLISH_TYPES.republish && isVisible) {
+ return NOTIFICATION_MESSAGES.hidingFromStudents;
}
- return notificationMessage;
+ // istanbul ignore next: should never hit this case
+ return NOTIFICATION_MESSAGES.empty;
};
/**
* Updates the 'id' property of objects in the data structure using the 'blockId' value where present.
- * @param {Object} data - The original data structure to be updated.
- * @returns {Object} - The updated data structure with updated 'id' values.
+ * @param data - The original data structure to be updated.
+ * @returns The updated data structure with updated 'id' values.
*/
-export const updateXBlockBlockIdToId = (data) => {
+export const updateXBlockBlockIdToId = (data: object): object => {
+ // istanbul ignore if: should never hit this case
if (typeof data !== 'object' || data === null) {
return data;
}
@@ -64,7 +73,7 @@ export const updateXBlockBlockIdToId = (data) => {
return data.map(updateXBlockBlockIdToId);
}
- const updatedData = {};
+ const updatedData: Record = {};
Object.keys(data).forEach(key => {
const value = data[key];
@@ -90,9 +99,11 @@ export const updateXBlockBlockIdToId = (data) => {
*
* Units sourced from libraries are read-only (temporary, for Teak).
*
- * @param {object} unit - uses the 'upstreamInfo' object if found.
- * @returns {boolean} True if readOnly, False if editable.
+ * @param unit - uses the 'upstreamInfo' object if found.
+ * @returns True if readOnly, False if editable.
*/
-export const isUnitReadOnly = ({ upstreamInfo }) => (
- upstreamInfo && upstreamInfo.upstreamRef && upstreamInfo.upstreamRef.startsWith('lct:')
+export const isUnitReadOnly = ({ upstreamInfo }: XBlock): boolean => (
+ !!upstreamInfo
+ && !!upstreamInfo.upstreamRef
+ && upstreamInfo.upstreamRef.startsWith('lct:')
);
diff --git a/src/data/types.ts b/src/data/types.ts
index f7fedfd820..c2ac2a469e 100644
--- a/src/data/types.ts
+++ b/src/data/types.ts
@@ -47,10 +47,11 @@ export interface XBlockPrereqs {
blockDisplayName: string;
}
-export interface UpstreeamInfo {
+export interface UpstreamInfo {
readyToSync: boolean,
upstreamRef: string,
versionSynced: number,
+ errorMessage: string | null,
}
export interface XBlock {
@@ -106,5 +107,5 @@ export interface XBlock {
prereqMinScore?: number;
prereqMinCompletion?: number;
discussionEnabled?: boolean;
- upstreamInfo?: UpstreeamInfo;
+ upstreamInfo?: UpstreamInfo;
}