feat: redirect to unit page if the hit or its parent is a unit (#957)
This change adds the feature to redirect from a search result to a Unit, in case the hit parent is a Unit, or the hit is a unit itself.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div ref={courseXBlockElementRef} {...props}>
|
||||
<div
|
||||
ref={courseXBlockElementRef}
|
||||
{...props}
|
||||
className={isScrolledToElement ? 'xblock-highlight' : undefined}
|
||||
>
|
||||
<Card className="mb-1">
|
||||
<Card.Header
|
||||
title={title}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import {
|
||||
@@ -31,6 +31,7 @@ import { useCopyToClipboard } from '../generic/clipboard';
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
5
src/custom.d.ts
vendored
5
src/custom.d.ts
vendored
@@ -2,3 +2,8 @@ declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Stack
|
||||
className={`border-bottom search-result p-2 align-items-start ${!redirectUrl ? 'text-muted' : ''}`}
|
||||
className={`border-bottom search-result p-2 align-items-start ${noRedirectUrl ? 'text-muted' : ''}`}
|
||||
direction="horizontal"
|
||||
gap={3}
|
||||
onClick={navigateToContext}
|
||||
onKeyDown={navigateToContext}
|
||||
tabIndex={redirectUrl ? 0 : undefined}
|
||||
tabIndex={noRedirectUrl ? undefined : 0}
|
||||
role="button"
|
||||
>
|
||||
<Icon className="text-muted" src={getItemIcon(hit.blockType)} />
|
||||
@@ -140,7 +218,7 @@ const SearchResult = ({ hit }) => {
|
||||
<IconButton
|
||||
src={OpenInNew}
|
||||
iconAs={Icon}
|
||||
disabled={!newWindowUrl}
|
||||
disabled={noRedirectUrl ? true : undefined}
|
||||
onClick={openContextInNewWindow}
|
||||
alt={intl.formatMessage(messages.openInNewWindow)}
|
||||
/>
|
||||
|
||||
@@ -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('<SearchUI />', () => {
|
||||
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('<SearchUI />', () => {
|
||||
// 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('<SearchUI />', () => {
|
||||
// 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(<Wrap><SearchUI {...defaults} /></Wrap>);
|
||||
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('<SearchUI />', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <mark>...</mark> highlights
|
||||
|
||||
Reference in New Issue
Block a user