fix: change container sync status icon [FC-0097] (#2360)
Changes the sync icon for Sections, Subsections, and Units in case the Upstream source is deleted.
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
@@ -123,6 +123,7 @@ const SectionCard = ({
|
||||
highlights,
|
||||
actions: sectionActions,
|
||||
isHeaderVisible = true,
|
||||
upstreamInfo,
|
||||
} = section;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -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} />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
@@ -105,6 +105,7 @@ const SubsectionCard = ({
|
||||
isHeaderVisible = true,
|
||||
enableCopyPasteUnits = false,
|
||||
proctoringExamConfigurationLink,
|
||||
upstreamInfo,
|
||||
} = subsection;
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -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} />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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" />}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
return notificationMessage;
|
||||
// istanbul ignore next: this is not used in the app
|
||||
if (type === PUBLISH_TYPES.republish && isVisible) {
|
||||
return NOTIFICATION_MESSAGES.hidingFromStudents;
|
||||
}
|
||||
|
||||
// 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<string, any> = {};
|
||||
|
||||
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:')
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
35
src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx
Normal file
35
src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx
Normal file
@@ -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
src/generic/upstream-info-icon/index.tsx
Normal file
41
src/generic/upstream-info-icon/index.tsx
Normal file
@@ -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
src/generic/upstream-info-icon/messages.ts
Normal file
16
src/generic/upstream-info-icon/messages.ts
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user