From ddc7a4c6e5488bdc73406a1ae4ce99764584bee6 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 5 Dec 2025 14:57:54 -0500 Subject: [PATCH 1/9] feat: Update Upstream Icon Info --- .../card-header/TitleButton.tsx | 4 +++- src/course-outline/card-header/TitleLink.tsx | 6 ++++-- .../section-card/SectionCard.tsx | 2 +- .../subsection-card/SubsectionCard.tsx | 2 +- src/course-outline/unit-card/UnitCard.tsx | 2 +- src/generic/styles.scss | 1 + .../upstream-info-icon/UpstreamInfoIcon.scss | 21 +++++++++++++++++++ src/generic/upstream-info-icon/index.tsx | 12 ++++++----- 8 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 src/generic/upstream-info-icon/UpstreamInfoIcon.scss diff --git a/src/course-outline/card-header/TitleButton.tsx b/src/course-outline/card-header/TitleButton.tsx index c1dd59082e..bd16d6c98e 100644 --- a/src/course-outline/card-header/TitleButton.tsx +++ b/src/course-outline/card-header/TitleButton.tsx @@ -47,7 +47,9 @@ const TitleButton = ({ onClick={onTitleClick} title={title} > - {prefixIcon} +
+ {prefixIcon} +
{title} diff --git a/src/course-outline/card-header/TitleLink.tsx b/src/course-outline/card-header/TitleLink.tsx index 622638fd99..e4920320ce 100644 --- a/src/course-outline/card-header/TitleLink.tsx +++ b/src/course-outline/card-header/TitleLink.tsx @@ -22,8 +22,10 @@ const TitleLink = ({ to={titleLink} title={title} > - {prefixIcon} - +
+ {prefixIcon} +
+ {title} diff --git a/src/course-outline/section-card/SectionCard.tsx b/src/course-outline/section-card/SectionCard.tsx index 02c1d5c644..fc5f9cb819 100644 --- a/src/course-outline/section-card/SectionCard.tsx +++ b/src/course-outline/section-card/SectionCard.tsx @@ -257,7 +257,7 @@ const SectionCard = ({ isExpanded={isExpanded} onTitleClick={handleExpandContent} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={} /> ); diff --git a/src/course-outline/subsection-card/SubsectionCard.tsx b/src/course-outline/subsection-card/SubsectionCard.tsx index 389338519c..be7c912145 100644 --- a/src/course-outline/subsection-card/SubsectionCard.tsx +++ b/src/course-outline/subsection-card/SubsectionCard.tsx @@ -205,7 +205,7 @@ const SubsectionCard = ({ isExpanded={isExpanded} onTitleClick={handleExpandContent} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={} /> ); diff --git a/src/course-outline/unit-card/UnitCard.tsx b/src/course-outline/unit-card/UnitCard.tsx index 8029f603cc..251183a23f 100644 --- a/src/course-outline/unit-card/UnitCard.tsx +++ b/src/course-outline/unit-card/UnitCard.tsx @@ -170,7 +170,7 @@ const UnitCard = ({ title={displayName} titleLink={getTitleLink(id)} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={} /> ); diff --git a/src/generic/styles.scss b/src/generic/styles.scss index 41475055ce..49e1cf64b4 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -15,3 +15,4 @@ @import "./modal-iframe"; @import "./alert-message"; @import "./inplace-text-editor/InplaceTextEditor"; +@import "./upstream-info-icon/UpstreamInfoIcon"; diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.scss b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss new file mode 100644 index 0000000000..1b3e0f5808 --- /dev/null +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss @@ -0,0 +1,21 @@ +.upstream-info-icon { + &.size-md { + width: 32px; + height: 26px; + } + + &.size-sm { + width: 28px; + height: 22px; + } + + &.size-xs { + width: 24px; + height: 18px; + } + + .pgn__icon { + // Removes the right margin + margin-right: 0 !important; + } +}; diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index cf12c1eb43..cafdb5812f 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -32,10 +32,12 @@ export const UpstreamInfoIcon: React.FC = ({ upstreamInfo }; return ( - +
+ +
); }; From 132e440932df80a607166506df8f0643ea429be9 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 5 Dec 2025 15:25:26 -0500 Subject: [PATCH 2/9] feat: Update Upstream info Icon in break state --- .../upstream-info-icon/UpstreamInfoIcon.scss | 24 ++++++++++----- .../UpstreamInfoIcon.test.tsx | 2 ++ src/generic/upstream-info-icon/index.tsx | 30 ++++++++++--------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.scss b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss index 1b3e0f5808..89099ec152 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.scss +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss @@ -1,21 +1,31 @@ .upstream-info-icon { - &.size-md { + &.size-one-md { width: 32px; height: 26px; } - &.size-sm { + &.size-one-sm { width: 28px; height: 22px; } - &.size-xs { + &.size-one-xs { width: 24px; height: 18px; } - .pgn__icon { - // Removes the right margin - margin-right: 0 !important; + &.size-two-md { + width: 60px; + height: 26px; + } + + &.size-two-sm { + width: 46px; + height: 22px; + } + + &.size-two-xs { + width: 36px; + height: 18px; } -}; +} diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx index 6dc3dc6c95..5584d3f6e2 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx @@ -16,10 +16,12 @@ describe('', () => { it('should render with link', () => { renderComponent({ upstreamRef: 'some-ref', errorMessage: null }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); + expect(screen.queryByTitle('The link to the library item is broken.')).not.toBeInTheDocument(); }); it('should render with broken link', () => { renderComponent({ upstreamRef: 'some-ref', errorMessage: 'upstream error' }); + expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.getByTitle('The link to the library item is broken.')).toBeInTheDocument(); }); diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index cafdb5812f..5fd2b4ecb9 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -19,25 +19,27 @@ export const UpstreamInfoIcon: React.FC = ({ upstreamInfo 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, - }; + let hasTwoIcons = false; + if (upstreamInfo?.errorMessage) { + hasTwoIcons = true; + } return ( -
+
+ {upstreamInfo?.errorMessage && ( + + )}
); }; From d628cd990abe8c1aa2224c649a7dbcde13a27577 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 5 Dec 2025 18:05:56 -0500 Subject: [PATCH 3/9] feat: Adds sync state to upstream link icon --- src/course-outline/card-header/TitleLink.tsx | 28 +++++---- .../section-card/SectionCard.tsx | 8 ++- .../subsection-card/SubsectionCard.tsx | 8 ++- src/course-outline/unit-card/UnitCard.tsx | 8 ++- .../UpstreamInfoIcon.test.tsx | 51 ++++++++++++---- src/generic/upstream-info-icon/index.tsx | 61 ++++++++++++++++--- src/generic/upstream-info-icon/messages.ts | 7 ++- 7 files changed, 135 insertions(+), 36 deletions(-) diff --git a/src/course-outline/card-header/TitleLink.tsx b/src/course-outline/card-header/TitleLink.tsx index e4920320ce..8e8c4d3000 100644 --- a/src/course-outline/card-header/TitleLink.tsx +++ b/src/course-outline/card-header/TitleLink.tsx @@ -14,21 +14,23 @@ const TitleLink = ({ namePrefix, prefixIcon, }: TitleLinkProps) => ( - + + ); export default TitleLink; diff --git a/src/course-outline/section-card/SectionCard.tsx b/src/course-outline/section-card/SectionCard.tsx index fc5f9cb819..5547fc46bb 100644 --- a/src/course-outline/section-card/SectionCard.tsx +++ b/src/course-outline/section-card/SectionCard.tsx @@ -257,7 +257,13 @@ const SectionCard = ({ isExpanded={isExpanded} onTitleClick={handleExpandContent} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={( + + )} /> ); diff --git a/src/course-outline/subsection-card/SubsectionCard.tsx b/src/course-outline/subsection-card/SubsectionCard.tsx index be7c912145..0e95a273ff 100644 --- a/src/course-outline/subsection-card/SubsectionCard.tsx +++ b/src/course-outline/subsection-card/SubsectionCard.tsx @@ -205,7 +205,13 @@ const SubsectionCard = ({ isExpanded={isExpanded} onTitleClick={handleExpandContent} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={( + + )} /> ); diff --git a/src/course-outline/unit-card/UnitCard.tsx b/src/course-outline/unit-card/UnitCard.tsx index 251183a23f..9c153b4fdf 100644 --- a/src/course-outline/unit-card/UnitCard.tsx +++ b/src/course-outline/unit-card/UnitCard.tsx @@ -170,7 +170,13 @@ const UnitCard = ({ title={displayName} titleLink={getTitleLink(id)} namePrefix={namePrefix} - prefixIcon={} + prefixIcon={( + + )} /> ); diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx index 5584d3f6e2..2d681b55d0 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx @@ -1,37 +1,66 @@ -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { render, screen } from '@testing-library/react'; +import { + render, screen, fireEvent, waitFor, initializeMocks, +} from '@src/testUtils'; import { UpstreamInfoIcon, UpstreamInfoIconProps } from '.'; type UpstreamInfo = UpstreamInfoIconProps['upstreamInfo']; +const mockOpenSyncModal = jest.fn(); const renderComponent = (upstreamInfo?: UpstreamInfo) => ( render( - - - , + , ) ); describe('', () => { + beforeEach(() => { + initializeMocks(); + }); + it('should render with link', () => { - renderComponent({ upstreamRef: 'some-ref', errorMessage: null }); + renderComponent({ + upstreamRef: 'some-ref', + errorMessage: null, + readyToSync: false, + }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); - expect(screen.queryByTitle('The link to the library item is broken.')).not.toBeInTheDocument(); + expect(screen.queryByTitle('The referenced library or library object is not available.')).not.toBeInTheDocument(); }); it('should render with broken link', () => { - renderComponent({ upstreamRef: 'some-ref', errorMessage: 'upstream error' }); + renderComponent({ + upstreamRef: 'some-ref', + errorMessage: 'upstream error', + readyToSync: false, + }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); - expect(screen.getByTitle('The link to the library item is broken.')).toBeInTheDocument(); + expect(screen.getByTitle('The referenced library or library object is not available.')).toBeInTheDocument(); + }); + + it('should render with ready to sync link and opens the sync modal', async () => { + renderComponent({ + upstreamRef: 'some-ref', + errorMessage: null, + readyToSync: true, + }); + + const icon = screen.getByTitle('This item is linked to a library item.'); + expect(icon).toBeInTheDocument(); + expect(screen.getByTitle('The linked library or library object has updates available.')).toBeInTheDocument(); + + fireEvent.click(icon); + await waitFor(() => expect(mockOpenSyncModal).toHaveBeenCalled()); }); it('should render null without upstream', () => { - const { container } = renderComponent(undefined); + renderComponent(undefined); + const container = screen.getByTestId('redux-provider'); expect(container).toBeEmptyDOMElement(); }); it('should render null without upstreamRf', () => { - const { container } = renderComponent({ upstreamRef: null, errorMessage: null }); + renderComponent({ upstreamRef: null, errorMessage: null }); + const container = screen.getByTestId('redux-provider'); expect(container).toBeEmptyDOMElement(); }); }); diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index 5fd2b4ecb9..cf6f7dd768 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -1,7 +1,7 @@ /* 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 { Button, Icon } from '@openedx/paragon'; +import { LinkOff, Newsstand, Sync } from '@openedx/paragon/icons'; import messages from './messages'; @@ -9,23 +9,28 @@ export interface UpstreamInfoIconProps { upstreamInfo?: { errorMessage?: string | null; upstreamRef?: string | null; + readyToSync?: boolean | null; }; size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline'; } -export const UpstreamInfoIcon: React.FC = ({ upstreamInfo, size }) => { +const UpstreamInfoIconContent = ({ + upstreamInfo, + size, +}: UpstreamInfoIconProps) => { const intl = useIntl(); - if (!upstreamInfo?.upstreamRef) { - return null; - } let hasTwoIcons = false; - if (upstreamInfo?.errorMessage) { + if (upstreamInfo?.errorMessage || upstreamInfo?.readyToSync) { hasTwoIcons = true; } return ( -
+
= ({ upstreamInfo size={size} /> )} + {upstreamInfo?.readyToSync && ( + + )}
); }; + +export const UpstreamInfoIcon: React.FC void }> = ({ + upstreamInfo, + size, + openSyncModal, +}) => { + if (!upstreamInfo?.upstreamRef) { + return null; + } + + const handleSyncModal = (e) => { + e.stopPropagation(); + openSyncModal(); + }; + + if (upstreamInfo?.readyToSync) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/src/generic/upstream-info-icon/messages.ts b/src/generic/upstream-info-icon/messages.ts index 7f3f6c69d3..057bcf0966 100644 --- a/src/generic/upstream-info-icon/messages.ts +++ b/src/generic/upstream-info-icon/messages.ts @@ -7,10 +7,15 @@ const messages = defineMessages({ description: 'Hint and aria-label for the upstream icon when the link is valid.', }, upstreamLinkError: { - defaultMessage: 'The link to the library item is broken.', + defaultMessage: 'The referenced library or library object is not available.', id: 'upstream-icon.error', description: 'Hint and aria-label for the upstream icon when the link is broken.', }, + upstreamLinkReadyToSyncAriaLabel: { + defaultMessage: 'The linked library or library object has updates available.', + id: 'upstream-icon.ready-to-sync.aria-label', + description: 'Hint and aria-label for the upstream icon when the link is ready to sync.', + }, }); export default messages; From 0969264a9c478bd44c501649b51816e558385083 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 8 Dec 2025 16:08:26 -0500 Subject: [PATCH 4/9] feat: Library sync icon with course overrides state --- .../UpstreamInfoIcon.test.tsx | 35 +++++++++++++- src/generic/upstream-info-icon/index.tsx | 47 +++++++++++++------ src/generic/upstream-info-icon/messages.ts | 5 ++ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx index 2d681b55d0..6033e83a69 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx @@ -22,6 +22,7 @@ describe('', () => { upstreamRef: 'some-ref', errorMessage: null, readyToSync: false, + downstreamCustomized: [], }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.queryByTitle('The referenced library or library object is not available.')).not.toBeInTheDocument(); @@ -32,6 +33,7 @@ describe('', () => { upstreamRef: 'some-ref', errorMessage: 'upstream error', readyToSync: false, + downstreamCustomized: [], }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.getByTitle('The referenced library or library object is not available.')).toBeInTheDocument(); @@ -42,6 +44,7 @@ describe('', () => { upstreamRef: 'some-ref', errorMessage: null, readyToSync: true, + downstreamCustomized: [], }); const icon = screen.getByTitle('This item is linked to a library item.'); @@ -52,6 +55,31 @@ describe('', () => { await waitFor(() => expect(mockOpenSyncModal).toHaveBeenCalled()); }); + it('should render with course overrides', () => { + renderComponent({ + upstreamRef: 'some-ref', + errorMessage: null, + readyToSync: false, + downstreamCustomized: ['data'], + }); + + expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); + expect(screen.getByTitle('This library reference has course overrides applied.')).toBeInTheDocument(); + }); + + it('should render with ready to sync and course overrides', () => { + renderComponent({ + upstreamRef: 'some-ref', + errorMessage: null, + readyToSync: true, + downstreamCustomized: ['data'], + }); + + expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); + expect(screen.queryByTitle('This library reference has course overrides applied.')).not.toBeInTheDocument(); + expect(screen.getByTitle('The linked library or library object has updates available.')).toBeInTheDocument(); + }); + it('should render null without upstream', () => { renderComponent(undefined); const container = screen.getByTestId('redux-provider'); @@ -59,7 +87,12 @@ describe('', () => { }); it('should render null without upstreamRf', () => { - renderComponent({ upstreamRef: null, errorMessage: null }); + renderComponent({ + upstreamRef: null, + errorMessage: null, + readyToSync: false, + downstreamCustomized: [], + }); const container = screen.getByTestId('redux-provider'); expect(container).toBeEmptyDOMElement(); }); diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index cf6f7dd768..21aee499d5 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -1,7 +1,9 @@ /* eslint-disable react/prop-types */ import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, Icon } from '@openedx/paragon'; -import { LinkOff, Newsstand, Sync } from '@openedx/paragon/icons'; +import { + CallSplit, LinkOff, Newsstand, Sync, +} from '@openedx/paragon/icons'; import messages from './messages'; @@ -9,7 +11,8 @@ export interface UpstreamInfoIconProps { upstreamInfo?: { errorMessage?: string | null; upstreamRef?: string | null; - readyToSync?: boolean | null; + readyToSync: boolean; + downstreamCustomized: string[]; }; size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline'; } @@ -21,10 +24,34 @@ const UpstreamInfoIconContent = ({ const intl = useIntl(); let hasTwoIcons = false; - if (upstreamInfo?.errorMessage || upstreamInfo?.readyToSync) { + const hasCourseOverrides = (upstreamInfo?.downstreamCustomized.length || 0) > 0; + if (upstreamInfo?.errorMessage || upstreamInfo?.readyToSync || hasCourseOverrides) { hasTwoIcons = true; } + const getSecondIconProps = () => { + if (upstreamInfo?.errorMessage) { + return { + title: intl.formatMessage(messages.upstreamLinkError), + ariaLabel: intl.formatMessage(messages.upstreamLinkError), + src: LinkOff, + }; + } if (upstreamInfo?.readyToSync) { + return { + title: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), + ariaLabel: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), + src: Sync, + }; + } if (hasCourseOverrides) { + return { + title: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), + ariaLabel: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), + src: CallSplit, + }; + } + return {}; + }; + return (
- {upstreamInfo?.errorMessage && ( - - )} - {upstreamInfo?.readyToSync && ( + {hasTwoIcons && ( )}
diff --git a/src/generic/upstream-info-icon/messages.ts b/src/generic/upstream-info-icon/messages.ts index 057bcf0966..b1bcce75d2 100644 --- a/src/generic/upstream-info-icon/messages.ts +++ b/src/generic/upstream-info-icon/messages.ts @@ -16,6 +16,11 @@ const messages = defineMessages({ id: 'upstream-icon.ready-to-sync.aria-label', description: 'Hint and aria-label for the upstream icon when the link is ready to sync.', }, + upstreamLinkOverridesAriaLabel: { + defaultMessage: 'This library reference has course overrides applied.', + id: 'upstream-icon.course-overrides.aria-label', + description: 'Hint and aria-label for the upstream icon when the link has course overrides.', + }, }); export default messages; From eb4fa6797aeebe7160d15fd6a8814746586bd079 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 8 Dec 2025 17:06:12 -0500 Subject: [PATCH 5/9] feat: Add tooltips to library sync icon --- .../section-card/SectionCard.test.tsx | 1 + .../subsection-card/SubsectionCard.test.tsx | 1 + .../unit-card/UnitCard.test.tsx | 1 + src/data/types.ts | 1 + .../UpstreamInfoIcon.test.tsx | 6 + src/generic/upstream-info-icon/index.tsx | 112 +++++++++++------- src/generic/upstream-info-icon/messages.ts | 10 ++ 7 files changed, 92 insertions(+), 40 deletions(-) diff --git a/src/course-outline/section-card/SectionCard.test.tsx b/src/course-outline/section-card/SectionCard.test.tsx index f2c37de59d..aca9cdd15d 100644 --- a/src/course-outline/section-card/SectionCard.test.tsx +++ b/src/course-outline/section-card/SectionCard.test.tsx @@ -75,6 +75,7 @@ const section = { versionDeclined: null, errorMessage: null, downstreamCustomized: [] as string[], + upstreamName: 'Upstream', }, } satisfies Partial as XBlock; diff --git a/src/course-outline/subsection-card/SubsectionCard.test.tsx b/src/course-outline/subsection-card/SubsectionCard.test.tsx index 159f8bd5aa..ce6ad09360 100644 --- a/src/course-outline/subsection-card/SubsectionCard.test.tsx +++ b/src/course-outline/subsection-card/SubsectionCard.test.tsx @@ -79,6 +79,7 @@ const subsection: XBlock = { versionDeclined: null, errorMessage: null, downstreamCustomized: [] as string[], + upstreamName: 'Upstream', }, } satisfies Partial as XBlock; diff --git a/src/course-outline/unit-card/UnitCard.test.tsx b/src/course-outline/unit-card/UnitCard.test.tsx index c8282a2735..9d8ef2b0d7 100644 --- a/src/course-outline/unit-card/UnitCard.test.tsx +++ b/src/course-outline/unit-card/UnitCard.test.tsx @@ -69,6 +69,7 @@ const unit = { versionDeclined: null, errorMessage: null, downstreamCustomized: [] as string[], + upstreamName: 'Upstream', }, } satisfies Partial as XBlock; diff --git a/src/data/types.ts b/src/data/types.ts index 4906c15c72..304dc61e73 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -57,6 +57,7 @@ export interface UpstreamChildrenInfo { export interface UpstreamInfo { readyToSync: boolean, upstreamRef: string, + upstreamName: string, versionSynced: number, versionAvailable: number | null, versionDeclined: number | null, diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx index 6033e83a69..abaef089ab 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx @@ -23,6 +23,7 @@ describe('', () => { errorMessage: null, readyToSync: false, downstreamCustomized: [], + upstreamName: 'Upstream', }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.queryByTitle('The referenced library or library object is not available.')).not.toBeInTheDocument(); @@ -34,6 +35,7 @@ describe('', () => { errorMessage: 'upstream error', readyToSync: false, downstreamCustomized: [], + upstreamName: 'Upstream', }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.getByTitle('The referenced library or library object is not available.')).toBeInTheDocument(); @@ -45,6 +47,7 @@ describe('', () => { errorMessage: null, readyToSync: true, downstreamCustomized: [], + upstreamName: 'Upstream', }); const icon = screen.getByTitle('This item is linked to a library item.'); @@ -61,6 +64,7 @@ describe('', () => { errorMessage: null, readyToSync: false, downstreamCustomized: ['data'], + upstreamName: 'Upstream', }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); @@ -73,6 +77,7 @@ describe('', () => { errorMessage: null, readyToSync: true, downstreamCustomized: ['data'], + upstreamName: 'Upstream', }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); @@ -92,6 +97,7 @@ describe('', () => { errorMessage: null, readyToSync: false, downstreamCustomized: [], + upstreamName: 'Upstream', }); const container = screen.getByTestId('redux-provider'); expect(container).toBeEmptyDOMElement(); diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index 21aee499d5..27b734156f 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -1,16 +1,21 @@ /* eslint-disable react/prop-types */ import { useIntl } from '@edx/frontend-platform/i18n'; -import { Button, Icon } from '@openedx/paragon'; +import { + Button, Icon, OverlayTrigger, Tooltip, +} from '@openedx/paragon'; import { CallSplit, LinkOff, Newsstand, Sync, } from '@openedx/paragon/icons'; +import { BoldText } from '@src/utils'; +import { ReactNode } from 'react'; import messages from './messages'; export interface UpstreamInfoIconProps { upstreamInfo?: { errorMessage?: string | null; upstreamRef?: string | null; + upstreamName: string; readyToSync: boolean; downstreamCustomized: string[]; }; @@ -23,54 +28,81 @@ const UpstreamInfoIconContent = ({ }: UpstreamInfoIconProps) => { const intl = useIntl(); + if (!upstreamInfo) { + return null; + } + let hasTwoIcons = false; - const hasCourseOverrides = (upstreamInfo?.downstreamCustomized.length || 0) > 0; - if (upstreamInfo?.errorMessage || upstreamInfo?.readyToSync || hasCourseOverrides) { + let secondIconProps = {}; + let tooltipMessage: string | ReactNode = intl.formatMessage( + messages.upstreamLinkTooltip, + { + upstreamName: upstreamInfo.upstreamName, + b: BoldText, + }, + ); + + if (upstreamInfo.errorMessage) { + hasTwoIcons = true; + tooltipMessage = intl.formatMessage(messages.upstreamLinkError); + secondIconProps = { + title: intl.formatMessage(messages.upstreamLinkError), + ariaLabel: intl.formatMessage(messages.upstreamLinkError), + src: LinkOff, + }; + } else if (upstreamInfo.readyToSync) { hasTwoIcons = true; + tooltipMessage = intl.formatMessage( + messages.upstreamLinkReadyToSyncTooltip, + { + upstreamName: upstreamInfo.upstreamName, + b: BoldText, + }, + ); + secondIconProps = { + title: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), + ariaLabel: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), + src: Sync, + }; + } else if ((upstreamInfo.downstreamCustomized.length || 0) > 0) { + hasTwoIcons = true; + tooltipMessage = intl.formatMessage(messages.upstreamLinkOverridesAriaLabel); + secondIconProps = { + title: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), + ariaLabel: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), + src: CallSplit, + }; } - const getSecondIconProps = () => { - if (upstreamInfo?.errorMessage) { - return { - title: intl.formatMessage(messages.upstreamLinkError), - ariaLabel: intl.formatMessage(messages.upstreamLinkError), - src: LinkOff, - }; - } if (upstreamInfo?.readyToSync) { - return { - title: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), - ariaLabel: intl.formatMessage(messages.upstreamLinkReadyToSyncAriaLabel), - src: Sync, - }; - } if (hasCourseOverrides) { - return { - title: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), - ariaLabel: intl.formatMessage(messages.upstreamLinkOverridesAriaLabel), - src: CallSplit, - }; - } - return {}; - }; - return ( -
+ {tooltipMessage} + + )} > - - {hasTwoIcons && ( +
- )} -
+ {hasTwoIcons && ( + + )} +
+ ); }; diff --git a/src/generic/upstream-info-icon/messages.ts b/src/generic/upstream-info-icon/messages.ts index b1bcce75d2..dddd15771e 100644 --- a/src/generic/upstream-info-icon/messages.ts +++ b/src/generic/upstream-info-icon/messages.ts @@ -16,11 +16,21 @@ const messages = defineMessages({ id: 'upstream-icon.ready-to-sync.aria-label', description: 'Hint and aria-label for the upstream icon when the link is ready to sync.', }, + upstreamLinkReadyToSyncTooltip: { + defaultMessage: 'The linked {upstreamName} has updates available.', + id: 'upstream-icon.ready-to-sync.tooltip', + description: 'Tooltip text for the upstream icon when the link is ready to sync.', + }, upstreamLinkOverridesAriaLabel: { defaultMessage: 'This library reference has course overrides applied.', id: 'upstream-icon.course-overrides.aria-label', description: 'Hint and aria-label for the upstream icon when the link has course overrides.', }, + upstreamLinkTooltip: { + defaultMessage: 'This is referenced via {upstreamName}', + id: 'upstream-icon.ok.tooltip', + description: 'Tooltip text for the upstream icon when the link is valid.', + }, }); export default messages; From 153b5bdf2475c687fcaf84a7fd2f2e2db67d121f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 9 Dec 2025 13:58:55 -0500 Subject: [PATCH 6/9] style: Update message --- src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx | 6 +++--- src/generic/upstream-info-icon/messages.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx index abaef089ab..ba5efad0a7 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.test.tsx @@ -26,7 +26,7 @@ describe('', () => { upstreamName: 'Upstream', }); expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); - expect(screen.queryByTitle('The referenced library or library object is not available.')).not.toBeInTheDocument(); + expect(screen.queryByTitle('The linked library object has updates available.')).not.toBeInTheDocument(); }); it('should render with broken link', () => { @@ -52,7 +52,7 @@ describe('', () => { const icon = screen.getByTitle('This item is linked to a library item.'); expect(icon).toBeInTheDocument(); - expect(screen.getByTitle('The linked library or library object has updates available.')).toBeInTheDocument(); + expect(screen.getByTitle('The linked library object has updates available.')).toBeInTheDocument(); fireEvent.click(icon); await waitFor(() => expect(mockOpenSyncModal).toHaveBeenCalled()); @@ -82,7 +82,7 @@ describe('', () => { expect(screen.getByTitle('This item is linked to a library item.')).toBeInTheDocument(); expect(screen.queryByTitle('This library reference has course overrides applied.')).not.toBeInTheDocument(); - expect(screen.getByTitle('The linked library or library object has updates available.')).toBeInTheDocument(); + expect(screen.getByTitle('The linked library object has updates available.')).toBeInTheDocument(); }); it('should render null without upstream', () => { diff --git a/src/generic/upstream-info-icon/messages.ts b/src/generic/upstream-info-icon/messages.ts index dddd15771e..ac844a64e8 100644 --- a/src/generic/upstream-info-icon/messages.ts +++ b/src/generic/upstream-info-icon/messages.ts @@ -12,7 +12,7 @@ const messages = defineMessages({ description: 'Hint and aria-label for the upstream icon when the link is broken.', }, upstreamLinkReadyToSyncAriaLabel: { - defaultMessage: 'The linked library or library object has updates available.', + defaultMessage: 'The linked library object has updates available.', id: 'upstream-icon.ready-to-sync.aria-label', description: 'Hint and aria-label for the upstream icon when the link is ready to sync.', }, From 5ac12b3a24402e89ad2a31d5e9047e1be745de40 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 9 Dec 2025 20:48:49 -0500 Subject: [PATCH 7/9] feat: Update styles --- src/generic/upstream-info-icon/UpstreamInfoIcon.scss | 11 +++++++++++ src/generic/upstream-info-icon/index.tsx | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/generic/upstream-info-icon/UpstreamInfoIcon.scss b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss index 89099ec152..aa655f8cce 100644 --- a/src/generic/upstream-info-icon/UpstreamInfoIcon.scss +++ b/src/generic/upstream-info-icon/UpstreamInfoIcon.scss @@ -1,4 +1,15 @@ .upstream-info-icon { + border: 1px solid var(--pgn-color-light-800); + color: var(--pgn-color-primary-500); + + &.sync-state { + &:hover { + border-color: var(--pgn-color-primary-500); + background-color: var(--pgn-color-primary-500); + color: white; + } + } + &.size-one-md { width: 32px; height: 26px; diff --git a/src/generic/upstream-info-icon/index.tsx b/src/generic/upstream-info-icon/index.tsx index 27b734156f..7903a61758 100644 --- a/src/generic/upstream-info-icon/index.tsx +++ b/src/generic/upstream-info-icon/index.tsx @@ -86,7 +86,7 @@ const UpstreamInfoIconContent = ({ >
From aa7b32f3fbac6e5e45eb5d84d4ac600c7004522c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 11 Dec 2025 11:50:53 -0500 Subject: [PATCH 8/9] style: Fix nits on the code --- src/course-outline/card-header/TitleLink.tsx | 2 +- src/course-outline/unit-card/UnitCard.scss | 1 + src/generic/upstream-info-icon/index.tsx | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/course-outline/card-header/TitleLink.tsx b/src/course-outline/card-header/TitleLink.tsx index 8e8c4d3000..0c0f402b26 100644 --- a/src/course-outline/card-header/TitleLink.tsx +++ b/src/course-outline/card-header/TitleLink.tsx @@ -15,7 +15,7 @@ const TitleLink = ({ prefixIcon, }: TitleLinkProps) => ( <> -
+
{prefixIcon}