diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index cded3d334..f0c0404c9 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -370,7 +370,7 @@ describe('', () => { fireEvent.change(searchBox, { target: { value: 'words to find' } }); // Default sort option changes to "Most Relevant" - expect(screen.getAllByText('Most Relevant').length).toEqual(2); + expect((await screen.findAllByText('Most Relevant')).length).toEqual(2); await waitFor(() => { expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { body: expect.stringContaining('"sort":[]'), diff --git a/src/library-authoring/collections/CollectionInfo.tsx b/src/library-authoring/collections/CollectionInfo.tsx index 277a6fc1c..68d2dcb07 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -48,7 +48,8 @@ const CollectionInfo = () => { if (componentPickerMode) { setCollectionId(collectionId); } else { - navigateTo({ collectionId }); + /* istanbul ignore next */ + navigateTo({ collectionId, doubleClicked: true }); } }, [componentPickerMode, navigateTo]); diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index 3c3bf4547..eb102d2ed 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -315,7 +315,7 @@ describe('', () => { fireEvent.change(searchBox, { target: { value: 'words to find' } }); // Default sort option changes to "Most Relevant" - expect(screen.getAllByText('Most Relevant').length).toEqual(2); + expect((await screen.findAllByText('Most Relevant')).length).toEqual(2); await waitFor(() => { expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { body: expect.stringContaining('"sort":[]'), diff --git a/src/library-authoring/components/BaseCard.tsx b/src/library-authoring/components/BaseCard.tsx index 591fc3be6..ef7dc5828 100644 --- a/src/library-authoring/components/BaseCard.tsx +++ b/src/library-authoring/components/BaseCard.tsx @@ -22,7 +22,7 @@ type BaseCardProps = { tags: ContentHitTags; actions: React.ReactNode; hasUnpublishedChanges?: boolean; - onSelect: () => void; + onSelect: (e?: React.MouseEvent) => void; selected?: boolean; }; diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 1f158808f..21d5ef5d4 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -114,7 +114,7 @@ type CollectionCardProps = { const CollectionCard = ({ hit } : CollectionCardProps) => { const { componentPickerMode } = useComponentPickerContext(); - const { showOnlyPublished } = useLibraryContext(); + const { showOnlyPublished, setCollectionId } = useLibraryContext(); const { openCollectionInfoSidebar, sidebarComponentInfo } = useSidebarContext(); const { @@ -136,11 +136,15 @@ const CollectionCard = ({ hit } : CollectionCardProps) => { && sidebarComponentInfo.id === collectionId; const { navigateTo } = useLibraryRoutes(); - const openCollection = useCallback(() => { + const openCollection = useCallback((e?: React.MouseEvent) => { openCollectionInfoSidebar(collectionId); + const doubleClicked = (e?.detail || 0) > 1; if (!componentPickerMode) { - navigateTo({ collectionId }); + navigateTo({ collectionId, doubleClicked }); + } else if (doubleClicked) { + /* istanbul ignore next */ + setCollectionId(collectionId); } }, [collectionId, navigateTo, openCollectionInfoSidebar]); diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index 087b0ad20..e0e2c44e2 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -11,6 +11,13 @@ import { ContentHit } from '../../search-manager'; import ComponentCard from './ComponentCard'; import { PublishStatus } from '../../search-manager/data/api'; +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts + useNavigate: () => mockNavigate, +})); + const contentHit: ContentHit = { id: '1', usageKey: 'lb:org1:demolib:html:a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d', @@ -41,6 +48,8 @@ const contentHit: ContentHit = { const libraryId = 'lib:org1:Demo_Course'; const render = () => baseRender(, { + path: '/library/:libraryId', + params: { libraryId }, extraWrapper: ({ children }) => ( { children } @@ -104,4 +113,24 @@ describe('', () => { expect(mockShowToast).toHaveBeenCalledWith('Error copying to clipboard'); }); }); + + it('should select component on clicking edit menu option', async () => { + initializeMocks(); + render(); + + // Open menu + const menu = await screen.findByTestId('component-card-menu-toggle'); + expect(menu).toBeInTheDocument(); + fireEvent.click(menu); + + // Click copy to clipboard + const editOption = await screen.findByRole('button', { name: 'Edit' }); + expect(editOption).toBeInTheDocument(); + fireEvent.click(editOption); + // Verify that the url is updated to component url i.e. component is selected + expect(mockNavigate).toHaveBeenCalledWith({ + pathname: `/library/${libraryId}/component/${contentHit.usageKey}`, + search: '', + }); + }); }); diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx index e3d0afe64..ba3711711 100644 --- a/src/library-authoring/components/ComponentMenu.tsx +++ b/src/library-authoring/components/ComponentMenu.tsx @@ -90,6 +90,12 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { }); }; + const handleEdit = useCallback(() => { + navigateTo({ componentId: usageKey }); + openComponentInfoSidebar(usageKey); + openComponentEditor(usageKey); + }, [usageKey]); + const scheduleJumpToCollection = useRunOnNextRender(() => { // TODO: Ugly hack to make sure sidebar shows add to collection section // This needs to run after all changes to url takes place to avoid conflicts. @@ -119,7 +125,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { data-testid="component-card-menu-toggle" /> - openComponentEditor(usageKey) } : { disabled: true })}> + diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx index a6442b1b5..ebcbbbeaf 100644 --- a/src/library-authoring/components/ContainerCard.tsx +++ b/src/library-authoring/components/ContainerCard.tsx @@ -204,12 +204,12 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { const { navigateTo } = useLibraryRoutes(); - const openContainer = useCallback(() => { + const openContainer = useCallback((e?: React.MouseEvent) => { if (itemType === 'unit') { openUnitInfoSidebar(unitId); setUnitId(unitId); if (!componentPickerMode) { - navigateTo({ unitId }); + navigateTo({ unitId, doubleClicked: (e?.detail || 0) > 1 }); } } }, [unitId, itemType, openUnitInfoSidebar, navigateTo]); diff --git a/src/library-authoring/containers/UnitInfo.test.tsx b/src/library-authoring/containers/UnitInfo.test.tsx index 9b154d13c..c44d19604 100644 --- a/src/library-authoring/containers/UnitInfo.test.tsx +++ b/src/library-authoring/containers/UnitInfo.test.tsx @@ -19,23 +19,28 @@ mockGetContainerChildren.applyMock(); const { libraryId } = mockContentLibrary; const { containerId } = mockGetContainerMetadata; -const render = (showOnlyPublished: boolean = false) => baseRender(, { - extraWrapper: ({ children }) => ( - - { + const params: { libraryId: string, unitId?: string } = { libraryId, unitId: containerId }; + return baseRender(, { + path: '/library/:libraryId/:unitId?', + params, + extraWrapper: ({ children }) => ( + - {children} - - - ), -}); + + {children} + + + ), + }); +}; let axiosMock: MockAdapter; let mockShowToast; diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts index f5882ee67..ece0773f4 100644 --- a/src/library-authoring/data/api.test.ts +++ b/src/library-authoring/data/api.test.ts @@ -142,4 +142,13 @@ describe('library data API', () => { await api.removeLibraryContainerChildren(containerId, ['test']); expect(axiosMock.history.delete[0].url).toEqual(url); }); + + it('getContentLibraryV2List', async () => { + const url = api.getContentLibraryV2ListApiUrl(); + + axiosMock.onGet(url).reply(200, { some: 'data' }); + + await api.getContentLibraryV2List({ type: 'complex' }); + expect(axiosMock.history.get[0].url).toEqual(url); + }); }); diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index 510368456..4615f229d 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -49,6 +49,7 @@ export type NavigateToData = { collectionId?: string, contentType?: ContentType, unitId?: string, + doubleClicked?: boolean, }; export type LibraryRoutesData = { @@ -80,6 +81,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { collectionId, unitId, contentType, + doubleClicked, }: NavigateToData = {}) => { const { collectionId: urlCollectionId, @@ -125,7 +127,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { } else if (insideCollections) { // We're inside the Collections tab, route = ( - (collectionId && collectionId === (urlCollectionId || urlSelectedItemId)) + (collectionId && doubleClicked) // now open the previously-selected collection, ? ROUTES.COLLECTION // or stay there to list all collections, or a selected collection. @@ -142,7 +144,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { } else if (insideUnits) { // We're inside the units tab, route = ( - (unitId && unitId === (urlUnitId || urlSelectedItemId)) + (unitId && doubleClicked) // now open the previously-selected unit, ? ROUTES.UNIT // or stay there to list all units, or a selected unit. @@ -156,10 +158,10 @@ export const useLibraryRoutes = (): LibraryRoutesData => { // We're inside the All Content tab, so stay there, // and select a component. route = ROUTES.COMPONENT; - } else if (collectionId && collectionId === (urlCollectionId || urlSelectedItemId)) { + } else if (collectionId && doubleClicked) { // now open the previously-selected collection route = ROUTES.COLLECTION; - } else if (unitId && unitId === (urlUnitId || urlSelectedItemId)) { + } else if (unitId && doubleClicked) { // now open the previously-selected unit route = ROUTES.UNIT; } else { diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx index 4d42bf2a4..ef7d62e1a 100644 --- a/src/library-authoring/units/LibraryUnitBlocks.tsx +++ b/src/library-authoring/units/LibraryUnitBlocks.tsx @@ -238,9 +238,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => { const [hidePreviewFor, setHidePreviewFor] = useState(null); const { showToast } = useContext(ToastContext); - const { readOnly, showOnlyPublished } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); - const unitId = sidebarComponentInfo?.id; + const { unitId, readOnly, showOnlyPublished } = useLibraryContext(); const { openAddContentSidebar } = useSidebarContext(); diff --git a/src/search-manager/SearchKeywordsField.tsx b/src/search-manager/SearchKeywordsField.tsx index 90c09fdd9..bbe3b67f8 100644 --- a/src/search-manager/SearchKeywordsField.tsx +++ b/src/search-manager/SearchKeywordsField.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { SearchField } from '@openedx/paragon'; +import { debounce } from 'lodash'; import messages from './messages'; import { useSearchContext } from './SearchManager'; @@ -17,10 +18,15 @@ const SearchKeywordsField: React.FC<{ const defaultPlaceholder = usageKey ? messages.clearUsageKeyToSearch : messages.inputPlaceholder; const { placeholder = intl.formatMessage(defaultPlaceholder) } = props; + const handleSearch = React.useCallback( + debounce((term) => setSearchKeywords(term.trim()), 400), + [searchKeywords], + );// Perform search after 500ms + return ( setSearchKeywords('')} value={searchKeywords} className={props.className}