feat: Sync units in course outline [FC-0083] (#1850)
* Adds the sync button in unit cards in the course outline. * Opens the compare previews. * Functionality to sync units. * Functionality to decline sync units.
This commit is contained in:
@@ -292,6 +292,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -675,6 +680,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
|
||||
@@ -759,6 +769,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
|
||||
@@ -843,6 +858,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
|
||||
@@ -927,6 +947,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
|
||||
@@ -1011,6 +1036,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1196,6 +1226,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
|
||||
@@ -1280,6 +1315,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
|
||||
@@ -1364,6 +1404,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
|
||||
@@ -1448,6 +1493,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
|
||||
@@ -1532,6 +1582,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1717,6 +1772,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1995,6 +2055,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
|
||||
@@ -2079,6 +2144,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
|
||||
@@ -2163,6 +2233,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
|
||||
@@ -2247,6 +2322,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
|
||||
@@ -2331,6 +2411,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
|
||||
@@ -2415,6 +2500,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
|
||||
@@ -2499,6 +2589,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
|
||||
@@ -2583,6 +2678,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
|
||||
@@ -2667,6 +2767,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2945,6 +3050,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import {
|
||||
MoreVert as MoveVertIcon,
|
||||
EditOutline as EditIcon,
|
||||
Sync as SyncIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { useContentTagsCount } from '../../generic/data/apiHooks';
|
||||
@@ -55,6 +56,8 @@ const CardHeader = ({
|
||||
discussionsSettings,
|
||||
parentInfo,
|
||||
extraActionsComponent,
|
||||
onClickSync,
|
||||
readyToSync,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -130,8 +133,17 @@ const CardHeader = ({
|
||||
) : (
|
||||
<>
|
||||
{titleComponent}
|
||||
{readyToSync && (
|
||||
<IconButton
|
||||
className="item-card-button-icon"
|
||||
data-testid={`${namePrefix}-sync-button`}
|
||||
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
|
||||
iconAs={SyncIcon}
|
||||
onClick={onClickSync}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
className="item-card-edit-icon"
|
||||
className="item-card-button-icon"
|
||||
data-testid={`${namePrefix}-edit-button`}
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
iconAs={EditIcon}
|
||||
@@ -259,6 +271,8 @@ CardHeader.defaultProps = {
|
||||
parentInfo: {},
|
||||
cardId: '',
|
||||
extraActionsComponent: null,
|
||||
readyToSync: false,
|
||||
onClickSync: null,
|
||||
};
|
||||
|
||||
CardHeader.propTypes = {
|
||||
@@ -305,6 +319,8 @@ CardHeader.propTypes = {
|
||||
// An optional component that is rendered before the dropdown. This is used by the Subsection
|
||||
// and Unit card components to render their plugin slots.
|
||||
extraActionsComponent: PropTypes.node,
|
||||
onClickSync: PropTypes.func,
|
||||
readyToSync: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CardHeader;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.item-card-edit-icon {
|
||||
.item-card-button-icon {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
margin-right: .5rem;
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.item-card-edit-icon {
|
||||
.item-card-button-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,4 +368,19 @@ describe('<CardHeader />', () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByText('0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render sync button when is ready to sync', () => {
|
||||
const mockClickSync = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
readyToSync: true,
|
||||
onClickSync: mockClickSync,
|
||||
});
|
||||
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
expect(syncButton).toBeInTheDocument();
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
expect(mockClickSync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,11 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.menu.manageTags',
|
||||
defaultMessage: 'Manage tags',
|
||||
},
|
||||
readyToSyncButtonAlt: {
|
||||
id: 'course-authoring.course-outline.card.button.sync.alt',
|
||||
defaultMessage: 'Update available - click to sync',
|
||||
description: 'Alt text for the sync icon button.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// @ts-check
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
@@ -8,6 +13,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { fetchCourseSectionQuery } from '../data/thunk';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { isUnitReadOnly } from '../../course-unit/data/utils';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
@@ -16,6 +22,7 @@ import TitleLink from '../card-header/TitleLink';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import { useClipboard } from '../../generic/clipboard';
|
||||
import { PreviewLibraryXBlockChanges } from '../../course-unit/preview-changes';
|
||||
|
||||
const UnitCard = ({
|
||||
unit,
|
||||
@@ -41,6 +48,7 @@ const UnitCard = ({
|
||||
const locatorId = searchParams.get('show');
|
||||
const isScrolledToElement = locatorId === unit.id;
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
|
||||
const namePrefix = 'unit';
|
||||
|
||||
const { copyToClipboard } = useClipboard();
|
||||
@@ -56,8 +64,22 @@ const UnitCard = ({
|
||||
isHeaderVisible = true,
|
||||
enableCopyPasteUnits = false,
|
||||
discussionEnabled,
|
||||
upstreamInfo,
|
||||
} = unit;
|
||||
|
||||
const blockSyncData = useMemo(() => {
|
||||
if (!upstreamInfo.readyToSync) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
displayName,
|
||||
downstreamBlockId: id,
|
||||
upstreamBlockId: upstreamInfo.upstreamRef,
|
||||
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
|
||||
isVertical: true,
|
||||
};
|
||||
}, [upstreamInfo]);
|
||||
|
||||
const readOnly = isUnitReadOnly(unit);
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -107,6 +129,10 @@ const UnitCard = ({
|
||||
copyToClipboard(id);
|
||||
};
|
||||
|
||||
const handleOnPostChangeSync = useCallback(async () => {
|
||||
await dispatch(fetchCourseSectionQuery([section.id]));
|
||||
}, [dispatch, section]);
|
||||
|
||||
const titleComponent = (
|
||||
<TitleLink
|
||||
title={displayName}
|
||||
@@ -147,59 +173,69 @@ const UnitCard = ({
|
||||
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#fdfdfd',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="unit-card"
|
||||
ref={currentRef}
|
||||
<>
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#fdfdfd',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={unitStatus}
|
||||
hasChanges={hasChanges}
|
||||
cardId={id}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleUnitMoveUp}
|
||||
onClickMoveDown={handleUnitMoveDown}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
isVertical
|
||||
enableCopyPasteUnits={enableCopyPasteUnits}
|
||||
onClickCopy={handleCopyClick}
|
||||
discussionEnabled={discussionEnabled}
|
||||
discussionsSettings={discussionsSettings}
|
||||
parentInfo={parentInfo}
|
||||
extraActionsComponent={extraActionsComponent}
|
||||
/>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
<div
|
||||
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="unit-card"
|
||||
ref={currentRef}
|
||||
>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={unitStatus}
|
||||
hasChanges={hasChanges}
|
||||
cardId={id}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleUnitMoveUp}
|
||||
onClickMoveDown={handleUnitMoveDown}
|
||||
onClickSync={openSyncModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
isVertical
|
||||
enableCopyPasteUnits={enableCopyPasteUnits}
|
||||
onClickCopy={handleCopyClick}
|
||||
discussionEnabled={discussionEnabled}
|
||||
discussionsSettings={discussionsSettings}
|
||||
parentInfo={parentInfo}
|
||||
extraActionsComponent={extraActionsComponent}
|
||||
readyToSync={upstreamInfo.readyToSync}
|
||||
/>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SortableItem>
|
||||
</SortableItem>
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -225,6 +261,11 @@ UnitCard.propTypes = {
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
enableCopyPasteUnits: PropTypes.bool,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
upstreamInfo: PropTypes.shape({
|
||||
readyToSync: PropTypes.bool.isRequired,
|
||||
upstreamRef: PropTypes.string.isRequired,
|
||||
versionSynced: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
subsection: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
act, render, fireEvent, within,
|
||||
act, render, fireEvent, within, screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -11,6 +12,17 @@ import UnitCard from './UnitCard';
|
||||
import cardMessages from '../card-header/messages';
|
||||
|
||||
let store;
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
|
||||
jest.mock('../../course-unit/data/apiHooks', () => ({
|
||||
useAcceptLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseAcceptLibraryBlockChanges,
|
||||
}),
|
||||
useIgnoreLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseIgnoreLibraryBlockChanges,
|
||||
}),
|
||||
}));
|
||||
|
||||
const section = {
|
||||
id: '1',
|
||||
@@ -43,6 +55,11 @@ const unit = {
|
||||
duplicable: true,
|
||||
},
|
||||
isHeaderVisible: true,
|
||||
upstreamInfo: {
|
||||
readyToSync: true,
|
||||
upstreamRef: 'lct:org1:lib1:unit:1',
|
||||
versionSynced: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -147,4 +164,51 @@ describe('<UnitCard />', () => {
|
||||
});
|
||||
expect(queryByRole('status')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should sync unit changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('unit-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on accept changes
|
||||
const acceptChangesButton = screen.getByText(/accept changes/i);
|
||||
fireEvent.click(acceptChangesButton);
|
||||
|
||||
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should decline sync unit changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('unit-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on ignore changes
|
||||
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
|
||||
fireEvent.click(ignoreChangesButton);
|
||||
|
||||
// Should open the confirmation modal
|
||||
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
|
||||
|
||||
// Click on ignore button
|
||||
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
|
||||
fireEvent.click(ignoreButton);
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,13 +12,13 @@ import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'
|
||||
import { messageTypes } from '../constants';
|
||||
import { libraryBlockChangesUrl } from '../data/api';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api';
|
||||
import { getLibraryBlockMetadataUrl, getLibraryContainerApiUrl } from '../../library-authoring/data/api';
|
||||
|
||||
const usageKey = 'some-id';
|
||||
const defaultEventData: LibraryChangesMessageData = {
|
||||
displayName: 'Test block',
|
||||
downstreamBlockId: usageKey,
|
||||
upstreamBlockId: 'some-lib-id',
|
||||
upstreamBlockId: 'lct:org:lib1:unit:1',
|
||||
upstreamBlockVersionSynced: 1,
|
||||
isVertical: false,
|
||||
};
|
||||
@@ -87,6 +87,15 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
expect(await screen.findByText('Preview changes: Test block -> New test block')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders both new and old title if they are different on units', async () => {
|
||||
axiosMock.onGet(getLibraryContainerApiUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
||||
displayName: 'New test Unit',
|
||||
});
|
||||
render({ ...defaultEventData, isVertical: true, displayName: 'Test Unit' });
|
||||
|
||||
expect(await screen.findByText('Preview changes: Test Unit -> New test Unit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accept changes works', async () => {
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
render();
|
||||
|
||||
@@ -14,7 +14,7 @@ import messages from './messages';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import LoadingButton from '../../generic/loading-button';
|
||||
import Loading from '../../generic/Loading';
|
||||
import { useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
|
||||
import { useContainer, useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
|
||||
|
||||
export interface LibraryChangesMessageData {
|
||||
displayName: string,
|
||||
@@ -48,14 +48,20 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
|
||||
// ignore changes confirmation modal toggle.
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||
|
||||
// TODO: Split into two different components to avoid making these two calls in which
|
||||
// one will definitely fail
|
||||
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
|
||||
const { data: unitMetadata } = useContainer(blockData?.upstreamBlockId);
|
||||
|
||||
const metadata = blockData?.isVertical ? unitMetadata : componentMetadata;
|
||||
|
||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
const oldName = blockData?.displayName;
|
||||
const newName = componentMetadata?.displayName;
|
||||
const newName = metadata?.displayName;
|
||||
|
||||
if (!oldName) {
|
||||
if (blockData?.isVertical) {
|
||||
@@ -67,7 +73,7 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
return intl.formatMessage(messages.title, { blockTitle: oldName });
|
||||
}
|
||||
return intl.formatMessage(messages.diffTitle, { oldName, newName });
|
||||
}, [blockData, componentMetadata]);
|
||||
}, [blockData, metadata]);
|
||||
|
||||
const getBody = useCallback(() => {
|
||||
if (!blockData) {
|
||||
@@ -78,6 +84,7 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
usageKey={blockData.upstreamBlockId}
|
||||
oldVersion={blockData.upstreamBlockVersionSynced || 'published'}
|
||||
newVersion="published"
|
||||
isContainer={blockData.isVertical}
|
||||
/>
|
||||
);
|
||||
}, [blockData]);
|
||||
|
||||
@@ -6,10 +6,21 @@ import { LibraryBlock, type VersionSpec } from '../LibraryBlock';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const PreviewNotAvailable = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="d-flex mt-4 justify-content-center">
|
||||
{intl.formatMessage(messages.previewNotAvailable)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
usageKey: string;
|
||||
oldVersion?: VersionSpec;
|
||||
newVersion?: VersionSpec;
|
||||
isContainer?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,7 +31,12 @@ interface Props {
|
||||
* In the future, it would be better to have a way of highlighting the changes
|
||||
* or showing a diff.
|
||||
*/
|
||||
const CompareChangesWidget = ({ usageKey, oldVersion = 'published', newVersion = 'draft' }: Props) => {
|
||||
const CompareChangesWidget = ({
|
||||
usageKey,
|
||||
oldVersion = 'published',
|
||||
newVersion = 'draft',
|
||||
isContainer = false,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -28,24 +44,28 @@ const CompareChangesWidget = ({ usageKey, oldVersion = 'published', newVersion =
|
||||
<Tabs variant="tabs" defaultActiveKey="new" id="preview-version-toggle" mountOnEnter>
|
||||
<Tab eventKey="old" title={intl.formatMessage(messages.oldVersionTitle)}>
|
||||
<div className="p-2 bg-white">
|
||||
<IframeProvider>
|
||||
<LibraryBlock
|
||||
usageKey={usageKey}
|
||||
version={oldVersion}
|
||||
minHeight="50vh"
|
||||
/>
|
||||
</IframeProvider>
|
||||
{isContainer ? (<PreviewNotAvailable />) : (
|
||||
<IframeProvider>
|
||||
<LibraryBlock
|
||||
usageKey={usageKey}
|
||||
version={oldVersion}
|
||||
minHeight="50vh"
|
||||
/>
|
||||
</IframeProvider>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab eventKey="new" title={intl.formatMessage(messages.newVersionTitle)}>
|
||||
<div className="p-2 bg-white">
|
||||
<IframeProvider>
|
||||
<LibraryBlock
|
||||
usageKey={usageKey}
|
||||
version={newVersion}
|
||||
minHeight="50vh"
|
||||
/>
|
||||
</IframeProvider>
|
||||
{isContainer ? (<PreviewNotAvailable />) : (
|
||||
<IframeProvider>
|
||||
<LibraryBlock
|
||||
usageKey={usageKey}
|
||||
version={newVersion}
|
||||
minHeight="50vh"
|
||||
/>
|
||||
</IframeProvider>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -17,6 +17,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Compare Changes',
|
||||
description: 'Title used for the compare changes dialog',
|
||||
},
|
||||
previewNotAvailable: {
|
||||
id: 'course-authoring.library-authoring.component-comparison.preview-not-available',
|
||||
defaultMessage: 'Preview not available',
|
||||
description: 'Message shown when preview is not available.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user