diff --git a/src/course-outline/CourseOutline.scss b/src/course-outline/CourseOutline.scss
index 19bdb37c40..316de58688 100644
--- a/src/course-outline/CourseOutline.scss
+++ b/src/course-outline/CourseOutline.scss
@@ -9,18 +9,3 @@
@import "./publish-modal/PublishModal";
@import "./drag-helper/SortableItem";
@import "./xblock-status/XBlockStatus";
-
-div.row:has(> div > div.highlight) {
- animation: 5s glow;
- animation-timing-function: cubic-bezier(1, 0, .72, .04);
-}
-
-@keyframes glow {
- 0% {
- box-shadow: 0 0 5px 5px $primary-500;
- }
-
- 100% {
- box-shadow: unset;
- }
-}
diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx
index 46ba76e32a..c84f6d776c 100644
--- a/src/course-unit/course-xblock/CourseXBlock.jsx
+++ b/src/course-unit/course-xblock/CourseXBlock.jsx
@@ -1,12 +1,12 @@
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
import {
ActionRow, Card, Dropdown, Icon, IconButton, useToggle,
} from '@openedx/paragon';
import { EditOutline as EditIcon, MoreVert as MoveVertIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
+import { useNavigate, useSearchParams } from 'react-router-dom';
import { getCanEdit, getCourseId } from 'CourseAuthoring/course-unit/data/selectors';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
@@ -31,6 +31,10 @@ const CourseXBlock = ({
const courseId = useSelector(getCourseId);
const intl = useIntl();
+ const [searchParams] = useSearchParams();
+ const locatorId = searchParams.get('show');
+ const isScrolledToElement = locatorId === id;
+
const visibilityMessage = userPartitionInfo.selectedGroupsLabel
? intl.formatMessage(messages.visibilityMessage, { selectedGroupsLabel: userPartitionInfo.selectedGroupsLabel })
: null;
@@ -64,13 +68,17 @@ const CourseXBlock = ({
useEffect(() => {
// if this item has been newly added, scroll to it.
- if (courseXBlockElementRef.current && shouldScroll) {
+ if (courseXBlockElementRef.current && (shouldScroll || isScrolledToElement)) {
scrollToElement(courseXBlockElementRef.current);
}
- }, []);
+ }, [isScrolledToElement]);
return (
-
+
{
const dispatch = useDispatch();
+ const [searchParams] = useSearchParams();
const [isErrorAlert, toggleErrorAlert] = useState(false);
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
@@ -84,7 +85,16 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const handleNavigate = (id) => {
if (sequenceId) {
- navigate(`/course/${courseId}/container/${blockId}/${id}`, { replace: true });
+ const path = `/course/${courseId}/container/${blockId}/${id}`;
+ const options = { replace: true };
+ if (searchParams.size) {
+ navigate({
+ pathname: path,
+ search: `?${searchParams}`,
+ }, options);
+ } else {
+ navigate(path, options);
+ }
}
};
diff --git a/src/custom.d.ts b/src/custom.d.ts
index cdb2b1a9a2..2b94311838 100644
--- a/src/custom.d.ts
+++ b/src/custom.d.ts
@@ -2,3 +2,8 @@ declare module '*.svg' {
const content: string;
export default content;
}
+
+declare module '*.json' {
+ const value: any;
+ export default value;
+}
diff --git a/src/index.scss b/src/index.scss
index bab24de4c9..41984ac61d 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -28,3 +28,25 @@
@import "search-modal/SearchModal";
@import "certificates/scss/Certificates";
@import "group-configurations/GroupConfigurations";
+
+// To apply the glow effect to the selected Section/Subsection, in the Course Outline
+div.row:has(> div > div.highlight) {
+ animation: 5s glow;
+ animation-timing-function: cubic-bezier(1, 0, .72, .04);
+}
+
+// To apply the glow effect to the selected xblock, in the Unit Outline
+div.xblock-highlight {
+ animation: 5s glow;
+ animation-timing-function: cubic-bezier(1, 0, .72, .04);
+}
+
+@keyframes glow {
+ 0% {
+ box-shadow: 0 0 5px 5px $primary-500;
+ }
+
+ 100% {
+ box-shadow: unset;
+ }
+}
diff --git a/src/search-modal/SearchResult.jsx b/src/search-modal/SearchResult.jsx
index c15371d81b..7fb4cb2599 100644
--- a/src/search-modal/SearchResult.jsx
+++ b/src/search-modal/SearchResult.jsx
@@ -33,6 +33,81 @@ function getItemIcon(blockType) {
return STRUCTURAL_TYPE_ICONS[blockType] ?? COMPONENT_TYPE_ICON_MAP[blockType] ?? Article;
}
+/**
+ * Returns the URL Suffix for library/library component hit
+ * @param {import('./data/api').ContentHit} hit
+ * @param {string} libraryAuthoringMfeUrl
+ * @returns string
+*/
+function getLibraryHitUrl(hit, libraryAuthoringMfeUrl) {
+ const { contextKey } = hit;
+ return `${libraryAuthoringMfeUrl}/library/${contextKey}`;
+}
+
+/**
+ * Returns the URL Suffix for a unit hit
+ * @param {import('./data/api').ContentHit} hit
+ * @returns string
+*/
+function getUnitUrlSuffix(hit) {
+ const { contextKey, usageKey } = hit;
+ return `course/${contextKey}/container/${usageKey}`;
+}
+
+/**
+ * Returns the URL Suffix for a unit component hit
+ * @param {import('./data/api').ContentHit} hit
+ * @returns string
+*/
+function getUnitComponentUrlSuffix(hit) {
+ const { breadcrumbs, contextKey, usageKey } = hit;
+ if (breadcrumbs.length > 1) {
+ const parent = breadcrumbs[breadcrumbs.length - 1];
+
+ if ('usageKey' in parent) {
+ return `course/${contextKey}/container/${parent.usageKey}?show=${encodeURIComponent(usageKey)}`;
+ }
+ }
+
+ // istanbul ignore next - This case should never be reached
+ return `course/${contextKey}`;
+}
+
+/**
+ * Returns the URL Suffix for a course component hit
+ * @param {import('./data/api').ContentHit} hit
+ * @returns string
+*/
+function getCourseComponentUrlSuffix(hit) {
+ const { contextKey, usageKey } = hit;
+ return `course/${contextKey}?show=${encodeURIComponent(usageKey)}`;
+}
+
+/**
+ * Returns the URL Suffix for the search hit param
+ * @param {import('./data/api').ContentHit} hit
+ * @returns string
+*/
+function getUrlSuffix(hit) {
+ const { blockType, breadcrumbs } = hit;
+
+ // Check if is a unit
+ if (blockType === 'vertical') {
+ return getUnitUrlSuffix(hit);
+ }
+
+ // Check if the parent is a unit
+ if (breadcrumbs.length > 1) {
+ const parent = breadcrumbs[breadcrumbs.length - 1];
+
+ if ('usageKey' in parent && parent.usageKey.includes('type@vertical')) {
+ return getUnitComponentUrlSuffix(hit);
+ }
+ }
+
+ return getCourseComponentUrlSuffix(hit);
+}
+
/**
* A single search result (row), usually represents an XBlock/Component
* @type {React.FC<{hit: import('./data/api').ContentHit}>}
@@ -43,33 +118,34 @@ const SearchResult = ({ hit }) => {
const { closeSearchModal } = useSearchContext();
const { libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe } = useSelector(getStudioHomeData);
+ const { usageKey } = hit;
+
+ const noRedirectUrl = usageKey.startsWith('lb:') && !redirectToLibraryAuthoringMfe;
+
/**
* Returns the URL for the context of the hit
*/
const getContextUrl = React.useCallback((newWindow = false) => {
- const { contextKey, usageKey } = hit;
+ const { contextKey } = hit;
+
if (contextKey.startsWith('course-v1:')) {
- const courseSufix = `course/${contextKey}?show=${encodeURIComponent(usageKey)}`;
+ const urlSuffix = getUrlSuffix(hit);
+
if (newWindow) {
- return `${getPath(getConfig().PUBLIC_PATH)}${courseSufix}`;
+ return `${getPath(getConfig().PUBLIC_PATH)}${urlSuffix}`;
}
- return `/${courseSufix}`;
+ return `/${urlSuffix}`;
}
+
if (usageKey.startsWith('lb:')) {
if (redirectToLibraryAuthoringMfe) {
- return `${libraryAuthoringMfeUrl}library/${contextKey}`;
+ return getLibraryHitUrl(hit, libraryAuthoringMfeUrl);
}
}
- // No context URL for this hit
+ // No context URL for this hit (e.g. a library without library authoring mfe)
return undefined;
- }, [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe]);
-
- const redirectUrl = React.useMemo(() => getContextUrl(), [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe]);
- const newWindowUrl = React.useMemo(
- () => getContextUrl(true),
- [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe],
- );
+ }, [libraryAuthoringMfeUrl, redirectToLibraryAuthoringMfe, hit]);
/**
* Opens the context of the hit in a new window
@@ -78,6 +154,7 @@ const SearchResult = ({ hit }) => {
*/
const openContextInNewWindow = (e) => {
e.stopPropagation();
+ const newWindowUrl = getContextUrl(true);
/* istanbul ignore next */
if (!newWindowUrl) {
return;
@@ -90,8 +167,9 @@ const SearchResult = ({ hit }) => {
* @param {(React.MouseEvent | React.KeyboardEvent)} e
* @returns {void}
*/
- const navigateToContext = (e) => {
+ const navigateToContext = React.useCallback((e) => {
e.stopPropagation();
+ const redirectUrl = getContextUrl();
/* istanbul ignore next */
if (!redirectUrl) {
@@ -112,16 +190,16 @@ const SearchResult = ({ hit }) => {
navigate(redirectUrl);
closeSearchModal();
- };
+ }, [getContextUrl]);
return (
@@ -140,7 +218,7 @@ const SearchResult = ({ hit }) => {
diff --git a/src/search-modal/SearchUI.test.jsx b/src/search-modal/SearchUI.test.jsx
index 752bd7c584..0d5aeb697e 100644
--- a/src/search-modal/SearchUI.test.jsx
+++ b/src/search-modal/SearchUI.test.jsx
@@ -17,17 +17,15 @@ import {
import fetchMock from 'fetch-mock-jest';
import initializeStore from '../store';
-// @ts-ignore
+import { executeThunk } from '../utils';
+import { getStudioHomeApiUrl } from '../studio-home/data/api';
+import { fetchStudioHomeData } from '../studio-home/data/thunks';
+import { generateGetStudioHomeDataApiResponse } from '../studio-home/factories/mockApiResponses';
import mockResult from './__mocks__/search-result.json';
-// @ts-ignore
import mockEmptyResult from './__mocks__/empty-search-result.json';
-// @ts-ignore
import mockTagsFacetResult from './__mocks__/facet-search.json';
-// @ts-ignore
import mockTagsFacetResultLevel0 from './__mocks__/facet-search-level0.json';
-// @ts-ignore
import mockTagsFacetResultLevel1 from './__mocks__/facet-search-level1.json';
-// @ts-ignore
import mockTagsKeywordSearchResult from './__mocks__/tags-keyword-search.json';
import SearchUI from './SearchUI';
import { getContentSearchConfigUrl } from './data/api';
@@ -95,6 +93,7 @@ describe('', () => {
index_name: 'studio',
api_key: 'test-key',
});
+
// The Meilisearch client-side API uses fetch, not Axios.
fetchMock.post(searchEndpoint, (_url, req) => {
const requestData = JSON.parse(req.body?.toString() ?? '');
@@ -156,26 +155,10 @@ describe('', () => {
// Now we should see the results:
expect(queryByText('Enter a keyword')).toBeNull();
// The result:
- expect(getByText('2 results found')).toBeInTheDocument();
+ expect(getByText('6 results found')).toBeInTheDocument();
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
// Breadcrumbs showing where the result came from:
expect(getByText('TheCourse / Section 2 / Subsection 3 / The Little Unit That Could')).toBeInTheDocument();
-
- const resultItem = getByRole('button', { name: /The Little Unit That Could/ });
-
- // Clicking the "Open in new window" button should open the result in a new window:
- const { open } = window;
- window.open = jest.fn();
- fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
- expect(window.open).toHaveBeenCalledWith(
- '/course/course-v1:edx+TestCourse+24?show=block-v1%3Aedx%2BTestCourse%2B24%2Btype%40html%2Bblock%40test_html',
- '_blank',
- );
- window.open = open;
-
- // Clicking in the result should navigate to the result's URL:
- fireEvent.click(resultItem);
- expect(mockNavigate).toHaveBeenCalledWith('/course/course-v1:edx+TestCourse+24?show=block-v1%3Aedx%2BTestCourse%2B24%2Btype%40html%2Bblock%40test_html');
});
it('defaults to searching "This Course" if used in a course', async () => {
@@ -198,12 +181,171 @@ describe('', () => {
// Now we should see the results:
expect(queryByText('Enter a keyword')).toBeNull();
// The result:
- expect(getByText('2 results found')).toBeInTheDocument();
+ expect(getByText('6 results found')).toBeInTheDocument();
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
// Breadcrumbs showing where the result came from:
expect(getByText('TheCourse / Section 2 / Subsection 3 / The Little Unit That Could')).toBeInTheDocument();
});
+ describe('results', () => {
+ /** @type {import('@testing-library/react').RenderResult} */
+ let rendered;
+ beforeEach(async () => {
+ rendered = render();
+ const { getByRole } = rendered;
+ fireEvent.change(getByRole('searchbox'), { target: { value: 'giraffe' } });
+ });
+
+ test('click section result navigates to the context', async () => {
+ const { findAllByRole } = rendered;
+
+ const [resultItem] = await findAllByRole('button', { name: /Section 1/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40chapter%2Bblock%40c7077c8cafcf420dbc0b440bf27bad04',
+ '_blank',
+ );
+ window.open = open;
+
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40chapter%2Bblock%40c7077c8cafcf420dbc0b440bf27bad04',
+ );
+ });
+
+ test('click subsection result navigates to the context', async () => {
+ const { findAllByRole } = rendered;
+
+ const [resultItem] = await findAllByRole('button', { name: /Subsection 1.1/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40sequential%2Bblock%4092e3e9ca156c44fa8a735f0e9e7c854f',
+ '_blank',
+ );
+ window.open = open;
+
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40sequential%2Bblock%4092e3e9ca156c44fa8a735f0e9e7c854f',
+ );
+ });
+
+ test('click unit result navigates to the context', async () => {
+ const { findAllByRole } = rendered;
+
+ const [resultItem] = await findAllByRole('button', { name: /Unit 1.1.1/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b',
+ '_blank',
+ );
+ window.open = open;
+
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b',
+ );
+ });
+
+ test('click unit component result navigates to the context', async () => {
+ const { findAllByRole } = rendered;
+
+ const [resultItem] = await findAllByRole('button', { name: /Announcement/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40html%2Bblock%400b2d1c0722f742489602b6d8645205f4',
+ '_blank',
+ );
+ window.open = open;
+
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ '/course/course-v1:SampleTaxonomyOrg1+STC1+2023_1/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'
+ + '?show=block-v1%3ASampleTaxonomyOrg1%2BSTC1%2B2023_1%2Btype%40html%2Bblock%400b2d1c0722f742489602b6d8645205f4',
+ );
+ });
+
+ test('click lib component result navigates to the context', async () => {
+ const data = generateGetStudioHomeDataApiResponse();
+ data.redirectToLibraryAuthoringMfe = true;
+ axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
+
+ await executeThunk(fetchStudioHomeData(), store.dispatch);
+
+ const { findByRole } = rendered;
+
+ const resultItem = await findByRole('button', { name: /Library Content/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open, location } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).toHaveBeenCalledWith(
+ 'http://localhost:3001/library/lib:org1:libafter1',
+ '_blank',
+ );
+ window.open = open;
+
+ // @ts-ignore
+ window.location = { href: '' };
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(window.location.href = 'http://localhost:3001/library/lib:org1:libafter1');
+ window.location = location;
+ });
+
+ test('click lib component result doesnt navigates to the context withou libraryAuthoringMfe', async () => {
+ const data = generateGetStudioHomeDataApiResponse();
+ data.redirectToLibraryAuthoringMfe = false;
+ axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
+
+ await executeThunk(fetchStudioHomeData(), store.dispatch);
+
+ const { findByRole } = rendered;
+
+ const resultItem = await findByRole('button', { name: /Library Content/ });
+
+ // Clicking the "Open in new window" button should open the result in a new window:
+ const { open, location } = window;
+ window.open = jest.fn();
+ fireEvent.click(within(resultItem).getByRole('button', { name: 'Open in new window' }));
+ expect(window.open).not.toHaveBeenCalled();
+ window.open = open;
+
+ // @ts-ignore
+ window.location = { href: '' };
+ // Clicking in the result should navigate to the result's URL:
+ fireEvent.click(resultItem);
+ expect(window.location.href === location.href);
+ window.location = location;
+ });
+ });
+
describe('filters', () => {
/** @type {import('@testing-library/react').RenderResult} */
let rendered;
@@ -233,7 +375,7 @@ describe('', () => {
return (requestedFilter?.length === 1); // the filter is: 'context_key = "course-v1:org+test+123"'
});
// Now we should see the results:
- expect(getByText('2 results found')).toBeInTheDocument();
+ expect(getByText('6 results found')).toBeInTheDocument();
expect(getByText(mockResultDisplayName)).toBeInTheDocument();
});
diff --git a/src/search-modal/__mocks__/search-result.json b/src/search-modal/__mocks__/search-result.json
index ff4397a406..5371f3d5c7 100644
--- a/src/search-modal/__mocks__/search-result.json
+++ b/src/search-modal/__mocks__/search-result.json
@@ -18,9 +18,15 @@
"org": "edx",
"breadcrumbs": [
{ "display_name": "TheCourse" },
- { "display_name": "Section 2" },
- { "display_name": "Subsection 3" },
- { "display_name": "The Little Unit That Could" }
+ { "display_name": "Section 2", "usage_key": "block-v1:edx+TestCourse+24+type@chapter+block@chapter_2" },
+ {
+ "display_name": "Subsection 3",
+ "usage_key": "block-v1:edx+TestCourse+24+type@sequential+block@sequential_3"
+ },
+ {
+ "display_name": "The Little Unit That Could",
+ "usage_key": "block-v1:edx+TestCourse+24+type@vertical+block@vertical_3_1"
+ }
],
"tags": {
"taxonomy": [
@@ -52,7 +58,7 @@
}
},
{
- "display_name": "Text1",
+ "display_name": "Library Content",
"block_id": "a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d",
"content": {
"html_content": " Test "
@@ -68,13 +74,201 @@
}
],
"type": "library_block"
+ },
+ {
+ "display_name": "Section 1",
+ "block_id": "c7077c8cafcf420dbc0b440bf27bad04",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typechapterblockc7077c8cafcf420dbc0b440bf27bad04-2af9d1ac",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04",
+ "block_type": "chapter",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": 6,
+ "_formatted": {
+ "display_name": "Section 1",
+ "block_id": "c7077c8cafcf420dbc0b440bf27bad04",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typechapterblockc7077c8cafcf420dbc0b440bf27bad04-2af9d1ac",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04",
+ "block_type": "chapter",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": "6"
+ }
+ },
+ {
+ "display_name": "Subsection 1.1",
+ "block_id": "92e3e9ca156c44fa8a735f0e9e7c854f",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typesequentialblock92e3e9ca156c44fa8a735f0e9e7c854f-ec0fb128",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f",
+ "block_type": "sequential",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": 6,
+ "_formatted": {
+ "display_name": "Subsection 1.1",
+ "block_id": "92e3e9ca156c44fa8a735f0e9e7c854f",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typesequentialblock92e3e9ca156c44fa8a735f0e9e7c854f-ec0fb128",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f",
+ "block_type": "sequential",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": "6"
+ }
+ },
+ {
+ "display_name": "Unit 1.1.1",
+ "block_id": "aaf8b8eb86b54281aeeab12499d2cb0b",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typeverticalblockaaf8b8eb86b54281aeeab12499d2cb0b-afa27c6e",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ },
+ {
+ "display_name": "Subsection 1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b",
+ "block_type": "vertical",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": 6,
+ "_formatted": {
+ "display_name": "Unit 1.1.1",
+ "block_id": "aaf8b8eb86b54281aeeab12499d2cb0b",
+ "content": {},
+ "id": "block-v1sampletaxonomyorg1stc12023_1typeverticalblockaaf8b8eb86b54281aeeab12499d2cb0b-afa27c6e",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ },
+ {
+ "display_name": "Subsection 1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b",
+ "block_type": "vertical",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": "6"
+ }
+ },
+ {
+ "display_name": "Announcement",
+ "block_id": "0b2d1c0722f742489602b6d8645205f4",
+ "content": {
+ "html_content": "To use this template, replace the example text with your own text. When you add the component, be sure to select Settings to specify a Display Name and other values that apply. Announcement Date Short note that introduces the topic Instructor's name Heading for announcement 1 Announcement 1 text Heading for announcement 2 Announcement 2 text "
+ },
+ "id": "block-v1sampletaxonomyorg1stc12023_1typehtmlblock0b2d1c0722f742489602b6d8645205f4-2db56dce",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ },
+ {
+ "display_name": "Subsection 1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
+ },
+ {
+ "display_name": "Unit 1.1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@html+block@0b2d1c0722f742489602b6d8645205f4",
+ "block_type": "html",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": 6,
+ "_formatted": {
+ "display_name": "Announcement",
+ "block_id": "0b2d1c0722f742489602b6d8645205f4",
+ "content": {
+ "html_content": "To use this template, replace the example text with your own text. When you add the component, be sure to…"
+ },
+ "id": "block-v1sampletaxonomyorg1stc12023_1typehtmlblock0b2d1c0722f742489602b6d8645205f4-2db56dce",
+ "type": "course_block",
+ "breadcrumbs": [
+ {
+ "display_name": "Sample Taxonomy Course"
+ },
+ {
+ "display_name": "Section 1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@chapter+block@c7077c8cafcf420dbc0b440bf27bad04"
+ },
+ {
+ "display_name": "Subsection 1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@sequential+block@92e3e9ca156c44fa8a735f0e9e7c854f"
+ },
+ {
+ "display_name": "Unit 1.1.1",
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b"
+ }
+ ],
+ "usage_key": "block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@html+block@0b2d1c0722f742489602b6d8645205f4",
+ "block_type": "html",
+ "context_key": "course-v1:SampleTaxonomyOrg1+STC1+2023_1",
+ "org": "SampleTaxonomyOrg1",
+ "access_id": "6"
+ }
}
],
"query": "learn",
"processingTimeMs": 1,
- "limit": 2,
+ "limit": 6,
"offset": 0,
- "estimatedTotalHits": 2
+ "estimatedTotalHits": 6
},
{
"indexUid": "studio",
diff --git a/src/search-modal/data/api.js b/src/search-modal/data/api.js
index 126df5248f..e526546e69 100644
--- a/src/search-modal/data/api.js
+++ b/src/search-modal/data/api.js
@@ -83,8 +83,9 @@ function formatTagsFilter(tagsFilter) {
* @property {string} blockType The block_type part of the usage key. What type of XBlock this is.
* @property {string} contextKey The course or library ID
* @property {string} org
- * @property {{displayName: string}[]} breadcrumbs First one is the name of the course/library itself.
- * After that is the name of any parent Section/Subsection/Unit/etc.
+ * @property {[{displayName: string}, ...Array<{displayName: string, usageKey: string}>]} breadcrumbs
+ * First one is the name of the course/library itself.
+ * After that is the name and usage key of any parent Section/Subsection/Unit/etc.
* @property {Record<'taxonomy'|'level0'|'level1'|'level2'|'level3', string[]>} tags
* @property {ContentDetails} [content]
* @property {{displayName: string, content: ContentDetails}} formatted Same fields with ... highlights