Skip to content

Commit d628cd9

Browse files
committed
feat: Adds sync state to upstream link icon
1 parent 132e440 commit d628cd9

7 files changed

Lines changed: 135 additions & 36 deletions

File tree

src/course-outline/card-header/TitleLink.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@ const TitleLink = ({
1414
namePrefix,
1515
prefixIcon,
1616
}: TitleLinkProps) => (
17-
<Button
18-
as={Link}
19-
variant="tertiary"
20-
data-testid={`${namePrefix}-card-header__title-link`}
21-
className="item-card-header__title-btn align-items-end"
22-
to={titleLink}
23-
title={title}
24-
>
25-
<div className="mr-2">
17+
<>
18+
<div className="mr-2 mb-1">
2619
{prefixIcon}
2720
</div>
28-
<span className={`${namePrefix}-card-title mb-0 truncate-1-line text-left`}>
29-
{title}
30-
</span>
31-
</Button>
21+
<Button
22+
as={Link}
23+
variant="tertiary"
24+
data-testid={`${namePrefix}-card-header__title-link`}
25+
className="item-card-header__title-btn align-items-end"
26+
to={titleLink}
27+
title={title}
28+
>
29+
<span className={`${namePrefix}-card-title mb-0 truncate-1-line text-left`}>
30+
{title}
31+
</span>
32+
</Button>
33+
</>
3234
);
3335

