Skip to content

Commit e7555de

Browse files
committed
feat: add UpstreamInfoComponent
1 parent ed81755 commit e7555de

File tree

6 files changed

+101
-21
lines changed

6 files changed

+101
-21
lines changed

src/course-outline/section-card/SectionCard.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import {
44
import { useDispatch } from 'react-redux';
55
import { useIntl } from '@edx/frontend-platform/i18n';
66
import {
7-
Bubble, Button, Icon, StandardModal, useToggle,
7+
Bubble, Button, StandardModal, useToggle,
88
} from '@openedx/paragon';
9-
import { LinkOff, Newsstand } from '@openedx/paragon/icons';
109
import { useSearchParams } from 'react-router-dom';
1110
import classNames from 'classnames';
1211

@@ -23,6 +22,7 @@ import { ContainerType } from '@src/generic/key-utils';
2322
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
2423
import { ContentType } from '@src/library-authoring/routes';
2524
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
25+
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2626
import type { XBlock } from '@src/data/types';
2727
import messages from './messages';
2828

@@ -220,17 +220,13 @@ const SectionCard = ({
220220
}
221221
}, [savingStatus]);
222222

223-
const upstreamRefOk = !upstreamInfo?.errorMessage;
224-
225223
const titleComponent = (
226224
<TitleButton
227225
title={displayName}
228226
isExpanded={isExpanded}
229227
onTitleClick={handleExpandContent}
230228
namePrefix={namePrefix}
231-
prefixIcon={!!upstreamInfo?.upstreamRef && (
232-
<Icon src={upstreamRefOk ? Newsstand : LinkOff} className="mr-1" />
233-
)}
229+
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} />}
234230
/>
235231
);
236232

src/course-outline/subsection-card/SubsectionCard.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import React, {
44
import { useDispatch } from 'react-redux';
55
import { useSearchParams } from 'react-router-dom';
66
import { useIntl } from '@edx/frontend-platform/i18n';
7-
import { Icon, StandardModal, useToggle } from '@openedx/paragon';
8-
import { LinkOff, Newsstand } from '@openedx/paragon/icons';
7+
import { StandardModal, useToggle } from '@openedx/paragon';
98
import classNames from 'classnames';
109
import { isEmpty } from 'lodash';
1110

@@ -22,6 +21,7 @@ import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course
2221
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
2322
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
2423
import { ContainerType } from '@src/generic/key-utils';
24+
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2525
import { ContentType } from '@src/library-authoring/routes';
2626
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
2727
import type { XBlock } from '@src/data/types';
@@ -168,17 +168,13 @@ const SubsectionCard = ({
168168
const handleNewButtonClick = () => onNewUnitSubmit(id);
169169
const handlePasteButtonClick = () => onPasteClick(id, section.id);
170170

171-
const upstreamRefOk = !upstreamInfo?.errorMessage;
172-
173171
const titleComponent = (
174172
<TitleButton
175173
title={displayName}
176174
isExpanded={isExpanded}
177175
onTitleClick={handleExpandContent}
178176
namePrefix={namePrefix}
179-
prefixIcon={!!upstreamInfo?.upstreamRef && (
180-
<Icon src={upstreamRefOk ? Newsstand : LinkOff} className="mr-1" />
181-
)}
177+
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} />}
182178
/>
183179
);
184180

src/course-outline/unit-card/UnitCard.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
useRef,
66
} from 'react';
77
import { useDispatch } from 'react-redux';
8-
import { Icon, useToggle } from '@openedx/paragon';
9-
import { LinkOff, Newsstand } from '@openedx/paragon/icons';
8+
import { useToggle } from '@openedx/paragon';
109
import { isEmpty } from 'lodash';
1110
import { useSearchParams } from 'react-router-dom';
1211

@@ -21,6 +20,7 @@ import TitleLink from '@src/course-outline/card-header/TitleLink';
2120
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
2221
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
2322
import { useClipboard } from '@src/generic/clipboard';
23+
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2424
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
2525
import type { XBlock } from '@src/data/types';
2626

