Skip to content

Commit ff67c9a

Browse files
feat: add component Details sidebar [FC-0062] (#1303)
* feat: add ComponentDetails component --------- Co-authored-by: Jillian <[email protected]>
1 parent c13ab00 commit ff67c9a

File tree

14 files changed

+203
-8
lines changed

14 files changed

+203
-8
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
initializeMocks,
3+
render,
4+
screen,
5+
} from '../../testUtils';
6+
import { mockLibraryBlockMetadata } from '../data/api.mocks';
7+
import ComponentDetails from './ComponentDetails';
8+
9+
describe('<ComponentDetails />', () => {
10+
it('should render the component details loading', async () => {
11+
initializeMocks();
12+
mockLibraryBlockMetadata.applyMock();
13+
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyThatNeverLoads} />);
14+
expect(await screen.findByText('Loading...')).toBeInTheDocument();
15+
});
16+
17+
it('should render the component details error', async () => {
18+
initializeMocks();
19+
mockLibraryBlockMetadata.applyMock();
20+
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyError404} />);
21+
expect(await screen.findByText(/Mocked request failed with status code 404/)).toBeInTheDocument();
22+
});
23+
24+
it('should render the component usage', async () => {
25+
initializeMocks();
26+
mockLibraryBlockMetadata.applyMock();
27+
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
28+
expect(await screen.findByText('Component Usage')).toBeInTheDocument();
29+
// TODO: replace with actual data when implement tag list
30+
expect(screen.queryByText('This will show the courses that use this component.')).toBeInTheDocument();
31+
});
32+
33+
it('should render the component history', async () => {
34+
initializeMocks();
35+
mockLibraryBlockMetadata.applyMock();
36+
render(<ComponentDetails usageKey={mockLibraryBlockMetadata.usageKeyNeverPublished} />);
37+
// Show created date
38+
expect(await screen.findByText('June 20, 2024')).toBeInTheDocument();
39+
// Show modified date
40+
expect(await screen.findByText('June 21, 2024')).toBeInTheDocument();
41+
});
42+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { Stack } from '@openedx/paragon';
3+
4+
import AlertError from '../../generic/alert-error';
5+
import Loading from '../../generic/Loading';
6+
import { useLibraryBlockMetadata } from '../data/apiHooks';
7+
import HistoryWidget from '../generic/history-widget';
8+
import { ComponentDeveloperInfo } from './ComponentDeveloperInfo';
9+
import messages from './messages';
10+
11+
interface ComponentDetailsProps {
12+
usageKey: string;
13+
}
14+
15+
const ComponentDetails = ({ usageKey }: ComponentDetailsProps) => {
16+
const intl = useIntl();
17+
const {
18+
data: componentMetadata,
19+
isError,
20+
error,
21+
isLoading,
22+
} = useLibraryBlockMetadata(usageKey);
23+
24+
if (isError) {
25+
return <AlertError error={error} />;
26+
}
27+
28+
if (isLoading) {
29+
return <Loading />;
30+
}
31+
32+
return (
33+
<Stack gap={3}>
34+
<div>
35+
<h3 className="h5">
36+
{intl.formatMessage(messages.detailsTabUsageTitle)}
37+
</h3>
38+
<small>This will show the courses that use this component.</small>
39+
</div>
40+
<hr className="w-100" />
41+
<div>
42+
<h3 className="h5">
43+
{intl.formatMessage(messages.detailsTabHistoryTitle)}
44+
</h3>
45+
<HistoryWidget
46+
{...componentMetadata}
47+
/>
48+
</div>
49+
{
50+
// istanbul ignore next: this is only shown in development
51+
(process.env.NODE_ENV === 'development' ? <ComponentDeveloperInfo usageKey={usageKey} /> : null)
52+
}
53+
</Stack>
54+
);
55+
};
56+
57+
export default ComponentDetails;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const ComponentDeveloperInfo: React.FC<Props> = ({ usageKey }) => {
1414
const { data: olx, isLoading: isOLXLoading } = useXBlockOLX(usageKey);
1515
return (
1616
<>
17-
<hr />
17+
<hr className="w-100" />
1818
<h3 className="h5">Developer Component Details</h3>
1919
<p><small>(This panel is only visible in development builds.)</small></p>
2020
<dl>

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Link } from 'react-router-dom';
1010

1111
import { getEditUrl } from '../components/utils';
1212
import { ComponentMenu } from '../components';
13-
import { ComponentDeveloperInfo } from './ComponentDeveloperInfo';
13+
import ComponentDetails from './ComponentDetails';
1414
import ComponentManagement from './ComponentManagement';
1515
import ComponentPreview from './ComponentPreview';
1616
import messages from './messages';
@@ -50,11 +50,7 @@ const ComponentInfo = ({ usageKey }: ComponentInfoProps) => {
5050
<ComponentManagement usageKey={usageKey} />
5151
</Tab>
5252
<Tab eventKey="details" title={intl.formatMessage(messages.detailsTabTitle)}>
53-
Details tab placeholder
54-
55-
{
56-
(process.env.NODE_ENV === 'development' ? <ComponentDeveloperInfo usageKey={usageKey} /> : null)
57-
}
53+
<ComponentDetails usageKey={usageKey} />
5854
</Tab>
5955
</Tabs>
6056
</Stack>

src/library-authoring/component-info/ComponentManagement.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { mockLibraryBlockMetadata } from '../data/api.mocks';
99
import ComponentManagement from './ComponentManagement';
1010

1111
/*
12-
* FIXME: Summarize the reason here
12+
* This function is used to get the inner text of an element.
1313
* https://stackoverflow.com/questions/47902335/innertext-is-undefined-in-jest-test
1414
*/
1515
const getInnerText = (element: Element) => element?.textContent

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const ComponentManagement = ({ usageKey }: ComponentManagementProps) => {
1414
const intl = useIntl();
1515
const { data: componentMetadata } = useLibraryBlockMetadata(usageKey);
1616

17+
// istanbul ignore if: this should never happen
1718
if (!componentMetadata) {
1819
return null;
1920
}

src/library-authoring/component-info/messages.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ const messages = defineMessages({
5151
defaultMessage: 'Details',
5252
description: 'Title for details tab',
5353
},
54+
detailsTabUsageTitle: {
55+
id: 'course-authoring.library-authoring.component.details-tab.usage-title',
56+
defaultMessage: 'Component Usage',
57+
description: 'Title for the Component Usage container in the details tab',
58+
},
59+
detailsTabHistoryTitle: {
60+
id: 'course-authoring.library-authoring.component.details-tab.history-title',
61+
defaultMessage: 'Component History',
62+
description: 'Title for the Component History container in the details tab',
63+
},
5464
previewExpandButtonTitle: {
5565
id: 'course-authoring.library-authoring.component.preview.expand.title',
5666
defaultMessage: 'Expand',

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ mockCreateLibraryBlock.newHtmlData = {
134134
lastDraftCreated: '2024-07-22T21:37:49Z',
135135
lastDraftCreatedBy: null,
136136
created: '2024-07-22T21:37:49Z',
137+
modified: '2024-07-22T21:37:49Z',
137138
tagsCount: 0,
138139
} satisfies api.LibraryBlockMetadata;
139140
mockCreateLibraryBlock.newProblemData = {
@@ -147,6 +148,7 @@ mockCreateLibraryBlock.newProblemData = {
147148
lastDraftCreated: '2024-07-22T21:37:49Z',
148149
lastDraftCreatedBy: null,
149150
created: '2024-07-22T21:37:49Z',
151+
modified: '2024-07-22T21:37:49Z',
150152
tagsCount: 0,
151153
} satisfies api.LibraryBlockMetadata;
152154
mockCreateLibraryBlock.newVideoData = {
@@ -160,6 +162,7 @@ mockCreateLibraryBlock.newVideoData = {
160162
lastDraftCreated: '2024-07-22T21:37:49Z',
161163
lastDraftCreatedBy: null,
162164
created: '2024-07-22T21:37:49Z',
165+
modified: '2024-07-22T21:37:49Z',
163166
tagsCount: 0,
164167
} satisfies api.LibraryBlockMetadata;
165168
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
@@ -224,11 +227,18 @@ mockXBlockFields.applyMock = () => jest.spyOn(api, 'getXBlockFields').mockImplem
224227
export async function mockLibraryBlockMetadata(usageKey: string): Promise<api.LibraryBlockMetadata> {
225228
const thisMock = mockLibraryBlockMetadata;
226229
switch (usageKey) {
230+
case thisMock.usageKeyThatNeverLoads:
231+
// Return a promise that never resolves, to simulate never loading:
232+
return new Promise<any>(() => {});
233+
case thisMock.usageKeyError404:
234+
throw createAxiosError({ code: 404, message: 'Not found.', path: api.getLibraryBlockMetadataUrl(usageKey) });
227235
case thisMock.usageKeyNeverPublished: return thisMock.dataNeverPublished;
228236
case thisMock.usageKeyPublished: return thisMock.dataPublished;
229237
default: throw new Error(`No mock has been set up for usageKey "${usageKey}"`);
230238
}
231239
}
240+
mockLibraryBlockMetadata.usageKeyThatNeverLoads = 'lb:Axim:infiniteLoading:html:123';
241+
mockLibraryBlockMetadata.usageKeyError404 = 'lb:Axim:error404:html:123';
232242
mockLibraryBlockMetadata.usageKeyNeverPublished = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1';
233243
mockLibraryBlockMetadata.dataNeverPublished = {
234244
id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
@@ -241,6 +251,7 @@ mockLibraryBlockMetadata.dataNeverPublished = {
241251
lastDraftCreatedBy: null,
242252
hasUnpublishedChanges: false,
243253
created: '2024-06-20T13:54:21Z',
254+
modified: '2024-06-21T13:54:21Z',
244255
tagsCount: 0,
245256
} satisfies api.LibraryBlockMetadata;
246257
mockLibraryBlockMetadata.usageKeyPublished = 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2';
@@ -255,6 +266,7 @@ mockLibraryBlockMetadata.dataPublished = {
255266
lastDraftCreatedBy: '2024-06-20T20:00:00Z',
256267
hasUnpublishedChanges: false,
257268
created: '2024-06-20T13:54:21Z',
269+
modified: '2024-06-21T13:54:21Z',
258270
tagsCount: 0,
259271
} satisfies api.LibraryBlockMetadata;
260272
/** Apply this mock. Returns a spy object that can tell you if it's been called. */

src/library-authoring/data/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export interface LibraryBlockMetadata {
149149
lastDraftCreatedBy: string | null,
150150
hasUnpublishedChanges: boolean;
151151
created: string | null,
152+
modified: string | null,
152153
tagsCount: number;
153154
}
154155

src/library-authoring/data/apiHooks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const xblockQueryKeys = {
9595
*/
9696
export function invalidateComponentData(queryClient: QueryClient, contentLibraryId: string, usageKey: string) {
9797
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockFields(usageKey) });
98+
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
9899
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
99100
}
100101

0 commit comments

Comments
 (0)