feat: simplify Library Home Page (v2) (#1443)
This commit is contained in:
committed by
GitHub
parent
d99e3f0f62
commit
979c69b48e
@@ -42,40 +42,12 @@ const returnEmptyResult = (_url, req) => {
|
||||
// We have to replace the query (search keywords) in the mock results with the actual query,
|
||||
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
|
||||
mockEmptyResult.results[0].query = query;
|
||||
mockEmptyResult.results[2].query = query;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockEmptyResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return mockEmptyResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns 2 components from the search query.
|
||||
* This lets us test that the StudioHome "View All" button is hidden when a
|
||||
* low number of search results are shown (<=4 by default).
|
||||
*/
|
||||
const returnLowNumberResults = (_url, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const query = requestData?.queries[0]?.q ?? '';
|
||||
const newMockResult = { ...mockResult };
|
||||
// We have to replace the query (search keywords) in the mock results with the actual query,
|
||||
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
|
||||
newMockResult.results[0].query = query;
|
||||
// Limit number of results to just 2
|
||||
newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2);
|
||||
newMockResult.results[2].hits = mockResult.results[2]?.hits.slice(0, 2);
|
||||
newMockResult.results[0].estimatedTotalHits = 2;
|
||||
newMockResult.results[2].estimatedTotalHits = 2;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
newMockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return newMockResult;
|
||||
};
|
||||
|
||||
const path = '/library/:libraryId/*';
|
||||
const libraryTitle = mockContentLibrary.libraryData.title;
|
||||
|
||||
@@ -136,35 +108,25 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
|
||||
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
|
||||
|
||||
// "Recently Modified" header + sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
|
||||
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Components (10)')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();
|
||||
|
||||
// Navigate to the components tab
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
|
||||
// "Recently Modified" default sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
|
||||
|
||||
// Navigate to the collections tab
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
|
||||
// "Recently Modified" default sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('There are 10 components in this library')).not.toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument();
|
||||
|
||||
// Go back to Home tab
|
||||
// This step is necessary to avoid the url change leak to other tests
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
|
||||
// "Recently Modified" header + sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
|
||||
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Components (10)')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('shows a library without components and collections', async () => {
|
||||
@@ -188,7 +150,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
fireEvent.click(cancelButton);
|
||||
expect(collectionModalHeading).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
|
||||
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
|
||||
|
||||
const addComponentButton = screen.getByRole('button', { name: /add component/i });
|
||||
@@ -246,7 +208,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
|
||||
// Go back to Home tab
|
||||
// This step is necessary to avoid the url change leak to other tests
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
|
||||
});
|
||||
|
||||
it('should open and close new content sidebar', async () => {
|
||||
@@ -328,68 +290,6 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(manageAccess).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show the "View All" button when viewing library with many components', async () => {
|
||||
await renderLibraryPage();
|
||||
|
||||
expect(screen.getByText('Content library')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
|
||||
// "Recently Modified" header + sort shown
|
||||
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
|
||||
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Components (10)')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
|
||||
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
|
||||
|
||||
// There should be two "View All" button, since the Components and Collections count
|
||||
// are above the preview limit (4)
|
||||
expect(screen.getAllByText('View All').length).toEqual(2);
|
||||
|
||||
// Clicking on first "View All" button should navigate to the Collections tab
|
||||
fireEvent.click(screen.getAllByText('View All')[0]);
|
||||
// "Recently Modified" default sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Collection 1')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
|
||||
// Clicking on second "View All" button should navigate to the Components tab
|
||||
fireEvent.click(screen.getAllByText('View All')[1]);
|
||||
// "Recently Modified" default sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
|
||||
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
|
||||
|
||||
// Go back to Home tab
|
||||
// This step is necessary to avoid the url change leak to other tests
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
|
||||
// "Recently Modified" header + sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
|
||||
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Components (10)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the "View All" button when viewing library with low number of components', async () => {
|
||||
fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true });
|
||||
await renderLibraryPage();
|
||||
|
||||
expect(screen.getByText('Content library')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
|
||||
// "Recently Modified" header + sort shown
|
||||
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
|
||||
expect(screen.getByText('Collections (2)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Components (2)')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
|
||||
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();
|
||||
|
||||
// There should not be any "View All" button on page since Components count
|
||||
// is less than the preview limit (4)
|
||||
expect(screen.queryByText('View All')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sorts library components', async () => {
|
||||
await renderLibraryPage();
|
||||
|
||||
@@ -444,7 +344,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
|
||||
// Re-selecting the previous sort option resets sort to default "Recently Modified"
|
||||
await testSortOption('Recently Published', 'modified:desc', true);
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(3);
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
|
||||
|
||||
// Enter a keyword into the search box
|
||||
const searchBox = screen.getByRole('searchbox');
|
||||
@@ -467,7 +367,6 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(mockResult0.display_name).toStrictEqual(displayName);
|
||||
await renderLibraryPage();
|
||||
|
||||
// Click on the first component. It should appear twice, in both "Recently Modified" and "Components"
|
||||
fireEvent.click((await screen.findAllByText(displayName))[0]);
|
||||
|
||||
const sidebar = screen.getByTestId('library-sidebar');
|
||||
@@ -579,7 +478,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
}
|
||||
|
||||
// Validate click on Problem type
|
||||
const problemMenu = screen.getByText('Problem');
|
||||
const problemMenu = screen.getAllByText('Problem')[0];
|
||||
expect(problemMenu).toBeInTheDocument();
|
||||
fireEvent.click(problemMenu);
|
||||
await waitFor(() => {
|
||||
@@ -647,7 +546,8 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(screen.getByText(/add content/i)).toBeInTheDocument();
|
||||
|
||||
// Open New collection Modal
|
||||
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
|
||||
const sidebar = screen.getByTestId('library-sidebar');
|
||||
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
|
||||
fireEvent.click(newCollectionButton);
|
||||
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
|
||||
expect(collectionModalHeading).toBeInTheDocument();
|
||||
@@ -691,7 +591,8 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(screen.getByText(/add content/i)).toBeInTheDocument();
|
||||
|
||||
// Open New collection Modal
|
||||
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
|
||||
const sidebar = screen.getByTestId('library-sidebar');
|
||||
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
|
||||
fireEvent.click(newCollectionButton);
|
||||
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
|
||||
expect(collectionModalHeading).toBeInTheDocument();
|
||||
@@ -724,7 +625,8 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
expect(screen.getByText(/add content/i)).toBeInTheDocument();
|
||||
|
||||
// Open New collection Modal
|
||||
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
|
||||
const sidebar = screen.getByTestId('library-sidebar');
|
||||
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
|
||||
fireEvent.click(newCollectionButton);
|
||||
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
|
||||
expect(collectionModalHeading).toBeInTheDocument();
|
||||
@@ -739,22 +641,6 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
fireEvent.click(createButton);
|
||||
});
|
||||
|
||||
it('shows both components and collections in recently modified section', async () => {
|
||||
await renderLibraryPage();
|
||||
|
||||
expect(await screen.findByText('Content library')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();
|
||||
|
||||
// "Recently Modified" header + sort shown
|
||||
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
|
||||
const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement;
|
||||
expect(recentModifiedContainer).toBeTruthy();
|
||||
|
||||
const container = within(recentModifiedContainer!);
|
||||
expect(container.queryAllByText('Text').length).toBeGreaterThan(0);
|
||||
expect(container.queryAllByText('Collection').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows a single block when usageKey query param is set', async () => {
|
||||
render(<LibraryLayout />, {
|
||||
path,
|
||||
|
||||
@@ -35,35 +35,11 @@ import {
|
||||
SearchKeywordsField,
|
||||
SearchSortWidget,
|
||||
} from '../search-manager';
|
||||
import LibraryComponents from './components/LibraryComponents';
|
||||
import LibraryCollections from './collections/LibraryCollections';
|
||||
import LibraryHome from './LibraryHome';
|
||||
import LibraryContent, { ContentType } from './LibraryContent';
|
||||
import { LibrarySidebar } from './library-sidebar';
|
||||
import { SidebarBodyComponentId, useLibraryContext } from './common/context';
|
||||
import messages from './messages';
|
||||
|
||||
enum TabList {
|
||||
home = '',
|
||||
components = 'components',
|
||||
collections = 'collections',
|
||||
}
|
||||
|
||||
interface TabContentProps {
|
||||
eventKey: string;
|
||||
handleTabChange: (key: string) => void;
|
||||
}
|
||||
|
||||
const TabContent = ({ eventKey, handleTabChange }: TabContentProps) => {
|
||||
switch (eventKey) {
|
||||
case TabList.components:
|
||||
return <LibraryComponents variant="full" />;
|
||||
case TabList.collections:
|
||||
return <LibraryCollections variant="full" />;
|
||||
default:
|
||||
return <LibraryHome tabList={TabList} handleTabChange={handleTabChange} />;
|
||||
}
|
||||
};
|
||||
|
||||
const HeaderActions = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
@@ -162,15 +138,15 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
openInfoSidebar,
|
||||
} = useLibraryContext();
|
||||
|
||||
const [activeKey, setActiveKey] = useState<string | undefined>('');
|
||||
const [activeKey, setActiveKey] = useState<ContentType | undefined>(ContentType.home);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname.split('/').pop();
|
||||
|
||||
if (componentPickerMode || currentPath === libraryId || currentPath === '') {
|
||||
setActiveKey(TabList.home);
|
||||
} else if (currentPath && currentPath in TabList) {
|
||||
setActiveKey(TabList[currentPath]);
|
||||
setActiveKey(ContentType.home);
|
||||
} else if (currentPath && currentPath in ContentType) {
|
||||
setActiveKey(ContentType[currentPath]);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
@@ -203,7 +179,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
return <NotFoundAlert />;
|
||||
}
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
const handleTabChange = (key: ContentType) => {
|
||||
setActiveKey(key);
|
||||
if (!componentPickerMode) {
|
||||
navigate({
|
||||
@@ -235,6 +211,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
extraFilter.push('last_published IS NOT NULL');
|
||||
}
|
||||
|
||||
const activeTypeFilters = {
|
||||
components: 'NOT type = "collection"',
|
||||
collections: 'type = "collection"',
|
||||
};
|
||||
if (activeKey !== ContentType.home) {
|
||||
extraFilter.push(activeTypeFilters[activeKey]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1">
|
||||
@@ -275,11 +259,11 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
onSelect={handleTabChange}
|
||||
className="my-3"
|
||||
>
|
||||
<Tab eventKey={TabList.home} title={intl.formatMessage(messages.homeTab)} />
|
||||
<Tab eventKey={TabList.components} title={intl.formatMessage(messages.componentsTab)} />
|
||||
<Tab eventKey={TabList.collections} title={intl.formatMessage(messages.collectionsTab)} />
|
||||
<Tab eventKey={ContentType.home} title={intl.formatMessage(messages.homeTab)} />
|
||||
<Tab eventKey={ContentType.components} title={intl.formatMessage(messages.componentsTab)} />
|
||||
<Tab eventKey={ContentType.collections} title={intl.formatMessage(messages.collectionsTab)} />
|
||||
</Tabs>
|
||||
<TabContent eventKey={activeKey} handleTabChange={handleTabChange} />
|
||||
<LibraryContent contentType={activeKey} />
|
||||
</SearchContextProvider>
|
||||
</Container>
|
||||
{!componentPickerMode && <StudioFooter containerProps={{ size: undefined }} />}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
initializeMocks,
|
||||
} from '../../testUtils';
|
||||
import { getContentSearchConfigUrl } from '../../search-manager/data/api';
|
||||
import { mockContentLibrary } from '../data/api.mocks';
|
||||
import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import LibraryCollections from './LibraryCollections';
|
||||
} from '../testUtils';
|
||||
import { getContentSearchConfigUrl } from '../search-manager/data/api';
|
||||
import { mockContentLibrary } from './data/api.mocks';
|
||||
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
|
||||
import { LibraryProvider } from './common/context';
|
||||
import LibraryContent from './LibraryContent';
|
||||
import { libraryComponentsMock } from './__mocks__';
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
|
||||
@@ -18,8 +20,8 @@ const mockFetchNextPage = jest.fn();
|
||||
const mockUseSearchContext = jest.fn();
|
||||
|
||||
const data = {
|
||||
totalHits: 1,
|
||||
hits: [],
|
||||
totalContentAndCollectionHits: 0,
|
||||
contentAndCollectionHits: [],
|
||||
isFetchingNextPage: false,
|
||||
hasNextPage: false,
|
||||
fetchNextPage: mockFetchNextPage,
|
||||
@@ -40,8 +42,8 @@ const returnEmptyResult = (_url: string, req) => {
|
||||
return mockEmptyResult;
|
||||
};
|
||||
|
||||
jest.mock('../../search-manager', () => ({
|
||||
...jest.requireActual('../../search-manager'),
|
||||
jest.mock('../search-manager', () => ({
|
||||
...jest.requireActual('../search-manager'),
|
||||
useSearchContext: () => mockUseSearchContext(),
|
||||
}));
|
||||
|
||||
@@ -58,7 +60,7 @@ const withLibraryId = (libraryId: string) => ({
|
||||
),
|
||||
});
|
||||
|
||||
describe('<LibraryCollections />', () => {
|
||||
describe('<LibraryHome />', () => {
|
||||
beforeEach(() => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
|
||||
@@ -83,7 +85,31 @@ describe('<LibraryCollections />', () => {
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
render(<LibraryCollections variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
render(<LibraryContent />, withLibraryId(mockContentLibrary.libraryId));
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an empty state when there are no results', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
render(<LibraryContent />, withLibraryId(mockContentLibrary.libraryId));
|
||||
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should load more results when the user scrolls to the bottom', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
hits: libraryComponentsMock,
|
||||
hasNextPage: true,
|
||||
});
|
||||
render(<LibraryContent />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800 });
|
||||
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });
|
||||
|
||||
fireEvent.scroll(window, { target: { scrollY: 1000 } });
|
||||
expect(mockFetchNextPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
92
src/library-authoring/LibraryContent.tsx
Normal file
92
src/library-authoring/LibraryContent.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect } from 'react';
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import { useSearchContext } from '../search-manager';
|
||||
import { NoComponents, NoSearchResults } from './EmptyStates';
|
||||
import { useLibraryContext } from './common/context';
|
||||
import CollectionCard from './components/CollectionCard';
|
||||
import ComponentCard from './components/ComponentCard';
|
||||
import { useLoadOnScroll } from '../hooks';
|
||||
import messages from './collections/messages';
|
||||
|
||||
export enum ContentType {
|
||||
home = '',
|
||||
components = 'components',
|
||||
collections = 'collections',
|
||||
}
|
||||
|
||||
/**
|
||||
* Library Content to show content grid
|
||||
*
|
||||
* Use content to:
|
||||
* - 'collections': Suggest to create a collection on empty state.
|
||||
* - Anything else to suggest to add content on empty state.
|
||||
*/
|
||||
|
||||
type LibraryContentProps = {
|
||||
contentType?: ContentType;
|
||||
};
|
||||
|
||||
const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) => {
|
||||
const {
|
||||
hits,
|
||||
totalHits,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isLoading,
|
||||
isFiltered,
|
||||
usageKey,
|
||||
} = useSearchContext();
|
||||
const { openAddContentSidebar, openComponentInfoSidebar, openCreateCollectionModal } = useLibraryContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (usageKey) {
|
||||
openComponentInfoSidebar(usageKey);
|
||||
}
|
||||
}, [usageKey]);
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
true,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
if (totalHits === 0) {
|
||||
if (contentType === ContentType.collections) {
|
||||
return isFiltered
|
||||
? <NoSearchResults infoText={messages.noSearchResultsCollections} />
|
||||
: (
|
||||
<NoComponents
|
||||
infoText={messages.noCollections}
|
||||
addBtnText={messages.addCollection}
|
||||
handleBtnClick={openCreateCollectionModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return isFiltered ? <NoSearchResults /> : <NoComponents handleBtnClick={openAddContentSidebar} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="library-cards-grid">
|
||||
{hits.map((contentHit) => (
|
||||
contentHit.type === 'collection' ? (
|
||||
<CollectionCard
|
||||
key={contentHit.id}
|
||||
collectionHit={contentHit}
|
||||
/>
|
||||
) : (
|
||||
<ComponentCard
|
||||
key={contentHit.id}
|
||||
contentHit={contentHit}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryContent;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import { useSearchContext } from '../search-manager';
|
||||
import { NoComponents, NoSearchResults } from './EmptyStates';
|
||||
import LibraryCollections from './collections/LibraryCollections';
|
||||
import { LibraryComponents } from './components';
|
||||
import LibrarySection from './components/LibrarySection';
|
||||
import LibraryRecentlyModified from './LibraryRecentlyModified';
|
||||
import messages from './messages';
|
||||
import { useLibraryContext } from './common/context';
|
||||
|
||||
type LibraryHomeProps = {
|
||||
tabList: { home: string, components: string, collections: string },
|
||||
handleTabChange: (key: string) => void,
|
||||
};
|
||||
|
||||
const LibraryHome = ({ tabList, handleTabChange } : LibraryHomeProps) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
totalHits: componentCount,
|
||||
totalCollectionHits: collectionCount,
|
||||
isLoading,
|
||||
isFiltered,
|
||||
} = useSearchContext();
|
||||
const { openAddContentSidebar } = useLibraryContext();
|
||||
|
||||
const renderEmptyState = () => {
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
if (componentCount === 0 && collectionCount === 0) {
|
||||
return isFiltered ? <NoSearchResults /> : <NoComponents handleBtnClick={openAddContentSidebar} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<LibraryRecentlyModified />
|
||||
{
|
||||
renderEmptyState()
|
||||
|| (
|
||||
<>
|
||||
<LibrarySection
|
||||
title={intl.formatMessage(messages.collectionsTitle, { collectionCount })}
|
||||
contentCount={collectionCount}
|
||||
viewAllAction={() => handleTabChange(tabList.collections)}
|
||||
>
|
||||
<LibraryCollections variant="preview" />
|
||||
</LibrarySection>
|
||||
<LibrarySection
|
||||
title={intl.formatMessage(messages.componentsTitle, { componentCount })}
|
||||
contentCount={componentCount}
|
||||
viewAllAction={() => handleTabChange(tabList.components)}
|
||||
>
|
||||
<LibraryComponents variant="preview" />
|
||||
</LibrarySection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryHome;
|
||||
@@ -1,80 +0,0 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
import { SearchContextProvider, useSearchContext } from '../search-manager';
|
||||
import { type CollectionHit, type ContentHit, SearchSortOption } from '../search-manager/data/api';
|
||||
import LibrarySection, { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection';
|
||||
import messages from './messages';
|
||||
import ComponentCard from './components/ComponentCard';
|
||||
import CollectionCard from './components/CollectionCard';
|
||||
import { useLibraryContext } from './common/context';
|
||||
|
||||
const RecentlyModified: React.FC<Record<never, never>> = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
hits,
|
||||
collectionHits,
|
||||
totalHits,
|
||||
totalCollectionHits,
|
||||
} = useSearchContext();
|
||||
|
||||
const componentCount = totalHits + totalCollectionHits;
|
||||
// Since we only display a fixed number of items in preview,
|
||||
// only these number of items are use in sort step below
|
||||
const componentList = hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT);
|
||||
const collectionList = collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT);
|
||||
// Sort them by `modified` field in reverse and display them
|
||||
const recentItems = orderBy([
|
||||
...componentList,
|
||||
...collectionList,
|
||||
], ['modified'], ['desc']).slice(0, LIBRARY_SECTION_PREVIEW_LIMIT);
|
||||
|
||||
return componentCount > 0
|
||||
? (
|
||||
<LibrarySection
|
||||
title={intl.formatMessage(messages.recentlyModifiedTitle)}
|
||||
contentCount={componentCount}
|
||||
>
|
||||
<div className="library-cards-grid">
|
||||
{recentItems.map((contentHit) => (
|
||||
contentHit.type === 'collection' ? (
|
||||
<CollectionCard
|
||||
key={contentHit.id}
|
||||
collectionHit={contentHit as CollectionHit}
|
||||
/>
|
||||
) : (
|
||||
<ComponentCard
|
||||
key={contentHit.id}
|
||||
contentHit={contentHit as ContentHit}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</LibrarySection>
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
const LibraryRecentlyModified: React.FC<Record<never, never>> = () => {
|
||||
const { libraryId, showOnlyPublished } = useLibraryContext();
|
||||
|
||||
const extraFilter = [`context_key = "${libraryId}"`];
|
||||
if (showOnlyPublished) {
|
||||
extraFilter.push('last_published IS NOT NULL');
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchContextProvider
|
||||
extraFilter={extraFilter}
|
||||
overrideSearchSortOrder={
|
||||
showOnlyPublished
|
||||
? SearchSortOption.RECENTLY_PUBLISHED
|
||||
: SearchSortOption.RECENTLY_MODIFIED
|
||||
}
|
||||
>
|
||||
<RecentlyModified />
|
||||
</SearchContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryRecentlyModified;
|
||||
@@ -180,34 +180,7 @@
|
||||
"display_name": "Blank Problem",
|
||||
"description": "Problem"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 5
|
||||
},
|
||||
{
|
||||
"indexUid": "studio_content",
|
||||
"hits": [],
|
||||
"query": "",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 5,
|
||||
"facetDistribution": {
|
||||
"block_type": {
|
||||
"html": 4,
|
||||
"problem": 1
|
||||
},
|
||||
"content.problem_types": {}
|
||||
},
|
||||
"facetStats": {}
|
||||
},
|
||||
{
|
||||
"indexUid": "studio_content",
|
||||
"hits": [
|
||||
{
|
||||
"display_name": "My first collection",
|
||||
"block_id": "my-first-collection",
|
||||
@@ -246,12 +219,30 @@
|
||||
"access_id": 16,
|
||||
"num_children": 1
|
||||
}
|
||||
|
||||
],
|
||||
"query": "",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 1,
|
||||
"processingTimeMs": 1,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 1
|
||||
"estimatedTotalHits": 5
|
||||
},
|
||||
{
|
||||
"indexUid": "studio_content",
|
||||
"hits": [],
|
||||
"query": "",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 0,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 5,
|
||||
"facetDistribution": {
|
||||
"block_type": {
|
||||
"html": 4,
|
||||
"problem": 1
|
||||
},
|
||||
"content.problem_types": {}
|
||||
},
|
||||
"facetStats": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -32,6 +32,199 @@
|
||||
"description": "Testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 1",
|
||||
"block_id": "col1",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer.",
|
||||
"id": 1,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.628254,
|
||||
"modified": 1725878053.420395,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 1",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "1",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.628254",
|
||||
"modified": "1725534795.628266",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 2",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 58",
|
||||
"id": 2,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.619101,
|
||||
"modified": 1725534795.619113,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 2",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "2",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.619101",
|
||||
"modified": "1725534795.619113",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 3",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 57",
|
||||
"id": 3,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.609781,
|
||||
"modified": 1725534795.609794,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 3",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "3",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.609781",
|
||||
"modified": "1725534795.609794",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 4",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 56",
|
||||
"id": 4,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.596287,
|
||||
"modified": 1725534795.5963,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 4",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "4",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.596287",
|
||||
"modified": "1725534795.5963",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 5",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 55",
|
||||
"id": 5,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.583068,
|
||||
"modified": 1725534795.583082,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 5",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "5",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.583068",
|
||||
"modified": "1725534795.583082",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 6",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 54",
|
||||
"id": 6,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.573794,
|
||||
"modified": 1725534795.573808,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 6",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "6",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.573794",
|
||||
"modified": "1725534795.573808",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lbaximtesthtml73a22298-bcd9-4f4c-ae34-0bc2b0612480-46b4a7f2",
|
||||
"display_name": "Second Text Component",
|
||||
@@ -318,209 +511,6 @@
|
||||
}
|
||||
},
|
||||
"facetStats": {}
|
||||
},
|
||||
{
|
||||
"indexUid": "studio",
|
||||
"hits": [
|
||||
{
|
||||
"display_name": "Collection 1",
|
||||
"block_id": "col1",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer.",
|
||||
"id": 1,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.628254,
|
||||
"modified": 1725878053.420395,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 1",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "1",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.628254",
|
||||
"modified": "1725534795.628266",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 2",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 58",
|
||||
"id": 2,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.619101,
|
||||
"modified": 1725534795.619113,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 2",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "2",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.619101",
|
||||
"modified": "1725534795.619113",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 3",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 57",
|
||||
"id": 3,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.609781,
|
||||
"modified": 1725534795.609794,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 3",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "3",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.609781",
|
||||
"modified": "1725534795.609794",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 4",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 56",
|
||||
"id": 4,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.596287,
|
||||
"modified": 1725534795.5963,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 4",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "4",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.596287",
|
||||
"modified": "1725534795.5963",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 5",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 55",
|
||||
"id": 5,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.583068,
|
||||
"modified": 1725534795.583082,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 5",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "5",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.583068",
|
||||
"modified": "1725534795.583082",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
},
|
||||
{
|
||||
"display_name": "Collection 6",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 54",
|
||||
"id": 6,
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": 1725534795.573794,
|
||||
"modified": 1725534795.573808,
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": 16,
|
||||
"_formatted": {
|
||||
"display_name": "Collection 6",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…",
|
||||
"id": "6",
|
||||
"type": "collection",
|
||||
"breadcrumbs": [
|
||||
{
|
||||
"display_name": "CS problems 2"
|
||||
}
|
||||
],
|
||||
"created": "1725534795.573794",
|
||||
"modified": "1725534795.573808",
|
||||
"context_key": "lib:OpenedX:CSPROB2",
|
||||
"org": "OpenedX",
|
||||
"access_id": "16"
|
||||
}
|
||||
}
|
||||
],
|
||||
"query": "learn",
|
||||
"processingTimeMs": 1,
|
||||
"limit": 6,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack } from '@openedx/paragon';
|
||||
import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
import { LibraryComponents } from '../components';
|
||||
import messages from './messages';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import LibraryContent, { ContentType } from '../LibraryContent';
|
||||
|
||||
const LibraryCollectionComponents = () => {
|
||||
const { totalHits: componentCount, isFiltered } = useSearchContext();
|
||||
@@ -24,7 +24,7 @@ const LibraryCollectionComponents = () => {
|
||||
return (
|
||||
<Stack direction="vertical" gap={3}>
|
||||
<h3 className="text-gray">Content ({componentCount})</h3>
|
||||
<LibraryComponents variant="full" />
|
||||
<LibraryContent contentType={ContentType.collections} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
const path = '/library/:libraryId/*';
|
||||
const libraryTitle = mockContentLibrary.libraryData.title;
|
||||
const mockCollection = {
|
||||
collectionId: mockResult.results[2].hits[0].block_id,
|
||||
collectionId: mockResult.results[0].hits[5].block_id,
|
||||
collectionNeverLoads: mockGetCollectionMetadata.collectionIdLoading,
|
||||
collectionNoComponents: 'collection-no-components',
|
||||
collectionEmpty: mockGetCollectionMetadata.collectionIdError,
|
||||
@@ -62,23 +62,21 @@ describe('<LibraryCollectionPage />', () => {
|
||||
// because otherwise Instantsearch will update the UI and change the query,
|
||||
// leading to unexpected results in the test cases.
|
||||
mockResultCopy.results[0].query = query;
|
||||
mockResultCopy.results[2].query = query;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockResultCopy.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
const collectionQueryId = requestData?.queries[0]?.filter?.[3]?.split('collections.key = "')[1].split('"')[0];
|
||||
const collectionQueryId = requestData?.queries[0]?.filter?.[2]?.split('collections.key = "')[1].split('"')[0];
|
||||
switch (collectionQueryId) {
|
||||
case mockCollection.collectionNeverLoads:
|
||||
return new Promise<any>(() => {});
|
||||
case mockCollection.collectionEmpty:
|
||||
mockResultCopy.results[2].hits = [];
|
||||
mockResultCopy.results[2].estimatedTotalHits = 0;
|
||||
mockResultCopy.results[0].hits = [];
|
||||
mockResultCopy.results[0].totalHits = 0;
|
||||
break;
|
||||
case mockCollection.collectionNoComponents:
|
||||
mockResultCopy.results[0].hits = [];
|
||||
mockResultCopy.results[0].estimatedTotalHits = 0;
|
||||
mockResultCopy.results[0].totalHits = 0;
|
||||
mockResultCopy.results[1].facetDistribution.block_type = {};
|
||||
mockResultCopy.results[2].hits[0].num_children = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -181,7 +179,7 @@ describe('<LibraryCollectionPage />', () => {
|
||||
// should not be impacted by the search
|
||||
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); });
|
||||
|
||||
expect(screen.queryByText('No matching components found in this collections.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('No matching components found in this collection.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open and close new content sidebar', async () => {
|
||||
|
||||
@@ -196,7 +196,6 @@ const LibraryCollectionPage = () => {
|
||||
<Container size="xl" className="px-4 mt-4 mb-5 library-authoring-page">
|
||||
<SearchContextProvider
|
||||
extraFilter={extraFilter}
|
||||
overrideQueries={{ collections: { limit: 0 } }}
|
||||
>
|
||||
<SubHeader
|
||||
title={(
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
import { useLoadOnScroll } from '../../hooks';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import CollectionCard from '../components/CollectionCard';
|
||||
import { LIBRARY_SECTION_PREVIEW_LIMIT } from '../components/LibrarySection';
|
||||
import messages from './messages';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
type LibraryCollectionsProps = {
|
||||
variant: 'full' | 'preview',
|
||||
};
|
||||
|
||||
/**
|
||||
* Library Collections to show collections grid
|
||||
*
|
||||
* Use style to:
|
||||
* - 'full': Show all collections with Infinite scroll pagination.
|
||||
* - 'preview': Show first 4 collections without pagination.
|
||||
*/
|
||||
const LibraryCollections = ({ variant }: LibraryCollectionsProps) => {
|
||||
const {
|
||||
collectionHits,
|
||||
totalCollectionHits,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isLoading,
|
||||
isFiltered,
|
||||
} = useSearchContext();
|
||||
|
||||
const { openCreateCollectionModal } = useLibraryContext();
|
||||
|
||||
const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits;
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
variant === 'full',
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (totalCollectionHits === 0) {
|
||||
return isFiltered
|
||||
? <NoSearchResults infoText={messages.noSearchResultsCollections} />
|
||||
: (
|
||||
<NoComponents
|
||||
infoText={messages.noCollections}
|
||||
addBtnText={messages.addCollection}
|
||||
handleBtnClick={openCreateCollectionModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="library-cards-grid">
|
||||
{collectionList.map((collectionHit) => (
|
||||
<CollectionCard
|
||||
key={collectionHit.id}
|
||||
collectionHit={collectionHit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryCollections;
|
||||
@@ -63,7 +63,7 @@ const messages = defineMessages({
|
||||
},
|
||||
noSearchResultsInCollection: {
|
||||
id: 'course-authoring.library-authoring.collections-pag.no-search-results.text',
|
||||
defaultMessage: 'No matching components found in this collections.',
|
||||
defaultMessage: 'No matching components found in this collection.',
|
||||
description: 'Message displayed when no matching components are found in collection',
|
||||
},
|
||||
newContentButton: {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { SidebarAdditionalActions, useLibraryContext } from '../common/context';
|
||||
import { ComponentMenu } from '../components';
|
||||
import ComponentMenu from '../components';
|
||||
import { canEditComponent } from '../components/ComponentEditorModal';
|
||||
import ComponentDetails from './ComponentDetails';
|
||||
import ComponentManagement from './ComponentManagement';
|
||||
|
||||
@@ -39,14 +39,14 @@ describe('<ManageCollections />', () => {
|
||||
fetchMock.mockReset();
|
||||
fetchMock.post(searchEndpoint, (_url, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const query = requestData?.queries[2]?.q ?? '';
|
||||
const query = requestData?.queries[0]?.q ?? '';
|
||||
// We have to replace the query (search keywords) in the mock results with the actual query,
|
||||
// because otherwise Instantsearch will update the UI and change the query,
|
||||
// leading to unexpected results in the test cases.
|
||||
mockCollectionsResults.results[2].query = query;
|
||||
mockCollectionsResults.results[0].query = query;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockCollectionsResults.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
mockCollectionsResults.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return mockCollectionsResults;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ interface CollectionsDrawerProps extends ManageCollectionsProps {
|
||||
const CollectionsSelectableBox = ({ usageKey, collections, onClose }: CollectionsDrawerProps) => {
|
||||
const type = 'checkbox';
|
||||
const intl = useIntl();
|
||||
const { collectionHits } = useSearchContext();
|
||||
const { hits } = useSearchContext();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const collectionKeys = collections.map((collection) => collection.key);
|
||||
const [selectedCollections, {
|
||||
@@ -67,7 +67,7 @@ const CollectionsSelectableBox = ({ usageKey, collections, onClose }: Collection
|
||||
columns={1}
|
||||
ariaLabelledby={intl.formatMessage(messages.manageCollectionsSelectionLabel)}
|
||||
>
|
||||
{collectionHits.map((collectionHit) => (
|
||||
{hits.map((collectionHit) => (
|
||||
<SelectableBox
|
||||
className="d-inline-flex align-items-center shadow-none border border-gray-100"
|
||||
value={collectionHit.blockId}
|
||||
@@ -112,12 +112,9 @@ const AddToCollectionsDrawer = ({ usageKey, collections, onClose }: CollectionsD
|
||||
|
||||
return (
|
||||
<SearchContextProvider
|
||||
overrideQueries={{
|
||||
components: { limit: 0 },
|
||||
blockTypes: { limit: 0 },
|
||||
}}
|
||||
extraFilter={`context_key = "${libraryId}"`}
|
||||
extraFilter={[`context_key = "${libraryId}"`, 'type = "collection"']}
|
||||
skipUrlUpdate
|
||||
skipBlockTypeFetch
|
||||
>
|
||||
<Stack className="mt-2" gap={3}>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -208,9 +208,8 @@ describe('<ComponentPicker />', () => {
|
||||
|
||||
onChange.mockClear();
|
||||
|
||||
// Select another component (the second "Select" button is the same component as the first,
|
||||
// but in the "Components" section instead of the "Recently Changed" section)
|
||||
fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[2]);
|
||||
// Select another component
|
||||
fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[1]);
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledWith([
|
||||
{
|
||||
usageKey: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd',
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
initializeMocks,
|
||||
} from '../../testUtils';
|
||||
import { getContentSearchConfigUrl } from '../../search-manager/data/api';
|
||||
import { mockContentLibrary } from '../data/api.mocks';
|
||||
import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { libraryComponentsMock } from '../__mocks__';
|
||||
import LibraryComponents from './LibraryComponents';
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
|
||||
mockContentLibrary.applyMock();
|
||||
const mockFetchNextPage = jest.fn();
|
||||
const mockUseSearchContext = jest.fn();
|
||||
|
||||
const data = {
|
||||
totalHits: 1,
|
||||
hits: [],
|
||||
isFetchingNextPage: false,
|
||||
hasNextPage: false,
|
||||
fetchNextPage: mockFetchNextPage,
|
||||
searchKeywords: '',
|
||||
isFiltered: false,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const returnEmptyResult = (_url: string, 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,
|
||||
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
|
||||
mockEmptyResult.results[0].query = query;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockEmptyResult.results[0]?.hits.forEach((hit: any) => { hit._formatted = { ...hit }; });
|
||||
return mockEmptyResult;
|
||||
};
|
||||
|
||||
jest.mock('../../search-manager', () => ({
|
||||
...jest.requireActual('../../search-manager'),
|
||||
useSearchContext: () => mockUseSearchContext(),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const withLibraryId = (libraryId: string) => ({
|
||||
extraWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<LibraryProvider libraryId={libraryId}>{children}</LibraryProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe('<LibraryComponents />', () => {
|
||||
beforeEach(() => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
|
||||
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
|
||||
|
||||
// The API method to get the Meilisearch connection details uses Axios:
|
||||
axiosMock.onGet(getContentSearchConfigUrl()).reply(200, {
|
||||
url: 'http://mock.meilisearch.local',
|
||||
index_name: 'studio',
|
||||
api_key: 'test-key',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('should render empty state', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
expect(await screen.findByText(/you have not added any content to this library yet\./i));
|
||||
expect(await screen.findByRole('button', { name: /add component/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state without add content button', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
totalHits: 0,
|
||||
});
|
||||
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryIdReadOnly));
|
||||
expect(await screen.findByText(/you have not added any content to this library yet\./i));
|
||||
expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a spinner while loading', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render components in full variant', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
hits: libraryComponentsMock,
|
||||
});
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Video Component 3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Video Component 4')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a problem: ID=5')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a problem: ID=6')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render components in preview variant', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
hits: libraryComponentsMock,
|
||||
});
|
||||
render(<LibraryComponents variant="preview" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Video Component 3')).toBeInTheDocument();
|
||||
expect(screen.getByText('Video Component 4')).toBeInTheDocument();
|
||||
expect(screen.queryByText('This is a problem: ID=5')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('This is a problem: ID=6')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call `fetchNextPage` on scroll to bottom in full variant', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
hits: libraryComponentsMock,
|
||||
hasNextPage: true,
|
||||
});
|
||||
|
||||
render(<LibraryComponents variant="full" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800 });
|
||||
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });
|
||||
|
||||
fireEvent.scroll(window, { target: { scrollY: 1000 } });
|
||||
|
||||
expect(mockFetchNextPage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call `fetchNextPage` on scroll to bottom in preview variant', async () => {
|
||||
mockUseSearchContext.mockReturnValue({
|
||||
...data,
|
||||
hits: libraryComponentsMock,
|
||||
hasNextPage: true,
|
||||
});
|
||||
|
||||
render(<LibraryComponents variant="preview" />, withLibraryId(mockContentLibrary.libraryId));
|
||||
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800 });
|
||||
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });
|
||||
|
||||
fireEvent.scroll(window, { target: { scrollY: 1000 } });
|
||||
|
||||
expect(mockFetchNextPage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
import { useLoadOnScroll } from '../../hooks';
|
||||
import { useSearchContext } from '../../search-manager';
|
||||
import { NoComponents, NoSearchResults } from '../EmptyStates';
|
||||
import ComponentCard from './ComponentCard';
|
||||
import { LIBRARY_SECTION_PREVIEW_LIMIT } from './LibrarySection';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
|
||||
type LibraryComponentsProps = {
|
||||
variant: 'full' | 'preview',
|
||||
};
|
||||
|
||||
/**
|
||||
* Library Components to show components grid
|
||||
*
|
||||
* Use style to:
|
||||
* - 'full': Show all components with Infinite scroll pagination.
|
||||
* - 'preview': Show first 4 components without pagination.
|
||||
*/
|
||||
const LibraryComponents = ({ variant }: LibraryComponentsProps) => {
|
||||
const {
|
||||
hits,
|
||||
totalHits: componentCount,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isLoading,
|
||||
isFiltered,
|
||||
usageKey,
|
||||
} = useSearchContext();
|
||||
const { openAddContentSidebar, openComponentInfoSidebar } = useLibraryContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (usageKey) {
|
||||
openComponentInfoSidebar(usageKey);
|
||||
}
|
||||
}, [usageKey]);
|
||||
|
||||
const componentList = variant === 'preview' ? hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : hits;
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
variant === 'full',
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (componentCount === 0) {
|
||||
return isFiltered ? <NoSearchResults /> : <NoComponents handleBtnClick={openAddContentSidebar} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="library-cards-grid">
|
||||
{ componentList.map((contentHit) => (
|
||||
<ComponentCard
|
||||
key={contentHit.id}
|
||||
contentHit={contentHit}
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryComponents;
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, ActionRow, Button } from '@openedx/paragon';
|
||||
|
||||
export const LIBRARY_SECTION_PREVIEW_LIMIT = 4;
|
||||
|
||||
const LibrarySection: React.FC<{
|
||||
title: string,
|
||||
viewAllAction?: () => void,
|
||||
contentCount: number,
|
||||
previewLimit?: number,
|
||||
children: React.ReactNode,
|
||||
}> = ({
|
||||
title,
|
||||
viewAllAction,
|
||||
contentCount,
|
||||
previewLimit = LIBRARY_SECTION_PREVIEW_LIMIT,
|
||||
children,
|
||||
}) => (
|
||||
<Card>
|
||||
<Card.Header
|
||||
title={title}
|
||||
actions={
|
||||
viewAllAction
|
||||
&& contentCount > previewLimit
|
||||
&& (
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={viewAllAction}>View All</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
{children}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default LibrarySection;
|
||||
@@ -1,2 +1 @@
|
||||
export { default as LibraryComponents } from './LibraryComponents';
|
||||
export { ComponentMenu } from './ComponentCard';
|
||||
export { ComponentMenu as default } from './ComponentCard';
|
||||
|
||||
@@ -33,7 +33,7 @@ const messages = defineMessages({
|
||||
},
|
||||
homeTab: {
|
||||
id: 'course-authoring.library-authoring.home-tab',
|
||||
defaultMessage: 'Home',
|
||||
defaultMessage: 'All Content',
|
||||
description: 'Tab label for the home tab',
|
||||
},
|
||||
componentsTab: {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MeiliSearch, type Filter } from 'meilisearch';
|
||||
import { union } from 'lodash';
|
||||
|
||||
import {
|
||||
CollectionHit, ContentHit, SearchSortOption, forceArray, OverrideQueries,
|
||||
CollectionHit, ContentHit, SearchSortOption, forceArray,
|
||||
} from './data/api';
|
||||
import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks';
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface SearchContextData {
|
||||
searchSortOrder: SearchSortOption;
|
||||
setSearchSortOrder: React.Dispatch<React.SetStateAction<SearchSortOption>>;
|
||||
defaultSearchSortOrder: SearchSortOption;
|
||||
hits: ContentHit[];
|
||||
hits: (ContentHit | CollectionHit)[];
|
||||
totalHits: number;
|
||||
isLoading: boolean;
|
||||
hasNextPage: boolean | undefined;
|
||||
@@ -42,8 +42,6 @@ export interface SearchContextData {
|
||||
fetchNextPage: () => void;
|
||||
closeSearchModal: () => void;
|
||||
hasError: boolean;
|
||||
collectionHits: CollectionHit[];
|
||||
totalCollectionHits: number;
|
||||
usageKey: string;
|
||||
}
|
||||
|
||||
@@ -93,10 +91,10 @@ export const SearchContextProvider: React.FC<{
|
||||
overrideSearchSortOrder?: SearchSortOption
|
||||
children: React.ReactNode,
|
||||
closeSearchModal?: () => void,
|
||||
overrideQueries?: OverrideQueries,
|
||||
skipBlockTypeFetch?: boolean,
|
||||
skipUrlUpdate?: boolean,
|
||||
}> = ({
|
||||
overrideSearchSortOrder, overrideQueries, skipUrlUpdate, ...props
|
||||
overrideSearchSortOrder, skipBlockTypeFetch, skipUrlUpdate, ...props
|
||||
}) => {
|
||||
const [searchKeywords, setSearchKeywords] = React.useState('');
|
||||
const [blockTypesFilter, setBlockTypesFilter] = React.useState<string[]>([]);
|
||||
@@ -165,7 +163,7 @@ export const SearchContextProvider: React.FC<{
|
||||
problemTypesFilter,
|
||||
tagsFilter,
|
||||
sort,
|
||||
overrideQueries,
|
||||
skipBlockTypeFetch,
|
||||
});
|
||||
|
||||
return React.createElement(SearchContext.Provider, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import type {
|
||||
Filter, MeiliSearch, MultiSearchQuery, SearchParams,
|
||||
Filter, MeiliSearch, MultiSearchQuery,
|
||||
} from 'meilisearch';
|
||||
|
||||
export const getContentSearchConfigUrl = () => new URL(
|
||||
@@ -126,6 +126,7 @@ export interface ContentHit extends BaseContentHit {
|
||||
* - 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.
|
||||
*/
|
||||
type: 'course_block' | 'library_block';
|
||||
breadcrumbs: [{ displayName: string }, ...Array<{ displayName: string, usageKey: string }>];
|
||||
description?: string;
|
||||
content?: ContentDetails;
|
||||
@@ -149,6 +150,7 @@ export interface ContentPublishedData {
|
||||
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
|
||||
*/
|
||||
export interface CollectionHit extends BaseContentHit {
|
||||
type: 'collection';
|
||||
description: string;
|
||||
numChildren?: number;
|
||||
}
|
||||
@@ -169,29 +171,6 @@ export function formatSearchHit(hit: Record<string, any>): ContentHit | Collecti
|
||||
return camelCaseObject(newHit);
|
||||
}
|
||||
|
||||
export interface OverrideQueries {
|
||||
components?: SearchParams,
|
||||
blockTypes?: SearchParams,
|
||||
collections?: SearchParams,
|
||||
}
|
||||
|
||||
function applyOverrideQueries(
|
||||
queries: MultiSearchQuery[],
|
||||
overrideQueries?: OverrideQueries,
|
||||
): MultiSearchQuery[] {
|
||||
const newQueries = [...queries];
|
||||
if (overrideQueries?.components) {
|
||||
newQueries[0] = { ...overrideQueries.components, indexUid: queries[0].indexUid };
|
||||
}
|
||||
if (overrideQueries?.blockTypes) {
|
||||
newQueries[1] = { ...overrideQueries.blockTypes, indexUid: queries[1].indexUid };
|
||||
}
|
||||
if (overrideQueries?.collections) {
|
||||
newQueries[2] = { ...overrideQueries.collections, indexUid: queries[2].indexUid };
|
||||
}
|
||||
return newQueries;
|
||||
}
|
||||
|
||||
interface FetchSearchParams {
|
||||
client: MeiliSearch,
|
||||
indexName: string,
|
||||
@@ -204,7 +183,7 @@ interface FetchSearchParams {
|
||||
sort?: SearchSortOption[],
|
||||
/** How many results to skip, e.g. if limit=20 then passing offset=20 gets the second page. */
|
||||
offset?: number,
|
||||
overrideQueries?: OverrideQueries,
|
||||
skipBlockTypeFetch?: boolean,
|
||||
}
|
||||
|
||||
export async function fetchSearchResults({
|
||||
@@ -216,18 +195,16 @@ export async function fetchSearchResults({
|
||||
tagsFilter,
|
||||
extraFilter,
|
||||
sort,
|
||||
overrideQueries,
|
||||
offset = 0,
|
||||
skipBlockTypeFetch = false,
|
||||
}: FetchSearchParams): Promise<{
|
||||
hits: ContentHit[],
|
||||
hits: (ContentHit | CollectionHit)[],
|
||||
nextOffset: number | undefined,
|
||||
totalHits: number,
|
||||
blockTypes: Record<string, number>,
|
||||
problemTypes: Record<string, number>,
|
||||
collectionHits: CollectionHit[],
|
||||
totalCollectionHits: number,
|
||||
}> {
|
||||
let queries: MultiSearchQuery[] = [];
|
||||
const queries: MultiSearchQuery[] = [];
|
||||
|
||||
// Convert 'extraFilter' into an array
|
||||
const extraFilterFormatted = forceArray(extraFilter);
|
||||
@@ -246,8 +223,6 @@ export async function fetchSearchResults({
|
||||
...problemTypesFilterFormatted,
|
||||
].flat()];
|
||||
|
||||
const collectionsFilter = 'type = "collection"';
|
||||
|
||||
// First query is always to get the hits, with all the filters applied.
|
||||
queries.push({
|
||||
indexUid: indexName,
|
||||
@@ -255,7 +230,6 @@ export async function fetchSearchResults({
|
||||
filter: [
|
||||
// top-level entries in the array are AND conditions and must all match
|
||||
// Inner arrays are OR conditions, where only one needs to match.
|
||||
`NOT ${collectionsFilter}`, // exclude collections
|
||||
...typeFilters,
|
||||
...extraFilterFormatted,
|
||||
...tagsFilterFormatted,
|
||||
@@ -270,52 +244,27 @@ export async function fetchSearchResults({
|
||||
});
|
||||
|
||||
// The second query is to get the possible values for the "block types" filter
|
||||
queries.push({
|
||||
indexUid: indexName,
|
||||
q: searchKeywords,
|
||||
facets: ['block_type', 'content.problem_types'],
|
||||
filter: [
|
||||
...extraFilterFormatted,
|
||||
// We exclude the block type filter here so we get all the other available options for it.
|
||||
...tagsFilterFormatted,
|
||||
],
|
||||
limit: 0, // We don't need any "hits" for this - just the facetDistribution
|
||||
});
|
||||
|
||||
// Third query is to get the hits for collections, with all the filters applied.
|
||||
queries.push({
|
||||
indexUid: indexName,
|
||||
q: searchKeywords,
|
||||
filter: [
|
||||
// top-level entries in the array are AND conditions and must all match
|
||||
// Inner arrays are OR conditions, where only one needs to match.
|
||||
collectionsFilter, // include only collections
|
||||
...extraFilterFormatted,
|
||||
// We exclude the block type filter as collections are only of 1 type i.e. collection.
|
||||
...tagsFilterFormatted,
|
||||
],
|
||||
attributesToHighlight: ['display_name', 'description'],
|
||||
highlightPreTag: HIGHLIGHT_PRE_TAG,
|
||||
highlightPostTag: HIGHLIGHT_POST_TAG,
|
||||
attributesToCrop: ['description'],
|
||||
sort,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
|
||||
queries = applyOverrideQueries(queries, overrideQueries);
|
||||
if (!skipBlockTypeFetch) {
|
||||
queries.push({
|
||||
indexUid: indexName,
|
||||
facets: ['block_type', 'content.problem_types'],
|
||||
filter: [
|
||||
...extraFilterFormatted,
|
||||
// We exclude the block type filter here so we get all the other available options for it.
|
||||
...tagsFilterFormatted,
|
||||
],
|
||||
limit: 0, // We don't need any "hits" for this - just the facetDistribution
|
||||
});
|
||||
}
|
||||
|
||||
const { results } = await client.multiSearch(({ queries }));
|
||||
const componentHitLength = results[0].hits.length;
|
||||
const collectionHitLength = results[2].hits.length;
|
||||
const hitLength = results[0].hits.length;
|
||||
return {
|
||||
hits: results[0].hits.map(formatSearchHit) as ContentHit[],
|
||||
totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? componentHitLength,
|
||||
blockTypes: results[1].facetDistribution?.block_type ?? {},
|
||||
problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {},
|
||||
nextOffset: componentHitLength === limit || collectionHitLength === limit ? offset + limit : undefined,
|
||||
collectionHits: results[2].hits.map(formatSearchHit) as CollectionHit[],
|
||||
totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? collectionHitLength,
|
||||
totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? hitLength,
|
||||
blockTypes: results[1]?.facetDistribution?.block_type ?? {},
|
||||
problemTypes: results[1]?.facetDistribution?.['content.problem_types'] ?? {},
|
||||
nextOffset: hitLength === limit ? offset + limit : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -553,19 +502,3 @@ export async function fetchTagsThatMatchKeyword({
|
||||
|
||||
return { matches: Array.from(matches).map((tagPath) => ({ tagPath })), mayBeMissingResults: hits.length === limit };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single document by its id
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
export async function fetchDocumentById({ client, indexName, id } : {
|
||||
/** The Meilisearch client instance */
|
||||
client: MeiliSearch;
|
||||
/** Which index to search */
|
||||
indexName: string;
|
||||
/** document id */
|
||||
id: string | number;
|
||||
}): Promise<ContentHit | CollectionHit> {
|
||||
const doc = await client.index(indexName).getDocument(id);
|
||||
return formatSearchHit(doc);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
fetchSearchResults,
|
||||
fetchTagsThatMatchKeyword,
|
||||
getContentSearchConfig,
|
||||
fetchDocumentById,
|
||||
fetchBlockTypes,
|
||||
OverrideQueries,
|
||||
} from './api';
|
||||
|
||||
/**
|
||||
@@ -57,7 +55,7 @@ export const useContentSearchResults = ({
|
||||
problemTypesFilter = [],
|
||||
tagsFilter = [],
|
||||
sort = [],
|
||||
overrideQueries,
|
||||
skipBlockTypeFetch = false,
|
||||
}: {
|
||||
/** The Meilisearch API client */
|
||||
client?: MeiliSearch;
|
||||
@@ -75,8 +73,8 @@ export const useContentSearchResults = ({
|
||||
tagsFilter?: string[];
|
||||
/** Sort search results using these options */
|
||||
sort?: SearchSortOption[];
|
||||
/** Set true to fetch collections along with components */
|
||||
overrideQueries?: OverrideQueries,
|
||||
/** If true, don't fetch the block types from the server */
|
||||
skipBlockTypeFetch?: boolean;
|
||||
}) => {
|
||||
const query = useInfiniteQuery({
|
||||
enabled: client !== undefined && indexName !== undefined,
|
||||
@@ -92,9 +90,9 @@ export const useContentSearchResults = ({
|
||||
problemTypesFilter,
|
||||
tagsFilter,
|
||||
sort,
|
||||
overrideQueries,
|
||||
],
|
||||
queryFn: ({ pageParam = 0 }) => {
|
||||
// istanbul ignore if: this should never happen
|
||||
if (client === undefined || indexName === undefined) {
|
||||
throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.');
|
||||
}
|
||||
@@ -110,7 +108,7 @@ export const useContentSearchResults = ({
|
||||
// For infinite pagination of results, we can retrieve additional pages if requested.
|
||||
// Note that if there are 20 results per page, the "second page" has offset=20, not 2.
|
||||
offset: pageParam,
|
||||
overrideQueries,
|
||||
skipBlockTypeFetch,
|
||||
});
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.nextOffset,
|
||||
@@ -125,14 +123,8 @@ export const useContentSearchResults = ({
|
||||
[pages],
|
||||
);
|
||||
|
||||
const collectionHits = React.useMemo(
|
||||
() => pages?.reduce((allHits, page) => [...allHits, ...page.collectionHits], []) ?? [],
|
||||
[pages],
|
||||
);
|
||||
|
||||
return {
|
||||
hits,
|
||||
collectionHits,
|
||||
// The distribution of block type filter options
|
||||
blockTypes: pages?.[0]?.blockTypes ?? {},
|
||||
problemTypes: pages?.[0]?.problemTypes ?? {},
|
||||
@@ -147,7 +139,6 @@ export const useContentSearchResults = ({
|
||||
hasNextPage: query.hasNextPage,
|
||||
// The last page has the most accurate count of total hits
|
||||
totalHits: pages?.[pages.length - 1]?.totalHits ?? 0,
|
||||
totalCollectionHits: pages?.[pages.length - 1]?.totalCollectionHits ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -186,6 +177,7 @@ export const useTagFilterOptions = (args: {
|
||||
],
|
||||
queryFn: () => {
|
||||
const { client, indexName } = args;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (client === undefined || indexName === undefined) {
|
||||
throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.');
|
||||
}
|
||||
@@ -210,6 +202,7 @@ export const useTagFilterOptions = (args: {
|
||||
],
|
||||
queryFn: () => {
|
||||
const { client, indexName } = args;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (client === undefined || indexName === undefined) {
|
||||
throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.');
|
||||
}
|
||||
@@ -259,27 +252,3 @@ export const useGetBlockTypes = (extraFilters: Filter) => {
|
||||
queryFn: () => fetchBlockTypes(client!, indexName!, extraFilters),
|
||||
});
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const useGetSingleDocument = ({ client, indexName, id }: {
|
||||
client?: MeiliSearch;
|
||||
indexName?: string;
|
||||
id: string | number;
|
||||
}) => (
|
||||
useQuery({
|
||||
enabled: client !== undefined && indexName !== undefined,
|
||||
queryKey: [
|
||||
'content_search',
|
||||
client?.config.apiKey,
|
||||
client?.config.host,
|
||||
indexName,
|
||||
id,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (client === undefined || indexName === undefined) {
|
||||
throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.');
|
||||
}
|
||||
return fetchDocumentById({ client, indexName, id });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { StatefulButton } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useSearchContext } from '../search-manager';
|
||||
import { ContentHit, useSearchContext } from '../search-manager';
|
||||
import SearchResult from './SearchResult';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -28,7 +28,9 @@ const SearchResults: React.FC<Record<never, never>> = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{hits.map((hit) => <SearchResult key={hit.id} hit={hit} />)}
|
||||
{hits.filter((hit): hit is ContentHit => hit.type !== 'collection').map(
|
||||
(hit) => <SearchResult key={hit.id} hit={hit} />,
|
||||
)}
|
||||
{hasNextPage
|
||||
? (
|
||||
<StatefulButton
|
||||
|
||||
@@ -96,12 +96,9 @@ describe('<SearchUI />', () => {
|
||||
// because otherwise Instantsearch will update the UI and change the query,
|
||||
// leading to unexpected results in the test cases.
|
||||
mockResult.results[0].query = query;
|
||||
mockResult.results[2].query = query;
|
||||
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
mockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return mockResult;
|
||||
});
|
||||
fetchMock.post(tagsKeywordSearchEndpoint, mockTagsKeywordSearchResult);
|
||||
@@ -173,8 +170,8 @@ describe('<SearchUI />', () => {
|
||||
expect(fetchMock).toHaveLastFetched((_url, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const requestedFilter = requestData?.queries[0].filter;
|
||||
return requestedFilter?.[2] === 'type = "course_block"'
|
||||
&& requestedFilter?.[3] === 'context_key = "course-v1:org+test+123"';
|
||||
return requestedFilter?.[1] === 'type = "course_block"'
|
||||
&& requestedFilter?.[2] === 'context_key = "course-v1:org+test+123"';
|
||||
});
|
||||
// Now we should see the results:
|
||||
expect(queryByText('Enter a keyword')).toBeNull();
|
||||
@@ -362,8 +359,8 @@ describe('<SearchUI />', () => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const requestedFilter = requestData?.queries[0].filter;
|
||||
// the filter is:
|
||||
// ['NOT type == "collection"', '', 'type = "course_block"', 'context_key = "course-v1:org+test+123"']
|
||||
return (requestedFilter?.length === 4);
|
||||
// ['', 'type = "course_block"', 'context_key = "course-v1:org+test+123"']
|
||||
return (requestedFilter?.length === 3);
|
||||
});
|
||||
// Now we should see the results:
|
||||
expect(getByText('6 results found')).toBeInTheDocument();
|
||||
@@ -389,7 +386,6 @@ describe('<SearchUI />', () => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const requestedFilter = requestData?.queries[0].filter;
|
||||
return JSON.stringify(requestedFilter) === JSON.stringify([
|
||||
'NOT type = "collection"',
|
||||
[
|
||||
'block_type = problem',
|
||||
'content.problem_types = choiceresponse',
|
||||
@@ -423,7 +419,6 @@ describe('<SearchUI />', () => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const requestedFilter = requestData?.queries?.[0]?.filter;
|
||||
return JSON.stringify(requestedFilter) === JSON.stringify([
|
||||
'NOT type = "collection"',
|
||||
[],
|
||||
'type = "course_block"',
|
||||
'context_key = "course-v1:org+test+123"',
|
||||
@@ -459,7 +454,6 @@ describe('<SearchUI />', () => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const requestedFilter = requestData?.queries?.[0]?.filter;
|
||||
return JSON.stringify(requestedFilter) === JSON.stringify([
|
||||
'NOT type = "collection"',
|
||||
[],
|
||||
'type = "course_block"',
|
||||
'context_key = "course-v1:org+test+123"',
|
||||
|
||||
@@ -22,15 +22,6 @@
|
||||
"block_type": {}
|
||||
},
|
||||
"facetStats": {}
|
||||
},
|
||||
{
|
||||
"indexUid": "studio",
|
||||
"hits": [],
|
||||
"query": "noresult",
|
||||
"processingTimeMs": 0,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user