From 0c35fe8fe9e1a0998bd28f763351508718858948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 20 Feb 2025 14:01:46 -0300 Subject: [PATCH 1/5] feat: add component usage data in the ComponentDetails component --- src/course-libraries/CourseLibraries.tsx | 14 +-- .../component-info/ComponentDetails.test.tsx | 35 ++++++- .../component-info/ComponentDetails.tsx | 11 ++- .../component-info/ComponentUsage.tsx | 94 +++++++++++++++++++ .../component-info/messages.ts | 8 +- src/library-authoring/data/api.mocks.ts | 23 +++++ src/library-authoring/data/api.ts | 16 ++++ src/library-authoring/data/apiHooks.ts | 13 +++ .../data/__mocks__/downstream-links.json | 84 +++++++++++++++++ src/search-manager/data/api.mock.ts | 18 +++- src/search-manager/data/api.ts | 5 +- src/search-manager/data/apiHooks.test.tsx | 2 +- src/search-manager/data/apiHooks.ts | 33 +++++-- src/search-manager/index.ts | 2 +- .../__mocks__/downstream-links.json | 0 15 files changed, 324 insertions(+), 34 deletions(-) create mode 100644 src/library-authoring/component-info/ComponentUsage.tsx create mode 100644 src/search-manager/data/__mocks__/downstream-links.json create mode 100644 src/search-modal/__mocks__/downstream-links.json diff --git a/src/course-libraries/CourseLibraries.tsx b/src/course-libraries/CourseLibraries.tsx index 009c42eef9..d15a433f9f 100644 --- a/src/course-libraries/CourseLibraries.tsx +++ b/src/course-libraries/CourseLibraries.tsx @@ -111,13 +111,13 @@ const LibraryCard: React.FC = ({ courseId, title, links }) => const totalComponents = links.length; const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]); const downstreamKeys = useMemo(() => uniq(Object.keys(linksInfo)), [links]); - const { data: downstreamInfo } = useFetchIndexDocuments( - [`context_key = "${courseId}"`, `usage_key IN ["${downstreamKeys.join('","')}"]`], - downstreamKeys.length, - ['usage_key', 'display_name', 'breadcrumbs', 'description', 'block_type'], - ['description:30'], - [SearchSortOption.TITLE_AZ], - ) as unknown as { data: ComponentInfo[] }; + const { data: downstreamInfo } = useFetchIndexDocuments({ + filter: [`context_key = "${courseId}"`, `usage_key IN ["${downstreamKeys.join('","')}"]`], + limit: downstreamKeys.length, + attributesToRetrieve: ['usage_key', 'display_name', 'breadcrumbs', 'description', 'block_type'], + attributesToCrop: ['description:30'], + sort: [SearchSortOption.TITLE_AZ], + }) as unknown as { data: ComponentInfo[] }; const renderBlockCards = (info: ComponentInfo) => { // eslint-disable-next-line no-param-reassign diff --git a/src/library-authoring/component-info/ComponentDetails.test.tsx b/src/library-authoring/component-info/ComponentDetails.test.tsx index 8278b3727c..6fa1c1333a 100644 --- a/src/library-authoring/component-info/ComponentDetails.test.tsx +++ b/src/library-authoring/component-info/ComponentDetails.test.tsx @@ -2,20 +2,26 @@ import { initializeMocks, render as baseRender, screen, + fireEvent, } from '../../testUtils'; +import { mockFetchIndexDocuments, mockContentSearchConfig } from '../../search-manager/data/api.mock'; import { mockContentLibrary, mockLibraryBlockMetadata, mockXBlockAssets, mockXBlockOLX, + mockComponentDownstreamLinks, } from '../data/api.mocks'; import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; import ComponentDetails from './ComponentDetails'; +mockContentSearchConfig.applyMock(); mockContentLibrary.applyMock(); mockLibraryBlockMetadata.applyMock(); mockXBlockAssets.applyMock(); mockXBlockOLX.applyMock(); +mockComponentDownstreamLinks.applyMock(); +mockFetchIndexDocuments.applyMock(); const render = (usageKey: string) => baseRender(, { extraWrapper: ({ children }) => ( @@ -46,10 +52,33 @@ describe('', () => { }); it('should render the component usage', async () => { - render(mockLibraryBlockMetadata.usageKeyNeverPublished); + render(mockComponentDownstreamLinks.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 course1 = await screen.findByText('Course 1'); + expect(course1).toBeInTheDocument(); + fireEvent.click(screen.getByText('Course 1')); + + const course2 = screen.getByText('Course 2'); + expect(course2).toBeInTheDocument(); + fireEvent.click(screen.getByText('Course 2')); + + const links = screen.getAllByRole('link'); + expect(links).toHaveLength(3); + expect(links[0]).toHaveTextContent('Unit 1'); + expect(links[0]).toHaveAttribute( + 'href', + '/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId1', + ); + expect(links[1]).toHaveTextContent('Unit 2'); + expect(links[1]).toHaveAttribute( + 'href', + '/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId2', + ); + expect(links[2]).toHaveTextContent('Problem Bank 3'); + expect(links[2]).toHaveAttribute( + 'href', + '/course/course-v1:org+course2+run/container/block-v1:org+course2+run+type@itembank+block@itembankId3', + ); }); it('should render the component history', async () => { diff --git a/src/library-authoring/component-info/ComponentDetails.tsx b/src/library-authoring/component-info/ComponentDetails.tsx index e41f3ad50c..ceb122126c 100644 --- a/src/library-authoring/component-info/ComponentDetails.tsx +++ b/src/library-authoring/component-info/ComponentDetails.tsx @@ -7,6 +7,7 @@ import { useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryBlockMetadata } from '../data/apiHooks'; import HistoryWidget from '../generic/history-widget'; import { ComponentAdvancedInfo } from './ComponentAdvancedInfo'; +import { ComponentUsage } from './ComponentUsage'; import messages from './messages'; const ComponentDetails = () => { @@ -36,19 +37,19 @@ const ComponentDetails = () => { return ( -
+ <>

- -
+ +
-
+ <>

-
+
); diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx new file mode 100644 index 0000000000..b7a3e93297 --- /dev/null +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -0,0 +1,94 @@ +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Collapsible } from '@openedx/paragon'; +import { Link } from 'react-router-dom'; + +import AlertError from '../../generic/alert-error'; +import Loading from '../../generic/Loading'; +import { useFetchIndexDocuments, type ContentHit } from '../../search-manager'; +import { useComponentDownstreamLinks } from '../data/apiHooks'; +import messages from './messages'; + +interface ComponentUsageProps { + usageKey: string; +} + +type ComponentUsageTree = Record; + +export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { + const { + data: dataDownstreamLinks, + isError: isErrorDownstreamLinks, + error: errorDownstreamLinks, + isLoading: isLoadingDownstreamLinks, + } = useComponentDownstreamLinks(usageKey); + + const downstreamKeys = dataDownstreamLinks || []; + + const { + data: downstreamHits, + isError: isErrorIndexDocuments, + error: errorIndexDocuments, + isLoading: isLoadingIndexDocuments, + } = useFetchIndexDocuments({ + filter: [`usage_key IN ["${downstreamKeys.join('","')}"]`], + limit: downstreamKeys.length, + attributesToRetrieve: ['usage_key', 'breadcrumbs', 'context_key'], + enabled: !!downstreamKeys.length, + }); + + if (isErrorDownstreamLinks || isErrorIndexDocuments) { + return ; + } + + if (isLoadingDownstreamLinks || isLoadingIndexDocuments) { + return ; + } + + if (!downstreamKeys.length) { + return ; + } + + const componentUsage = downstreamHits.reduce((acc, hit) => { + const link = hit.breadcrumbs.at(-1); + // istanbul ignore if: this should never happen. it is a type guard for the breadcrumb last item + if (!(link && ('usageKey' in link))) { + return acc; + } + + if (hit.contextKey in acc) { + acc[hit.contextKey].links.push(link); + } else { + acc[hit.contextKey] = { + key: hit.contextKey, + contextName: hit.breadcrumbs[0].displayName, + links: [link], + }; + } + return acc; + }, {}); + + const componentUsageList = Object.values(componentUsage); + + return ( + <> + { + componentUsageList.map((context) => ( + + {context.links.map(({ usageKey: downstreamUsageKey, url, displayName }) => ( + url ? ( + {displayName} + ) : ( + // istanbul ignore next: this should never happen + {displayName} + ) + ))} + + )) + } + + ); +}; diff --git a/src/library-authoring/component-info/messages.ts b/src/library-authoring/component-info/messages.ts index 1c02d867f4..7e4068d53e 100644 --- a/src/library-authoring/component-info/messages.ts +++ b/src/library-authoring/component-info/messages.ts @@ -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', diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts index ce4be29168..bbdf4b86ea 100644 --- a/src/library-authoring/data/api.mocks.ts +++ b/src/library-authoring/data/api.mocks.ts @@ -526,3 +526,26 @@ 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 mockComponentDownstreamLinks( + usageKey: string, +): ReturnType { + const thisMock = mockComponentDownstreamLinks; + switch (usageKey) { + case thisMock.usageKey: return thisMock.componentUsage; + default: return []; + } +} +mockComponentDownstreamLinks.usageKey = mockXBlockFields.usageKeyHtml; +mockComponentDownstreamLinks.componentUsage = [ + 'block-v1:org+course1+run+type@html+block@blockid1', + 'block-v1:org+course1+run+type@html+block@blockid2', + 'block-v1:org+course2+run+type@html+block@blockid1', +] satisfies Awaited>; +mockComponentDownstreamLinks.emptyUsageKey = 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1'; +mockComponentDownstreamLinks.emptyComponentUsage = [] as string[]; + +mockComponentDownstreamLinks.applyMock = () => jest.spyOn( + api, + 'getComponentDownstreamLinks', +).mockImplementation(mockComponentDownstreamLinks); diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 6432aaafd3..2f4251d283 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -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-links`; export interface ContentLibrary { id: string; @@ -533,3 +541,11 @@ export async function updateComponentCollections(usageKey: string, collectionKey collection_keys: collectionKeys, }); } + +/** + * Fetch downstream links for a component. + */ +export async function getComponentDownstreamLinks(usageKey: string): Promise { + const { data } = await getAuthenticatedHttpClient().get(getComponentDownstreamContextsApiUrl(usageKey)); + return data; +} diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 46cd148925..a707e4681b 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -45,6 +45,7 @@ import { publishXBlock, deleteXBlockAsset, restoreLibraryBlock, + getComponentDownstreamLinks, } from './api'; import { VersionSpec } from '../LibraryBlock'; @@ -99,6 +100,7 @@ export const xblockQueryKeys = { /** assets (static files) */ xblockAssets: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'assets'], componentMetadata: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'componentMetadata'], + componentDownstreamLinks: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'downstreamLinks'], }; /** @@ -542,3 +544,14 @@ export const useUpdateComponentCollections = (libraryId: string, usageKey: strin }, }); }; + +/** + * Get the downstream links of a component in a library + */ +export const useComponentDownstreamLinks = (usageKey: string) => ( + useQuery({ + queryKey: xblockQueryKeys.componentDownstreamLinks(usageKey), + queryFn: () => getComponentDownstreamLinks(usageKey), + enabled: !!usageKey, + }) +); diff --git a/src/search-manager/data/__mocks__/downstream-links.json b/src/search-manager/data/__mocks__/downstream-links.json new file mode 100644 index 0000000000..39d6834661 --- /dev/null +++ b/src/search-manager/data/__mocks__/downstream-links.json @@ -0,0 +1,84 @@ +{ + "comment": "This is a mock of the response from Meilisearch for downstream links", + "estimatedTotalHits": 3, + "query": "", + "limit": 3, + "offset": 0, + "processingTimeMs": 1, + "hits": [ + { + "usageKey": "block-v1:org+course1+run+type@html+block@blockid1", + "contextKey": "course-v1:org+course1+run", + "breadcrumbs": [ + { + "display_name": "Course 1", + "url": "/course/course-v1:org+course1+run" + }, + { + "usage_key": "unit-v1:org+course1+run+section1", + "display_name": "Section 1" + }, + { + "usage_key": "unit-v1:org+course1+run+subsection1", + "display_name": "Sub Section 1" + }, + { + "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId3", + "display_name": "Unit 1", + "url": "/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId1" + } + ] + }, + { + "usage_key": "block-v1:org+course1+run+type@html+block@blockid2", + "contextKey": "course-v1:org+course1+run", + "breadcrumbs": [ + { + "display_name": "Course 1", + "url": "/course/course-v1:org+course1+run" + }, + { + "usage_key": "unit-v1:org+course1+run+section1", + "display_name": "Section 1" + }, + { + "usage_key": "unit-v1:org+course1+run+subsection1", + "display_name": "Sub Section 1" + }, + { + "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId2", + "display_name": "Unit 2", + "url": "/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId2" + } + ] + }, + { + "usage_key": "block-v1:org+course2+run+type@html+block@blockid1", + "contextKey": "course-v1:org+course2+run", + "breadcrumbs": [ + { + "display_name": "Course 2", + "url": "/course/course-v1:org+course2+run" + }, + { + "usage_key": "unit-v1:org+course2+run+section1", + "display_name": "Section 1" + }, + { + "usage_key": "unit-v1:org+course2+run+subsection1", + "display_name": "Sub Section 1" + }, + { + "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId3", + "display_name": "Unit 3", + "url": "/course/course-v1:org+course2+run/container/block-v1:org+course1+run+type@vertical+block@verticalId2" + }, + { + "usage_key": "block-v1:org+course2+run+type@itembank+block@itembankId3", + "display_name": "Problem Bank 3", + "url": "/course/course-v1:org+course2+run/container/block-v1:org+course2+run+type@itembank+block@itembankId3" + } + ] + } + ] +} diff --git a/src/search-manager/data/api.mock.ts b/src/search-manager/data/api.mock.ts index e4673dd01b..54f4671851 100644 --- a/src/search-manager/data/api.mock.ts +++ b/src/search-manager/data/api.mock.ts @@ -2,6 +2,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import fetchMock from 'fetch-mock-jest'; import type { MultiSearchResponse } from 'meilisearch'; +import mockLinksResult from './__mocks__/downstream-links.json'; import * as api from './api'; /** @@ -14,7 +15,8 @@ export async function mockContentSearchConfig(): ReturnType ( jest.spyOn(api, 'getContentSearchConfig').mockImplementation(mockContentSearchConfig) ); @@ -26,7 +28,7 @@ mockContentSearchConfig.applyMock = () => ( * a different mock response, or you call `fetchMock.mockReset()` */ export function mockSearchResult(mockResponse: MultiSearchResponse) { - fetchMock.post(mockContentSearchConfig.searchEndpointUrl, (_url, req) => { + fetchMock.post(mockContentSearchConfig.multisearchEndpointUrl, (_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const query = requestData?.queries[0]?.q ?? ''; // We have to replace the query (search keywords) in the mock results with the actual query, @@ -64,3 +66,15 @@ export async function mockGetBlockTypes( jest.spyOn(api, 'fetchBlockTypes').mockResolvedValue(mockResponseMap[mockResponse]); } mockGetBlockTypes.applyMock = () => jest.spyOn(api, 'fetchBlockTypes').mockResolvedValue({}); + +export async function mockFetchIndexDocuments() { + return mockLinksResult; +} + +mockFetchIndexDocuments.applyMock = () => { + fetchMock.post( + mockContentSearchConfig.searchEndpointUrl, + mockFetchIndexDocuments, + { overwriteRoutes: true }, + ); +}; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 6d112eefc8..c3b181a4c2 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -133,7 +133,10 @@ export interface ContentHit extends BaseContentHit { * - After that is the name and usage key of any parent Section/Subsection/Unit/etc. */ type: 'course_block' | 'library_block'; - breadcrumbs: [{ displayName: string }, ...Array<{ displayName: string, usageKey: string }>]; + breadcrumbs: [ + { displayName: string, url?: string }, + ...Array<{ displayName: string, usageKey: string, url?: string }>, + ]; description?: string; content?: ContentDetails; lastPublished: number | null; diff --git a/src/search-manager/data/apiHooks.test.tsx b/src/search-manager/data/apiHooks.test.tsx index b6fc63f49a..e21c581bf6 100644 --- a/src/search-manager/data/apiHooks.test.tsx +++ b/src/search-manager/data/apiHooks.test.tsx @@ -27,7 +27,7 @@ const wrapper = ({ children }) => ( const fetchMockResponse = () => { fetchMock.post( - mockContentSearchConfig.searchEndpointUrl, + mockContentSearchConfig.multisearchEndpointUrl, () => mockResult, { overwriteRoutes: true }, ); diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index 3a13971b88..857d59f41f 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -260,16 +260,29 @@ export const useGetBlockTypes = (extraFilters: Filter) => { }); }; -export const useFetchIndexDocuments = ( - filter: Filter, - limit: number, - attributesToRetrieve?: string[], - attributesToCrop?: string[], - sort?: SearchSortOption[], -) => { +interface UseFetchIndexDocumentsParams { + filter: Filter; + limit: number; + attributesToRetrieve?: string[]; + attributesToCrop?: string[]; + sort?: SearchSortOption[]; + enabled?: boolean; +} + +/** + * Fetch documents from the index. + */ +export const useFetchIndexDocuments = ({ + filter, + limit, + attributesToRetrieve, + attributesToCrop, + sort, + enabled = true, +} : UseFetchIndexDocumentsParams) => { const { client, indexName } = useContentSearchConnection(); return useQuery({ - enabled: client !== undefined && indexName !== undefined, + enabled: enabled && client !== undefined && indexName !== undefined, queryKey: [ 'content_search', client?.config.apiKey, @@ -278,7 +291,7 @@ export const useFetchIndexDocuments = ( filter, 'generic-one-off', ], - queryFn: () => fetchIndexDocuments( + queryFn: enabled ? () => fetchIndexDocuments( client!, indexName!, filter, @@ -286,6 +299,6 @@ export const useFetchIndexDocuments = ( attributesToRetrieve, attributesToCrop, sort, - ), + ) : undefined, }); }; diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index 65e515d847..7cf40620d3 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -9,7 +9,7 @@ export { default as SearchKeywordsField } from './SearchKeywordsField'; export { default as SearchSortWidget } from './SearchSortWidget'; export { default as Stats } from './Stats'; export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; -export { useGetBlockTypes } from './data/apiHooks'; +export { useFetchIndexDocuments, useGetBlockTypes } from './data/apiHooks'; export { TypesFilterData } from './hooks'; export type { CollectionHit, ContentHit, ContentHitTags } from './data/api'; diff --git a/src/search-modal/__mocks__/downstream-links.json b/src/search-modal/__mocks__/downstream-links.json new file mode 100644 index 0000000000..e69de29bb2 From 586c3d80eea67bfb66ab71329fdb7d900c8ffd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 21 Feb 2025 16:13:23 -0300 Subject: [PATCH 2/5] refactor: construct container url on frontend --- .../component-info/ComponentUsage.tsx | 14 ++++++------- .../data/__mocks__/downstream-links.json | 21 +++++++------------ src/search-manager/data/api.ts | 4 ++-- .../__mocks__/downstream-links.json | 0 4 files changed, 16 insertions(+), 23 deletions(-) delete mode 100644 src/search-modal/__mocks__/downstream-links.json diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index b7a3e93297..7258b12065 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -78,13 +78,13 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { { componentUsageList.map((context) => ( - {context.links.map(({ usageKey: downstreamUsageKey, url, displayName }) => ( - url ? ( - {displayName} - ) : ( - // istanbul ignore next: this should never happen - {displayName} - ) + {context.links.map(({ usageKey: downstreamUsageKey, displayName }) => ( + + {displayName} + ))} )) diff --git a/src/search-manager/data/__mocks__/downstream-links.json b/src/search-manager/data/__mocks__/downstream-links.json index 39d6834661..1355f7a4c3 100644 --- a/src/search-manager/data/__mocks__/downstream-links.json +++ b/src/search-manager/data/__mocks__/downstream-links.json @@ -11,8 +11,7 @@ "contextKey": "course-v1:org+course1+run", "breadcrumbs": [ { - "display_name": "Course 1", - "url": "/course/course-v1:org+course1+run" + "display_name": "Course 1" }, { "usage_key": "unit-v1:org+course1+run+section1", @@ -24,8 +23,7 @@ }, { "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId3", - "display_name": "Unit 1", - "url": "/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId1" + "display_name": "Unit 1" } ] }, @@ -34,8 +32,7 @@ "contextKey": "course-v1:org+course1+run", "breadcrumbs": [ { - "display_name": "Course 1", - "url": "/course/course-v1:org+course1+run" + "display_name": "Course 1" }, { "usage_key": "unit-v1:org+course1+run+section1", @@ -47,8 +44,7 @@ }, { "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId2", - "display_name": "Unit 2", - "url": "/course/course-v1:org+course1+run/container/block-v1:org+course1+run+type@vertical+block@verticalId2" + "display_name": "Unit 2" } ] }, @@ -57,8 +53,7 @@ "contextKey": "course-v1:org+course2+run", "breadcrumbs": [ { - "display_name": "Course 2", - "url": "/course/course-v1:org+course2+run" + "display_name": "Course 2" }, { "usage_key": "unit-v1:org+course2+run+section1", @@ -70,13 +65,11 @@ }, { "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId3", - "display_name": "Unit 3", - "url": "/course/course-v1:org+course2+run/container/block-v1:org+course1+run+type@vertical+block@verticalId2" + "display_name": "Unit 3" }, { "usage_key": "block-v1:org+course2+run+type@itembank+block@itembankId3", - "display_name": "Problem Bank 3", - "url": "/course/course-v1:org+course2+run/container/block-v1:org+course2+run+type@itembank+block@itembankId3" + "display_name": "Problem Bank 3" } ] } diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index c3b181a4c2..4a7fe02d01 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -134,8 +134,8 @@ export interface ContentHit extends BaseContentHit { */ type: 'course_block' | 'library_block'; breadcrumbs: [ - { displayName: string, url?: string }, - ...Array<{ displayName: string, usageKey: string, url?: string }>, + { displayName: string }, + ...Array<{ displayName: string, usageKey: string }>, ]; description?: string; content?: ContentDetails; diff --git a/src/search-modal/__mocks__/downstream-links.json b/src/search-modal/__mocks__/downstream-links.json deleted file mode 100644 index e69de29bb2..0000000000 From a3d86b40c43492dd849cf18a2a01e52c58d20d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 21 Feb 2025 17:47:10 -0300 Subject: [PATCH 3/5] fix: stuck loading issue Co-authored-by: Navin Karkera --- src/library-authoring/component-info/ComponentUsage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index 7258b12065..8e92351c1b 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -44,7 +44,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { return ; } - if (isLoadingDownstreamLinks || isLoadingIndexDocuments) { + if (isLoadingDownstreamLinks || (isLoadingIndexDocuments && !!downstreamKeys.length)) { return ; } From f462f23ed94c6ba706a08247bc51565a45bc1612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 21 Feb 2025 17:49:50 -0300 Subject: [PATCH 4/5] fix: use correct url --- .../component-info/ComponentUsage.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index 8e92351c1b..01f07f03f0 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import AlertError from '../../generic/alert-error'; import Loading from '../../generic/Loading'; -import { useFetchIndexDocuments, type ContentHit } from '../../search-manager'; +import { useFetchIndexDocuments } from '../../search-manager'; import { useComponentDownstreamLinks } from '../data/apiHooks'; import messages from './messages'; @@ -15,7 +15,11 @@ interface ComponentUsageProps { type ComponentUsageTree = Record; export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { @@ -60,12 +64,18 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { } if (hit.contextKey in acc) { - acc[hit.contextKey].links.push(link); + acc[hit.contextKey].links.push({ + ...link, + url: `/course/${hit.contextKey}/container/${link.usageKey}`, + }); } else { acc[hit.contextKey] = { key: hit.contextKey, contextName: hit.breadcrumbs[0].displayName, - links: [link], + links: [{ + ...link, + url: `/course/${hit.contextKey}/container/${link.usageKey}`, + }], }; } return acc; @@ -78,11 +88,8 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { { componentUsageList.map((context) => ( - {context.links.map(({ usageKey: downstreamUsageKey, displayName }) => ( - + {context.links.map(({ usageKey: downstreamUsageKey, displayName, url }) => ( + {displayName} ))} From 42a4949d4d6bd00c4d10317d37fea9dfb18c3b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 21 Feb 2025 17:56:16 -0300 Subject: [PATCH 5/5] fix: open in new tab --- .../component-info/ComponentUsage.tsx | 29 ++++++++++--------- .../data/__mocks__/downstream-links.json | 2 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index 01f07f03f0..eef71f49de 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -1,6 +1,6 @@ +import { getConfig, getPath } from '@edx/frontend-platform'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { Collapsible } from '@openedx/paragon'; -import { Link } from 'react-router-dom'; +import { Collapsible, Hyperlink } from '@openedx/paragon'; import AlertError from '../../generic/alert-error'; import Loading from '../../generic/Loading'; @@ -22,6 +22,10 @@ type ComponentUsageTree = Record; +const getContainerUrl = (contextKey: string, usageKey: string) => ( + `${getPath(getConfig().PUBLIC_PATH)}course/${contextKey}/container/${usageKey}` +); + export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { const { data: dataDownstreamLinks, @@ -52,7 +56,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { return ; } - if (!downstreamKeys.length) { + if (!downstreamKeys.length || !downstreamHits) { return ; } @@ -63,19 +67,18 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { return acc; } + const linkData = { + ...link, + url: getContainerUrl(hit.contextKey, link.usageKey), + }; + if (hit.contextKey in acc) { - acc[hit.contextKey].links.push({ - ...link, - url: `/course/${hit.contextKey}/container/${link.usageKey}`, - }); + acc[hit.contextKey].links.push(linkData); } else { acc[hit.contextKey] = { key: hit.contextKey, contextName: hit.breadcrumbs[0].displayName, - links: [{ - ...link, - url: `/course/${hit.contextKey}/container/${link.usageKey}`, - }], + links: [linkData], }; } return acc; @@ -89,9 +92,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { componentUsageList.map((context) => ( {context.links.map(({ usageKey: downstreamUsageKey, displayName, url }) => ( - - {displayName} - + {displayName} ))} )) diff --git a/src/search-manager/data/__mocks__/downstream-links.json b/src/search-manager/data/__mocks__/downstream-links.json index 1355f7a4c3..20f4b14f11 100644 --- a/src/search-manager/data/__mocks__/downstream-links.json +++ b/src/search-manager/data/__mocks__/downstream-links.json @@ -22,7 +22,7 @@ "display_name": "Sub Section 1" }, { - "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId3", + "usage_key": "block-v1:org+course1+run+type@vertical+block@verticalId1", "display_name": "Unit 1" } ]