Skip to content

feat: show sync button on section/subsections [FC-0097] #2324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion src/course-outline/section-card/SectionCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import {
act, fireEvent, initializeMocks, render, screen, within,
act, fireEvent, initializeMocks, render, screen, waitFor, within,
} from '@src/testUtils';
import { XBlock } from '@src/data/types';
import SectionCard from './SectionCard';

const mockPathname = '/foo-bar';
const mockUseAcceptLibraryBlockChanges = jest.fn();
const mockUseIgnoreLibraryBlockChanges = jest.fn();

jest.mock('@src/course-unit/data/apiHooks', () => ({
useAcceptLibraryBlockChanges: () => ({
mutateAsync: mockUseAcceptLibraryBlockChanges,
}),
useIgnoreLibraryBlockChanges: () => ({
mutateAsync: mockUseIgnoreLibraryBlockChanges,
}),
}));

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Expand Down Expand Up @@ -213,4 +224,51 @@ describe('<SectionCard />', () => {
expect(cardSubsections).toBeNull();
expect(newSubsectionButton).toBeNull();
});

it('should sync section changes from upstream', async () => {
renderComponent();

expect(await screen.findByTestId('section-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: section 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 section changes from upstream', async () => {
renderComponent();

expect(await screen.findByTestId('section-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: section 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());
});
});
35 changes: 33 additions & 2 deletions src/course-outline/section-card/SectionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
useContext, useEffect, useState, useRef, useCallback, ReactNode,
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
Expand All @@ -17,13 +17,15 @@ import SortableItem from '@src/course-outline/drag-helper/SortableItem';
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
import TitleButton from '@src/course-outline/card-header/TitleButton';
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
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 { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
import messages from './messages';

interface SectionCardProps {
Expand Down Expand Up @@ -107,6 +109,7 @@ const SectionCard = ({
};
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || isSectionsExpanded);
const [isFormOpen, openForm, closeForm] = useToggle(false);
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
const namePrefix = 'section';

useEffect(() => {
Expand All @@ -123,8 +126,22 @@ const SectionCard = ({
highlights,
actions: sectionActions,
isHeaderVisible = true,
upstreamInfo,
} = section;

const blockSyncData = useMemo(() => {
if (!upstreamInfo?.readyToSync) {
return undefined;
}
return {
displayName,
downstreamBlockId: id,
upstreamBlockId: upstreamInfo.upstreamRef,
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
isVertical: true,
};
}, [upstreamInfo]);

useEffect(() => {
if (activeId === id && isExpanded) {
setIsExpanded(false);
Expand All @@ -148,6 +165,10 @@ const SectionCard = ({
setIsExpanded((prevState) => containsSearchResult() || prevState);
}, [locatorId, setIsExpanded]);

const handleOnPostChangeSync = useCallback(() => {
dispatch(fetchCourseSectionQuery([section.id]));
}, [dispatch, section]);

// re-create actions object for customizations
const actions = { ...sectionActions };
// add actions to control display of move up & down menu buton.
Expand Down Expand Up @@ -225,7 +246,7 @@ const SectionCard = ({
isExpanded={isExpanded}
onTitleClick={handleExpandContent}
namePrefix={namePrefix}
prefixIcon={!!section.upstreamInfo?.upstreamRef && (
prefixIcon={!!upstreamInfo?.upstreamRef && (
<Icon src={Newsstand} className="mr-1" />
)}
/>
Expand Down Expand Up @@ -264,6 +285,7 @@ const SectionCard = ({
onClickDelete={onOpenDeleteModal}
onClickMoveUp={handleSectionMoveUp}
onClickMoveDown={handleSectionMoveDown}
onClickSync={openSyncModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
Expand All @@ -272,6 +294,7 @@ const SectionCard = ({
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
readyToSync={upstreamInfo?.readyToSync}
/>
)}
<div className="section-card__content" data-testid="section-card__content">
Expand Down Expand Up @@ -327,6 +350,14 @@ const SectionCard = ({
visibleTabs={[ContentType.subsections]}
/>
</StandardModal>
{blockSyncData && (
<PreviewLibraryXBlockChanges
blockData={blockSyncData}
isModalOpen={isSyncModalOpen}
closeModal={closeSyncModal}
postChange={handleOnPostChangeSync}
/>
)}
</>
);
};
Expand Down
61 changes: 60 additions & 1 deletion src/course-outline/subsection-card/SubsectionCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
import {
act, fireEvent, initializeMocks, render, screen, within,
act, fireEvent, initializeMocks, render, screen, waitFor, within,
} from '@src/testUtils';
import { XBlock } from '@src/data/types';
import cardHeaderMessages from '../card-header/messages';
Expand All @@ -11,6 +11,18 @@ const mockPathname = '/foo-bar';
const containerKey = 'lct:org:lib:unit:1';
const handleOnAddUnitFromLibrary = jest.fn();

const mockUseAcceptLibraryBlockChanges = jest.fn();
const mockUseIgnoreLibraryBlockChanges = jest.fn();

jest.mock('@src/course-unit/data/apiHooks', () => ({
useAcceptLibraryBlockChanges: () => ({
mutateAsync: mockUseAcceptLibraryBlockChanges,
}),
useIgnoreLibraryBlockChanges: () => ({
mutateAsync: mockUseIgnoreLibraryBlockChanges,
}),
}));

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
Expand Down Expand Up @@ -290,4 +302,51 @@ describe('<SubsectionCard />', () => {
libraryContentKey: containerKey,
});
});

it('should sync subsection changes from upstream', async () => {
renderComponent();

expect(await screen.findByTestId('subsection-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: subsection 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 subsection changes from upstream', async () => {
renderComponent();

expect(await screen.findByTestId('subsection-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: subsection 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());
});
});
35 changes: 33 additions & 2 deletions src/course-outline/subsection-card/SubsectionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {
useContext, useEffect, useState, useRef, useCallback, ReactNode,
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
Expand All @@ -17,6 +17,7 @@ import SortableItem from '@src/course-outline/drag-helper/SortableItem';
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
import { useClipboard, PasteComponent } from '@src/generic/clipboard';
import TitleButton from '@src/course-outline/card-header/TitleButton';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
Expand All @@ -25,6 +26,7 @@ import { ContainerType } from '@src/generic/key-utils';
import { ContentType } from '@src/library-authoring/routes';
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
import { XBlock } from '@src/data/types';
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
import messages from './messages';

interface SubsectionCardProps {
Expand Down Expand Up @@ -86,6 +88,7 @@ const SubsectionCard = ({
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === subsection.id;
const [isFormOpen, openForm, closeForm] = useToggle(false);
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
const namePrefix = 'subsection';
const { sharedClipboardData, showPasteUnit } = useClipboard();
const [
Expand All @@ -105,8 +108,22 @@ const SubsectionCard = ({
isHeaderVisible = true,
enableCopyPasteUnits = false,
proctoringExamConfigurationLink,
upstreamInfo,
} = subsection;

const blockSyncData = useMemo(() => {
if (!upstreamInfo?.readyToSync) {
return undefined;
}
return {
displayName,
downstreamBlockId: id,
upstreamBlockId: upstreamInfo.upstreamRef,
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
isVertical: true,
};
}, [upstreamInfo]);

// re-create actions object for customizations
const actions = { ...subsectionActions };
// add actions to control display of move up & down menu button.
Expand Down Expand Up @@ -145,6 +162,10 @@ const SubsectionCard = ({
dispatch(setCurrentItem(subsection));
};

const handleOnPostChangeSync = useCallback(() => {
dispatch(fetchCourseSectionQuery([section.id]));
}, [dispatch, section]);

const handleEditSubmit = (titleValue: string) => {
if (displayName !== titleValue) {
onEditSubmit(id, section.id, titleValue);
Expand All @@ -171,7 +192,7 @@ const SubsectionCard = ({
isExpanded={isExpanded}
onTitleClick={handleExpandContent}
namePrefix={namePrefix}
prefixIcon={!!subsection.upstreamInfo?.upstreamRef && (
prefixIcon={!!upstreamInfo?.upstreamRef && (
<Icon src={Newsstand} className="mr-1" />
)}
/>
Expand Down Expand Up @@ -262,6 +283,7 @@ const SubsectionCard = ({
onClickMoveUp={handleSubsectionMoveUp}
onClickMoveDown={handleSubsectionMoveDown}
onClickConfigure={onOpenConfigureModal}
onClickSync={openSyncModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
Expand All @@ -273,6 +295,7 @@ const SubsectionCard = ({
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
isSequential
extraActionsComponent={extraActionsComponent}
readyToSync={upstreamInfo?.readyToSync}
/>
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
<XBlockStatus
Expand Down Expand Up @@ -325,6 +348,14 @@ const SubsectionCard = ({
visibleTabs={[ContentType.units]}
/>
</StandardModal>
{blockSyncData && (
<PreviewLibraryXBlockChanges
blockData={blockSyncData}
isModalOpen={isSyncModalOpen}
closeModal={closeSyncModal}
postChange={handleOnPostChangeSync}
/>
)}
</>
);
};
Expand Down