feat: "add to collection" menu item functionality (#1413)

This commit is contained in:
Navin Karkera
2024-10-22 22:19:51 +05:30
committed by GitHub
parent 841aede8cd
commit 675e02fcbd
27 changed files with 289 additions and 127 deletions

View File

@@ -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();

View File

@@ -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>

View File

@@ -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>
),

View File

@@ -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');

View File

@@ -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(() => {

View File

@@ -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>
),

View File

@@ -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');

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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>
),

View File

@@ -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');

View File

@@ -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>
),

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>
),

View File

@@ -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');

View File

@@ -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>
),

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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');

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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 (
<>

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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">