Skip to content

Commit 0ad4c1a

Browse files
committed
feat: Added HistoryContainerLog && refactor to use new Pre and Post Verawood logic
1 parent aaee15c commit 0ad4c1a

13 files changed

Lines changed: 569 additions & 165 deletions

File tree

src/library-authoring/common/context/SidebarContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const CONTAINER_INFO_TABS = {
4545
Manage: 'manage',
4646
Usage: 'usage',
4747
Settings: 'settings',
48+
Details: 'details',
4849
} as const;
4950
export type ContainerInfoTab = typeof CONTAINER_INFO_TABS[keyof typeof CONTAINER_INFO_TABS];
5051
export const isContainerInfoTab = (tab: string): tab is ContainerInfoTab => (

src/library-authoring/component-info/ComponentDetails.tsx

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { FormattedMessage } from '@edx/frontend-platform/i18n';
22
import { Stack } from '@openedx/paragon';
33

4-
import AlertError from '@src/generic/alert-error';
5-
import Loading from '@src/generic/Loading';
64
import { useSidebarContext } from '../common/context/SidebarContext';
7-
import { useLibraryBlockMetadata } from '../data/apiHooks';
85
import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
96
import { ComponentUsage } from './ComponentUsage';
107
import messages from './messages';
@@ -20,21 +17,6 @@ const ComponentDetails = () => {
2017
throw new Error('usageKey is required');
2118
}
2219

23-
const {
24-
data: componentMetadata,
25-
isError,
26-
error,
27-
isPending,
28-
} = useLibraryBlockMetadata(usageKey);
29-
30-
if (isError) {
31-
return <AlertError error={error} />;
32-
}
33-
34-
if (isPending) {
35-
return <Loading />;
36-
}
37-
3820
return (
3921
<Stack gap={3}>
4022
<>
@@ -50,7 +32,6 @@ const ComponentDetails = () => {
5032
</h3>
5133
<HistoryComponentLog
5234
componentId={usageKey}
53-
displayName={componentMetadata.displayName}
5435
/>
5536
</>
5637
<ComponentAdvancedInfo />
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import messages from './messages';
3+
import { HistoryContainerLog } from '../generic/history-log/HistoryLog';
4+
import { useSidebarContext } from '../common/context/SidebarContext';
5+
6+
export const ContainerDetails = () => {
7+
const intl = useIntl();
8+
9+
const { sidebarItemInfo } = useSidebarContext();
10+
11+
const usageKey = sidebarItemInfo?.id;
12+
13+
return (
14+
<>
15+
<h4>{intl.formatMessage(messages.detailsTabHistoryHeading)}</h4>
16+
{usageKey && (
17+
<HistoryContainerLog
18+
containerId={usageKey}
19+
/>
20+
)}
21+
</>
22+
);
23+
};

src/library-authoring/containers/ContainerInfo.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { useContainer } from '../data/apiHooks';
3434
import ContainerDeleter from './ContainerDeleter';
3535
import { ContainerPublisher } from './ContainerPublisher';
3636
import { PublishDraftButton, PublishedChip } from '../generic/publish-status-buttons';
37+
import { ContainerDetails } from './ContainerDetails';
3738

3839
type ContainerPreviewProps = {
3940
containerId: string;
@@ -239,6 +240,11 @@ const ContainerInfo = () => {
239240
intl.formatMessage(messages.settingsTabTitle),
240241
<ContainerSettings />,
241242
)}
243+
{renderTab(
244+
CONTAINER_INFO_TABS.Details,
245+
intl.formatMessage(messages.detailsTabTitle),
246+
<ContainerDetails />,
247+
)}
242248
</Tabs>
243249
</Stack>
244250
);

src/library-authoring/containers/messages.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ const messages = defineMessages({
4141
defaultMessage: 'Settings',
4242
description: 'Title for settings tab',
4343
},
44+
detailsTabTitle: {
45+
id: 'course-authoring.library-authoring.container-sidebar.details-tab.title',
46+
defaultMessage: 'Details',
47+
description: 'Title for details tab',
48+
},
49+
detailsTabHistoryHeading: {
50+
id: 'course-authoring.library-authoring.container-sidebar.details-tab.history-heading',
51+
defaultMessage: 'History',
52+
description: 'Heading for details tab history section',
53+
},
4454
updateContainerSuccessMsg: {
4555
id: 'course-authoring.library-authoring.update-container-success-msg',
4656
defaultMessage: 'Container updated successfully.',

src/library-authoring/data/api.mocks.ts

Lines changed: 160 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,9 +1245,12 @@ mockGetCourseImports.applyMock = () =>
12451245
export async function mockLibraryBlockDraftHistory(usageKey: string): Promise<api.LibraryHistoryEntry[]> {
12461246
const thisMock = mockLibraryBlockDraftHistory;
12471247
switch (usageKey) {
1248-
case thisMock.usageKey: return thisMock.data;
1249-
case thisMock.usageKeyEmpty: return [];
1250-
default: throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
1248+
case thisMock.usageKey:
1249+
return thisMock.data;
1250+
case thisMock.usageKeyEmpty:
1251+
return [];
1252+
default:
1253+
throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
12511254
}
12521255
}
12531256
mockLibraryBlockDraftHistory.usageKey = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
@@ -1268,17 +1271,18 @@ mockLibraryBlockDraftHistory.data = [
12681271
changedAt: '2026-03-16T11:00:00Z',
12691272
title: 'Electron Arcs',
12701273
action: 'edited',
1271-
blockType: 'html',
1274+
itemType: 'html',
12721275
},
12731276
{
12741277
changedBy: mockContributor('test_user_2'),
12751278
changedAt: '2026-03-13T10:00:00Z',
12761279
title: 'More on Quarks',
12771280
action: 'renamed',
1278-
blockType: 'html',
1281+
itemType: 'html',
12791282
},
12801283
] satisfies api.LibraryHistoryEntry[];
1281-
mockLibraryBlockDraftHistory.applyMock = () => jest.spyOn(api, 'getLibraryBlockDraftHistory').mockImplementation(mockLibraryBlockDraftHistory);
1284+
mockLibraryBlockDraftHistory.applyMock = () =>
1285+
jest.spyOn(api, 'getLibraryBlockDraftHistory').mockImplementation(mockLibraryBlockDraftHistory);
12821286

12831287
/**
12841288
* Mock for `getLibraryBlockPublishHistory()`
@@ -1288,34 +1292,39 @@ mockLibraryBlockDraftHistory.applyMock = () => jest.spyOn(api, 'getLibraryBlockD
12881292
export async function mockLibraryBlockPublishHistory(usageKey: string): Promise<api.LibraryPublishHistoryGroup[]> {
12891293
const thisMock = mockLibraryBlockPublishHistory;
12901294
switch (usageKey) {
1291-
case thisMock.usageKeyWithGroups: return thisMock.data;
1292-
case thisMock.usageKeyEmpty: return [];
1293-
default: throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
1295+
case thisMock.usageKeyWithGroups:
1296+
return thisMock.data;
1297+
case thisMock.usageKeyEmpty:
1298+
return [];
1299+
default:
1300+
throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
12941301
}
12951302
}
12961303
mockLibraryBlockPublishHistory.usageKeyWithGroups = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
12971304
mockLibraryBlockPublishHistory.usageKeyEmpty = 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
12981305
mockLibraryBlockPublishHistory.data = [
12991306
{
13001307
publishLogUuid: 'abc-123',
1301-
title: 'Protons',
1302-
blockType: 'html',
1308+
directPublishedEntities: [
1309+
{ entityKey: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1', entityType: 'html', title: 'Protons' },
1310+
],
13031311
publishedBy: 'author',
13041312
publishedAt: '2026-03-14T10:00:00Z',
13051313
contributors: ['test_user_1', 'test_user_2', 'test_user_3', 'test_user_4', 'test_user_5'].map(mockContributor),
13061314
contributorsCount: 5,
13071315
},
13081316
] satisfies api.LibraryPublishHistoryGroup[];
1309-
mockLibraryBlockPublishHistory.applyMock = () => jest.spyOn(api, 'getLibraryBlockPublishHistory').mockImplementation(mockLibraryBlockPublishHistory);
1317+
mockLibraryBlockPublishHistory.applyMock = () =>
1318+
jest.spyOn(api, 'getLibraryBlockPublishHistory').mockImplementation(mockLibraryBlockPublishHistory);
13101319

13111320
/**
1312-
* Mock for `getLibraryBlockPublishHistoryEntries()`
1321+
* Mock for `getLibraryPublishHistoryEntries()`
13131322
*
13141323
* Use `mockLibraryBlockPublishHistoryEntries.applyMock()` to apply it to the whole test suite.
13151324
*/
13161325
export async function mockLibraryBlockPublishHistoryEntries(
1317-
_usageKey: string,
1318-
_publishGroupId: string,
1326+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
1327+
..._: Parameters<typeof api.getLibraryPublishHistoryEntries>
13191328
): Promise<api.LibraryHistoryEntry[]> {
13201329
return mockLibraryBlockPublishHistoryEntries.data;
13211330
}
@@ -1325,13 +1334,14 @@ mockLibraryBlockPublishHistoryEntries.data = [
13251334
changedAt: '2026-03-10T09:00:00Z',
13261335
title: 'Protons',
13271336
action: 'edited',
1328-
blockType: 'html',
1337+
itemType: 'html',
13291338
},
13301339
] satisfies api.LibraryHistoryEntry[];
1331-
mockLibraryBlockPublishHistoryEntries.applyMock = () => jest.spyOn(
1332-
api,
1333-
'getLibraryBlockPublishHistoryEntries',
1334-
).mockImplementation(mockLibraryBlockPublishHistoryEntries);
1340+
mockLibraryBlockPublishHistoryEntries.applyMock = () =>
1341+
jest.spyOn(
1342+
api,
1343+
'getLibraryPublishHistoryEntries',
1344+
).mockImplementation(mockLibraryBlockPublishHistoryEntries);
13351345

13361346
/**
13371347
* Mock for `getLibraryBlockCreationEntry()`
@@ -1343,9 +1353,12 @@ export async function mockLibraryBlockCreationEntry(usageKey: string): Promise<a
13431353
switch (usageKey) {
13441354
case thisMock.usageKeyThatNeverLoads:
13451355
return new Promise<any>(() => {});
1346-
case thisMock.usageKey: return thisMock.data;
1347-
case thisMock.usageKeyEmpty: return thisMock.dataEmpty;
1348-
default: throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
1356+
case thisMock.usageKey:
1357+
return thisMock.data;
1358+
case thisMock.usageKeyEmpty:
1359+
return thisMock.dataEmpty;
1360+
default:
1361+
throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
13491362
}
13501363
}
13511364
mockLibraryBlockCreationEntry.usageKeyThatNeverLoads = 'lb:Axim:infiniteLoading:html:123';
@@ -1355,17 +1368,138 @@ mockLibraryBlockCreationEntry.data = {
13551368
changedBy: mockContributor('author'),
13561369
changedAt: '2024-01-01T00:00:00Z',
13571370
title: 'Introduction to Testing 1',
1358-
blockType: 'html',
1371+
itemType: 'html',
13591372
action: 'created',
13601373
} satisfies api.LibraryHistoryEntry;
13611374
mockLibraryBlockCreationEntry.dataEmpty = {
13621375
changedBy: mockContributor('Author'),
13631376
changedAt: '2024-01-01T00:00:00Z',
13641377
title: 'Introduction to Testing 2',
1365-
blockType: 'html',
1378+
itemType: 'html',
1379+
action: 'created',
1380+
} satisfies api.LibraryHistoryEntry;
1381+
mockLibraryBlockCreationEntry.applyMock = () =>
1382+
jest.spyOn(api, 'getLibraryBlockCreationEntry').mockImplementation(mockLibraryBlockCreationEntry);
1383+
1384+
/**
1385+
* Mock for `getLibraryContainerDraftHistory()`
1386+
*
1387+
* Use `mockLibraryContainerDraftHistory.applyMock()` to apply it to the whole test suite.
1388+
*/
1389+
export async function mockLibraryContainerDraftHistory(containerKey: string): Promise<api.LibraryHistoryEntry[]> {
1390+
const thisMock = mockLibraryContainerDraftHistory;
1391+
switch (containerKey) {
1392+
case thisMock.containerKeyThatNeverLoads:
1393+
return new Promise<any>(() => {});
1394+
case thisMock.containerKey:
1395+
return thisMock.data;
1396+
case thisMock.containerKeyEmpty:
1397+
return [];
1398+
default:
1399+
throw new Error(`No mock has been set up for containerKey "${containerKey}"`);
1400+
}
1401+
}
1402+
mockLibraryContainerDraftHistory.containerKeyThatNeverLoads = 'lct:Axim:TEST1:unit:infiniteLoading';
1403+
mockLibraryContainerDraftHistory.containerKey = 'lct:Axim:TEST1:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
1404+
mockLibraryContainerDraftHistory.containerKeyEmpty = 'lct:Axim:TEST2:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
1405+
mockLibraryContainerDraftHistory.data = [
1406+
{
1407+
changedBy: mockContributor('container_user_1'),
1408+
changedAt: '2026-03-16T11:00:00Z',
1409+
title: 'Intro Unit',
1410+
action: 'edited',
1411+
itemType: 'unit',
1412+
},
1413+
{
1414+
changedBy: mockContributor('container_user_2'),
1415+
changedAt: '2026-03-13T10:00:00Z',
1416+
title: 'Unit Renamed',
1417+
action: 'renamed',
1418+
itemType: 'unit',
1419+
},
1420+
] satisfies api.LibraryHistoryEntry[];
1421+
mockLibraryContainerDraftHistory.applyMock = () =>
1422+
jest.spyOn(api, 'getLibraryContainerDraftHistory').mockImplementation(mockLibraryContainerDraftHistory);
1423+
1424+
/**
1425+
* Mock for `getLibraryContainerPublishHistory()`
1426+
*
1427+
* Use `mockLibraryContainerPublishHistory.applyMock()` to apply it to the whole test suite.
1428+
*/
1429+
export async function mockLibraryContainerPublishHistory(
1430+
containerKey: string,
1431+
): Promise<api.LibraryPublishHistoryGroup[]> {
1432+
const thisMock = mockLibraryContainerPublishHistory;
1433+
switch (containerKey) {
1434+
case thisMock.containerKeyThatNeverLoads:
1435+
return new Promise<any>(() => {});
1436+
case thisMock.containerKeyWithGroups:
1437+
return thisMock.data;
1438+
case thisMock.containerKeyEmpty:
1439+
return [];
1440+
default:
1441+
throw new Error(`No mock has been set up for containerKey "${containerKey}"`);
1442+
}
1443+
}
1444+
mockLibraryContainerPublishHistory.containerKeyThatNeverLoads = 'lct:Axim:TEST1:unit:infiniteLoading';
1445+
mockLibraryContainerPublishHistory.containerKeyWithGroups = 'lct:Axim:TEST1:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
1446+
mockLibraryContainerPublishHistory.containerKeyEmpty = 'lct:Axim:TEST2:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
1447+
mockLibraryContainerPublishHistory.data = [
1448+
{
1449+
publishLogUuid: 'def-456',
1450+
directPublishedEntities: [
1451+
{
1452+
entityKey: 'lct:Axim:TEST1:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
1453+
entityType: 'unit',
1454+
title: 'Intro Unit',
1455+
},
1456+
],
1457+
publishedBy: 'container_author',
1458+
publishedAt: '2026-03-14T10:00:00Z',
1459+
contributors: ['container_user_1', 'container_user_2'].map(mockContributor),
1460+
contributorsCount: 2,
1461+
},
1462+
] satisfies api.LibraryPublishHistoryGroup[];
1463+
mockLibraryContainerPublishHistory.applyMock = () =>
1464+
jest.spyOn(api, 'getLibraryContainerPublishHistory').mockImplementation(mockLibraryContainerPublishHistory);
1465+
1466+
/**
1467+
* Mock for `getLibraryContainerCreationEntry()`
1468+
*
1469+
* Use `mockLibraryContainerCreationEntry.applyMock()` to apply it to the whole test suite.
1470+
*/
1471+
export async function mockLibraryContainerCreationEntry(containerKey: string): Promise<api.LibraryHistoryEntry> {
1472+
const thisMock = mockLibraryContainerCreationEntry;
1473+
switch (containerKey) {
1474+
case thisMock.usageKeyThatNeverLoads:
1475+
return new Promise<any>(() => {});
1476+
case thisMock.usageKey:
1477+
return thisMock.data;
1478+
case thisMock.usageKeyEmpty:
1479+
return thisMock.dataEmpty;
1480+
default:
1481+
throw new Error(`No mock has been set up for containerKey "${containerKey}"`);
1482+
}
1483+
}
1484+
mockLibraryContainerCreationEntry.usageKeyThatNeverLoads = 'lct:Axim:TEST1:unit:infiniteLoading';
1485+
mockLibraryContainerCreationEntry.usageKey = 'lct:Axim:TEST1:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
1486+
mockLibraryContainerCreationEntry.usageKeyEmpty = 'lct:Axim:TEST2:unit:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
1487+
mockLibraryContainerCreationEntry.data = {
1488+
changedBy: mockContributor('author'),
1489+
changedAt: '2024-01-01T00:00:00Z',
1490+
title: 'Introduction to Testing Unit 1',
1491+
itemType: 'unit',
1492+
action: 'created',
1493+
} satisfies api.LibraryHistoryEntry;
1494+
mockLibraryContainerCreationEntry.dataEmpty = {
1495+
changedBy: mockContributor('Author'),
1496+
changedAt: '2024-01-01T00:00:00Z',
1497+
title: 'Introduction to Testing Unit 2',
1498+
itemType: 'unit',
13661499
action: 'created',
13671500
} satisfies api.LibraryHistoryEntry;
1368-
mockLibraryBlockCreationEntry.applyMock = () => jest.spyOn(api, 'getLibraryBlockCreationEntry').mockImplementation(mockLibraryBlockCreationEntry);
1501+
mockLibraryContainerCreationEntry.applyMock = () =>
1502+
jest.spyOn(api, 'getLibraryContainerCreationEntry').mockImplementation(mockLibraryContainerCreationEntry);
13691503

13701504
export const mockGetMigrationInfo = {
13711505
applyMock: () =>

0 commit comments

Comments
 (0)