* feat: select component and show sidebar on edit (#1949) Select component that is being edited in library and show its sidebar. Also fixes issue with children component listing in library unit page (cherry picked from commit08ac1c0c4d) * fix: search text flickering (#1999) Fix flickering issue in search field. (cherry picked from commit6f3b7ab962) * feat: open collection or unit page on double click only (#2002) Opens collection or unit page only on double click. (cherry picked from commit503642be8c)
This commit is contained in:
@@ -370,7 +370,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
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":[]'),
|
||||
|
||||
@@ -48,7 +48,8 @@ const CollectionInfo = () => {
|
||||
if (componentPickerMode) {
|
||||
setCollectionId(collectionId);
|
||||
} else {
|
||||
navigateTo({ collectionId });
|
||||
/* istanbul ignore next */
|
||||
navigateTo({ collectionId, doubleClicked: true });
|
||||
}
|
||||
}, [componentPickerMode, navigateTo]);
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('<LibraryCollectionPage />', () => {
|
||||
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":[]'),
|
||||
|
||||
@@ -22,7 +22,7 @@ type BaseCardProps = {
|
||||
tags: ContentHitTags;
|
||||
actions: React.ReactNode;
|
||||
hasUnpublishedChanges?: boolean;
|
||||
onSelect: () => void;
|
||||
onSelect: (e?: React.MouseEvent) => void;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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(<ComponentCard hit={contentHit} />, {
|
||||
path: '/library/:libraryId',
|
||||
params: { libraryId },
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
{ children }
|
||||
@@ -104,4 +113,24 @@ describe('<ComponentCard />', () => {
|
||||
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: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item {...(canEdit ? { onClick: () => openComponentEditor(usageKey) } : { disabled: true })}>
|
||||
<Dropdown.Item {...(canEdit ? { onClick: handleEdit } : { disabled: true })}>
|
||||
<FormattedMessage {...messages.menuEdit} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={updateClipboardClick}>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -19,23 +19,28 @@ mockGetContainerChildren.applyMock();
|
||||
const { libraryId } = mockContentLibrary;
|
||||
const { containerId } = mockGetContainerMetadata;
|
||||
|
||||
const render = (showOnlyPublished: boolean = false) => baseRender(<UnitInfo />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
showOnlyPublished={showOnlyPublished}
|
||||
>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.UnitInfo,
|
||||
}}
|
||||
const render = (showOnlyPublished: boolean = false) => {
|
||||
const params: { libraryId: string, unitId?: string } = { libraryId, unitId: containerId };
|
||||
return baseRender(<UnitInfo />, {
|
||||
path: '/library/:libraryId/:unitId?',
|
||||
params,
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
showOnlyPublished={showOnlyPublished}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.UnitInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -238,9 +238,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
const [hidePreviewFor, setHidePreviewFor] = useState<string | null>(null);
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
const { readOnly, showOnlyPublished } = useLibraryContext();
|
||||
const { sidebarComponentInfo } = useSidebarContext();
|
||||
const unitId = sidebarComponentInfo?.id;
|
||||
const { unitId, readOnly, showOnlyPublished } = useLibraryContext();
|
||||
|
||||
const { openAddContentSidebar } = useSidebarContext();
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<SearchField.Advanced
|
||||
onSubmit={setSearchKeywords}
|
||||
onChange={setSearchKeywords}
|
||||
onChange={handleSearch}
|
||||
onClear={() => setSearchKeywords('')}
|
||||
value={searchKeywords}
|
||||
className={props.className}
|
||||
|
||||
Reference in New Issue
Block a user