Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/course-outline/section-card/SectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Bubble, Button, Icon, StandardModal, useToggle,
Bubble, Button, StandardModal, useToggle,
} from '@openedx/paragon';
import { Newsstand } from '@openedx/paragon/icons';
import { useSearchParams } from 'react-router-dom';
import classNames from 'classnames';

Expand All @@ -23,7 +22,8 @@ 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 { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
import type { XBlock } from '@src/data/types';
import messages from './messages';

interface SectionCardProps {
Expand Down Expand Up @@ -123,6 +123,7 @@ const SectionCard = ({
highlights,
actions: sectionActions,
isHeaderVisible = true,
upstreamInfo,
} = section;

useEffect(() => {
Expand Down Expand Up @@ -225,9 +226,7 @@ const SectionCard = ({
isExpanded={isExpanded}
onTitleClick={handleExpandContent}
namePrefix={namePrefix}
prefixIcon={!!section.upstreamInfo?.upstreamRef && (
<Icon src={Newsstand} className="mr-1" />
)}
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} />}
/>
);

Expand Down
11 changes: 5 additions & 6 deletions src/course-outline/subsection-card/SubsectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import React, {
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 { StandardModal, useToggle } from '@openedx/paragon';
import classNames from 'classnames';
import { isEmpty } from 'lodash';

Expand All @@ -22,9 +21,10 @@ import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import { ContainerType } from '@src/generic/key-utils';
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
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 {
Expand Down Expand Up @@ -105,6 +105,7 @@ const SubsectionCard = ({
isHeaderVisible = true,
enableCopyPasteUnits = false,
proctoringExamConfigurationLink,
upstreamInfo,
} = subsection;

// re-create actions object for customizations
Expand Down Expand Up @@ -173,9 +174,7 @@ const SubsectionCard = ({
isExpanded={isExpanded}
onTitleClick={handleExpandContent}
namePrefix={namePrefix}
prefixIcon={!!subsection.upstreamInfo?.upstreamRef && (
<Icon src={Newsstand} className="mr-1" />
)}
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} />}
/>
);

Expand Down
10 changes: 4 additions & 6 deletions src/course-outline/unit-card/UnitCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
useRef,
} from 'react';
import { useDispatch } from 'react-redux';
import { Icon, useToggle } from '@openedx/paragon';
import { Newsstand } from '@openedx/paragon/icons';
import { useToggle } from '@openedx/paragon';
import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router-dom';

Expand All @@ -21,8 +20,9 @@ import TitleLink from '@src/course-outline/card-header/TitleLink';
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 { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
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;
Expand Down Expand Up @@ -162,9 +162,7 @@ const UnitCard = ({
title={displayName}
titleLink={getTitleLink(id)}
namePrefix={namePrefix}
prefixIcon={!!unit.upstreamInfo?.upstreamRef && (
<Icon src={Newsstand} size="sm" className="mr-1" />
)}
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} size="sm" />}
/>
);

Expand Down
61 changes: 36 additions & 25 deletions src/course-unit/data/utils.js → src/course-unit/data/utils.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
Expand All @@ -64,7 +73,7 @@ export const updateXBlockBlockIdToId = (data) => {
return data.map(updateXBlockBlockIdToId);
}

const updatedData = {};
const updatedData: Record<string, any> = {};

Object.keys(data).forEach(key => {
const value = data[key];
Expand All @@ -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:')
);
5 changes: 3 additions & 2 deletions src/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -106,5 +107,5 @@ export interface XBlock {
prereqMinScore?: number;
prereqMinCompletion?: number;
discussionEnabled?: boolean;
upstreamInfo?: UpstreeamInfo;
upstreamInfo?: UpstreamInfo;
}
35 changes: 35 additions & 0 deletions src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import { UpstreamInfoIcon, UpstreamInfoIconProps } from '.';

type UpstreamInfo = UpstreamInfoIconProps['upstreamInfo'];

const renderComponent = (upstreamInfo?: UpstreamInfo) => (
render(
<IntlProvider locale="en">
<UpstreamInfoIcon upstreamInfo={upstreamInfo} />
</IntlProvider>,
)
);

describe('<UpstreamInfoIcon>', () => {
it('should render with link', () => {
renderComponent({ upstreamRef: 'some-ref', errorMessage: null });
expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument();
});

it('should render with broken link', () => {
renderComponent({ upstreamRef: 'some-ref', errorMessage: 'upstream error' });
expect(screen.getByTitle('The link to the library item is broken.')).toBeInTheDocument();
});

it('should render null without upstream', () => {
const { container } = renderComponent(undefined);
expect(container).toBeEmptyDOMElement();
});

it('should render null without upstreamRf', () => {
const { container } = renderComponent({ upstreamRef: null, errorMessage: null });
expect(container).toBeEmptyDOMElement();
});
});
41 changes: 41 additions & 0 deletions src/generic/upstream-info-icon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable react/prop-types */
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { LinkOff, Newsstand } from '@openedx/paragon/icons';

import messages from './messages';

export interface UpstreamInfoIconProps {
upstreamInfo?: {
errorMessage?: string | null;
upstreamRef?: string | null;
};
size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline';
}

export const UpstreamInfoIcon: React.FC<UpstreamInfoIconProps> = ({ upstreamInfo, size }) => {
const intl = useIntl();
if (!upstreamInfo?.upstreamRef) {
return null;
}

const iconProps = !upstreamInfo?.errorMessage
? {
title: intl.formatMessage(messages.upstreamLinkOk),
ariaLabel: intl.formatMessage(messages.upstreamLinkOk),
src: Newsstand,
}
: {
title: intl.formatMessage(messages.upstreamLinkError),
ariaLabel: intl.formatMessage(messages.upstreamLinkError),
src: LinkOff,
};

return (
<Icon
{...iconProps}
size={size}
className="mr-1"
/>
);
};
16 changes: 16 additions & 0 deletions src/generic/upstream-info-icon/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
upstreamLinkOk: {
defaultMessage: 'This item is linked to a library item.',
id: 'upstream-icon.ok',
description: 'Hint and aria-label for the upstream icon when the link is valid.',
},
upstreamLinkError: {
defaultMessage: 'The link to the library item is broken.',
id: 'upstream-icon.error',
description: 'Hint and aria-label for the upstream icon when the link is broken.',
},
});

export default messages;