Skip to content
Merged
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
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
11 changes: 7 additions & 4 deletions src/library-authoring/LibraryBlock/LibraryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -62,6 +65,8 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
};
}, []);

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

return (
<div style={{
height: `${iFrameHeight}px`,
Expand All @@ -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%',
Expand All @@ -89,5 +94,3 @@ const LibraryBlock = ({ onBlockNotification, usageKey }: LibraryBlockProps) => {
</div>
);
};

export default LibraryBlock;
3 changes: 1 addition & 2 deletions src/library-authoring/LibraryBlock/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
/* eslint-disable-next-line import/prefer-default-export */
export { default as LibraryBlock } from './LibraryBlock';
export { LibraryBlock, type VersionSpec } from './LibraryBlock';
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, 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;
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