From 675e02fcbdb1e21f0036ed64027d7dc2b121e0a3 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 22 Oct 2024 22:19:51 +0530 Subject: [PATCH] feat: "add to collection" menu item functionality (#1413) --- .../LibraryAuthoringPage.test.tsx | 28 ++++- .../LibraryAuthoringPage.tsx | 8 +- .../collections/CollectionDetails.test.tsx | 10 +- .../collections/CollectionDetails.tsx | 6 +- .../collections/CollectionInfo.tsx | 13 +-- .../collections/CollectionInfoHeader.test.tsx | 10 +- .../collections/CollectionInfoHeader.tsx | 3 +- .../collections/LibraryCollectionPage.tsx | 4 +- src/library-authoring/common/context.tsx | 88 ++++++++------- .../ComponentAdvancedInfo.test.tsx | 10 +- .../component-info/ComponentAdvancedInfo.tsx | 3 +- .../component-info/ComponentDetails.test.tsx | 10 +- .../component-info/ComponentDetails.tsx | 4 +- .../component-info/ComponentInfo.test.tsx | 7 +- .../component-info/ComponentInfo.tsx | 26 ++++- .../ComponentInfoHeader.test.tsx | 10 +- .../component-info/ComponentInfoHeader.tsx | 3 +- .../ComponentManagement.test.tsx | 10 +- .../component-info/ComponentManagement.tsx | 100 +++++++++++++----- .../component-info/ComponentPreview.test.tsx | 7 +- .../component-info/ComponentPreview.tsx | 3 +- .../component-info/ManageCollections.tsx | 21 +++- .../component-picker/ComponentPicker.test.tsx | 2 +- .../components/CollectionCard.tsx | 6 +- .../components/ComponentCard.tsx | 13 ++- .../components/ComponentDeleter.tsx | 3 +- .../library-sidebar/LibrarySidebar.tsx | 8 +- 27 files changed, 289 insertions(+), 127 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 981a8af60c..70853f18b6 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -89,11 +89,12 @@ describe('', () => { // 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 ... 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('', () => { }); 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('', () => { 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(); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 786c976196..e546dff26b 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -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 {!componentPickerMode && } - {!!sidebarBodyComponent && ( + {!!sidebarComponentInfo?.type && (
diff --git a/src/library-authoring/collections/CollectionDetails.test.tsx b/src/library-authoring/collections/CollectionDetails.test.tsx index 5ae9a2e72b..e2651a1395 100644 --- a/src/library-authoring/collections/CollectionDetails.test.tsx +++ b/src/library-authoring/collections/CollectionDetails.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( - + { children } ), diff --git a/src/library-authoring/collections/CollectionDetails.tsx b/src/library-authoring/collections/CollectionDetails.tsx index 73f9f69593..07a883dea5 100644 --- a/src/library-authoring/collections/CollectionDetails.tsx +++ b/src/library-authoring/collections/CollectionDetails.tsx @@ -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'); diff --git a/src/library-authoring/collections/CollectionInfo.tsx b/src/library-authoring/collections/CollectionInfo.tsx index b548c6b2a3..a3098dc178 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -22,20 +22,21 @@ const CollectionInfo = () => { libraryId, collectionId, setCollectionId, - sidebarCollectionId, + sidebarComponentInfo, componentPickerMode, } = useLibraryContext(); - const url = `/library/${libraryId}/collection/${sidebarCollectionId}/`; - const urlMatch = useMatch(url); - - const showOpenCollectionButton = !urlMatch && collectionId !== sidebarCollectionId; - + 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; + const collectionUsageKey = buildCollectionUsageKey(libraryId, sidebarCollectionId); const handleOpenCollection = useCallback(() => { diff --git a/src/library-authoring/collections/CollectionInfoHeader.test.tsx b/src/library-authoring/collections/CollectionInfoHeader.test.tsx index 1272e594dd..7e1b7af374 100644 --- a/src/library-authoring/collections/CollectionInfoHeader.test.tsx +++ b/src/library-authoring/collections/CollectionInfoHeader.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( - + { children } ), diff --git a/src/library-authoring/collections/CollectionInfoHeader.tsx b/src/library-authoring/collections/CollectionInfoHeader.tsx index 58db6c0e32..83be4c2139 100644 --- a/src/library-authoring/collections/CollectionInfoHeader.tsx +++ b/src/library-authoring/collections/CollectionInfoHeader.tsx @@ -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'); diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 8caac5a8c6..22692c5b5d 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -104,7 +104,7 @@ const LibraryCollectionPage = () => { } const { - sidebarBodyComponent, + sidebarComponentInfo, openCollectionInfoSidebar, componentPickerMode, setCollectionId, @@ -215,7 +215,7 @@ const LibraryCollectionPage = () => { - {!!sidebarBodyComponent && ( + {!!sidebarComponentInfo?.type && (
diff --git a/src/library-authoring/common/context.tsx b/src/library-authoring/common/context.tsx index cc085ef77c..d564886209 100644 --- a/src/library-authoring/common/context.tsx +++ b/src/library-authoring/common/context.tsx @@ -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(null); - const [sidebarComponentUsageKey, setSidebarComponentUsageKey] = useState( - initialSidebarComponentUsageKey, + const [sidebarComponentInfo, setSidebarComponentInfo] = useState( + initialSidebarComponentInfo, ); - const [sidebarCollectionId, setSidebarCollectionId] = useState(initialSidebarCollectionId); const [isLibraryTeamModalOpen, openLibraryTeamModal, closeLibraryTeamModal] = useToggle(false); const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false); const [componentBeingEdited, openComponentEditor] = useState(); 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 ( diff --git a/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx b/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx index 7c52d1f3a3..773a2faaff 100644 --- a/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx @@ -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 = ( , { extraWrapper: ({ children }: { children: React.ReactNode }) => ( - + {children} ), diff --git a/src/library-authoring/component-info/ComponentAdvancedInfo.tsx b/src/library-authoring/component-info/ComponentAdvancedInfo.tsx index 6fac7da4d5..4039fb44e5 100644 --- a/src/library-authoring/component-info/ComponentAdvancedInfo.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedInfo.tsx @@ -22,8 +22,9 @@ import messages from './messages'; const ComponentAdvancedInfoInner: React.FC> = () => { 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'); diff --git a/src/library-authoring/component-info/ComponentDetails.test.tsx b/src/library-authoring/component-info/ComponentDetails.test.tsx index b4a6a307ee..514e66bdf8 100644 --- a/src/library-authoring/component-info/ComponentDetails.test.tsx +++ b/src/library-authoring/component-info/ComponentDetails.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( - + {children} ), diff --git a/src/library-authoring/component-info/ComponentDetails.tsx b/src/library-authoring/component-info/ComponentDetails.tsx index 52834d1a7f..0bac9d3234 100644 --- a/src/library-authoring/component-info/ComponentDetails.tsx +++ b/src/library-authoring/component-info/ComponentDetails.tsx @@ -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) { diff --git a/src/library-authoring/component-info/ComponentInfo.test.tsx b/src/library-authoring/component-info/ComponentInfo.test.tsx index 5fbad3c474..f0f094fedd 100644 --- a/src/library-authoring/component-info/ComponentInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentInfo.test.tsx @@ -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 }) => ( {children} diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 4cbb35598f..74268936c2 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -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 = () => { setTab(k)} > diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx index de32c80158..fe55839859 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( - + {children} ), diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx index 0456aa3d1c..5c27071255 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -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'); diff --git a/src/library-authoring/component-info/ComponentManagement.test.tsx b/src/library-authoring/component-info/ComponentManagement.test.tsx index ac081ebad6..93a5872df2 100644 --- a/src/library-authoring/component-info/ComponentManagement.test.tsx +++ b/src/library-authoring/component-info/ComponentManagement.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( - + {children} ), diff --git a/src/library-authoring/component-info/ComponentManagement.tsx b/src/library-authoring/component-info/ComponentManagement.tsx index 18de562704..0cfb1c14c9 100644 --- a/src/library-authoring/component-info/ComponentManagement.tsx +++ b/src/library-authoring/component-info/ComponentManagement.tsx @@ -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) && ( - - - {intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })} - - )} - className="border-0" - > - - + + setTagsCollapseOpen((prev) => !prev)} + className="collapsible-trigger d-flex justify-content-between p-2" + > + + + {intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })} + + + + + + + + + + + + )} - + setCollectionsCollapseOpen((prev) => !prev)} + className="collapsible-trigger d-flex justify-content-between p-2" + > {intl.formatMessage(messages.manageTabCollectionsTitle, { count: collectionsCount })} - )} - className="border-0" - > - - + + + + + + + + + + + ); }; diff --git a/src/library-authoring/component-info/ComponentPreview.test.tsx b/src/library-authoring/component-info/ComponentPreview.test.tsx index fea05e65e6..cf71dab382 100644 --- a/src/library-authoring/component-info/ComponentPreview.test.tsx +++ b/src/library-authoring/component-info/ComponentPreview.test.tsx @@ -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(, { extraWrapper: ({ children }) => ( { children } diff --git a/src/library-authoring/component-info/ComponentPreview.tsx b/src/library-authoring/component-info/ComponentPreview.tsx index 69eb597d7d..a7448f3b8f 100644 --- a/src/library-authoring/component-info/ComponentPreview.tsx +++ b/src/library-authoring/component-info/ComponentPreview.tsx @@ -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'); diff --git a/src/library-authoring/component-info/ManageCollections.tsx b/src/library-authoring/component-info/ManageCollections.tsx index 22cc151cbf..10b1643cb0 100644 --- a/src/library-authoring/component-info/ManageCollections.tsx +++ b/src/library-authoring/component-info/ManageCollections.tsx @@ -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 ( ', () => { initializeMocks(); postMessageSpy = jest.spyOn(window.parent, 'postMessage'); - mockSearchResult(mockResult); + mockSearchResult({ ...mockResult }); }); it('should pick component using the component card button', async () => { diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 8181b35e24..3908321449 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -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 ( <> diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 03b6b3aaa9..2d61b858cc 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -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 ( { )} - + diff --git a/src/library-authoring/components/ComponentDeleter.tsx b/src/library-authoring/components/ComponentDeleter.tsx index 53d52e8a4e..b71f139ae4 100644 --- a/src/library-authoring/components/ComponentDeleter.tsx +++ b/src/library-authoring/components/ComponentDeleter.tsx @@ -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(() => { diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index c877c77c97..be9c33c426 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -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 (