@@ -157,16 +157,12 @@ const UnitCard = ({
157157
dispatch(fetchCourseSectionQuery([section.id]));
158158
}, [dispatch, section]);
159159

160-
const upstreamRefOk = !upstreamInfo?.errorMessage;
161-
162160
const titleComponent = (
163161
<TitleLink
164162
title={displayName}
165163
titleLink={getTitleLink(id)}
166164
namePrefix={namePrefix}
167-
prefixIcon={!!upstreamInfo?.upstreamRef && (
168-
<Icon src={upstreamRefOk ? Newsstand : LinkOff} size="sm" className="mr-1" />
169-
)}
165+
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} size="sm" />}
170166
/>
171167
);
172168

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { IntlProvider } from '@edx/frontend-platform/i18n';
2+
import { render, screen } from '@testing-library/react';
3+
import { UpstreamInfoIcon, UpstreamInfoIconProps } from '.';
4+
5+
type UpstreamInfo = UpstreamInfoIconProps['upstreamInfo'];
6+
7+
const renderComponent = (upstreamInfo?: UpstreamInfo) => (
8+
render(
9+
<IntlProvider locale="en">
10+
<UpstreamInfoIcon upstreamInfo={upstreamInfo} />
11+
</IntlProvider>,
12+
)
13+
);
14+
15+
describe('<UpstreamInfoIcon>', () => {
16+
it('should render with link', () => {
17+
renderComponent({ upstreamRef: 'some-ref', errorMessage: null });
18+
expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument();
19+
});
20+
21+
it('should render with broken link', () => {
22+
renderComponent({ upstreamRef: 'some-ref', errorMessage: 'upstream error' });
23+
expect(screen.getByTitle('The link to the library item is broken.')).toBeInTheDocument();
24+
});
25+
26+
it('should render null without upstream', () => {
27+
const { container } = renderComponent(undefined);
28+
expect(container).toBeEmptyDOMElement();
29+
});
30+
31+
it('should render null without upstreamRf', () => {
32+
const { container } = renderComponent({ upstreamRef: null, errorMessage: null });
33+
expect(container).toBeEmptyDOMElement();
34+
});
35+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable react/prop-types */
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { Icon } from '@openedx/paragon';
4+
import { LinkOff, Newsstand } from '@openedx/paragon/icons';
5+
6+
import messages from './messages';
7+
8+
export interface UpstreamInfoIconProps {
9+
upstreamInfo?: {
10+
errorMessage?: string | null;
11+
upstreamRef?: string | null;
12+
};
13+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline';
14+
}
15+
16+
export const UpstreamInfoIcon: React.FC<UpstreamInfoIconProps> = ({ upstreamInfo, size }) => {
17+
const intl = useIntl();
18+
if (!upstreamInfo?.upstreamRef) {
19+
return null;
20+
}
21+
22+
const iconProps = !upstreamInfo?.errorMessage
23+
? {
24+
title: intl.formatMessage(messages.upstreamLinkOk),
25+
ariaLabel: intl.formatMessage(messages.upstreamLinkOk),
26+
src: Newsstand,
27+
}
28+
: {
29+
title: intl.formatMessage(messages.upstreamLinkError),
30+
ariaLabel: intl.formatMessage(messages.upstreamLinkError),
31+
src: LinkOff,
32+
};
33+
34+
return (
35+
<Icon
36+
{...iconProps}
37+
size={size}
38+
className="mr-1"
39+
/>
40+
);
41+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
upstreamLinkOk: {
5+
defaultMessage: 'This item is linked to a library item.',
6+
id: 'upstream-icon.ok',
7+
description: 'Hint and aria-label for the upstream icon when the link is valid.',
8+
},
9+
upstreamLinkError: {
10+
defaultMessage: 'The link to the library item is broken.',
11+
id: 'upstream-icon.error',
12+
description: 'Hint and aria-label for the upstream icon when the link is broken.',
13+
},
14+
});
15+
16+
export default messages;

0 commit comments

Comments
 (0)