Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 7 additions & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' && (
Expand Down
7 changes: 5 additions & 2 deletions src/library-authoring/LibraryBlock/LibraryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import messages from './messages';
interface LibraryBlockProps {
onBlockNotification?: (event: { eventType: string; [key: string]: any }) => void;
usageKey: string;
version?: 'published' | 'draft' | number;
Comment thread
bradenmacdonald marked this conversation as resolved.
Resolved
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
Expand All @@ -17,7 +18,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) => {
const LibraryBlock = ({ onBlockNotification, usageKey, version }: LibraryBlockProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iFrameHeight, setIFrameHeight] = useState(600);
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
Expand Down Expand Up @@ -62,6 +63,8 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
};
}, []);

const queryStr = version ? `?version=${version}` : '';

return (
<div style={{
height: `${iFrameHeight}px`,
Expand All @@ -74,7 +77,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%',
Expand Down
Original file line number Diff line number Diff line change
@@ -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`,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Tab, Tabs } from '@openedx/paragon';

import { LibraryBlock } from '../LibraryBlock';

import messages from './messages';

interface Props {
usageKey: string;
oldVersion?: number | 'draft' | 'published';
newVersion?: number | 'draft' | 'published';
}

/**
* 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;
22 changes: 22 additions & 0 deletions src/library-authoring/component-comparison/messages.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/library-authoring/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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`,
);
});
});
44 changes: 44 additions & 0 deletions src/library-authoring/legacy-integration/PreviewChangesEmbed.tsx
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 0 additions & 4 deletions src/library-authoring/library-info/LibraryInfoHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ describe('<LibraryInfoHeader />', () => {
mockShowToast = mocks.mockShowToast;
});

afterEach(() => {
jest.clearAllMocks();
});

it('should render Library info Header', async () => {
render();

Expand Down