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; }