feat: View for comparing published version of library block to previous (#1393)

This commit is contained in:
Braden MacDonald
2024-10-18 11:27:10 -07:00
committed by GitHub
parent 4facf1cf5d
commit 40a6ee9ca5
10 changed files with 255 additions and 11 deletions

View File

@@ -16,7 +16,12 @@ import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { logError } from '@edx/frontend-platform/logging';
import messages from './i18n';
import { ComponentPicker, CreateLibrary, LibraryLayout } from './library-authoring';
import {
ComponentPicker,
CreateLibrary,
LibraryLayout,
PreviewChangesEmbed,
} from './library-authoring';
import initializeStore from './store';
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
import Head from './head/Head';
@@ -56,6 +61,7 @@ const App = () => {
<Route path="/library/create" element={<CreateLibrary />} />
<Route path="/library/:libraryId/*" element={<LibraryLayout />} />
<Route path="/component-picker" element={<ComponentPicker />} />
<Route path="/legacy/preview-changes/:usageKey" element={<PreviewChangesEmbed />} />
<Route path="/course/:courseId/*" element={<CourseAuthoringRoutes />} />
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
{getConfig().ENABLE_ACCESSIBILITY_PAGE === 'true' && (

View File

@@ -4,9 +4,12 @@ import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
export type VersionSpec = 'published' | 'draft' | number;
interface LibraryBlockProps {
onBlockNotification?: (event: { eventType: string; [key: string]: any }) => void;
usageKey: string;
version?: VersionSpec;
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
@@ -17,7 +20,7 @@ interface LibraryBlockProps {
* cannot access things like the user's cookies, nor can it make GET/POST
* requests as the user. However, it is allowed to call any XBlock handlers.
*/
const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
export const LibraryBlock = ({ onBlockNotification, usageKey, version }: LibraryBlockProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iFrameHeight, setIFrameHeight] = useState(600);
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
@@ -62,6 +65,8 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
};
}, []);
const queryStr = version ? `?version=${version}` : '';
return (
<div style={{
height: `${iFrameHeight}px`,
@@ -74,7 +79,7 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
<iframe
ref={iframeRef}
title={intl.formatMessage(messages.iframeTitle)}
src={`${studioBaseUrl}/xblocks/v2/${usageKey}/embed/student_view/`}
src={`${studioBaseUrl}/xblocks/v2/${usageKey}/embed/student_view/${queryStr}`}
data-testid="block-preview"
style={{
width: '100%',
@@ -89,5 +94,3 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
</div>
);
};
export default LibraryBlock;

View File

@@ -1,2 +1 @@
/* eslint-disable-next-line import/prefer-default-export */
export { default as LibraryBlock } from './LibraryBlock';
export { LibraryBlock, type VersionSpec } from './LibraryBlock';

View File

@@ -0,0 +1,77 @@
import {
fireEvent,
render,
screen,
initializeMocks,
within,
} from '../../testUtils';
import CompareChangesWidget from './CompareChangesWidget';
const usageKey = 'lb:org:lib:type:id';
describe('<CompareChangesWidget />', () => {
beforeEach(() => {
initializeMocks();
});
it('can compare published (old) and draft (new) versions by default', async () => {
render(<CompareChangesWidget usageKey={usageKey} />);
// By default we see the new version:
const newTab = screen.getByRole('tab', { name: 'New version' });
expect(newTab).toBeInTheDocument();
expect(newTab).toHaveClass('active');
const newTabPanel = screen.getByRole('tabpanel', { name: 'New version' });
const newIframe = within(newTabPanel).getByTitle('Preview');
expect(newIframe).toBeVisible();
expect(newIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=draft`,
);
// Now switch to the "old version" tab:
const oldTab = screen.getByRole('tab', { name: 'Old version' });
fireEvent.click(oldTab);
const oldTabPanel = screen.getByRole('tabpanel', { name: 'Old version' });
expect(oldTabPanel).toBeVisible();
const oldIframe = within(oldTabPanel).getByTitle('Preview');
expect(oldIframe).toBeVisible();
expect(oldIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=published`,
);
});
it('can compare a specific old and published (new) version', async () => {
render(<CompareChangesWidget usageKey={usageKey} oldVersion={7} newVersion="published" />);
// By default we see the new version:
const newTab = screen.getByRole('tab', { name: 'New version' });
expect(newTab).toBeInTheDocument();
expect(newTab).toHaveClass('active');
const newTabPanel = screen.getByRole('tabpanel', { name: 'New version' });
const newIframe = within(newTabPanel).getByTitle('Preview');
expect(newIframe).toBeVisible();
expect(newIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=published`,
);
// Now switch to the "old version" tab:
const oldTab = screen.getByRole('tab', { name: 'Old version' });
fireEvent.click(oldTab);
const oldTabPanel = screen.getByRole('tabpanel', { name: 'Old version' });
expect(oldTabPanel).toBeVisible();
const oldIframe = within(oldTabPanel).getByTitle('Preview');
expect(oldIframe).toBeVisible();
expect(oldIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=7`,
);
});
});

View File

@@ -0,0 +1,43 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Tab, Tabs } from '@openedx/paragon';
import { LibraryBlock, type VersionSpec } from '../LibraryBlock';
import messages from './messages';
interface Props {
usageKey: string;
oldVersion?: VersionSpec;
newVersion?: VersionSpec;
}
/**
* A widget with tabs that can be used to show the old version and the new
* version of a component (XBlock), so users can switch back and forth to spot
* the differences.
*
* 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 intl = useIntl();
return (
<div>
<Tabs variant="tabs" defaultActiveKey="new" id="preview-version-toggle">
<Tab eventKey="old" title={intl.formatMessage(messages.oldVersionTitle)}>
<div className="p-2 bg-white">
<LibraryBlock usageKey={usageKey} version={oldVersion} />
</div>
</Tab>
<Tab eventKey="new" title={intl.formatMessage(messages.newVersionTitle)}>
<div className="p-2 bg-white">
<LibraryBlock usageKey={usageKey} version={newVersion} />
</div>
</Tab>
</Tabs>
</div>
);
};
export default CompareChangesWidget;

View File

@@ -0,0 +1,22 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
oldVersionTitle: {
id: 'course-authoring.library-authoring.component-comparison.oldVersion',
defaultMessage: 'Old version',
description: 'Title shown for old version when comparing changes',
},
newVersionTitle: {
id: 'course-authoring.library-authoring.component-comparison.newVersion',
defaultMessage: 'New version',
description: 'Title shown for new version when comparing changes',
},
iframeTitlePrefix: {
// This is only used in the "PreviewChangesEmbed" iframe for the legacy UI
id: 'course-authoring.library-authoring.component-comparison.iframeTitlePrefix',
defaultMessage: 'Compare Changes',
description: 'Title used for the compare changes dialog',
},
});
export default messages;

View File

@@ -2,3 +2,4 @@ export { default as LibraryLayout } from './LibraryLayout';
export { ComponentPicker } from './component-picker';
export { CreateLibrary } from './create-library';
export { libraryAuthoringQueryKeys, useContentLibraryV2List } from './data/apiHooks';
export { default as PreviewChangesEmbed } from './legacy-integration/PreviewChangesEmbed';

View File

@@ -0,0 +1,53 @@
import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks';
import {
fireEvent,
render,
screen,
initializeMocks,
within,
} from '../../testUtils';
import PreviewChangesEmbed from './PreviewChangesEmbed';
mockContentLibrary.applyMock();
mockLibraryBlockMetadata.applyMock();
const usageKey = mockLibraryBlockMetadata.usageKeyWithCollections;
describe('<CompareChangesWidget />', () => {
beforeEach(() => {
initializeMocks();
});
it('can compare published (old) and draft (new) versions by default', async () => {
render(<PreviewChangesEmbed />, {
path: '/legacy/preview-changes/:usageKey',
routerProps: { initialEntries: [`/legacy/preview-changes/${usageKey}/?old=13`] },
});
// By default we see the new version (the published version):
const newTab = screen.getByRole('tab', { name: 'New version' });
expect(newTab).toBeInTheDocument();
expect(newTab).toHaveClass('active');
const newTabPanel = screen.getByRole('tabpanel', { name: 'New version' });
const newIframe = within(newTabPanel).getByTitle('Preview');
expect(newIframe).toBeVisible();
expect(newIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=published`,
);
// Now switch to the "old version" tab:
const oldTab = screen.getByRole('tab', { name: 'Old version' });
fireEvent.click(oldTab);
const oldTabPanel = screen.getByRole('tabpanel', { name: 'Old version' });
expect(oldTabPanel).toBeVisible();
const oldIframe = within(oldTabPanel).getByTitle('Preview');
expect(oldIframe).toBeVisible();
expect(oldIframe).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=13`,
);
});
});

View File

@@ -0,0 +1,44 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useParams, useSearchParams } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { LibraryProvider } from '../common/context';
import { getLibraryId } from '../../generic/key-utils';
import CompareChangesWidget from '../component-comparison/CompareChangesWidget';
import { useLibraryBlockMetadata } from '../data/apiHooks';
import messages from '../component-comparison/messages';
/**
* This view is only used to support the legacy UI.
* On the legacy unit page, when a v2 library block has been used in a course
* AND an updated version of that block is available, this view is rendered in
* an iframe in a modal, and allows the author to preview the changes before
* accepting them (before syncing the course version with the latest library
* version).
*
* The inner <CompareChangesWidget> will be used by this MFE as well, on the
* new MFE unit page.
*/
const PreviewChangesEmbed = () => {
const intl = useIntl();
const { usageKey } = useParams();
if (usageKey === undefined) {
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
throw new Error('Error: route is missing usageKey.');
}
const [queryString] = useSearchParams();
const oldVersion = parseInt(queryString.get('old') ?? '', 10) || 'published';
const libraryId = getLibraryId(usageKey);
const { data: metadata } = useLibraryBlockMetadata(usageKey);
return (
<LibraryProvider libraryId={libraryId}>
{/* It's not necessary since this will usually be in an <iframe>,
but it's good practice to set a title for any top level page */}
<Helmet><title>{intl.formatMessage(messages.iframeTitlePrefix)} | {metadata?.displayName ?? ''} | {process.env.SITE_NAME}</title></Helmet>
<CompareChangesWidget usageKey={usageKey} oldVersion={oldVersion} newVersion="published" />
</LibraryProvider>
);
};
export default PreviewChangesEmbed;

View File

@@ -30,10 +30,6 @@ describe('<LibraryInfoHeader />', () => {
mockShowToast = mocks.mockShowToast;
});
afterEach(() => {
jest.clearAllMocks();
});
it('should render Library info Header', async () => {
render();