Skip to content
Open
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: 60 additions & 0 deletions src/generic/key-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
ContainerType,
getBlockType,
getLibraryId,
isContainerType,
isContainerUsageKey,
isLibraryKey,
isLibraryV1Key,
normalizeContainerType,
Expand Down Expand Up @@ -165,4 +167,62 @@ describe('component utils', () => {
});
}
});

describe('isContainerType', () => {
for (
const containerType of [
ContainerType.Vertical,
ContainerType.Sequential,
ContainerType.Chapter,
ContainerType.Unit,
ContainerType.Subsection,
ContainerType.Section,
] as const
) {
it(`returns true for '${containerType}'`, () => {
expect(isContainerType(containerType)).toBe(true);
});
}

for (const containerType of ['html', 'problem', '', 'video']) {
it(`returns false for '${containerType}'`, () => {
expect(isContainerType(containerType)).toBe(false);
});
}
});

describe('isContainerUsageKey', () => {
for (
const usageKey of [
'lct:org:lib:section:my-section-9284e2',
'lct:org:lib:subsection:my-subsection-9284e2',
'lct:org:lib:unit:my-unit-9284e2',
'block-v1:org+type@chapter+block@1',
'block-v1:org+type@sequential+block@1',
'block-v1:org+type@vertical+block@1',
'block-v1:org+type@section+block@1',
'block-v1:org+type@subsection+block@1',
'block-v1:org+type@unit+block@1',
]
) {
it(`returns true for '${usageKey}'`, () => {
expect(isContainerUsageKey(usageKey)).toBe(true);
});
}

for (
const usageKey of [
'lb:org:lib:html:id',
'block-v1:org+type@problem+block@1',
'not a key',
'',
undefined,
null,
]
) {
it(`returns false for '${usageKey}'`, () => {
expect(isContainerUsageKey(usageKey as any)).toBe(false);
});
}
});
});
24 changes: 24 additions & 0 deletions src/generic/key-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,27 @@ export function normalizeContainerType(containerType: ContainerType | string) {
return containerType;
}
}

/**
* Check whether a block/container type points to a container (section/subsection/unit),
* including legacy names (chapter/sequential/vertical).
*/
export function isContainerType(containerType: ContainerType | string): boolean {
const normalizedType = normalizeContainerType(containerType);

return [ContainerType.Section, ContainerType.Subsection, ContainerType.Unit].includes(
normalizedType as ContainerType,
);
}

