Skip to content

Commit

Permalink
feat: add component usage data in the ComponentDetails component
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido committed Feb 14, 2025
1 parent 59243b0 commit c2ce1e2
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 11 deletions.
12 changes: 9 additions & 3 deletions src/library-authoring/component-info/ComponentDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
mockLibraryBlockMetadata,
mockXBlockAssets,
mockXBlockOLX,
mockComponentDownstreamContexts,
} from '../data/api.mocks';
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
import ComponentDetails from './ComponentDetails';
Expand All @@ -16,6 +17,7 @@ mockContentLibrary.applyMock();
mockLibraryBlockMetadata.applyMock();
mockXBlockAssets.applyMock();
mockXBlockOLX.applyMock();
mockComponentDownstreamContexts.applyMock();

const render = (usageKey: string) => baseRender(<ComponentDetails />, {
extraWrapper: ({ children }) => (
Expand Down Expand Up @@ -46,10 +48,14 @@ describe('<ComponentDetails />', () => {
});

it('should render the component usage', async () => {
render(mockLibraryBlockMetadata.usageKeyNeverPublished);
render(mockComponentDownstreamContexts.usageKey);
expect(await screen.findByText('Component Usage')).toBeInTheDocument();
// TODO: replace with actual data when implement course list
expect(screen.queryByText(/This will show the courses that use this component./)).toBeInTheDocument();
const links = screen.getAllByRole('link');
expect(links).toHaveLength(2);
expect(links[0]).toHaveTextContent('Course 1');
expect(links[0]).toHaveAttribute('href', '/course/course-v1:org+course+run');
expect(links[1]).toHaveTextContent('Course 2');
expect(links[1]).toHaveAttribute('href', '/course/course-v1:org+course2+run');
});

it('should render the component history', async () => {
Expand Down
20 changes: 16 additions & 4 deletions src/library-authoring/component-info/ComponentDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Stack } from '@openedx/paragon';
import { Link } from 'react-router-dom';

import AlertError from '../../generic/alert-error';
import Loading from '../../generic/Loading';
import { useSidebarContext } from '../common/context/SidebarContext';
import { useLibraryBlockMetadata } from '../data/apiHooks';
import { useComponentDownstreamContexts, useLibraryBlockMetadata } from '../data/apiHooks';
import HistoryWidget from '../generic/history-widget';
import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
import messages from './messages';
Expand All @@ -23,14 +24,16 @@ const ComponentDetails = () => {
data: componentMetadata,
isError,
error,
isLoading,
isLoading: isLoadingComponentMetadata,
} = useLibraryBlockMetadata(usageKey);

const { data: componentUsage, isLoading: isLoadingComponentUsage } = useComponentDownstreamContexts(usageKey);

if (isError) {
return <AlertError error={error} />;
}

if (isLoading) {
if (isLoadingComponentMetadata || isLoadingComponentUsage) {
return <Loading />;
}

Expand All @@ -40,7 +43,16 @@ const ComponentDetails = () => {
<h3 className="h5">
<FormattedMessage {...messages.detailsTabUsageTitle} />
</h3>
<small><FormattedMessage {...messages.detailsTabUsagePlaceholder} /></small>
{
componentUsage?.length ? (
componentUsage.map(({ id, displayName, url }) => (
<Link key={id} to={url}>{displayName}</Link>
))
) : (
<FormattedMessage {...messages.detailsTabUsageEmpty} />
)
}

</div>
<hr className="w-100" />
<div>
Expand Down
8 changes: 4 additions & 4 deletions src/library-authoring/component-info/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ const messages = defineMessages({
defaultMessage: 'Component Usage',
description: 'Title for the Component Usage container in the details tab',
},
detailsTabUsagePlaceholder: {
id: 'course-authoring.library-authoring.component.details-tab.usage-placeholder',
defaultMessage: 'This will show the courses that use this component. Feature coming soon.',
description: 'Explanation/placeholder for the future "Component Usage" feature',
detailsTabUsageEmpty: {
id: 'course-authoring.library-authoring.component.details-tab.usage-empty',
defaultMessage: 'This component is not used in any course.',
description: 'Message to display in usage section when component is not used in any course',
},
detailsTabHistoryTitle: {
id: 'course-authoring.library-authoring.component.details-tab.history-title',
Expand Down
28 changes: 28 additions & 0 deletions src/library-authoring/data/api.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,31 @@ mockGetLibraryTeam.notMember = {

/** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockGetLibraryTeam.applyMock = () => jest.spyOn(api, 'getLibraryTeam').mockImplementation(mockGetLibraryTeam);

export async function mockComponentDownstreamContexts(usageKey: string): Promise<api.ComponentDownstreamContext[]> {
const thisMock = mockComponentDownstreamContexts;
switch (usageKey) {
case thisMock.usageKey: return thisMock.componentUsage;
default: return [];
}
}
mockComponentDownstreamContexts.usageKey = mockXBlockFields.usageKeyHtml;
mockComponentDownstreamContexts.componentUsage = [
{
id: 'course-v1:org+course1+run',
displayName: 'Course 1',
url: '/course/course-v1:org+course+run',
count: 2,
},
{
id: 'course-v1:org+course2+run',
displayName: 'Course 2',
url: '/course/course-v1:org+course2+run',
count: 1,
},
] satisfies api.ComponentDownstreamContext[];

mockComponentDownstreamContexts.applyMock = () => jest.spyOn(
api,
'getComponentDownstreamContexts',
).mockImplementation(mockComponentDownstreamContexts);
20 changes: 20 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export const getLibraryCollectionRestoreApiUrl = (libraryId: string, collectionI
* Get the URL for the xblock api.
*/
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
/**
* Get the URL for the content store api.
*/
export const getContentStoreApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/`;
/**
* Get the URL for the component downstream contexts API.
*/
export const getComponentDownstreamContextsApiUrl = (usageKey: string) => `${getContentStoreApiUrl()}upstream/${usageKey}/downstream-contexts`;

export interface ContentLibrary {
id: string;
Expand Down Expand Up @@ -533,3 +541,15 @@ export async function updateComponentCollections(usageKey: string, collectionKey
collection_keys: collectionKeys,
});
}

export interface ComponentDownstreamContext {
id: string;
displayName: string;
count: number;
url: string;
}

export async function getComponentDownstreamContexts(usageKey: string): Promise<ComponentDownstreamContext[]> {
const { data } = await getAuthenticatedHttpClient().get(getComponentDownstreamContextsApiUrl(usageKey));
return camelCaseObject(data);
}
1 change: 1 addition & 0 deletions src/library-authoring/data/apiHooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getLibraryCollectionComponentApiUrl,
getLibraryCollectionsApiUrl,
getLibraryCollectionApiUrl,
getComponentDownstreamContextsApiUrl,

Check failure on line 15 in src/library-authoring/data/apiHooks.test.tsx

View workflow job for this annotation

GitHub Actions / tests

'getComponentDownstreamContextsApiUrl' is defined but never used
} from './api';
import {
useCommitLibraryChanges,
Expand Down
10 changes: 10 additions & 0 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
publishXBlock,
deleteXBlockAsset,
restoreLibraryBlock,
getComponentDownstreamContexts,
} from './api';
import { VersionSpec } from '../LibraryBlock';

Expand Down Expand Up @@ -99,6 +100,7 @@ export const xblockQueryKeys = {
/** assets (static files) */
xblockAssets: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'assets'],
componentMetadata: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'componentMetadata'],
componentDownstreamContexts: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'downstreamContexts'],
};

/**
Expand Down Expand Up @@ -542,3 +544,11 @@ export const useUpdateComponentCollections = (libraryId: string, usageKey: strin
},
});
};

export const useComponentDownstreamContexts = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.componentDownstreamContexts(usageKey),
queryFn: () => getComponentDownstreamContexts(usageKey),
enabled: !!usageKey,
})
);

0 comments on commit c2ce1e2

Please sign in to comment.