3436
export default TitleLink;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,13 @@ const SectionCard = ({
257257
isExpanded={isExpanded}
258258
onTitleClick={handleExpandContent}
259259
namePrefix={namePrefix}
260-
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} size="md" />}
260+
prefixIcon={(
261+
<UpstreamInfoIcon
262+
upstreamInfo={upstreamInfo}
263+
size="md"
264+
openSyncModal={openSyncModal}
265+
/>
266+
)}
261267
/>
262268
);
263269

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,13 @@ const SubsectionCard = ({
205205
isExpanded={isExpanded}
206206
onTitleClick={handleExpandContent}
207207
namePrefix={namePrefix}
208-
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} size="sm" />}
208+
prefixIcon={(
209+
<UpstreamInfoIcon
210+
upstreamInfo={upstreamInfo}
211+
size="sm"
212+
openSyncModal={openSyncModal}
213+
/>
214+
)}
209215
/>
210216
);
211217

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,13 @@ const UnitCard = ({
170170
title={displayName}
171171
titleLink={getTitleLink(id)}
172172
namePrefix={namePrefix}
173-
prefixIcon={<UpstreamInfoIcon upstreamInfo={upstreamInfo} size="xs" />}
173+
prefixIcon={(
174+
<UpstreamInfoIcon
175+
upstreamInfo={upstreamInfo}
176+
size="xs"
177+
openSyncModal={openSyncModal}
178+
/>
179+
)}
174180
/>
175181
);
176182

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,66 @@
1-
import { IntlProvider } from '@edx/frontend-platform/i18n';
2-
import { render, screen } from '@testing-library/react';
1+
import {
2+
render, screen, fireEvent, waitFor, initializeMocks,
3+
} from '@src/testUtils';
34
import { UpstreamInfoIcon, UpstreamInfoIconProps } from '.';
45

56
type UpstreamInfo = UpstreamInfoIconProps['upstreamInfo'];
7+
const mockOpenSyncModal = jest.fn();
68

79
const renderComponent = (upstreamInfo?: UpstreamInfo) => (
810
render(
9-
<IntlProvider locale="en">
10-
<UpstreamInfoIcon upstreamInfo={upstreamInfo} />
11-
</IntlProvider>,
11+
<UpstreamInfoIcon upstreamInfo={upstreamInfo} openSyncModal={mockOpenSyncModal} />,
1212
)
1313
);
1414

1515
describe('<UpstreamInfoIcon>', () => {
16+
beforeEach(() => {
17+
initializeMocks();
18+
});
19+
1620
it('should render with link', () => {
17-
renderComponent({ upstreamRef: 'some-ref', errorMessage: null });
21+
renderComponent({
22+
upstreamRef: 'some-ref',
23+
errorMessage: null,
24+
readyToSync: false,
25+
});
1826
expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument();
19-
expect(screen.queryByTitle('The link to the library item is broken.')).not.toBeInTheDocument();
27+
expect(screen.queryByTitle('The referenced library or library object is not available.')).not.toBeInTheDocument();
2028
});
2129

2230
it('should render with broken link', () => {
23-
renderComponent({ upstreamRef: 'some-ref', errorMessage: 'upstream error' });
31+
renderComponent({
32+
upstreamRef: 'some-ref',
33+
errorMessage: 'upstream error',
34+
readyToSync: false,
35+
});
2436
expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument();
25-
expect(screen.getByTitle('The link to the library item is broken.')).toBeInTheDocument();
37+
expect(screen.getByTitle('The referenced library or library object is not available.')).toBeInTheDocument();
38+
});
39+
40+
it('should render with ready to sync link and opens the sync modal', async () => {
41+
renderComponent({
42+
upstreamRef: 'some-ref',
43+
errorMessage: null,
44+
readyToSync: true,
45+
});
46+
47+
const icon = screen.getByTitle('This item is linked to a library item.');
48+
expect(icon).toBeInTheDocument();
49+
expect(screen.getByTitle('The linked library or library object has updates available.')).toBeInTheDocument();
50+
51+
fireEvent.click(icon);
52+
await waitFor(() => expect(mockOpenSyncModal).toHaveBeenCalled());
2653
});
2754

2855
it('should render null without upstream', () => {
29-
const { container } = renderComponent(undefined);
56+
renderComponent(undefined);
57+
const container = screen.getByTestId('redux-provider');
3058
expect(container).toBeEmptyDOMElement();
3159
});
3260

3361
it('should render null without upstreamRf', () => {
34-
const { container } = renderComponent({ upstreamRef: null, errorMessage: null });
62+
renderComponent({ upstreamRef: null, errorMessage: null });
63+
const container = screen.getByTestId('redux-provider');
3564
expect(container).toBeEmptyDOMElement();
3665
});
3766
});
Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,36 @@
11
/* eslint-disable react/prop-types */
22
import { useIntl } from '@edx/frontend-platform/i18n';
3-
import { Icon } from '@openedx/paragon';
4-
import { LinkOff, Newsstand } from '@openedx/paragon/icons';
3+
import { Button, Icon } from '@openedx/paragon';
4+
import { LinkOff, Newsstand, Sync } from '@openedx/paragon/icons';
55

66
import messages from './messages';
77

88
export interface UpstreamInfoIconProps {
99
upstreamInfo?: {
1010
errorMessage?: string | null;
1111
upstreamRef?: string | null;
12+
readyToSync?: boolean | null;
1213
};
1314
size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline';
1415
}
1516

16-
export const UpstreamInfoIcon: React.FC<UpstreamInfoIconProps> = ({ upstreamInfo, size }) => {
17+
const UpstreamInfoIconContent = ({
18+
upstreamInfo,
19+
size,
20+
}: UpstreamInfoIconProps) => {
1721
const intl = useIntl();
18-
if (!upstreamInfo?.upstreamRef) {
19-
return null;
20-
}
2122

2223
let hasTwoIcons = false;
23-
if (upstreamInfo?.errorMessage) {
24+
if (upstreamInfo?.errorMessage || upstreamInfo?.readyToSync) {
2425
hasTwoIcons = true;
2526
}
2627

2728
return (
28-
<div className={`upstream-info-icon size-${hasTwoIcons ? 'two' : 'one'}-${size} box-shadow-centered-1 d-flex justify-content-center`}>
29+
<div
30+
className={
31+
`upstream-info-icon size-${hasTwoIcons ? 'two' : 'one'}-${size} box-shadow-centered-1 d-flex justify-content-center`
32+
}
33+
>
2934
<Icon
3035
title={intl.formatMessage(messages.upstreamLinkOk)}
3136
aria-label={intl.formatMessage(messages.upstreamLinkOk)}
@@ -40,6 +45,46 @@ export const UpstreamInfoIcon: React.FC<UpstreamInfoIconProps> = ({ upstreamInfo
4045
size={size}
4146
/>
4247
)}
48+
{upstreamInfo?.readyToSync && (
49+
<Icon
50+
title={intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel)}
51+
aria-label={intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel)}
52+
src={Sync}
53+
size={size}
54+
/>
55+
)}
4356
</div>
4457
);
4558
};
59+
60+
export const UpstreamInfoIcon: React.FC<UpstreamInfoIconProps & { openSyncModal: () => void }> = ({
61+
upstreamInfo,
62+
size,
63+
openSyncModal,
64+
}) => {
65+
if (!upstreamInfo?.upstreamRef) {
66+
return null;
67+
}
68+
69+
const handleSyncModal = (e) => {
70+
e.stopPropagation();
71+
openSyncModal();
72+
};
73+
74+
if (upstreamInfo?.readyToSync) {
75+
return (
76+
<Button
77+
variant="tertiary"
78+
size="inline"
79+
className="px-0"
80+
onClick={handleSyncModal}
81+
>
82+
<UpstreamInfoIconContent upstreamInfo={upstreamInfo} size={size} />
83+
</Button>
84+
);
85+
}
86+
87+
return (
88+
<UpstreamInfoIconContent upstreamInfo={upstreamInfo} size={size} />
89+
);
90+
};

src/generic/upstream-info-icon/messages.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ const messages = defineMessages({
77
description: 'Hint and aria-label for the upstream icon when the link is valid.',
88
},
99
upstreamLinkError: {
10-
defaultMessage: 'The link to the library item is broken.',
10+
defaultMessage: 'The referenced library or library object is not available.',
1111
id: 'upstream-icon.error',
1212
description: 'Hint and aria-label for the upstream icon when the link is broken.',
1313
},
14+
upstreamLinkReadyToSyncAriaLabel: {
15+
defaultMessage: 'The linked library or library object has updates available.',
16+
id: 'upstream-icon.ready-to-sync.aria-label',
17+
description: 'Hint and aria-label for the upstream icon when the link is ready to sync.',
18+
},
1419
});
1520

1621
export default messages;

0 commit comments

Comments
 (0)