/**
* Check whether a usage key belongs to a container.
*/
export function isContainerUsageKey(usageKey: string | undefined | null): boolean {
if (typeof usageKey !== 'string') {
return false;
}

const blockType = getBlockType(usageKey, 'empty');
return blockType ? isContainerType(blockType) : false;
}
9 changes: 8 additions & 1 deletion src/library-authoring/LibraryBlock/LibraryBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface LibraryBlockProps {
minHeight?: string;
scrollIntoView?: boolean;
showTitle?: boolean;
addHeight?: number;
}
/**
* React component that displays an XBlock in a sandboxed IFrame.
Expand All @@ -40,6 +41,7 @@ export const LibraryBlock = ({
scrolling = 'no',
scrollIntoView = false,
showTitle = false,
addHeight = 0,
}: LibraryBlockProps) => {
const { iframeRef, setIframeRef } = useIframe();
const xblockView = view ?? 'student_view';
Expand All @@ -50,6 +52,7 @@ export const LibraryBlock = ({

const intl = useIntl();
const params = new URLSearchParams();

if (version) {
params.set('version', version.toString());
}
Expand Down Expand Up @@ -79,6 +82,10 @@ export const LibraryBlock = ({

useIframeContent(iframeRef, setIframeRef);

if (version === 0) {
return null;
}

return (
<iframe
ref={iframeRef}
Expand All @@ -92,7 +99,7 @@ export const LibraryBlock = ({
referrerPolicy="origin"
style={{
width: '100%',
height: iframeHeight,
height: iframeHeight + addHeight,
pointerEvents: 'auto',
minHeight,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ describe('<CompareChangesWidget />', () => {
render(<CompareChangesWidget usageKey={usageKey} />);

// By default we see the new version:
const newTab = screen.getByRole('tab', { name: 'New version' });
const newTab = await screen.findByRole('tab', { name: 'New version' });
expect(newTab).toBeInTheDocument();
expect(newTab).toHaveClass('active');

const newTabPanel = screen.getByRole('tabpanel', { name: 'New version' });
const newTabPanel = await screen.findByRole('tabpanel', { name: 'New version' });
const newIframe = within(newTabPanel).getByTitle('Preview');
expect(newIframe).toBeVisible();
expect(newIframe).toHaveAttribute(
Expand All @@ -32,10 +32,10 @@ describe('<CompareChangesWidget />', () => {
);

// Now switch to the "old version" tab:
const oldTab = screen.getByRole('tab', { name: 'Old version' });
const oldTab = await screen.findByRole('tab', { name: 'Old version' });
fireEvent.click(oldTab);

const oldTabPanel = screen.getByRole('tabpanel', { name: 'Old version' });
const oldTabPanel = await screen.findByRole('tabpanel', { name: 'Old version' });
expect(oldTabPanel).toBeVisible();
const oldIframe = within(oldTabPanel).getByTitle('Preview');
expect(oldIframe).toBeVisible();
Expand All @@ -49,11 +49,11 @@ describe('<CompareChangesWidget />', () => {
render(<CompareChangesWidget usageKey={usageKey} oldVersion={7} newVersion="published" />);

// By default we see the new version:
const newTab = screen.getByRole('tab', { name: 'New version' });
const newTab = await screen.findByRole('tab', { name: 'New version' });
expect(newTab).toBeInTheDocument();
expect(newTab).toHaveClass('active');

const newTabPanel = screen.getByRole('tabpanel', { name: 'New version' });
const newTabPanel = await screen.findByRole('tabpanel', { name: 'New version' });
const newIframe = within(newTabPanel).getByTitle('Preview');
expect(newIframe).toBeVisible();
expect(newIframe).toHaveAttribute(
Expand All @@ -62,10 +62,10 @@ describe('<CompareChangesWidget />', () => {
);

// Now switch to the "old version" tab:
const oldTab = screen.getByRole('tab', { name: 'Old version' });
const oldTab = await screen.findByRole('tab', { name: 'Old version' });
fireEvent.click(oldTab);

const oldTabPanel = screen.getByRole('tabpanel', { name: 'Old version' });
const oldTabPanel = await screen.findByRole('tabpanel', { name: 'Old version' });
expect(oldTabPanel).toBeVisible();
const oldIframe = within(oldTabPanel).getByTitle('Preview');
expect(oldIframe).toBeVisible();
Expand All @@ -74,4 +74,22 @@ describe('<CompareChangesWidget />', () => {
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=7`,
);
});

it('renders side-by-side compare mode with both iframes and expected sources', async () => {
render(<CompareChangesWidget usageKey={usageKey} oldVersion={7} newVersion="published" sideBySide />);

expect(await screen.findByText('Old version')).toBeInTheDocument();
expect(await screen.findByText('New version')).toBeInTheDocument();

const iframes = await screen.findAllByTitle('Preview');
expect(iframes).toHaveLength(2);
expect(iframes[0]).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=7`,
);
expect(iframes[1]).toHaveAttribute(
'src',
`http://localhost:18010/xblocks/v2/${usageKey}/embed/student_view/?version=published`,
);
});
});
109 changes: 75 additions & 34 deletions src/library-authoring/component-comparison/CompareChangesWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Tab, Tabs } from '@openedx/paragon';
import { Card, Stack, Tab, Tabs } from '@openedx/paragon';
import classNames from 'classnames';
import { IframeProvider } from '@src/generic/hooks/context/iFrameContext';

import { LibraryBlock, type VersionSpec } from '../LibraryBlock';
Expand All @@ -14,6 +15,8 @@ interface Props {
showNewTitle?: boolean;
hasLocalChanges?: boolean;
oldUsageKey?: string;
sideBySide?: boolean;
showTitle?: boolean;
}

/**
Expand All @@ -32,6 +35,8 @@ const CompareChangesWidget = ({
showNewTitle = false,
oldUsageKey,
hasLocalChanges = false,
sideBySide = false,
showTitle = false,
}: Props) => {
const intl = useIntl();

Expand All @@ -42,40 +47,76 @@ const CompareChangesWidget = ({
? intl.formatMessage(messages.publishedLibraryContentTitle)
: intl.formatMessage(messages.newVersionTitle);

return (
<div className="bg-white p-2">
<Tabs variant="tabs" defaultActiveKey="new" id="preview-version-toggle" mountOnEnter>
<Tab eventKey="old" title={oldTabMessage}>
<div className="p-2 bg-white">
{oldTitle && hasLocalChanges && (
<div className="h3 mt-3.5">
{oldTitle}
</div>
)}
<div style={hasLocalChanges ? { marginLeft: '-35px', marginTop: '-8px' } : {}}>
<IframeProvider>
<LibraryBlock
usageKey={oldUsageKey || usageKey}
version={oldVersion}
minHeight="50vh"
/>
</IframeProvider>
</div>
</div>
</Tab>
<Tab eventKey="new" title={newTabMessage}>
<div className="p-2 bg-white">
<IframeProvider>
<LibraryBlock
usageKey={usageKey}
version={newVersion}
showTitle={showNewTitle}
minHeight="50vh"
/>
</IframeProvider>
const oldBlock = oldVersion !== 0 && (
<Card className={classNames('flex-1 min-w-0', { 'border-0': !sideBySide })}>
<Card.Body
className="p-4 bg-white overflow-auto"
style={{ height: '60vh' }}
>
{sideBySide && (
<h3 className="w-100 text-center mb-4">
{oldTabMessage}
</h3>
)}
{oldTitle && hasLocalChanges && (
<div className="h3 mt-3.5">
{oldTitle}
</div>
</Tab>
</Tabs>
)}
<div style={hasLocalChanges ? { marginLeft: '-35px', marginTop: '-8px' } : {}}>
<IframeProvider>
<LibraryBlock
usageKey={oldUsageKey || usageKey}
version={oldVersion}
minHeight="45vh"
showTitle={showTitle}
addHeight={70}
/>
</IframeProvider>
</div>
</Card.Body>
</Card>
);

const newBlock = (
<Card className={classNames('flex-1 min-w-0', { 'border-0': !sideBySide })}>
<Card.Body
className="p-4 bg-white overflow-auto"
style={{ height: '60vh' }}
>
{sideBySide && (
<h3 className="w-100 text-center mb-4">
{newTabMessage}
</h3>
)}
<IframeProvider>
<LibraryBlock
usageKey={usageKey}
version={newVersion}
showTitle={showNewTitle || showTitle}
minHeight="45vh"
addHeight={70}
/>
</IframeProvider>
</Card.Body>
</Card>
);

return (
<div className="bg-light-300 py-2 px-1">
{sideBySide ?
(
<Stack direction="horizontal" gap={3}>
{oldBlock}
{newBlock}
</Stack>
) :
(
<Tabs variant="tabs" defaultActiveKey="new" id="preview-version-toggle" mountOnEnter>
{oldBlock && <Tab eventKey="old" title={oldTabMessage}>{oldBlock}</Tab>}
<Tab eventKey="new" title={newTabMessage}>{newBlock}</Tab>
</Tabs>
)}
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,8 @@ export interface LibraryHistoryEntry {
title: string;
itemType: string;
action: 'edited' | 'renamed' | 'created';
oldVersion?: number;
newVersion?: number;
}

/**
Expand Down
Loading