feat: "add to collection" menu item functionality (#1413)
This commit is contained in:
@@ -89,11 +89,12 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
// 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.
|
||||
mockResult.results[0].query = query;
|
||||
const newMockResult = { ...mockResult };
|
||||
newMockResult.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
|
||||
mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return mockResult;
|
||||
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
|
||||
return newMockResult;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -458,7 +459,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
});
|
||||
|
||||
it('should open and close the component sidebar', async () => {
|
||||
const mockResult0 = mockResult.results[0].hits[0];
|
||||
const mockResult0 = { ...mockResult }.results[0].hits[0];
|
||||
const displayName = 'Introduction to Testing';
|
||||
expect(mockResult0.display_name).toStrictEqual(displayName);
|
||||
await renderLibraryPage();
|
||||
@@ -478,6 +479,25 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should open component sidebar, showing manage tab on clicking add to collection menu item', async () => {
|
||||
const mockResult0 = { ...mockResult }.results[0].hits[0];
|
||||
const displayName = 'Introduction to Testing';
|
||||
expect(mockResult0.display_name).toStrictEqual(displayName);
|
||||
await renderLibraryPage();
|
||||
|
||||
// Open menu
|
||||
fireEvent.click(screen.getAllByTestId('component-card-menu-toggle')[0]);
|
||||
// Click add to collection
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Add to collection' }));
|
||||
|
||||
const sidebar = screen.getByTestId('library-sidebar');
|
||||
|
||||
const { getByRole, queryByText } = within(sidebar);
|
||||
|
||||
await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument());
|
||||
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
|
||||
});
|
||||
|
||||
it('should open and close the collection sidebar', async () => {
|
||||
await renderLibraryPage();
|
||||
|
||||
|
||||
@@ -69,12 +69,12 @@ const HeaderActions = () => {
|
||||
openAddContentSidebar,
|
||||
openInfoSidebar,
|
||||
closeLibrarySidebar,
|
||||
sidebarBodyComponent,
|
||||
sidebarComponentInfo,
|
||||
readOnly,
|
||||
} = useLibraryContext();
|
||||
|
||||
const infoSidebarIsOpen = () => (
|
||||
sidebarBodyComponent === SidebarBodyComponentId.Info
|
||||
sidebarComponentInfo?.type === SidebarBodyComponentId.Info
|
||||
);
|
||||
|
||||
const handleOnClickInfoSidebar = () => {
|
||||
@@ -148,7 +148,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
libraryData,
|
||||
isLoadingLibraryData,
|
||||
componentPickerMode,
|
||||
sidebarBodyComponent,
|
||||
sidebarComponentInfo,
|
||||
openInfoSidebar,
|
||||
} = useLibraryContext();
|
||||
|
||||
@@ -261,7 +261,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
</Container>
|
||||
{!componentPickerMode && <StudioFooter containerProps={{ size: undefined }} />}
|
||||
</div>
|
||||
{!!sidebarBodyComponent && (
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
|
||||
<LibrarySidebar />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import * as api from '../data/api';
|
||||
import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks';
|
||||
import CollectionDetails from './CollectionDetails';
|
||||
@@ -30,7 +30,13 @@ const library = mockContentLibrary.libraryData;
|
||||
|
||||
const render = () => baseRender(<CollectionDetails />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={library.id} initialSidebarCollectionId={collectionId}>
|
||||
<LibraryProvider
|
||||
libraryId={library.id}
|
||||
initialSidebarComponentInfo={{
|
||||
id: collectionId,
|
||||
type: SidebarBodyComponentId.CollectionInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -37,7 +37,8 @@ const BlockCount = ({
|
||||
};
|
||||
|
||||
const CollectionStatsWidget = () => {
|
||||
const { libraryId, sidebarCollectionId: collectionId } = useLibraryContext();
|
||||
const { libraryId, sidebarComponentInfo } = useLibraryContext();
|
||||
const collectionId = sidebarComponentInfo?.id;
|
||||
|
||||
const { data: blockTypes } = useGetBlockTypes([
|
||||
`context_key = "${libraryId}"`,
|
||||
@@ -98,10 +99,11 @@ const CollectionDetails = () => {
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const {
|
||||
libraryId,
|
||||
sidebarCollectionId: collectionId,
|
||||
sidebarComponentInfo,
|
||||
readOnly,
|
||||
} = useLibraryContext();
|
||||
|
||||
const collectionId = sidebarComponentInfo?.id;
|
||||
// istanbul ignore next: This should never happen
|
||||
if (!collectionId) {
|
||||
throw new Error('collectionId is required');
|
||||
|
||||
@@ -22,20 +22,21 @@ const CollectionInfo = () => {
|
||||
libraryId,
|
||||
collectionId,
|
||||
setCollectionId,
|
||||
sidebarCollectionId,
|
||||
sidebarComponentInfo,
|
||||
componentPickerMode,
|
||||
} = useLibraryContext();
|
||||
|
||||
const sidebarCollectionId = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!sidebarCollectionId) {
|
||||
throw new Error('sidebarCollectionId is required');
|
||||
}
|
||||
|
||||
const url = `/library/${libraryId}/collection/${sidebarCollectionId}/`;
|
||||
const urlMatch = useMatch(url);
|
||||
|
||||
const showOpenCollectionButton = !urlMatch && collectionId !== sidebarCollectionId;
|
||||
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!sidebarCollectionId) {
|
||||
throw new Error('sidebarCollectionId is required');
|
||||
}
|
||||
|
||||
const collectionUsageKey = buildCollectionUsageKey(libraryId, sidebarCollectionId);
|
||||
|
||||
const handleOpenCollection = useCallback(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks';
|
||||
import * as api from '../data/api';
|
||||
import CollectionInfoHeader from './CollectionInfoHeader';
|
||||
@@ -28,7 +28,13 @@ const { collectionId } = mockGetCollectionMetadata;
|
||||
|
||||
const render = (libraryId: string = mockLibraryId) => baseRender(<CollectionInfoHeader />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId} initialSidebarCollectionId={collectionId}>
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
initialSidebarComponentInfo={{
|
||||
id: collectionId,
|
||||
type: SidebarBodyComponentId.CollectionInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -19,10 +19,11 @@ const CollectionInfoHeader = () => {
|
||||
|
||||
const {
|
||||
libraryId,
|
||||
sidebarCollectionId: collectionId,
|
||||
sidebarComponentInfo,
|
||||
readOnly,
|
||||
} = useLibraryContext();
|
||||
|
||||
const collectionId = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!collectionId) {
|
||||
throw new Error('collectionId is required');
|
||||
|
||||
@@ -104,7 +104,7 @@ const LibraryCollectionPage = () => {
|
||||
}
|
||||
|
||||
const {
|
||||
sidebarBodyComponent,
|
||||
sidebarComponentInfo,
|
||||
openCollectionInfoSidebar,
|
||||
componentPickerMode,
|
||||
setCollectionId,
|
||||
@@ -215,7 +215,7 @@ const LibraryCollectionPage = () => {
|
||||
</Container>
|
||||
<StudioFooter />
|
||||
</div>
|
||||
{!!sidebarBodyComponent && (
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
|
||||
<LibrarySidebar />
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,17 @@ export enum SidebarBodyComponentId {
|
||||
CollectionInfo = 'collection-info',
|
||||
}
|
||||
|
||||
export enum SidebarAdditionalActions {
|
||||
JumpToAddCollections = 'jump-to-add-collections',
|
||||
}
|
||||
|
||||
export interface SidebarComponentInfo {
|
||||
type: SidebarBodyComponentId;
|
||||
id: string;
|
||||
/** Additional action on Sidebar display */
|
||||
additionalAction?: SidebarAdditionalActions;
|
||||
}
|
||||
|
||||
export interface LibraryContextData {
|
||||
/** The ID of the current library */
|
||||
libraryId: string;
|
||||
@@ -27,12 +38,11 @@ export interface LibraryContextData {
|
||||
// Whether we're in "component picker" mode
|
||||
componentPickerMode: boolean;
|
||||
// Sidebar stuff - only one sidebar is active at any given time:
|
||||
sidebarBodyComponent: SidebarBodyComponentId | null;
|
||||
closeLibrarySidebar: () => void;
|
||||
openAddContentSidebar: () => void;
|
||||
openInfoSidebar: () => void;
|
||||
openComponentInfoSidebar: (usageKey: string) => void;
|
||||
sidebarComponentUsageKey?: string;
|
||||
openComponentInfoSidebar: (usageKey: string, additionalAction?: SidebarAdditionalActions) => void;
|
||||
sidebarComponentInfo?: SidebarComponentInfo;
|
||||
// "Library Team" modal
|
||||
isLibraryTeamModalOpen: boolean;
|
||||
openLibraryTeamModal: () => void;
|
||||
@@ -42,13 +52,13 @@ export interface LibraryContextData {
|
||||
openCreateCollectionModal: () => void;
|
||||
closeCreateCollectionModal: () => void;
|
||||
// Current collection
|
||||
openCollectionInfoSidebar: (collectionId: string) => void;
|
||||
sidebarCollectionId?: string;
|
||||
openCollectionInfoSidebar: (collectionId: string, additionalAction?: SidebarAdditionalActions) => void;
|
||||
// Editor modal - for editing some component
|
||||
/** If the editor is open and the user is editing some component, this is its usageKey */
|
||||
componentBeingEdited: string | undefined;
|
||||
openComponentEditor: (usageKey: string) => void;
|
||||
closeComponentEditor: () => void;
|
||||
resetSidebarAdditionalActions: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,9 +80,7 @@ interface LibraryProviderProps {
|
||||
* XBlock) */
|
||||
componentPickerMode?: boolean;
|
||||
/** Only used for testing */
|
||||
initialSidebarComponentUsageKey?: string;
|
||||
/** Only used for testing */
|
||||
initialSidebarCollectionId?: string;
|
||||
initialSidebarComponentInfo?: SidebarComponentInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,49 +91,49 @@ export const LibraryProvider = ({
|
||||
libraryId,
|
||||
collectionId: collectionIdProp,
|
||||
componentPickerMode = false,
|
||||
initialSidebarComponentUsageKey,
|
||||
initialSidebarCollectionId,
|
||||
initialSidebarComponentInfo,
|
||||
}: LibraryProviderProps) => {
|
||||
const [collectionId, setCollectionId] = useState(collectionIdProp);
|
||||
const [sidebarBodyComponent, setSidebarBodyComponent] = useState<SidebarBodyComponentId | null>(null);
|
||||
const [sidebarComponentUsageKey, setSidebarComponentUsageKey] = useState<string | undefined>(
|
||||
initialSidebarComponentUsageKey,
|
||||
const [sidebarComponentInfo, setSidebarComponentInfo] = useState<SidebarComponentInfo | undefined>(
|
||||
initialSidebarComponentInfo,
|
||||
);
|
||||
const [sidebarCollectionId, setSidebarCollectionId] = useState<string | undefined>(initialSidebarCollectionId);
|
||||
const [isLibraryTeamModalOpen, openLibraryTeamModal, closeLibraryTeamModal] = useToggle(false);
|
||||
const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false);
|
||||
const [componentBeingEdited, openComponentEditor] = useState<string | undefined>();
|
||||
const closeComponentEditor = useCallback(() => openComponentEditor(undefined), []);
|
||||
|
||||
const resetSidebar = useCallback(() => {
|
||||
setSidebarComponentUsageKey(undefined);
|
||||
setSidebarCollectionId(undefined);
|
||||
setSidebarBodyComponent(null);
|
||||
/** Helper function to consume addtional action once performed.
|
||||
Required to redo the action.
|
||||
*/
|
||||
const resetSidebarAdditionalActions = useCallback(() => {
|
||||
setSidebarComponentInfo((prev) => (prev && { ...prev, additionalAction: undefined }));
|
||||
}, []);
|
||||
|
||||
const closeLibrarySidebar = useCallback(() => {
|
||||
resetSidebar();
|
||||
setSidebarComponentInfo(undefined);
|
||||
}, []);
|
||||
const openAddContentSidebar = useCallback(() => {
|
||||
resetSidebar();
|
||||
setSidebarBodyComponent(SidebarBodyComponentId.AddContent);
|
||||
setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.AddContent });
|
||||
}, []);
|
||||
const openInfoSidebar = useCallback(() => {
|
||||
resetSidebar();
|
||||
setSidebarBodyComponent(SidebarBodyComponentId.Info);
|
||||
setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.Info });
|
||||
}, []);
|
||||
const openComponentInfoSidebar = useCallback(
|
||||
(usageKey: string) => {
|
||||
resetSidebar();
|
||||
setSidebarComponentUsageKey(usageKey);
|
||||
setSidebarBodyComponent(SidebarBodyComponentId.ComponentInfo);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openCollectionInfoSidebar = useCallback((newCollectionId: string) => {
|
||||
resetSidebar();
|
||||
setSidebarCollectionId(newCollectionId);
|
||||
setSidebarBodyComponent(SidebarBodyComponentId.CollectionInfo);
|
||||
const openComponentInfoSidebar = useCallback((usageKey: string, additionalAction?: SidebarAdditionalActions) => {
|
||||
setSidebarComponentInfo({
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
additionalAction,
|
||||
});
|
||||
}, []);
|
||||
const openCollectionInfoSidebar = useCallback((
|
||||
newCollectionId: string,
|
||||
additionalAction?: SidebarAdditionalActions,
|
||||
) => {
|
||||
setSidebarComponentInfo({
|
||||
id: newCollectionId,
|
||||
type: SidebarBodyComponentId.CollectionInfo,
|
||||
additionalAction,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { data: libraryData, isLoading: isLoadingLibraryData } = useContentLibrary(libraryId);
|
||||
@@ -140,12 +148,11 @@ export const LibraryProvider = ({
|
||||
readOnly,
|
||||
isLoadingLibraryData,
|
||||
componentPickerMode,
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
openInfoSidebar,
|
||||
openComponentInfoSidebar,
|
||||
sidebarComponentUsageKey,
|
||||
sidebarComponentInfo,
|
||||
isLibraryTeamModalOpen,
|
||||
openLibraryTeamModal,
|
||||
closeLibraryTeamModal,
|
||||
@@ -153,10 +160,10 @@ export const LibraryProvider = ({
|
||||
openCreateCollectionModal,
|
||||
closeCreateCollectionModal,
|
||||
openCollectionInfoSidebar,
|
||||
sidebarCollectionId,
|
||||
componentBeingEdited,
|
||||
openComponentEditor,
|
||||
closeComponentEditor,
|
||||
resetSidebarAdditionalActions,
|
||||
}), [
|
||||
libraryId,
|
||||
collectionId,
|
||||
@@ -165,12 +172,11 @@ export const LibraryProvider = ({
|
||||
readOnly,
|
||||
isLoadingLibraryData,
|
||||
componentPickerMode,
|
||||
sidebarBodyComponent,
|
||||
closeLibrarySidebar,
|
||||
openAddContentSidebar,
|
||||
openInfoSidebar,
|
||||
openComponentInfoSidebar,
|
||||
sidebarComponentUsageKey,
|
||||
sidebarComponentInfo,
|
||||
isLibraryTeamModalOpen,
|
||||
openLibraryTeamModal,
|
||||
closeLibraryTeamModal,
|
||||
@@ -178,10 +184,10 @@ export const LibraryProvider = ({
|
||||
openCreateCollectionModal,
|
||||
closeCreateCollectionModal,
|
||||
openCollectionInfoSidebar,
|
||||
sidebarCollectionId,
|
||||
componentBeingEdited,
|
||||
openComponentEditor,
|
||||
closeComponentEditor,
|
||||
resetSidebarAdditionalActions,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
mockXBlockAssets,
|
||||
mockXBlockOLX,
|
||||
} from '../data/api.mocks';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
|
||||
|
||||
mockContentLibrary.applyMock();
|
||||
@@ -28,7 +28,13 @@ const render = (
|
||||
<ComponentAdvancedInfo />,
|
||||
{
|
||||
extraWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<LibraryProvider libraryId={libraryId} initialSidebarComponentUsageKey={usageKey}>
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
initialSidebarComponentInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -22,8 +22,9 @@ import messages from './messages';
|
||||
|
||||
const ComponentAdvancedInfoInner: React.FC<Record<never, never>> = () => {
|
||||
const intl = useIntl();
|
||||
const { readOnly, sidebarComponentUsageKey: usageKey } = useLibraryContext();
|
||||
const { readOnly, sidebarComponentInfo } = useLibraryContext();
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen in production
|
||||
if (!usageKey) {
|
||||
throw new Error('sidebarComponentUsageKey is required to render ComponentAdvancedInfo');
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
mockXBlockAssets,
|
||||
mockXBlockOLX,
|
||||
} from '../data/api.mocks';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import ComponentDetails from './ComponentDetails';
|
||||
|
||||
mockContentLibrary.applyMock();
|
||||
@@ -21,7 +21,13 @@ const { libraryId: mockLibraryId } = mockContentLibrary;
|
||||
|
||||
const render = (usageKey: string) => baseRender(<ComponentDetails />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={mockLibraryId} initialSidebarComponentUsageKey={usageKey}>
|
||||
<LibraryProvider
|
||||
libraryId={mockLibraryId}
|
||||
initialSidebarComponentInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -10,7 +10,9 @@ import { ComponentAdvancedInfo } from './ComponentAdvancedInfo';
|
||||
import messages from './messages';
|
||||
|
||||
const ComponentDetails = () => {
|
||||
const { sidebarComponentUsageKey: usageKey } = useLibraryContext();
|
||||
const { sidebarComponentInfo } = useLibraryContext();
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!usageKey) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../../testUtils';
|
||||
import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks';
|
||||
import { mockBroadcastChannel } from '../../generic/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import ComponentInfo from './ComponentInfo';
|
||||
|
||||
mockBroadcastChannel();
|
||||
@@ -25,7 +25,10 @@ const withLibraryId = (libraryId: string, sidebarComponentUsageKey: string) => (
|
||||
extraWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
initialSidebarComponentUsageKey={sidebarComponentUsageKey}
|
||||
initialSidebarComponentInfo={{
|
||||
id: sidebarComponentUsageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
Stack,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import { SidebarAdditionalActions, useLibraryContext } from '../common/context';
|
||||
import { ComponentMenu } from '../components';
|
||||
import { canEditComponent } from '../components/ComponentEditorModal';
|
||||
import ComponentDetails from './ComponentDetails';
|
||||
@@ -19,12 +20,30 @@ const ComponentInfo = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
sidebarComponentUsageKey: usageKey,
|
||||
sidebarComponentInfo,
|
||||
readOnly,
|
||||
openComponentEditor,
|
||||
componentPickerMode,
|
||||
resetSidebarAdditionalActions,
|
||||
} = useLibraryContext();
|
||||
|
||||
const jumpToCollections = sidebarComponentInfo?.additionalAction === SidebarAdditionalActions.JumpToAddCollections;
|
||||
// Show Manage tab if JumpToAddCollections action is set in sidebarComponentInfo
|
||||
const [tab, setTab] = useState(jumpToCollections ? 'manage' : 'preview');
|
||||
useEffect(() => {
|
||||
if (jumpToCollections) {
|
||||
setTab('manage');
|
||||
}
|
||||
}, [jumpToCollections]);
|
||||
|
||||
useEffect(() => {
|
||||
// This is required to redo actions.
|
||||
if (tab !== 'manage') {
|
||||
resetSidebarAdditionalActions();
|
||||
}
|
||||
}, [tab]);
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!usageKey) {
|
||||
throw new Error('usageKey is required');
|
||||
@@ -65,7 +84,8 @@ const ComponentInfo = () => {
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
className="my-3 d-flex justify-content-around"
|
||||
defaultActiveKey="preview"
|
||||
activeKey={tab}
|
||||
onSelect={(k: string) => setTab(k)}
|
||||
>
|
||||
<Tab eventKey="preview" title={intl.formatMessage(messages.previewTabTitle)}>
|
||||
<ComponentPreview />
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '../../testUtils';
|
||||
import { mockContentLibrary } from '../data/api.mocks';
|
||||
import { getXBlockFieldsApiUrl } from '../data/api';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import ComponentInfoHeader from './ComponentInfoHeader';
|
||||
|
||||
const { libraryId: mockLibraryId, libraryIdReadOnly } = mockContentLibrary;
|
||||
@@ -24,7 +24,13 @@ const xBlockFields = {
|
||||
|
||||
const render = (libraryId: string = mockLibraryId) => baseRender(<ComponentInfoHeader />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId} initialSidebarComponentUsageKey={usageKey}>
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
initialSidebarComponentInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -18,10 +18,11 @@ const ComponentInfoHeader = () => {
|
||||
const [inputIsActive, setIsActive] = useState(false);
|
||||
|
||||
const {
|
||||
sidebarComponentUsageKey: usageKey,
|
||||
sidebarComponentInfo,
|
||||
readOnly,
|
||||
} = useLibraryContext();
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
// istanbul ignore next
|
||||
if (!usageKey) {
|
||||
throw new Error('usageKey is required');
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks';
|
||||
import ComponentManagement from './ComponentManagement';
|
||||
|
||||
@@ -37,7 +37,13 @@ const matchInnerText = (nodeName: string, textToMatch: string) => (_: string, el
|
||||
|
||||
const render = (usageKey: string, libraryId?: string) => baseRender(<ComponentManagement />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId || mockContentLibrary.libraryId} initialSidebarComponentUsageKey={usageKey}>
|
||||
<LibraryProvider
|
||||
libraryId={libraryId || mockContentLibrary.libraryId}
|
||||
initialSidebarComponentInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryProvider>
|
||||
),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, Icon, Stack } from '@openedx/paragon';
|
||||
import { BookOpen, Tag } from '@openedx/paragon/icons';
|
||||
import {
|
||||
BookOpen, ExpandLess, ExpandMore, Tag,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import { SidebarAdditionalActions, useLibraryContext } from '../common/context';
|
||||
import { useLibraryBlockMetadata } from '../data/apiHooks';
|
||||
import StatusWidget from '../generic/status-widget';
|
||||
import messages from './messages';
|
||||
@@ -14,8 +16,28 @@ import ManageCollections from './ManageCollections';
|
||||
|
||||
const ComponentManagement = () => {
|
||||
const intl = useIntl();
|
||||
const { sidebarComponentUsageKey: usageKey, readOnly, isLoadingLibraryData } = useLibraryContext();
|
||||
const {
|
||||
sidebarComponentInfo, readOnly, resetSidebarAdditionalActions, isLoadingLibraryData,
|
||||
} = useLibraryContext();
|
||||
const jumpToCollections = sidebarComponentInfo?.additionalAction === SidebarAdditionalActions.JumpToAddCollections;
|
||||
const [tagsCollapseIsOpen, setTagsCollapseOpen] = React.useState(!jumpToCollections);
|
||||
const [collectionsCollapseIsOpen, setCollectionsCollapseOpen] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCollections) {
|
||||
setTagsCollapseOpen(false);
|
||||
setCollectionsCollapseOpen(true);
|
||||
}
|
||||
}, [jumpToCollections, tagsCollapseIsOpen, collectionsCollapseIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
// This is required to redo actions.
|
||||
if (tagsCollapseIsOpen || !collectionsCollapseIsOpen) {
|
||||
resetSidebarAdditionalActions();
|
||||
}
|
||||
}, [tagsCollapseIsOpen, collectionsCollapseIsOpen]);
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!usageKey) {
|
||||
throw new Error('usageKey is required');
|
||||
@@ -61,35 +83,57 @@ const ComponentManagement = () => {
|
||||
/>
|
||||
{[true, 'true'].includes(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES)
|
||||
&& (
|
||||
<Collapsible
|
||||
defaultOpen
|
||||
title={(
|
||||
<Stack gap={1} direction="horizontal">
|
||||
<Icon src={Tag} />
|
||||
{intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })}
|
||||
</Stack>
|
||||
)}
|
||||
className="border-0"
|
||||
>
|
||||
<ContentTagsDrawer
|
||||
id={usageKey}
|
||||
variant="component"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible.Advanced
|
||||
open={tagsCollapseIsOpen}
|
||||
className="collapsible-card border-0"
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
onClick={() => setTagsCollapseOpen((prev) => !prev)}
|
||||
className="collapsible-trigger d-flex justify-content-between p-2"
|
||||
>
|
||||
<Stack gap={1} direction="horizontal">
|
||||
<Icon src={Tag} />
|
||||
{intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })}
|
||||
</Stack>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ExpandMore} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ExpandLess} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body">
|
||||
<ContentTagsDrawer
|
||||
id={usageKey}
|
||||
variant="component"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
)}
|
||||
<Collapsible
|
||||
defaultOpen
|
||||
title={(
|
||||
<Collapsible.Advanced
|
||||
open={collectionsCollapseIsOpen}
|
||||
className="collapsible-card border-0"
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
onClick={() => setCollectionsCollapseOpen((prev) => !prev)}
|
||||
className="collapsible-trigger d-flex justify-content-between p-2"
|
||||
>
|
||||
<Stack gap={1} direction="horizontal">
|
||||
<Icon src={BookOpen} />
|
||||
{intl.formatMessage(messages.manageTabCollectionsTitle, { count: collectionsCount })}
|
||||
</Stack>
|
||||
)}
|
||||
className="border-0"
|
||||
>
|
||||
<ManageCollections usageKey={usageKey} collections={componentMetadata.collections} />
|
||||
</Collapsible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ExpandMore} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ExpandLess} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body">
|
||||
<ManageCollections usageKey={usageKey} collections={componentMetadata.collections} />
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
render as baseRender,
|
||||
screen,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context';
|
||||
import { LibraryProvider, SidebarBodyComponentId } from '../common/context';
|
||||
import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks';
|
||||
import ComponentPreview from './ComponentPreview';
|
||||
|
||||
@@ -21,7 +21,10 @@ const render = () => baseRender(<ComponentPreview />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
initialSidebarComponentUsageKey={usageKey}
|
||||
initialSidebarComponentInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</LibraryProvider>
|
||||
|
||||
@@ -33,8 +33,9 @@ const ComponentPreview = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [isModalOpen, openModal, closeModal] = useToggle();
|
||||
const { sidebarComponentUsageKey: usageKey } = useLibraryContext();
|
||||
const { sidebarComponentInfo } = useLibraryContext();
|
||||
|
||||
const usageKey = sidebarComponentInfo?.id;
|
||||
// istanbul ignore if: this should never happen
|
||||
if (!usageKey) {
|
||||
throw new Error('usageKey is required');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Icon, Scrollable, SelectableBox, Stack, StatefulButton, useCheckboxSetValues,
|
||||
@@ -15,7 +15,7 @@ import messages from './messages';
|
||||
import { useUpdateComponentCollections } from '../data/apiHooks';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { CollectionMetadata } from '../data/api';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import { SidebarAdditionalActions, useLibraryContext } from '../common/context';
|
||||
|
||||
interface ManageCollectionsProps {
|
||||
usageKey: string;
|
||||
@@ -193,9 +193,24 @@ const ComponentCollections = ({ collections, onManageClick }: {
|
||||
};
|
||||
|
||||
const ManageCollections = ({ usageKey, collections }: ManageCollectionsProps) => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const { sidebarComponentInfo, resetSidebarAdditionalActions } = useLibraryContext();
|
||||
const jumpToCollections = sidebarComponentInfo?.additionalAction === SidebarAdditionalActions.JumpToAddCollections;
|
||||
const [editing, setEditing] = useState(jumpToCollections);
|
||||
const collectionNames = collections.map((collection) => collection.title);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCollections) {
|
||||
setEditing(true);
|
||||
}
|
||||
}, [sidebarComponentInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
// This is required to redo actions.
|
||||
if (!editing) {
|
||||
resetSidebarAdditionalActions();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<AddToCollectionsDrawer
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('<ComponentPicker />', () => {
|
||||
initializeMocks();
|
||||
postMessageSpy = jest.spyOn(window.parent, 'postMessage');
|
||||
|
||||
mockSearchResult(mockResult);
|
||||
mockSearchResult({ ...mockResult });
|
||||
});
|
||||
|
||||
it('should pick component using the component card button', async () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => {
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
const [confirmBtnState, setConfirmBtnState] = useState('default');
|
||||
const { closeLibrarySidebar, sidebarCollectionId } = useLibraryContext();
|
||||
const { closeLibrarySidebar, sidebarComponentInfo } = useLibraryContext();
|
||||
|
||||
const restoreCollectionMutation = useRestoreCollection(collectionHit.contextKey, collectionHit.blockId);
|
||||
const restoreCollection = useCallback(() => {
|
||||
@@ -42,7 +42,7 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => {
|
||||
const deleteCollectionMutation = useDeleteCollection(collectionHit.contextKey, collectionHit.blockId);
|
||||
const deleteCollection = useCallback(() => {
|
||||
setConfirmBtnState('pending');
|
||||
if (sidebarCollectionId === collectionHit.blockId) {
|
||||
if (sidebarComponentInfo?.id === collectionHit.blockId) {
|
||||
// Close sidebar if current collection is open to avoid displaying
|
||||
// deleted collection in sidebar
|
||||
closeLibrarySidebar();
|
||||
@@ -62,7 +62,7 @@ const CollectionMenu = ({ collectionHit } : CollectionMenuProps) => {
|
||||
setConfirmBtnState('default');
|
||||
closeDeleteModal();
|
||||
});
|
||||
}, [sidebarCollectionId]);
|
||||
}, [sidebarComponentInfo?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants';
|
||||
import { updateClipboard } from '../../generic/data/api';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type ContentHit } from '../../search-manager';
|
||||
import { useLibraryContext } from '../common/context';
|
||||
import { SidebarAdditionalActions, useLibraryContext } from '../common/context';
|
||||
import { useRemoveComponentsFromCollection } from '../data/apiHooks';
|
||||
import BaseComponentCard from './BaseComponentCard';
|
||||
import { canEditComponent } from './ComponentEditorModal';
|
||||
@@ -30,7 +30,8 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
const {
|
||||
libraryId,
|
||||
collectionId,
|
||||
sidebarComponentUsageKey,
|
||||
sidebarComponentInfo,
|
||||
openComponentInfoSidebar,
|
||||
openComponentEditor,
|
||||
closeLibrarySidebar,
|
||||
} = useLibraryContext();
|
||||
@@ -52,7 +53,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
|
||||
const removeFromCollection = () => {
|
||||
removeComponentsMutation.mutateAsync([usageKey]).then(() => {
|
||||
if (sidebarComponentUsageKey === usageKey) {
|
||||
if (sidebarComponentInfo?.id === usageKey) {
|
||||
// Close sidebar if current component is open
|
||||
closeLibrarySidebar();
|
||||
}
|
||||
@@ -62,6 +63,10 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const showManageCollections = () => {
|
||||
openComponentInfoSidebar(usageKey, SidebarAdditionalActions.JumpToAddCollections);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown id="component-card-dropdown">
|
||||
<Dropdown.Toggle
|
||||
@@ -88,7 +93,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
|
||||
<FormattedMessage {...messages.menuRemoveFromCollection} />
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item disabled>
|
||||
<Dropdown.Item onClick={showManageCollections}>
|
||||
<FormattedMessage {...messages.menuAddToCollection} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
|
||||
@@ -35,9 +35,10 @@ interface Props {
|
||||
const ComponentDeleter = ({ usageKey, ...props }: Props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
sidebarComponentUsageKey,
|
||||
sidebarComponentInfo,
|
||||
closeLibrarySidebar,
|
||||
} = useLibraryContext();
|
||||
const sidebarComponentUsageKey = sidebarComponentInfo?.id;
|
||||
|
||||
const deleteComponentMutation = useDeleteLibraryBlock();
|
||||
const doDelete = React.useCallback(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import messages from '../messages';
|
||||
* Sidebar container for library pages.
|
||||
*
|
||||
* It's designed to "squash" the page when open.
|
||||
* Uses `sidebarBodyComponent` of the `context` to
|
||||
* Uses `sidebarComponentInfo.type` of the `context` to
|
||||
* choose which component is rendered.
|
||||
* You can add more components in `bodyComponentMap`.
|
||||
* Use the returned actions to open and close this sidebar.
|
||||
@@ -26,7 +26,7 @@ import messages from '../messages';
|
||||
const LibrarySidebar = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
sidebarBodyComponent,
|
||||
sidebarComponentInfo,
|
||||
closeLibrarySidebar,
|
||||
} = useLibraryContext();
|
||||
|
||||
@@ -46,8 +46,8 @@ const LibrarySidebar = () => {
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
const buildBody = () : React.ReactNode => bodyComponentMap[sidebarBodyComponent || 'unknown'];
|
||||
const buildHeader = (): React.ReactNode => headerComponentMap[sidebarBodyComponent || 'unknown'];
|
||||
const buildBody = () : React.ReactNode => bodyComponentMap[sidebarComponentInfo?.type || 'unknown'];
|
||||
const buildHeader = (): React.ReactNode => headerComponentMap[sidebarComponentInfo?.type || 'unknown'];
|
||||
|
||||
return (
|
||||
<Stack gap={4} className="p-3 text-primary-700">
|
||||
|
||||
Reference in New Issue
Block a user