Skip to content

Commit 3a8eb99

Browse files
authored
feat: History log in content library components and containers [FC-0123] (#2948)
- Implements the API functions and hooks to fetch: - Draft history entries for components and containers. - Published history groups for components and containers. - Published entries within a history group. - Creation entry for components and containers. - Implements: - `HistoryCreatedLogGroup`: Group for the create event. - `HistoryDraftLogGroup`: Group and entries for draft changes (collapsible). - `HistoryPublishLogGroup`: Group and entries for published changes (lazy-loaded on expand). - `HistoryComponentLog`: Renders the full history for a library component. - `HistoryContainerLog`: Renders the full history for a library container (unit/section/subsection). - `ContainerDetails`: New "Details" tab in the container sidebar that shows the container history log.
1 parent 2d9b882 commit 3a8eb99

19 files changed

Lines changed: 1657 additions & 34 deletions

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.test.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import {
44
render as baseRender,
55
screen,
66
fireEvent,
7+
findByDeepTextContent,
78
} from '@src/testUtils';
89
import { mockFetchIndexDocuments, mockContentSearchConfig } from '@src/search-manager/data/api.mock';
910

1011
import {
1112
mockContentLibrary,
1213
mockGetEntityLinks,
14+
mockLibraryBlockCreationEntry,
15+
mockLibraryBlockDraftHistory,
1316
mockLibraryBlockMetadata,
17+
mockLibraryBlockPublishHistory,
18+
mockLibraryBlockPublishHistoryEntries,
1419
mockXBlockAssets,
1520
mockXBlockOLX,
1621
} from '../data/api.mocks';
@@ -21,6 +26,10 @@ import ComponentDetails from './ComponentDetails';
2126
mockContentSearchConfig.applyMock();
2227
mockContentLibrary.applyMock();
2328
mockLibraryBlockMetadata.applyMock();
29+
mockLibraryBlockCreationEntry.applyMock();
30+
mockLibraryBlockDraftHistory.applyMock();
31+
mockLibraryBlockPublishHistory.applyMock();
32+
mockLibraryBlockPublishHistoryEntries.applyMock();
2433
mockXBlockAssets.applyMock();
2534
mockXBlockOLX.applyMock();
2635
mockGetEntityLinks.applyMock();
@@ -60,7 +69,9 @@ describe('<ComponentDetails />', () => {
6069

6170
it('should render the component details error', async () => {
6271
render(mockLibraryBlockMetadata.usageKeyError404);
63-
expect(await screen.findByText(/Mocked request failed with status code 404/)).toBeInTheDocument();
72+
// Metadata and history queries fail silently; the section renders empty without crashing
73+
expect(await screen.findByText('Component History')).toBeInTheDocument();
74+
expect(screen.queryByText(/Mocked request failed/)).not.toBeInTheDocument();
6475
});
6576

6677
it('should render the component usage', async () => {
@@ -96,11 +107,7 @@ describe('<ComponentDetails />', () => {
96107

97108
it('should render the component history', async () => {
98109
render(mockLibraryBlockMetadata.usageKeyPublished);
99-
// Show created date
100-
expect(await screen.findByText('June 20, 2024')).toBeInTheDocument();
101-
// Show modified date
102-
expect(await screen.findByText('June 21, 2024')).toBeInTheDocument();
103-
// Show last published date
104-
expect(await screen.findByText('June 22, 2024')).toBeInTheDocument();
110+
// usageKeyPublished matches usageKeyEmpty in mockLibraryBlockCreationEntry (TEST2 key)
111+
expect(await findByDeepTextContent(/Author created.*Introduction to Testing 2/i)).toBeInTheDocument();
105112
});
106113
});

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

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

4-
import AlertError from '../../generic/alert-error';
5-
import Loading from '../../generic/Loading';
64
import { useSidebarContext } from '../common/context/SidebarContext';
7-
import { useLibraryBlockMetadata } from '../data/apiHooks';
8-
import HistoryWidget from '../generic/history-widget';
95
import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
106
import { ComponentUsage } from './ComponentUsage';
117
import messages from './messages';
8+
import { HistoryComponentLog } from '../generic/history-log/HistoryLog';
129

1310
const ComponentDetails = () => {
1411
const { sidebarItemInfo } = useSidebarContext();
@@ -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
<>
@@ -48,7 +30,9 @@ const ComponentDetails = () => {
4830
<h3 className="h5">
4931
<FormattedMessage {...messages.detailsTabHistoryTitle} />
5032
</h3>
51-
<HistoryWidget {...componentMetadata} />
33+
<HistoryComponentLog
34+
componentId={usageKey}
35+
/>
5236
</>
5337
<ComponentAdvancedInfo />
5438
</Stack>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { FormattedMessage } 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 { sidebarItemInfo } = useSidebarContext();
8+
9+
const usageKey = sidebarItemInfo?.id;
10+
11+
return (
12+
<>
13+
<h4>
14+
<FormattedMessage {...messages.detailsTabHistoryHeading} />
15+
</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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ const messages = defineMessages({
4141
defaultMessage: 'Settings',
4242
description: 'Title for settings tab',
4343
},
44+
// TODO: These messages (detailsTabTitle, detailsTabHistoryHeading, etc.) should be shared
45+
// across all sidebar types (components, containers, collections) since the tab names and
46+
// headings are identical. Currently they are duplicated in each sidebar's messages file.
47+
detailsTabTitle: {
48+
id: 'course-authoring.library-authoring.container-sidebar.details-tab.title',
49+
defaultMessage: 'Details',
50+
description: 'Title for details tab',
51+
},
52+
detailsTabHistoryHeading: {
53+
id: 'course-authoring.library-authoring.container-sidebar.details-tab.history-heading',
54+
defaultMessage: 'History',
55+
description: 'Heading for details tab history section',
56+
},
4457
updateContainerSuccessMsg: {
4558
id: 'course-authoring.library-authoring.update-container-success-msg',
4659
defaultMessage: 'Container updated successfully.',

0 commit comments

Comments
 (0)