From b3605fa1b82fb9584fcbc7ade47afd03b787f9f5 Mon Sep 17 00:00:00 2001 From: Jillian Date: Mon, 9 Jun 2025 13:28:58 -0400 Subject: [PATCH] refactor: make the unit sidebar code work for any type of container [FC-0090] (#2066) Refactors the library sidebar and unit info code to make it work for subsections and subsections too --- .../LibraryAuthoringPage.test.tsx | 2 +- .../LibraryAuthoringPage.tsx | 12 +- src/library-authoring/LibraryLayout.tsx | 8 +- .../add-content/AddContent.test.tsx | 12 +- .../add-content/AddContent.tsx | 8 +- .../PickLibraryContentModal.test.tsx | 8 +- .../add-content/PickLibraryContentModal.tsx | 21 +-- .../collections/CollectionDetails.test.tsx | 6 +- .../collections/CollectionDetails.tsx | 8 +- .../collections/CollectionInfo.tsx | 4 +- .../collections/CollectionInfoHeader.test.tsx | 6 +- .../collections/CollectionInfoHeader.tsx | 4 +- .../LibraryCollectionPage.test.tsx | 5 +- .../collections/LibraryCollectionPage.tsx | 12 +- .../common/context/LibraryContext.tsx | 38 ++--- .../common/context/SidebarContext.tsx | 136 ++++++++-------- .../ComponentAdvancedAssets.tsx | 4 +- .../ComponentAdvancedInfo.test.tsx | 6 +- .../component-info/ComponentAdvancedInfo.tsx | 4 +- .../component-info/ComponentDetails.test.tsx | 6 +- .../component-info/ComponentDetails.tsx | 4 +- .../component-info/ComponentInfo.test.tsx | 6 +- .../component-info/ComponentInfo.tsx | 8 +- .../ComponentInfoHeader.test.tsx | 6 +- .../component-info/ComponentInfoHeader.tsx | 4 +- .../ComponentManagement.test.tsx | 6 +- .../component-info/ComponentManagement.tsx | 4 +- .../component-info/ComponentPreview.test.tsx | 6 +- .../component-info/ComponentPreview.tsx | 4 +- .../components/CollectionCard.tsx | 14 +- .../components/ComponentCard.tsx | 8 +- .../components/ComponentDeleter.tsx | 4 +- .../components/ComponentMenu.tsx | 18 +-- .../components/ContainerCard.tsx | 52 ++----- .../components/ContainerDeleter.tsx | 6 +- .../containers/ContainerInfo.test.tsx | 145 ++++++++++++++++++ .../{UnitInfo.tsx => ContainerInfo.tsx} | 94 ++++++++---- .../containers/ContainerInfoHeader.test.tsx | 8 +- .../containers/ContainerInfoHeader.tsx | 4 +- .../containers/ContainerOrganize.test.tsx | 12 +- .../containers/ContainerOrganize.tsx | 4 +- .../containers/HeaderActions.tsx | 22 +-- .../containers/UnitInfo.test.tsx | 127 --------------- src/library-authoring/containers/index.tsx | 2 +- src/library-authoring/containers/messages.ts | 4 +- .../create-container/CreateContainerModal.tsx | 2 +- src/library-authoring/data/api.mocks.ts | 20 +-- .../ManageCollections.test.tsx | 4 +- .../library-sidebar/LibrarySidebar.tsx | 34 ++-- src/library-authoring/routes.test.tsx | 4 +- src/library-authoring/routes.ts | 73 ++++----- .../LibraryContainerChildren.tsx | 27 ++-- .../LibrarySectionPage.tsx | 22 ++- .../LibrarySectionSubsectionPage.test.tsx | 22 +-- .../LibrarySubsectionPage.tsx | 22 ++- .../units/LibraryUnitBlocks.tsx | 6 +- .../units/LibraryUnitPage.test.tsx | 28 ++-- .../units/LibraryUnitPage.tsx | 50 ++---- 58 files changed, 571 insertions(+), 625 deletions(-) create mode 100644 src/library-authoring/containers/ContainerInfo.test.tsx rename src/library-authoring/containers/{UnitInfo.tsx => ContainerInfo.tsx} (59%) delete mode 100644 src/library-authoring/containers/UnitInfo.test.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 4a70a52de..cbc38bf12 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -771,7 +771,7 @@ describe('', () => { const title = `This is a Test ${containerType}`; const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(url).reply(200, { - id: '1', + id: `lct:org:libId:${containerType}:1`, slug: 'this-is-a-test', title, }); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 5a739643d..ffdbae47d 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -41,7 +41,7 @@ import LibraryContent from './LibraryContent'; import { LibrarySidebar } from './library-sidebar'; import { useComponentPickerContext } from './common/context/ComponentPickerContext'; import { useLibraryContext } from './common/context/LibraryContext'; -import { SidebarBodyComponentId, useSidebarContext } from './common/context/SidebarContext'; +import { SidebarBodyItemId, useSidebarContext } from './common/context/SidebarContext'; import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes'; import messages from './messages'; @@ -56,12 +56,12 @@ const HeaderActions = () => { openAddContentSidebar, openLibrarySidebar, closeLibrarySidebar, - sidebarComponentInfo, + sidebarItemInfo, } = useSidebarContext(); const { componentPickerMode } = useComponentPickerContext(); - const infoSidebarIsOpen = sidebarComponentInfo?.type === SidebarBodyComponentId.Info; + const infoSidebarIsOpen = sidebarItemInfo?.type === SidebarBodyItemId.Info; const { navigateTo } = useLibraryRoutes(); const handleOnClickInfoSidebar = useCallback(() => { @@ -75,7 +75,7 @@ const HeaderActions = () => { // If not in component picker mode, reset selected item when opening the info sidebar navigateTo({ selectedItemId: '' }); } - }, [navigateTo, sidebarComponentInfo, closeLibrarySidebar, openLibrarySidebar]); + }, [navigateTo, sidebarItemInfo, closeLibrarySidebar, openLibrarySidebar]); return (
@@ -153,7 +153,7 @@ const LibraryAuthoringPage = ({ showOnlyPublished, extraFilter: contextExtraFilter, } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); const { insideCollections, @@ -333,7 +333,7 @@ const LibraryAuthoringPage = ({ {!componentPickerMode && }
- {!!sidebarComponentInfo?.type && ( + {!!sidebarItemInfo?.type && (
diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index f10b2cfa6..cc7d27df3 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -20,7 +20,7 @@ import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections const LibraryLayoutWrapper: React.FC = ({ children }) => { const { - libraryId, collectionId, unitId, sectionId, subsectionId, + libraryId, collectionId, containerId, } = useParams(); if (libraryId === undefined) { @@ -30,11 +30,11 @@ const LibraryLayoutWrapper: React.FC = ({ children }) = return ( { ), }); }; -const renderWithUnit = (unitId: string) => { - const params: { libraryId: string, unitId?: string } = { libraryId, unitId }; +const renderWithContainer = (containerId: string) => { + const params: { libraryId: string, containerId?: string } = { libraryId, containerId }; return baseRender(, { - path: '/library/:libraryId/unit/:unitId?', + path: '/library/:libraryId/unit/:containerId?', params, extraWrapper: ({ children }) => ( ', () => { it('should not show collection, unit, section and subsection buttons when create component in unit', async () => { const unitId = 'lct:orf1:lib1:unit:test-1'; - renderWithUnit(unitId); + renderWithContainer(unitId); expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument(); @@ -351,7 +351,7 @@ describe('', () => { axiosMock.onPost(updateBlockUrl).reply(200, mockXBlockFields.dataHtml); axiosMock.onPost(linkUrl).reply(200); - renderWithUnit(unitId); + renderWithContainer(unitId); const textButton = screen.getByRole('button', { name: /text/i }); fireEvent.click(textButton); @@ -379,7 +379,7 @@ describe('', () => { axiosMock.onPost(updateBlockUrl).reply(200, mockXBlockFields.dataHtml); axiosMock.onPost(linkUrl).reply(400); - renderWithUnit(unitId); + renderWithContainer(unitId); const textButton = screen.getByRole('button', { name: /text/i }); fireEvent.click(textButton); diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 4ffa76ab0..489ac79bc 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -258,17 +258,19 @@ const AddContent = () => { const { libraryId, collectionId, + containerId, openCreateCollectionModal, setCreateContainerModalType, openComponentEditor, - unitId, } = useLibraryContext(); const { insideCollection, insideUnit, + insideSubsection, + insideSection, } = useLibraryRoutes(); const addComponentsToCollectionMutation = useAddItemsToCollection(libraryId, collectionId); - const addComponentsToContainerMutation = useAddItemsToContainer(unitId); + const addComponentsToContainerMutation = useAddItemsToContainer(containerId); const createBlockMutation = useCreateLibraryBlock(); const pasteClipboardMutation = useLibraryPasteClipboard(); const { showToast } = useContext(ToastContext); @@ -352,7 +354,7 @@ const AddContent = () => { showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }); } - if (unitId && insideUnit) { + if (containerId && (insideUnit || insideSubsection || insideSection)) { addComponentsToContainerMutation.mutateAsync([opaqueKey]).catch(() => { showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage)); }); diff --git a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx index b9ed6fd63..c874a6fe6 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx @@ -53,13 +53,13 @@ const getIdFromContext = (context: ContextType) => { const render = (context: ContextType) => baseRender( , { - path: `/library/:libraryId/${context}/:${context}Id/*`, + path: `/library/:libraryId/${context}/:${context === 'collection' ? context : 'container' }Id/*`, params: { libraryId, ...(context === 'collection' && { collectionId: 'collectionId' }), - ...(context === 'unit' && { unitId }), - ...(context === 'section' && { sectionId }), - ...(context === 'subsection' && { subsectionId }), + ...(context === 'unit' && { containerId: unitId }), + ...(context === 'section' && { containerId: sectionId }), + ...(context === 'subsection' && { containerId: subsectionId }), }, extraWrapper: ({ children }) => ( = ( const { libraryId, collectionId, - sectionId, - subsectionId, - unitId, + containerId, /** We need to get it as a reference instead of directly importing it to avoid the import cycle: * ComponentPicker > LibraryAuthoringPage/LibraryCollectionPage > * Sidebar > AddContent > ComponentPicker */ @@ -55,12 +53,7 @@ export const PickLibraryContentModal: React.FC = ( } = useLibraryRoutes(); const updateCollectionItemsMutation = useAddItemsToCollection(libraryId, collectionId); - const updateContainerChildrenMutation = useAddItemsToContainer( - (insideSection && sectionId) - || (insideSubsection && subsectionId) - || (insideUnit && unitId) - || '', - ); + const updateContainerChildrenMutation = useAddItemsToContainer(containerId); const { showToast } = useContext(ToastContext); @@ -77,7 +70,7 @@ export const PickLibraryContentModal: React.FC = ( .catch(() => { showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }); - } else if (insideSection || insideSubsection || insideUnit) { + } else if ((insideSection || insideSubsection || insideUnit) && containerId) { updateContainerChildrenMutation.mutateAsync(usageKeys) .then(() => { showToast(intl.formatMessage(messages.successAssociateComponentToContainerMessage)); @@ -92,9 +85,7 @@ export const PickLibraryContentModal: React.FC = ( insideSubsection, insideUnit, collectionId, - sectionId, - subsectionId, - unitId, + containerId, ]); // determine filter an visibleTabs based on current location @@ -123,8 +114,8 @@ export const PickLibraryContentModal: React.FC = ( } // istanbul ignore if: this should never happen, just here to satisfy type checker - if (!(collectionId || unitId || sectionId || subsectionId) || !ComponentPicker) { - throw new Error('collectionId/sectionId/unitId and componentPicker are required'); + if (!(collectionId || containerId) || !ComponentPicker) { + throw new Error('collectionId/containerId and componentPicker are required'); } return ( diff --git a/src/library-authoring/collections/CollectionDetails.test.tsx b/src/library-authoring/collections/CollectionDetails.test.tsx index b38e4cb48..b865b45fc 100644 --- a/src/library-authoring/collections/CollectionDetails.test.tsx +++ b/src/library-authoring/collections/CollectionDetails.test.tsx @@ -11,7 +11,7 @@ import { within, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import * as api from '../data/api'; import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks'; import CollectionDetails from './CollectionDetails'; @@ -33,9 +33,9 @@ const render = () => baseRender(, { extraWrapper: ({ children }) => ( { children } diff --git a/src/library-authoring/collections/CollectionDetails.tsx b/src/library-authoring/collections/CollectionDetails.tsx index 4594ed58a..a98f99ca0 100644 --- a/src/library-authoring/collections/CollectionDetails.tsx +++ b/src/library-authoring/collections/CollectionDetails.tsx @@ -39,8 +39,8 @@ const BlockCount = ({ const CollectionStatsWidget = () => { const { libraryId } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); - const collectionId = sidebarComponentInfo?.id; + const { sidebarItemInfo } = useSidebarContext(); + const collectionId = sidebarItemInfo?.id; const { data: blockTypes } = useGetBlockTypes([ `context_key = "${libraryId}"`, @@ -100,9 +100,9 @@ const CollectionDetails = () => { const intl = useIntl(); const { showToast } = useContext(ToastContext); const { libraryId, readOnly } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const collectionId = sidebarComponentInfo?.id; + const collectionId = sidebarItemInfo?.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 a9d55a912..ed34e5816 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -27,13 +27,13 @@ const CollectionInfo = () => { const { componentPickerMode } = useComponentPickerContext(); const { libraryId, setCollectionId } = useLibraryContext(); - const { sidebarComponentInfo, sidebarTab, setSidebarTab } = useSidebarContext(); + const { sidebarItemInfo, sidebarTab, setSidebarTab } = useSidebarContext(); const tab: CollectionInfoTab = ( sidebarTab && isCollectionInfoTab(sidebarTab) ) ? sidebarTab : COLLECTION_INFO_TABS.Details; - const collectionId = sidebarComponentInfo?.id; + const collectionId = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!collectionId) { throw new Error('collectionId is required'); diff --git a/src/library-authoring/collections/CollectionInfoHeader.test.tsx b/src/library-authoring/collections/CollectionInfoHeader.test.tsx index 13df15c56..ec26cade8 100644 --- a/src/library-authoring/collections/CollectionInfoHeader.test.tsx +++ b/src/library-authoring/collections/CollectionInfoHeader.test.tsx @@ -9,7 +9,7 @@ import { waitFor, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockGetCollectionMetadata } from '../data/api.mocks'; import * as api from '../data/api'; import CollectionInfoHeader from './CollectionInfoHeader'; @@ -31,9 +31,9 @@ const render = (libraryId: string = mockLibraryId) => baseRender( ( { children } diff --git a/src/library-authoring/collections/CollectionInfoHeader.tsx b/src/library-authoring/collections/CollectionInfoHeader.tsx index 93b02e278..7dca055a6 100644 --- a/src/library-authoring/collections/CollectionInfoHeader.tsx +++ b/src/library-authoring/collections/CollectionInfoHeader.tsx @@ -12,9 +12,9 @@ const CollectionInfoHeader = () => { const intl = useIntl(); const { libraryId, readOnly } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const collectionId = sidebarComponentInfo?.id; + const collectionId = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!collectionId) { diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index 9b3aad1ba..b748a2569 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -445,9 +445,10 @@ describe('', () => { ])('$label', async ({ containerType }) => { await renderLibraryCollectionPage(); const containerTitle = `This is a Test ${containerType}`; + const containerId = `lct:org:libId:${containerType}:1`; const containerUrl = getLibraryContainersApiUrl(mockContentLibrary.libraryId); axiosMock.onPost(containerUrl).reply(200, { - id: 'container-id', + id: containerId, slug: 'this-is-a-test', title: containerTitle, }); @@ -491,6 +492,6 @@ describe('', () => { // Check that the unit was added to the collection expect(axiosMock.history.patch.length).toBe(1); expect(axiosMock.history.patch[0].url).toBe(collectionUrl); - expect(axiosMock.history.patch[0].data).toContain('"usage_keys":["container-id"]'); + expect(axiosMock.history.patch[0].data).toContain(`"usage_keys":["${containerId}"]`); }); }); diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 3263dc0bc..bfdc1d4ef 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -30,7 +30,7 @@ import { SubHeaderTitle } from '../LibraryAuthoringPage'; import { useCollection, useContentLibrary } from '../data/apiHooks'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; import messages from './messages'; import { LibrarySidebar } from '../library-sidebar'; import LibraryCollectionComponents from './LibraryCollectionComponents'; @@ -45,7 +45,7 @@ const HeaderActions = () => { closeLibrarySidebar, openAddContentSidebar, openCollectionInfoSidebar, - sidebarComponentInfo, + sidebarItemInfo, } = useSidebarContext(); const { navigateTo } = useLibraryRoutes(); @@ -54,8 +54,8 @@ const HeaderActions = () => { throw new Error('it should not be possible to render HeaderActions without a collectionId'); } - const infoSidebarIsOpen = sidebarComponentInfo?.type === SidebarBodyComponentId.CollectionInfo - && sidebarComponentInfo?.id === collectionId; + const infoSidebarIsOpen = sidebarItemInfo?.type === SidebarBodyItemId.CollectionInfo + && sidebarItemInfo?.id === collectionId; const handleOnClickInfoSidebar = () => { if (infoSidebarIsOpen) { @@ -108,7 +108,7 @@ const LibraryCollectionPage = () => { extraFilter: contextExtraFilter, setCollectionId, } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); const { data: collectionData, @@ -224,7 +224,7 @@ const LibraryCollectionPage = () => { {!componentPickerMode && } - {!!sidebarComponentInfo?.type && ( + {!!sidebarItemInfo?.type && (
diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index 87e719297..13a38472f 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -26,15 +26,11 @@ export type LibraryContextData = { libraryData?: ContentLibrary; readOnly: boolean; isLoadingLibraryData: boolean; - /** The ID of the current collection/component/unit, on the sidebar OR page */ + /** The ID of the current collection/container, on the sidebar OR page */ collectionId: string | undefined; setCollectionId: (collectionId?: string) => void; - unitId: string | undefined; - setUnitId: (unitId?: string) => void; - sectionId: string | undefined; - setSectionId: (sectionId?: string) => void; - subsectionId: string | undefined; - setSubsectionId: (sectionId?: string) => void; + containerId: string | undefined; + setContainerId: (containerId?: string) => void; // Only show published components showOnlyPublished: boolean; // Additional filtering @@ -113,25 +109,17 @@ export const LibraryProvider = ({ const readOnly = !!componentPickerMode || !libraryData?.canEditLibrary; - // Parse the initial collectionId and/or componentId from the current URL params + // Parse the initial collectionId and/or container ID(s) from the current URL params const params = useParams(); const { collectionId: urlCollectionId, - unitId: urlUnitId, - sectionId: urlSectionId, - subsectionId: urlSubsectionId, + containerId: urlContainerId, } = params; const [collectionId, setCollectionId] = useState( skipUrlUpdate ? undefined : urlCollectionId, ); - const [unitId, setUnitId] = useState( - skipUrlUpdate ? undefined : urlUnitId, - ); - const [sectionId, setSectionId] = useState( - skipUrlUpdate ? undefined : urlSectionId, - ); - const [subsectionId, setSubsectionId] = useState( - skipUrlUpdate ? undefined : urlSubsectionId, + const [containerId, setContainerId] = useState( + skipUrlUpdate ? undefined : urlContainerId, ); const context = useMemo(() => { @@ -140,12 +128,8 @@ export const LibraryProvider = ({ libraryData, collectionId, setCollectionId, - unitId, - setUnitId, - sectionId, - setSectionId, - subsectionId, - setSubsectionId, + containerId, + setContainerId, readOnly, isLoadingLibraryData, showOnlyPublished, @@ -167,8 +151,8 @@ export const LibraryProvider = ({ libraryData, collectionId, setCollectionId, - unitId, - setUnitId, + containerId, + setContainerId, readOnly, isLoadingLibraryData, showOnlyPublished, diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index ea963106b..62d61ebba 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -8,18 +8,15 @@ import { } from 'react'; import { useParams } from 'react-router-dom'; import { useStateWithUrlSearchParam } from '../../../hooks'; -import { getBlockType } from '../../../generic/key-utils'; import { useComponentPickerContext } from './ComponentPickerContext'; import { useLibraryContext } from './LibraryContext'; -export enum SidebarBodyComponentId { +export enum SidebarBodyItemId { AddContent = 'add-content', Info = 'info', ComponentInfo = 'component-info', CollectionInfo = 'collection-info', - UnitInfo = 'unit-info', - SectionInfo = 'section-info', - SubsectionInfo = 'subsection-info', + ContainerInfo = 'container-info', } export const COLLECTION_INFO_TABS = { @@ -41,31 +38,37 @@ export const isComponentInfoTab = (tab: string): tab is ComponentInfoTab => ( Object.values(COMPONENT_INFO_TABS).includes(tab) ); -export const UNIT_INFO_TABS = { +export const CONTAINER_INFO_TABS = { Preview: 'preview', Manage: 'manage', Usage: 'usage', Settings: 'settings', } as const; -export type UnitInfoTab = typeof UNIT_INFO_TABS[keyof typeof UNIT_INFO_TABS]; -export const isUnitInfoTab = (tab: string): tab is UnitInfoTab => ( - Object.values(UNIT_INFO_TABS).includes(tab) +export type ContainerInfoTab = typeof CONTAINER_INFO_TABS[keyof typeof CONTAINER_INFO_TABS]; +export const isContainerInfoTab = (tab: string): tab is ContainerInfoTab => ( + Object.values(CONTAINER_INFO_TABS).includes(tab) ); -type SidebarInfoTab = ComponentInfoTab | CollectionInfoTab | UnitInfoTab; +const DEFAULT_TAB = { + component: COMPONENT_INFO_TABS.Preview, + container: CONTAINER_INFO_TABS.Preview, + collection: COLLECTION_INFO_TABS.Manage, +}; + +type SidebarInfoTab = ComponentInfoTab | CollectionInfoTab | ContainerInfoTab; const toSidebarInfoTab = (tab: string): SidebarInfoTab | undefined => ( - isComponentInfoTab(tab) || isCollectionInfoTab(tab) || isUnitInfoTab(tab) + isComponentInfoTab(tab) || isCollectionInfoTab(tab) || isContainerInfoTab(tab) ? tab : undefined ); export interface DefaultTabs { component: ComponentInfoTab; - unit: UnitInfoTab; + container: ContainerInfoTab; collection: CollectionInfoTab; } -export interface SidebarComponentInfo { - type: SidebarBodyComponentId; +export interface SidebarItemInfo { + type: SidebarBodyItemId; id: string; } @@ -82,17 +85,15 @@ export type SidebarContextData = { openLibrarySidebar: () => void; openCollectionInfoSidebar: (collectionId: string) => void; openComponentInfoSidebar: (usageKey: string) => void; - openUnitInfoSidebar: (usageKey: string) => void; - sidebarComponentInfo?: SidebarComponentInfo; + openContainerInfoSidebar: (usageKey: string) => void; + sidebarItemInfo?: SidebarItemInfo; sidebarAction: SidebarActions; setSidebarAction: (action: SidebarActions) => void; resetSidebarAction: () => void; sidebarTab: SidebarInfoTab; setSidebarTab: (tab: SidebarInfoTab) => void; defaultTab: DefaultTabs; - setDefaultTab: (tabs: DefaultTabs) => void; hiddenTabs: Array; - setHiddenTabs: (tabs: ComponentInfoTab[]) => void; }; /** @@ -106,7 +107,7 @@ const SidebarContext = createContext(undefined); type SidebarProviderProps = { children?: React.ReactNode; /** Only used for testing */ - initialSidebarComponentInfo?: SidebarComponentInfo; + initialSidebarItemInfo?: SidebarItemInfo; }; /** @@ -114,17 +115,13 @@ type SidebarProviderProps = { */ export const SidebarProvider = ({ children, - initialSidebarComponentInfo, + initialSidebarItemInfo, }: SidebarProviderProps) => { - const [sidebarComponentInfo, setSidebarComponentInfo] = useState( - initialSidebarComponentInfo, + const [sidebarItemInfo, setSidebarItemInfo] = useState( + initialSidebarItemInfo, ); - const [defaultTab, setDefaultTab] = useState({ - component: COMPONENT_INFO_TABS.Preview, - unit: UNIT_INFO_TABS.Preview, - collection: COLLECTION_INFO_TABS.Manage, - }); + const [defaultTab, setDefaultTab] = useState(DEFAULT_TAB); const [hiddenTabs, setHiddenTabs] = useState>([]); const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam( @@ -145,43 +142,43 @@ export const SidebarProvider = ({ }, [setSidebarAction]); const closeLibrarySidebar = useCallback(() => { - setSidebarComponentInfo(undefined); + setSidebarItemInfo(undefined); }, []); const openAddContentSidebar = useCallback(() => { - setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.AddContent }); + setSidebarItemInfo({ id: '', type: SidebarBodyItemId.AddContent }); }, []); const openLibrarySidebar = useCallback(() => { - setSidebarComponentInfo({ id: '', type: SidebarBodyComponentId.Info }); + setSidebarItemInfo({ id: '', type: SidebarBodyItemId.Info }); }, []); const openComponentInfoSidebar = useCallback((usageKey: string) => { - setSidebarComponentInfo({ + setSidebarItemInfo({ id: usageKey, - type: SidebarBodyComponentId.ComponentInfo, + type: SidebarBodyItemId.ComponentInfo, }); }, []); const openCollectionInfoSidebar = useCallback((newCollectionId: string) => { - setSidebarComponentInfo({ + setSidebarItemInfo({ id: newCollectionId, - type: SidebarBodyComponentId.CollectionInfo, + type: SidebarBodyItemId.CollectionInfo, }); }, []); - const openUnitInfoSidebar = useCallback((usageKey: string) => { - setSidebarComponentInfo({ + const openContainerInfoSidebar = useCallback((usageKey: string) => { + setSidebarItemInfo({ id: usageKey, - type: SidebarBodyComponentId.UnitInfo, + type: SidebarBodyItemId.ContainerInfo, }); }, []); // Set the initial sidebar state based on the URL parameters and context. - const { selectedItemId } = useParams(); - const { unitId, collectionId } = useLibraryContext(); + const { selectedItemId, containerId: selectedContainerId } = useParams(); + const { collectionId, containerId } = useLibraryContext(); const { componentPickerMode } = useComponentPickerContext(); useEffect(() => { - if (initialSidebarComponentInfo) { + if (initialSidebarItemInfo) { // If the sidebar is already open with a selected item, we don't need to do anything. return; } @@ -192,20 +189,8 @@ export const SidebarProvider = ({ // Handle selected item id changes if (selectedItemId) { - let containerType: undefined | string; - try { - containerType = getBlockType(selectedItemId); - } catch { - // ignore - } - if (containerType === 'unit') { - openUnitInfoSidebar(selectedItemId); - } else if (containerType === 'section') { - // istanbul ignore next - // Open section info sidebar - } else if (containerType === 'subsection') { - // istanbul ignore next - // Open subsection info sidebar + if (selectedItemId.startsWith('lct:')) { + openContainerInfoSidebar(selectedItemId); } else if (selectedItemId.startsWith('lb:')) { openComponentInfoSidebar(selectedItemId); } else { @@ -213,12 +198,25 @@ export const SidebarProvider = ({ } } else if (collectionId) { openCollectionInfoSidebar(collectionId); - } else if (unitId) { - openUnitInfoSidebar(unitId); + } else if (containerId) { + openContainerInfoSidebar(containerId); } else { openLibrarySidebar(); } - }, [selectedItemId]); + + // Hide the Preview tab if we're inside a collection + if (selectedContainerId) { + setDefaultTab({ + collection: COLLECTION_INFO_TABS.Details, + component: COMPONENT_INFO_TABS.Manage, + container: CONTAINER_INFO_TABS.Manage, + }); + setHiddenTabs([ + COMPONENT_INFO_TABS.Preview, + CONTAINER_INFO_TABS.Preview, + ]); + } + }, [selectedItemId, selectedContainerId, collectionId, containerId]); const context = useMemo(() => { const contextValue = { @@ -226,18 +224,16 @@ export const SidebarProvider = ({ openAddContentSidebar, openLibrarySidebar, openComponentInfoSidebar, - sidebarComponentInfo, + sidebarItemInfo, openCollectionInfoSidebar, - openUnitInfoSidebar, + openContainerInfoSidebar, sidebarAction, setSidebarAction, resetSidebarAction, sidebarTab, setSidebarTab, defaultTab, - setDefaultTab, hiddenTabs, - setHiddenTabs, }; return contextValue; @@ -246,18 +242,16 @@ export const SidebarProvider = ({ openAddContentSidebar, openLibrarySidebar, openComponentInfoSidebar, - sidebarComponentInfo, + sidebarItemInfo, openCollectionInfoSidebar, - openUnitInfoSidebar, + openContainerInfoSidebar, sidebarAction, setSidebarAction, resetSidebarAction, sidebarTab, setSidebarTab, defaultTab, - setDefaultTab, hiddenTabs, - setHiddenTabs, ]); return ( @@ -277,21 +271,15 @@ export function useSidebarContext(): SidebarContextData { openLibrarySidebar: () => {}, openComponentInfoSidebar: () => {}, openCollectionInfoSidebar: () => {}, - openUnitInfoSidebar: () => {}, + openContainerInfoSidebar: () => {}, sidebarAction: SidebarActions.None, setSidebarAction: () => {}, resetSidebarAction: () => {}, sidebarTab: COMPONENT_INFO_TABS.Preview, setSidebarTab: () => {}, - sidebarComponentInfo: undefined, - defaultTab: { - component: COMPONENT_INFO_TABS.Preview, - unit: UNIT_INFO_TABS.Preview, - collection: COLLECTION_INFO_TABS.Manage, - }, - setDefaultTab: () => {}, + sidebarItemInfo: undefined, + defaultTab: DEFAULT_TAB, hiddenTabs: [], - setHiddenTabs: () => {}, }; } return ctx; diff --git a/src/library-authoring/component-info/ComponentAdvancedAssets.tsx b/src/library-authoring/component-info/ComponentAdvancedAssets.tsx index 38bf5c3e6..b258f5b39 100644 --- a/src/library-authoring/component-info/ComponentAdvancedAssets.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedAssets.tsx @@ -19,9 +19,9 @@ import messages from './messages'; export const ComponentAdvancedAssets: React.FC> = () => { const intl = useIntl(); const { readOnly } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.id; // istanbul ignore if: this should never happen in production if (!usageKey) { throw new Error('sidebarComponentUsageKey is required to render ComponentAdvancedAssets'); diff --git a/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx b/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx index 360f7d777..01d2befa3 100644 --- a/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedInfo.test.tsx @@ -13,7 +13,7 @@ import { mockXBlockOLX, } from '../data/api.mocks'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import * as apiHooks from '../data/apiHooks'; import { ComponentAdvancedInfo } from './ComponentAdvancedInfo'; import { getXBlockAssetsApiUrl } from '../data/api'; @@ -34,9 +34,9 @@ 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 c9010201f..924cde59b 100644 --- a/src/library-authoring/component-info/ComponentAdvancedInfo.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedInfo.tsx @@ -23,9 +23,9 @@ import { ComponentAdvancedAssets } from './ComponentAdvancedAssets'; const ComponentAdvancedInfoInner: React.FC> = () => { const intl = useIntl(); const { readOnly, showOnlyPublished } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.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 ee79aef00..92ffc2a99 100644 --- a/src/library-authoring/component-info/ComponentDetails.test.tsx +++ b/src/library-authoring/component-info/ComponentDetails.test.tsx @@ -14,7 +14,7 @@ import { mockXBlockOLX, } from '../data/api.mocks'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import ComponentDetails from './ComponentDetails'; mockContentSearchConfig.applyMock(); @@ -35,9 +35,9 @@ 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 ceb122126..84bdffe4b 100644 --- a/src/library-authoring/component-info/ComponentDetails.tsx +++ b/src/library-authoring/component-info/ComponentDetails.tsx @@ -11,9 +11,9 @@ import { ComponentUsage } from './ComponentUsage'; import messages from './messages'; const ComponentDetails = () => { - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.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 0427e1b34..9f4b94d62 100644 --- a/src/library-authoring/component-info/ComponentInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentInfo.test.tsx @@ -11,7 +11,7 @@ import { } from '../data/api.mocks'; import { mockContentSearchConfig, mockFetchIndexDocuments } from '../../search-manager/data/api.mock'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import ComponentInfo from './ComponentInfo'; import { getXBlockPublishApiUrl } from '../data/api'; @@ -33,9 +33,9 @@ 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 635e54cc7..b76361d31 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -34,7 +34,7 @@ import PublishConfirmationModal from '../components/PublishConfirmationModal'; const AddComponentWidget = () => { const intl = useIntl(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); const { componentPickerMode, @@ -44,7 +44,7 @@ const AddComponentWidget = () => { selectedComponents, } = useComponentPickerContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!usageKey) { @@ -105,7 +105,7 @@ const ComponentInfo = () => { const { sidebarTab, setSidebarTab, - sidebarComponentInfo, + sidebarItemInfo, defaultTab, hiddenTabs, resetSidebarAction, @@ -127,7 +127,7 @@ const ComponentInfo = () => { setSidebarTab(newTab); }; - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!usageKey) { throw new Error('usageKey is required'); diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx index 62ad7573d..c5c3f6d7e 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx @@ -10,7 +10,7 @@ import { import { mockContentLibrary } from '../data/api.mocks'; import { getXBlockFieldsVersionApiUrl, getXBlockFieldsApiUrl } from '../data/api'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import ComponentInfoHeader from './ComponentInfoHeader'; const { libraryId: mockLibraryId, libraryIdReadOnly } = mockContentLibrary; @@ -27,9 +27,9 @@ const render = (libraryId: string = mockLibraryId) => baseRender( ( {children} diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx index 9615209c3..b8654221c 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -12,9 +12,9 @@ const ComponentInfoHeader = () => { const intl = useIntl(); const { readOnly, showOnlyPublished } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.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 0e4491265..e8fe425af 100644 --- a/src/library-authoring/component-info/ComponentManagement.test.tsx +++ b/src/library-authoring/component-info/ComponentManagement.test.tsx @@ -8,7 +8,7 @@ import { waitFor, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarActions, SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarActions, SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks'; import ComponentManagement from './ComponentManagement'; @@ -51,9 +51,9 @@ const render = (usageKey: string, libraryId?: string) => baseRender( ( {children} diff --git a/src/library-authoring/component-info/ComponentManagement.tsx b/src/library-authoring/component-info/ComponentManagement.tsx index 5c3b6a664..ee16908bc 100644 --- a/src/library-authoring/component-info/ComponentManagement.tsx +++ b/src/library-authoring/component-info/ComponentManagement.tsx @@ -17,7 +17,7 @@ import messages from './messages'; const ComponentManagement = () => { const intl = useIntl(); const { readOnly, isLoadingLibraryData } = useLibraryContext(); - const { sidebarComponentInfo, sidebarAction, resetSidebarAction } = useSidebarContext(); + const { sidebarItemInfo, sidebarAction, resetSidebarAction } = useSidebarContext(); const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; const jumpToTags = sidebarAction === SidebarActions.JumpToManageTags; const [tagsCollapseIsOpen, setTagsCollapseOpen] = React.useState(!jumpToCollections); @@ -40,7 +40,7 @@ const ComponentManagement = () => { } }, [tagsCollapseIsOpen, collectionsCollapseIsOpen]); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!usageKey) { throw new Error('usageKey is required'); diff --git a/src/library-authoring/component-info/ComponentPreview.test.tsx b/src/library-authoring/component-info/ComponentPreview.test.tsx index 139f9055f..6fc199fb6 100644 --- a/src/library-authoring/component-info/ComponentPreview.test.tsx +++ b/src/library-authoring/component-info/ComponentPreview.test.tsx @@ -5,7 +5,7 @@ import { screen, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks'; import ComponentPreview from './ComponentPreview'; @@ -24,9 +24,9 @@ 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 103cddf36..8e4804a7d 100644 --- a/src/library-authoring/component-info/ComponentPreview.tsx +++ b/src/library-authoring/component-info/ComponentPreview.tsx @@ -42,9 +42,9 @@ const ComponentPreview = () => { const [isModalOpen, openModal, closeModal] = useToggle(); const { showOnlyPublished } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const usageKey = sidebarComponentInfo?.id; + const usageKey = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!usageKey) { throw new Error('usageKey is required'); diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 4e6c6535d..e90b8ae67 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -12,7 +12,7 @@ import { MoreVert } from '@openedx/paragon/icons'; import { type CollectionHit } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; import BaseCard from './BaseCard'; import { ToastContext } from '../../generic/toast-context'; @@ -29,7 +29,7 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => { const { showToast } = useContext(ToastContext); const { navigateTo } = useLibraryRoutes(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); - const { closeLibrarySidebar, sidebarComponentInfo } = useSidebarContext(); + const { closeLibrarySidebar, sidebarItemInfo } = useSidebarContext(); const { contextKey, blockId, @@ -49,7 +49,7 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => { const deleteCollectionMutation = useDeleteCollection(contextKey, blockId); const deleteCollection = useCallback(async () => { - if (sidebarComponentInfo?.id === blockId) { + if (sidebarItemInfo?.id === blockId) { // Close sidebar if current collection is open to avoid displaying // deleted collection in sidebar closeLibrarySidebar(); @@ -68,7 +68,7 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => { } finally { closeDeleteModal(); } - }, [sidebarComponentInfo?.id]); + }, [sidebarItemInfo?.id]); const openCollection = useCallback(() => { navigateTo({ collectionId: blockId }); @@ -116,7 +116,7 @@ type CollectionCardProps = { const CollectionCard = ({ hit } : CollectionCardProps) => { const { componentPickerMode } = useComponentPickerContext(); const { setCollectionId, showOnlyPublished } = useLibraryContext(); - const { openCollectionInfoSidebar, sidebarComponentInfo } = useSidebarContext(); + const { openCollectionInfoSidebar, sidebarItemInfo } = useSidebarContext(); const { type: itemType, @@ -133,8 +133,8 @@ const CollectionCard = ({ hit } : CollectionCardProps) => { const { displayName = '', description = '' } = formatted; - const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.CollectionInfo - && sidebarComponentInfo.id === collectionId; + const selected = sidebarItemInfo?.type === SidebarBodyItemId.CollectionInfo + && sidebarItemInfo.id === collectionId; const { navigateTo } = useLibraryRoutes(); const selectCollection = useCallback((e?: React.MouseEvent) => { diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 65c449b5f..68e1f6cdb 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -6,7 +6,7 @@ import { import { type ContentHit, PublishStatus } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; import AddComponentWidget from './AddComponentWidget'; import BaseCard from './BaseCard'; @@ -18,7 +18,7 @@ type ComponentCardProps = { const ComponentCard = ({ hit }: ComponentCardProps) => { const { showOnlyPublished } = useLibraryContext(); - const { openComponentInfoSidebar, sidebarComponentInfo } = useSidebarContext(); + const { openComponentInfoSidebar, sidebarItemInfo } = useSidebarContext(); const { componentPickerMode } = useComponentPickerContext(); const { @@ -46,8 +46,8 @@ const ComponentCard = ({ hit }: ComponentCardProps) => { } }, [usageKey, navigateTo, openComponentInfoSidebar]); - const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.ComponentInfo - && sidebarComponentInfo.id === usageKey; + const selected = sidebarItemInfo?.type === SidebarBodyItemId.ComponentInfo + && sidebarItemInfo.id === usageKey; return ( { const intl = useIntl(); - const { sidebarComponentInfo, closeLibrarySidebar } = useSidebarContext(); + const { sidebarItemInfo, closeLibrarySidebar } = useSidebarContext(); const { showToast } = useContext(ToastContext); - const sidebarComponentUsageKey = sidebarComponentInfo?.id; + const sidebarComponentUsageKey = sidebarItemInfo?.id; const restoreComponentMutation = useRestoreLibraryBlock(); const restoreComponent = useCallback(async () => { diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx index 507c3a85d..c1777c7cb 100644 --- a/src/library-authoring/components/ComponentMenu.tsx +++ b/src/library-authoring/components/ComponentMenu.tsx @@ -28,12 +28,12 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const { libraryId, collectionId, - unitId, + containerId, openComponentEditor, } = useLibraryContext(); const { - sidebarComponentInfo, + sidebarItemInfo, openComponentInfoSidebar, closeLibrarySidebar, setSidebarAction, @@ -42,9 +42,9 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const canEdit = usageKey && canEditComponent(usageKey); const { showToast } = useContext(ToastContext); - const addComponentToContainerMutation = useAddItemsToContainer(unitId); + const addItemToContainerMutation = useAddItemsToContainer(containerId); const removeCollectionComponentsMutation = useRemoveItemsFromCollection(libraryId, collectionId); - const removeContainerComponentsMutation = useRemoveContainerChildren(unitId); + const removeContainerItemMutation = useRemoveContainerChildren(containerId); const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false); const { copyToClipboard } = useClipboard(); @@ -54,7 +54,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const removeFromCollection = () => { removeCollectionComponentsMutation.mutateAsync([usageKey]).then(() => { - if (sidebarComponentInfo?.id === usageKey) { + if (sidebarItemInfo?.id === usageKey) { // Close sidebar if current component is open closeLibrarySidebar(); } @@ -66,15 +66,15 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const removeFromContainer = () => { const restoreComponent = () => { - addComponentToContainerMutation.mutateAsync([usageKey]).then(() => { + addItemToContainerMutation.mutateAsync([usageKey]).then(() => { showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastSuccess)); }).catch(() => { showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastFailed)); }); }; - removeContainerComponentsMutation.mutateAsync([usageKey]).then(() => { - if (sidebarComponentInfo?.id === usageKey) { + removeContainerItemMutation.mutateAsync([usageKey]).then(() => { + if (sidebarItemInfo?.id === usageKey) { // Close sidebar if current component is open closeLibrarySidebar(); } @@ -129,7 +129,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { - {unitId && ( + {containerId && ( diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx index ecad91940..3468842b5 100644 --- a/src/library-authoring/components/ContainerCard.tsx +++ b/src/library-authoring/components/ContainerCard.tsx @@ -11,7 +11,7 @@ import { import { MoreVert } from '@openedx/paragon/icons'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; -import { ContainerType, getBlockType } from '../../generic/key-utils'; +import { getBlockType } from '../../generic/key-utils'; import { ToastContext } from '../../generic/toast-context'; import { type ContainerHit, Highlight, PublishStatus } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; @@ -27,15 +27,14 @@ import { useRunOnNextRender } from '../../utils'; type ContainerMenuProps = { containerKey: string; - containerType: ContainerType; displayName: string; }; -export const ContainerMenu = ({ containerKey, containerType, displayName } : ContainerMenuProps) => { +export const ContainerMenu = ({ containerKey, displayName } : ContainerMenuProps) => { const intl = useIntl(); const { libraryId, collectionId } = useLibraryContext(); const { - sidebarComponentInfo, + sidebarItemInfo, closeLibrarySidebar, setSidebarAction, } = useSidebarContext(); @@ -47,7 +46,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con const removeFromCollection = () => { removeComponentsMutation.mutateAsync([containerKey]).then(() => { - if (sidebarComponentInfo?.id === containerKey) { + if (sidebarItemInfo?.id === containerKey) { // Close sidebar if current component is open closeLibrarySidebar(); } @@ -69,7 +68,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con }, [scheduleJumpToCollection, navigateTo, containerKey]); const openContainer = useCallback(() => { - navigateTo({ [`${containerType}Id`]: containerKey }); + navigateTo({ containerId: containerKey }); }, [navigateTo, containerKey]); return ( @@ -88,7 +87,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con - + {insideCollection && ( @@ -96,7 +95,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con )} - + @@ -214,7 +213,7 @@ type ContainerCardProps = { const ContainerCard = ({ hit } : ContainerCardProps) => { const { componentPickerMode } = useComponentPickerContext(); const { showOnlyPublished } = useLibraryContext(); - const { openUnitInfoSidebar, sidebarComponentInfo } = useSidebarContext(); + const { openContainerInfoSidebar, sidebarItemInfo } = useSidebarContext(); const { blockType: itemType, @@ -234,44 +233,22 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { showOnlyPublished ? formatted.published?.displayName : formatted.displayName ) ?? ''; - const selected = sidebarComponentInfo?.id === containerKey; + const selected = sidebarItemInfo?.id === containerKey; const { navigateTo } = useLibraryRoutes(); const selectContainer = useCallback((e?: React.MouseEvent) => { const doubleClicked = (e?.detail || 0) > 1; if (componentPickerMode) { - switch (itemType) { - case ContainerType.Unit: - openUnitInfoSidebar(containerKey); - break; - case ContainerType.Section: - // TODO: open section sidebar - break; - case ContainerType.Subsection: - // TODO: open subsection sidebar - break; - default: - break; - } + // In component picker mode, we want to open the sidebar + // without changing the URL + openContainerInfoSidebar(containerKey); } else if (!doubleClicked) { navigateTo({ selectedItemId: containerKey }); } else { - switch (itemType) { - case ContainerType.Unit: - navigateTo({ unitId: containerKey }); - break; - case ContainerType.Section: - navigateTo({ sectionId: containerKey }); - break; - case ContainerType.Subsection: - navigateTo({ subsectionId: containerKey }); - break; - default: - break; - } + navigateTo({ containerId: containerKey }); } - }, [containerKey, itemType, openUnitInfoSidebar, navigateTo]); + }, [containerKey, openContainerInfoSidebar, navigateTo]); return ( { ) : ( )} diff --git a/src/library-authoring/components/ContainerDeleter.tsx b/src/library-authoring/components/ContainerDeleter.tsx index a4a1affc1..5d2e7b03a 100644 --- a/src/library-authoring/components/ContainerDeleter.tsx +++ b/src/library-authoring/components/ContainerDeleter.tsx @@ -24,7 +24,7 @@ const ContainerDeleter = ({ }: ContainerDeleterProps) => { const intl = useIntl(); const { - sidebarComponentInfo, + sidebarItemInfo, closeLibrarySidebar, } = useSidebarContext(); const deleteContainerMutation = useDeleteContainer(containerId); @@ -63,7 +63,7 @@ const ContainerDeleter = ({ const onDelete = useCallback(async () => { await deleteContainerMutation.mutateAsync().then(() => { - if (sidebarComponentInfo?.id === containerId) { + if (sidebarItemInfo?.id === containerId) { closeLibrarySidebar(); } showToast( @@ -78,7 +78,7 @@ const ContainerDeleter = ({ }).finally(() => { close(); }); - }, [sidebarComponentInfo, showToast, deleteContainerMutation]); + }, [sidebarItemInfo, showToast, deleteContainerMutation]); return ( (condition ? it : it.skip); + +const { libraryId } = mockContentLibrary; +const { unitId, subsectionId, sectionId } = mockGetContainerMetadata; + +const render = (containerId, showOnlyPublished: boolean = false) => { + const params: { libraryId: string, selectedItemId?: string } = { libraryId, selectedItemId: containerId }; + return baseRender(, { + path: '/library/:libraryId/:selectedItemId?', + params, + extraWrapper: ({ children }) => ( + + + {children} + + + ), + }); +}; +let axiosMock: MockAdapter; +let mockShowToast; + +describe('', () => { + beforeEach(() => { + ({ axiosMock, mockShowToast } = initializeMocks()); + }); + + [ + { + containerType: 'Unit', + containerId: unitId, + }, + { + containerType: 'Subsection', + containerId: subsectionId, + }, + { + containerType: 'Section', + containerId: sectionId, + }, + ].forEach(({ containerId, containerType }) => { + testIf(containerType === 'Unit')(`should delete the ${containerType} using the menu`, async () => { + axiosMock.onDelete(getLibraryContainerApiUrl(containerId)).reply(200); + render(containerId); + + // Open menu + expect(await screen.findByTestId('container-info-menu-toggle')).toBeInTheDocument(); + userEvent.click(screen.getByTestId('container-info-menu-toggle')); + + // Click on Delete Item + const deleteMenuItem = screen.getByRole('button', { name: 'Delete' }); + expect(deleteMenuItem).toBeInTheDocument(); + fireEvent.click(deleteMenuItem); + + // Confirm delete Modal is open + expect(screen.getByText(`Delete ${containerType}`)); + const deleteButton = screen.getByRole('button', { name: /delete/i }); + fireEvent.click(deleteButton); + + await waitFor(() => { + expect(axiosMock.history.delete.length).toBe(1); + }); + expect(mockShowToast).toHaveBeenCalled(); + }); + + it('can publish the container', async () => { + axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200); + render(containerId); + + // Click on Publish button + const publishButton = await screen.findByRole('button', { name: 'Publish' }); + expect(publishButton).toBeInTheDocument(); + userEvent.click(publishButton); + + await waitFor(() => { + expect(axiosMock.history.post.length).toBe(1); + }); + expect(mockShowToast).toHaveBeenCalledWith('All changes published'); + }); + + it(`shows an error if publishing the ${containerType} fails`, async () => { + axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(500); + render(containerId); + + // Click on Publish button + const publishButton = await screen.findByRole('button', { name: 'Publish' }); + expect(publishButton).toBeInTheDocument(); + userEvent.click(publishButton); + + await waitFor(() => { + expect(axiosMock.history.post.length).toBe(1); + }); + expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes'); + }); + + testIf(containerType === 'Unit')(`show only published ${containerType} content`, async () => { + render(containerId, true); + expect(await screen.findByTestId('container-info-menu-toggle')).toBeInTheDocument(); + expect(screen.getByText(/text block published 1/i)).toBeInTheDocument(); + }); + + it(`shows the ${containerType} Preview tab by default and the children are readonly`, async () => { + render(containerId); + const previewTab = await screen.findByText('Preview'); + expect(previewTab).toBeInTheDocument(); + expect(previewTab).toHaveAttribute('aria-selected', 'true'); + + // Check that there are no edit buttons for components titles + expect(screen.queryAllByRole('button', { name: /edit/i }).length).toBe(0); + + // Check that there are no drag handle for components + expect(screen.queryAllByRole('button', { name: 'Drag to reorder' }).length).toBe(0); + + // Check that there are no menu buttons for components + expect(screen.queryAllByRole('button', { name: /component actions menu/i }).length).toBe(0); + }); + }); +}); diff --git a/src/library-authoring/containers/UnitInfo.tsx b/src/library-authoring/containers/ContainerInfo.tsx similarity index 59% rename from src/library-authoring/containers/UnitInfo.tsx rename to src/library-authoring/containers/ContainerInfo.tsx index 3437fba1b..8be15f53d 100644 --- a/src/library-authoring/containers/UnitInfo.tsx +++ b/src/library-authoring/containers/ContainerInfo.tsx @@ -16,18 +16,20 @@ import { MoreVert } from '@openedx/paragon/icons'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; import { - type UnitInfoTab, - UNIT_INFO_TABS, - isUnitInfoTab, + type ContainerInfoTab, + CONTAINER_INFO_TABS, + isContainerInfoTab, useSidebarContext, } from '../common/context/SidebarContext'; import ContainerOrganize from './ContainerOrganize'; import { useLibraryRoutes } from '../routes'; import { LibraryUnitBlocks } from '../units/LibraryUnitBlocks'; +import { LibraryContainerChildren } from '../section-subsections/LibraryContainerChildren'; import messages from './messages'; import componentMessages from '../components/messages'; import ContainerDeleter from '../components/ContainerDeleter'; import { useContainer, usePublishContainer } from '../data/apiHooks'; +import { ContainerType, getBlockType } from '../../generic/key-utils'; import { ToastContext } from '../../generic/toast-context'; type ContainerMenuProps = { @@ -35,22 +37,22 @@ type ContainerMenuProps = { displayName: string, }; -const UnitMenu = ({ containerId, displayName }: ContainerMenuProps) => { +const ContainerMenu = ({ containerId, displayName }: ContainerMenuProps) => { const intl = useIntl(); const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false); return ( <> - + @@ -68,7 +70,19 @@ const UnitMenu = ({ containerId, displayName }: ContainerMenuProps) => { ); }; -const UnitInfo = () => { +type ContainerPreviewProps = { + containerId: string, +}; + +const ContainerPreview = ({ containerId } : ContainerPreviewProps) => { + const containerType = getBlockType(containerId); + if (containerType === ContainerType.Unit) { + return ; + } + return ; +}; + +const ContainerInfo = () => { const intl = useIntl(); const { libraryId, readOnly } = useLibraryContext(); @@ -79,28 +93,32 @@ const UnitInfo = () => { hiddenTabs, sidebarTab, setSidebarTab, - sidebarComponentInfo, + sidebarItemInfo, resetSidebarAction, } = useSidebarContext(); - const { insideUnit } = useLibraryRoutes(); + const { insideUnit, insideSubsection, insideSection } = useLibraryRoutes(); - const tab: UnitInfoTab = ( - sidebarTab && isUnitInfoTab(sidebarTab) - ) ? sidebarTab : defaultTab.unit; + const containerId = sidebarItemInfo?.id; + const containerType = containerId ? getBlockType(containerId) : undefined; + const { data: container } = useContainer(containerId); + const publishContainer = usePublishContainer(containerId!); - const unitId = sidebarComponentInfo?.id; - const { data: container } = useContainer(unitId); - const publishContainer = usePublishContainer(unitId!); + const defaultContainerTab = defaultTab.container; + const tab: ContainerInfoTab = ( + sidebarTab && isContainerInfoTab(sidebarTab) + ) ? sidebarTab : defaultContainerTab; - const showOpenUnitButton = !insideUnit && !componentPickerMode; + const showOpenButton = !componentPickerMode && !( + insideUnit || insideSubsection || insideSection + ); /* istanbul ignore next */ - const handleTabChange = (newTab: UnitInfoTab) => { + const handleTabChange = (newTab: ContainerInfoTab) => { resetSidebarAction(); setSidebarTab(newTab); }; - const renderTab = useCallback((infoTab: UnitInfoTab, component: React.ReactNode, title: string) => { + const renderTab = useCallback((infoTab: ContainerInfoTab, title: string, component?: React.ReactNode) => { if (hiddenTabs.includes(infoTab)) { // For some reason, returning anything other than empty list breaks the tab style return []; @@ -110,9 +128,9 @@ const UnitInfo = () => { {component} ); - }, [hiddenTabs, defaultTab.unit, unitId]); + }, [hiddenTabs, defaultContainerTab, containerId]); - const handlePublish = React.useCallback(async () => { + const handlePublish = useCallback(async () => { try { await publishContainer.mutateAsync(); showToast(intl.formatMessage(messages.publishContainerSuccess)); @@ -121,21 +139,21 @@ const UnitInfo = () => { } }, [publishContainer]); - if (!container || !unitId) { + if (!container || !containerId || !containerType) { return null; } return (
- {showOpenUnitButton && ( + {showOpenButton && ( )} {!componentPickerMode && !readOnly && ( @@ -148,9 +166,9 @@ const UnitInfo = () => { {intl.formatMessage(messages.publishContainerButton)} )} - {showOpenUnitButton && ( // Check: should we still show this on the unit page? - )} @@ -158,20 +176,28 @@ const UnitInfo = () => { {renderTab( - UNIT_INFO_TABS.Preview, - , + CONTAINER_INFO_TABS.Preview, intl.formatMessage(messages.previewTabTitle), + , + )} + {renderTab( + CONTAINER_INFO_TABS.Manage, + intl.formatMessage(messages.manageTabTitle), + , + )} + {renderTab( + CONTAINER_INFO_TABS.Settings, + intl.formatMessage(messages.settingsTabTitle), + // TODO: container settings component )} - {renderTab(UNIT_INFO_TABS.Manage, , intl.formatMessage(messages.manageTabTitle))} - {renderTab(UNIT_INFO_TABS.Settings, 'Unit Settings', intl.formatMessage(messages.settingsTabTitle))} ); }; -export default UnitInfo; +export default ContainerInfo; diff --git a/src/library-authoring/containers/ContainerInfoHeader.test.tsx b/src/library-authoring/containers/ContainerInfoHeader.test.tsx index 6349bfcbf..516cbae69 100644 --- a/src/library-authoring/containers/ContainerInfoHeader.test.tsx +++ b/src/library-authoring/containers/ContainerInfoHeader.test.tsx @@ -9,7 +9,7 @@ import { waitFor, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks'; import * as api from '../data/api'; import ContainerInfoHeader from './ContainerInfoHeader'; @@ -25,15 +25,15 @@ const { libraryIdReadOnly, } = mockContentLibrary; -const { containerId } = mockGetContainerMetadata; +const { unitId: containerId } = mockGetContainerMetadata; const render = (libraryId: string = mockLibraryId) => baseRender(, { extraWrapper: ({ children }) => ( { children } diff --git a/src/library-authoring/containers/ContainerInfoHeader.tsx b/src/library-authoring/containers/ContainerInfoHeader.tsx index a53be2065..2e5c78316 100644 --- a/src/library-authoring/containers/ContainerInfoHeader.tsx +++ b/src/library-authoring/containers/ContainerInfoHeader.tsx @@ -12,9 +12,9 @@ const ContainerInfoHeader = () => { const intl = useIntl(); const { readOnly } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); - const containerId = sidebarComponentInfo?.id; + const containerId = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!containerId) { throw new Error('containerId is required'); diff --git a/src/library-authoring/containers/ContainerOrganize.test.tsx b/src/library-authoring/containers/ContainerOrganize.test.tsx index 8fb366fa1..3e5c33f97 100644 --- a/src/library-authoring/containers/ContainerOrganize.test.tsx +++ b/src/library-authoring/containers/ContainerOrganize.test.tsx @@ -8,7 +8,7 @@ import { waitFor, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks'; import ContainerOrganize from './ContainerOrganize'; @@ -25,7 +25,7 @@ mockContentTaxonomyTagsData.applyMock(); const render = ({ libraryId = mockContentLibrary.libraryId, - containerId = mockGetContainerMetadata.containerId, + containerId = mockGetContainerMetadata.unitId, }: { libraryId?: string; containerId?: string; @@ -33,9 +33,9 @@ const render = ({ extraWrapper: ({ children }) => ( {children} @@ -77,12 +77,12 @@ describe('', () => { ...getConfig(), ENABLE_TAGGING_TAXONOMY_PAGES: 'true', }); - render({ containerId: mockGetContainerMetadata.containerIdForTags }); + render({ containerId: mockGetContainerMetadata.unitIdForTags }); expect(await screen.findByText('Tags (6)')).toBeInTheDocument(); }); it('should render collection count in collection info section', async () => { - render({ containerId: mockGetContainerMetadata.containerIdWithCollections }); + render({ containerId: mockGetContainerMetadata.unitIdWithCollections }); expect(await screen.findByText('Collections (1)')).toBeInTheDocument(); }); }); diff --git a/src/library-authoring/containers/ContainerOrganize.tsx b/src/library-authoring/containers/ContainerOrganize.tsx index 6419bfd43..b76670efd 100644 --- a/src/library-authoring/containers/ContainerOrganize.tsx +++ b/src/library-authoring/containers/ContainerOrganize.tsx @@ -27,10 +27,10 @@ const ContainerOrganize = () => { const [collectionsCollapseIsOpen, setCollectionsCollapseOpen, , toggleCollections] = useToggle(true); const { readOnly } = useLibraryContext(); - const { sidebarComponentInfo, sidebarAction } = useSidebarContext(); + const { sidebarItemInfo, sidebarAction } = useSidebarContext(); const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; - const containerId = sidebarComponentInfo?.id; + const containerId = sidebarItemInfo?.id; // istanbul ignore if: this should never happen if (!containerId) { throw new Error('containerId is required'); diff --git a/src/library-authoring/containers/HeaderActions.tsx b/src/library-authoring/containers/HeaderActions.tsx index 63e0a526d..be1871f71 100644 --- a/src/library-authoring/containers/HeaderActions.tsx +++ b/src/library-authoring/containers/HeaderActions.tsx @@ -1,50 +1,40 @@ import { Button } from '@openedx/paragon'; import { Add, InfoOutline } from '@openedx/paragon/icons'; import { useCallback } from 'react'; -import { ContainerType } from '../../generic/key-utils'; import { useLibraryContext } from '../common/context/LibraryContext'; import { useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; interface HeaderActionsProps { containerKey: string; - containerType: ContainerType; infoBtnText: string; addContentBtnText: string; } export const HeaderActions = ({ containerKey, - containerType, infoBtnText, addContentBtnText, }: HeaderActionsProps) => { const { readOnly } = useLibraryContext(); const { closeLibrarySidebar, - sidebarComponentInfo, - openUnitInfoSidebar, + sidebarItemInfo, + openContainerInfoSidebar, openAddContentSidebar, } = useSidebarContext(); const { navigateTo } = useLibraryRoutes(); - const infoSidebarIsOpen = sidebarComponentInfo?.id === containerKey; + const infoSidebarIsOpen = sidebarItemInfo?.id === containerKey; const handleOnClickInfoSidebar = useCallback(() => { if (infoSidebarIsOpen) { closeLibrarySidebar(); } else { - switch (containerType) { - case ContainerType.Unit: - openUnitInfoSidebar(containerKey); - break; - /* istanbul ignore next */ - default: - break; - } + openContainerInfoSidebar(containerKey); } - navigateTo({ [`${containerType}Id`]: containerKey }); - }, [containerKey, infoSidebarIsOpen, navigateTo]); + navigateTo({ containerId: containerKey }); + }, [containerKey, infoSidebarIsOpen, navigateTo, openContainerInfoSidebar]); return (
diff --git a/src/library-authoring/containers/UnitInfo.test.tsx b/src/library-authoring/containers/UnitInfo.test.tsx deleted file mode 100644 index e20e27f51..000000000 --- a/src/library-authoring/containers/UnitInfo.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import userEvent from '@testing-library/user-event'; -import type MockAdapter from 'axios-mock-adapter'; - -import { - initializeMocks, render as baseRender, screen, waitFor, - fireEvent, -} from '../../testUtils'; -import { mockContentLibrary, mockGetContainerChildren, mockGetContainerMetadata } from '../data/api.mocks'; -import { LibraryProvider } from '../common/context/LibraryContext'; -import UnitInfo from './UnitInfo'; -import { getLibraryContainerApiUrl, getLibraryContainerPublishApiUrl } from '../data/api'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; - -mockGetContainerMetadata.applyMock(); -mockContentLibrary.applyMock(); -mockGetContainerMetadata.applyMock(); -mockGetContainerChildren.applyMock(); - -const { libraryId } = mockContentLibrary; -const { containerId } = mockGetContainerMetadata; - -const render = (showOnlyPublished: boolean = false) => { - const params: { libraryId: string, unitId?: string } = { libraryId, unitId: containerId }; - return baseRender(, { - path: '/library/:libraryId/:unitId?', - params, - extraWrapper: ({ children }) => ( - - - {children} - - - ), - }); -}; -let axiosMock: MockAdapter; -let mockShowToast; - -describe('', () => { - beforeEach(() => { - ({ axiosMock, mockShowToast } = initializeMocks()); - }); - - it('should delete the unit using the menu', async () => { - axiosMock.onDelete(getLibraryContainerApiUrl(containerId)).reply(200); - render(); - - // Open menu - expect(await screen.findByTestId('unit-info-menu-toggle')).toBeInTheDocument(); - userEvent.click(screen.getByTestId('unit-info-menu-toggle')); - - // Click on Delete Item - const deleteMenuItem = screen.getByRole('button', { name: 'Delete' }); - expect(deleteMenuItem).toBeInTheDocument(); - fireEvent.click(deleteMenuItem); - - // Confirm delete Modal is open - expect(screen.getByText('Delete Unit')); - const deleteButton = screen.getByRole('button', { name: /delete/i }); - fireEvent.click(deleteButton); - - await waitFor(() => { - expect(axiosMock.history.delete.length).toBe(1); - }); - expect(mockShowToast).toHaveBeenCalled(); - }); - - it('can publish the container', async () => { - axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(200); - render(); - - // Click on Publish button - const publishButton = await screen.findByRole('button', { name: 'Publish' }); - expect(publishButton).toBeInTheDocument(); - userEvent.click(publishButton); - - await waitFor(() => { - expect(axiosMock.history.post.length).toBe(1); - }); - expect(mockShowToast).toHaveBeenCalledWith('All changes published'); - }); - - it('shows an error if publishing the container fails', async () => { - axiosMock.onPost(getLibraryContainerPublishApiUrl(containerId)).reply(500); - render(); - - // Click on Publish button - const publishButton = await screen.findByRole('button', { name: 'Publish' }); - expect(publishButton).toBeInTheDocument(); - userEvent.click(publishButton); - - await waitFor(() => { - expect(axiosMock.history.post.length).toBe(1); - }); - expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes'); - }); - - it('show only published content', async () => { - render(true); - expect(await screen.findByTestId('unit-info-menu-toggle')).toBeInTheDocument(); - expect(screen.getByText(/text block published 1/i)).toBeInTheDocument(); - }); - - it('shows the preview tab by default and the component are readonly', async () => { - render(); - const previewTab = await screen.findByText('Preview'); - expect(previewTab).toBeInTheDocument(); - expect(previewTab).toHaveAttribute('aria-selected', 'true'); - - // Check that there are no edit buttons for components titles - expect(screen.queryAllByRole('button', { name: /edit/i }).length).toBe(0); - - // Check that there are no drag handle for components - expect(screen.queryAllByRole('button', { name: 'Drag to reorder' }).length).toBe(0); - - // Check that there are no menu buttons for components - expect(screen.queryAllByRole('button', { name: /component actions menu/i }).length).toBe(0); - }); -}); diff --git a/src/library-authoring/containers/index.tsx b/src/library-authoring/containers/index.tsx index 66449377e..69a1596f5 100644 --- a/src/library-authoring/containers/index.tsx +++ b/src/library-authoring/containers/index.tsx @@ -1,4 +1,4 @@ -export { default as UnitInfo } from './UnitInfo'; +export { default as ContainerInfo } from './ContainerInfo'; export { default as ContainerInfoHeader } from './ContainerInfoHeader'; export { ContainerEditableTitle } from './ContainerEditableTitle'; export { HeaderActions } from './HeaderActions'; diff --git a/src/library-authoring/containers/messages.ts b/src/library-authoring/containers/messages.ts index fe8e0139e..1dec3cafc 100644 --- a/src/library-authoring/containers/messages.ts +++ b/src/library-authoring/containers/messages.ts @@ -1,10 +1,10 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ - openUnitButton: { + openButton: { id: 'course-authoring.library-authoring.container-sidebar.open-button', defaultMessage: 'Open', - description: 'Button text to open unit', + description: 'Button text to open container', }, previewTabTitle: { id: 'course-authoring.library-authoring.container-sidebar.preview-tab.title', diff --git a/src/library-authoring/create-container/CreateContainerModal.tsx b/src/library-authoring/create-container/CreateContainerModal.tsx index dab088e9e..a5fd56b61 100644 --- a/src/library-authoring/create-container/CreateContainerModal.tsx +++ b/src/library-authoring/create-container/CreateContainerModal.tsx @@ -87,7 +87,7 @@ const CreateContainerModal = () => { await updateItemsMutation.mutateAsync([container.id]); } // Navigate to the new container - navigateTo({ [`${containerType}Id`]: container.id }); + navigateTo({ containerId: container.id }); showToast(labels.successMsg); } catch (error) { showToast(labels.errorMsg); diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts index 9f5a4c975..0e70b148e 100644 --- a/src/library-authoring/data/api.mocks.ts +++ b/src/library-authoring/data/api.mocks.ts @@ -472,7 +472,7 @@ mockGetCollectionMetadata.applyMock = () => { */ export async function mockGetContainerMetadata(containerId: string): Promise { switch (containerId) { - case mockGetContainerMetadata.containerIdError: + case mockGetContainerMetadata.unitIdError: case mockGetContainerMetadata.sectionIdError: case mockGetContainerMetadata.subsectionIdError: throw createAxiosError({ @@ -480,11 +480,11 @@ export async function mockGetContainerMetadata(containerId: string): Promise { }); - case mockGetContainerMetadata.containerIdWithCollections: + case mockGetContainerMetadata.unitIdWithCollections: return Promise.resolve(mockGetContainerMetadata.containerDataWithCollections); case mockGetContainerMetadata.sectionId: case mockGetContainerMetadata.sectionIdEmpty: @@ -496,19 +496,19 @@ export async function mockGetContainerMetadata(containerId: string): Promise { export async function mockGetContainerChildren(containerId: string): Promise { let numChildren: number; switch (containerId) { - case mockGetContainerMetadata.containerId: + case mockGetContainerMetadata.unitId: case mockGetContainerMetadata.sectionId: case mockGetContainerMetadata.subsectionId: numChildren = 3; diff --git a/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx b/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx index 7f7b79d43..b90a29ded 100644 --- a/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx +++ b/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx @@ -85,10 +85,10 @@ describe('', () => { }); it('should show all collections in library and allow users to select for the current container', async () => { - const url = getLibraryContainerCollectionsUrl(mockGetContainerMetadata.containerIdWithCollections); + const url = getLibraryContainerCollectionsUrl(mockGetContainerMetadata.unitIdWithCollections); axiosMock.onPatch(url).reply(200); render(); diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index ddcddfafd..1d6a0e0ed 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -9,9 +9,9 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { AddContent, AddContentHeader } from '../add-content'; import { CollectionInfo, CollectionInfoHeader } from '../collections'; -import { ContainerInfoHeader, UnitInfo } from '../containers'; +import { ContainerInfoHeader, ContainerInfo } from '../containers'; import { - COMPONENT_INFO_TABS, SidebarActions, SidebarBodyComponentId, useSidebarContext, + COMPONENT_INFO_TABS, SidebarActions, SidebarBodyItemId, useSidebarContext, } from '../common/context/SidebarContext'; import { ComponentInfo, ComponentInfoHeader } from '../component-info'; import { LibraryInfo, LibraryInfoHeader } from '../library-info'; @@ -21,7 +21,7 @@ import messages from '../messages'; * Sidebar container for library pages. * * It's designed to "squash" the page when open. - * Uses `sidebarComponentInfo.type` of the `context` to + * Uses `sidebarItemInfo.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. @@ -31,7 +31,7 @@ const LibrarySidebar = () => { const { sidebarAction, setSidebarTab, - sidebarComponentInfo, + sidebarItemInfo, closeLibrarySidebar, } = useSidebarContext(); const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; @@ -41,31 +41,31 @@ const LibrarySidebar = () => { // Show Manage tab if JumpToManageCollections or JumpToManageTags action is set if (jumpToCollections || jumpToTags) { // COMPONENT_INFO_TABS.Manage works for containers as well as its value - // is same as UNIT_INFO_TABS.Manage. + // is same as CONTAINER_INFO_TABS.Manage. setSidebarTab(COMPONENT_INFO_TABS.Manage); } }, [jumpToCollections, setSidebarTab, jumpToTags]); const bodyComponentMap = { - [SidebarBodyComponentId.AddContent]: , - [SidebarBodyComponentId.Info]: , - [SidebarBodyComponentId.ComponentInfo]: , - [SidebarBodyComponentId.CollectionInfo]: , - [SidebarBodyComponentId.UnitInfo]: , + [SidebarBodyItemId.AddContent]: , + [SidebarBodyItemId.Info]: , + [SidebarBodyItemId.ComponentInfo]: , + [SidebarBodyItemId.CollectionInfo]: , + [SidebarBodyItemId.ContainerInfo]: , unknown: null, }; const headerComponentMap = { - [SidebarBodyComponentId.AddContent]: , - [SidebarBodyComponentId.Info]: , - [SidebarBodyComponentId.ComponentInfo]: , - [SidebarBodyComponentId.CollectionInfo]: , - [SidebarBodyComponentId.UnitInfo]: , + [SidebarBodyItemId.AddContent]: , + [SidebarBodyItemId.Info]: , + [SidebarBodyItemId.ComponentInfo]: , + [SidebarBodyItemId.CollectionInfo]: , + [SidebarBodyItemId.ContainerInfo]: , unknown: null, }; - const buildBody = () : React.ReactNode => bodyComponentMap[sidebarComponentInfo?.type || 'unknown']; - const buildHeader = (): React.ReactNode => headerComponentMap[sidebarComponentInfo?.type || 'unknown']; + const buildBody = () : React.ReactNode => bodyComponentMap[sidebarItemInfo?.type || 'unknown']; + const buildHeader = (): React.ReactNode => headerComponentMap[sidebarItemInfo?.type || 'unknown']; return ( diff --git a/src/library-authoring/routes.test.tsx b/src/library-authoring/routes.test.tsx index 57aff79cf..cc819602d 100644 --- a/src/library-authoring/routes.test.tsx +++ b/src/library-authoring/routes.test.tsx @@ -141,7 +141,7 @@ describe('Library Authoring routes', () => { }, destination: { params: { - unitId: 'lct:org:lib:unit:unitId', + containerId: 'lct:org:lib:unit:unitId', }, path: '/unit/lct:org:lib:unit:unitId', }, @@ -503,7 +503,7 @@ describe('Library Authoring routes', () => { path: `/library/:libraryId${origin.path}/*`, params: { libraryId: mockContentLibrary.libraryId, - unitId: '', + containerId: '', collectionId: '', selectedItemId: '', ...origin.params, diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index 7906cb641..57e5791cb 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -11,6 +11,7 @@ import { useSearchParams, type PathMatch, } from 'react-router-dom'; +import { ContainerType, getBlockType } from '../generic/key-utils'; export const BASE_ROUTE = '/library/:libraryId'; @@ -20,11 +21,11 @@ export const ROUTES = { COMPONENTS: '/components/:selectedItemId?', // * Collections tab, with an optionally selected collectionId in the sidebar. COLLECTIONS: '/collections/:selectedItemId?', - // * Sections tab, with an optionally selected sectionId in the sidebar. + // * Sections tab, with an optionally selected section in the sidebar. SECTIONS: '/sections/:selectedItemId?', - // * Subsections tab, with an optionally selected subsectionId in the sidebar. + // * Subsections tab, with an optionally selected subsection in the sidebar. SUBSECTIONS: '/subsections/:selectedItemId?', - // * Units tab, with an optionally selected unitId in the sidebar. + // * Units tab, with an optionally selected unit in the sidebar. UNITS: '/units/:selectedItemId?', // * All Content tab, with an optionally selected collection or unit in the sidebar. HOME: '/:selectedItemId?', @@ -32,14 +33,14 @@ export const ROUTES = { // * with a selected collectionId and/or an optionally selected componentId. COLLECTION: '/collection/:collectionId/:selectedItemId?', // LibrarySectionPage route: - // * with a selected sectionId and/or an optionally selected subsectionId. - SECTION: '/section/:sectionId/:selectedItemId?', + // * with a selected containerId and an optionally selected subsection. + SECTION: '/section/:containerId/:selectedItemId?', // LibrarySubsectionPage route: - // * with a selected subsectionId and/or an optionally selected unitId. - SUBSECTION: '/subsection/:subsectionId/:selectedItemId?', + // * with a selected containerId and an optionally selected unit. + SUBSECTION: '/subsection/:containerId/:selectedItemId?', // LibraryUnitPage route: - // * with a selected unitId and/or an optionally selected componentId. - UNIT: '/unit/:unitId/:selectedItemId?', + // * with a selected containerId and/or an optionally selected componentId. + UNIT: '/unit/:containerId/:selectedItemId?', }; export enum ContentType { @@ -56,10 +57,8 @@ export const allLibraryPageTabs: ContentType[] = Object.values(ContentType); export type NavigateToData = { selectedItemId?: string, collectionId?: string, + containerId?: string, contentType?: ContentType, - sectionId?: string, - subsectionId?: string, - unitId?: string, }; export type LibraryRoutesData = { @@ -118,18 +117,14 @@ export const useLibraryRoutes = (): LibraryRoutesData => { const navigateTo = useCallback(({ selectedItemId, collectionId, - sectionId, - subsectionId, - unitId, + containerId, contentType, }: NavigateToData = {}) => { const routeParams = { ...params, // Overwrite the params with the provided values. ...((selectedItemId !== undefined) && { selectedItemId }), - ...((sectionId !== undefined) && { sectionId }), - ...((subsectionId !== undefined) && { subsectionId }), - ...((unitId !== undefined) && { unitId }), + ...((containerId !== undefined) && { containerId }), ...((collectionId !== undefined) && { collectionId }), }; let route: string; @@ -140,24 +135,22 @@ export const useLibraryRoutes = (): LibraryRoutesData => { routeParams.selectedItemId = undefined; } - // Update sectionId/subsectionId/unitId/collectionId in library context if is not undefined. + // Update containerId/collectionId in library context if is not undefined. // Ids can be cleared from route by passing in empty string so we need to set it. - if (unitId !== undefined || sectionId !== undefined || subsectionId !== undefined) { + if (containerId !== undefined) { routeParams.selectedItemId = undefined; - // If we can have a unitId/subsectionId/sectionId alongside a routeParams.collectionId, + // If we can have a containerId alongside a routeParams.collectionId, // it means we are inside a collection trying to navigate to a unit/section/subsection, - // so we want to clear the collectionId to not have ambiquity. + // so we want to clear the collectionId to not have ambiguity. if (routeParams.collectionId !== undefined) { routeParams.collectionId = undefined; } } else if (collectionId !== undefined) { routeParams.selectedItemId = undefined; } else if (contentType) { - // We are navigating to the library home, so we need to clear the sectionId, subsectionId, unitId and collectionId - routeParams.unitId = undefined; - routeParams.sectionId = undefined; - routeParams.subsectionId = undefined; + // We are navigating to the library home, so we need to clear the containerId and collectionId + routeParams.containerId = undefined; routeParams.collectionId = undefined; } @@ -174,9 +167,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { // FIXME: We are using the Collection key, not the full OpaqueKey. So we // can't directly use the selectedItemId to determine if it's a collection. // We need to change this to use the full OpaqueKey in the future. - if (routeParams.selectedItemId?.includes(':unit:') - || routeParams.selectedItemId?.includes(':subsection:') - || routeParams.selectedItemId?.includes(':section:') + if (routeParams.selectedItemId?.startsWith('lct:') || routeParams.selectedItemId?.startsWith('lb:')) { routeParams.selectedItemId = undefined; } @@ -201,12 +192,24 @@ export const useLibraryRoutes = (): LibraryRoutesData => { route = ROUTES.SECTIONS; } else if (contentType === ContentType.home) { route = ROUTES.HOME; - } else if (routeParams.unitId) { - route = ROUTES.UNIT; - } else if (routeParams.subsectionId) { - route = ROUTES.SUBSECTION; - } else if (routeParams.sectionId) { - route = ROUTES.SECTION; + } else if (routeParams.containerId) { + const containerType = getBlockType(routeParams.containerId); + switch (containerType) { + case ContainerType.Unit: + route = ROUTES.UNIT; + break; + case ContainerType.Subsection: + route = ROUTES.SUBSECTION; + break; + case ContainerType.Section: + route = ROUTES.SECTION; + break; + default: + // Fall back to home if unrecognized container type + route = ROUTES.HOME; + routeParams.containerId = undefined; + break; + } } else if (routeParams.collectionId) { route = ROUTES.COLLECTION; // From here, we will just stay in the current route diff --git a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx index 3953a80df..70913afd6 100644 --- a/src/library-authoring/section-subsections/LibraryContainerChildren.tsx +++ b/src/library-authoring/section-subsections/LibraryContainerChildren.tsx @@ -84,11 +84,12 @@ const ContainerRow = ({ container, readOnly }: ContainerRowProps) => { )} - + {!readOnly && ( + + )} ); @@ -99,8 +100,8 @@ export const LibraryContainerChildren = ({ containerKey, readOnly }: LibraryCont const intl = useIntl(); const [orderedChildren, setOrderedChildren] = useState([]); const { showOnlyPublished, readOnly: libReadOnly } = useLibraryContext(); - const { navigateTo, insideSection, insideSubsection } = useLibraryRoutes(); - const { sidebarComponentInfo } = useSidebarContext(); + const { navigateTo, insideSection } = useLibraryRoutes(); + const { sidebarItemInfo } = useSidebarContext(); const [activeDraggingId, setActiveDraggingId] = useState(null); const orderMutator = useUpdateContainerChildren(containerKey); const { showToast } = useContext(ToastContext); @@ -142,10 +143,8 @@ export const LibraryContainerChildren = ({ containerKey, readOnly }: LibraryCont const doubleClicked = numberOfClicks > 1; if (!doubleClicked) { navigateTo({ selectedItemId: child.originalId }); - } else if (insideSection) { - navigateTo({ subsectionId: child.originalId }); - } else if (insideSubsection) { - navigateTo({ unitId: child.originalId }); + } else { + navigateTo({ containerId: child.originalId }); } }, [navigateTo]); @@ -200,10 +199,10 @@ export const LibraryContainerChildren = ({ containerKey, readOnly }: LibraryCont borderRadius: '8px', borderLeft: '8px solid #E1DDDB', }} - isClickable - onClick={(e) => handleChildClick(child, e.detail)} + isClickable={!readOnly} + onClick={(e) => !readOnly && handleChildClick(child, e.detail)} disabled={readOnly || libReadOnly} - cardClassName={sidebarComponentInfo?.id === child.originalId ? 'selected' : undefined} + cardClassName={sidebarItemInfo?.id === child.originalId ? 'selected' : undefined} actions={( { const intl = useIntl(); - const { libraryId, sectionId } = useLibraryContext(); + const { libraryId, containerId } = useLibraryContext(); const { - sidebarComponentInfo, + sidebarItemInfo, } = useSidebarContext(); - if (!sectionId || !libraryId) { + if (!containerId || !libraryId) { // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. - throw new Error('Rendered without sectionId or libraryId URL parameter'); + throw new Error('Rendered without containerId or libraryId URL parameter'); } const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); @@ -36,9 +35,9 @@ export const LibrarySectionPage = () => { isLoading, isError, error, - } = useContainer(sectionId); + } = useContainer(containerId); - // show loading if sectionId or libraryId is not set or section or library data is not fetched from index yet + // show loading if containerId or libraryId is not set or section or library data is not fetched from index yet if (isLibLoading || isLoading) { return ; } @@ -91,12 +90,11 @@ export const LibrarySectionPage = () => {
} />} + title={} />} breadcrumbs={breadcrumbs} headerActions={( @@ -105,7 +103,7 @@ export const LibrarySectionPage = () => { />
- + {
- {!!sidebarComponentInfo?.type && ( + {!!sidebarItemInfo?.type && (
', () => { expect(await screen.findByRole('button', { name: new RegExp(`${cType} Info`, 'i') })).toBeInTheDocument(); expect((await screen.findAllByRole('button', { name: 'Drag to reorder' })).length).toEqual(3); // check all children components are rendered. - expect(await screen.findByText(`${childType} block 0`)).toBeInTheDocument(); - expect(await screen.findByText(`${childType} block 1`)).toBeInTheDocument(); - expect(await screen.findByText(`${childType} block 2`)).toBeInTheDocument(); + expect((await screen.findAllByText(`${childType} block 0`))[0]).toBeInTheDocument(); + expect((await screen.findAllByText(`${childType} block 1`))[0]).toBeInTheDocument(); + expect((await screen.findAllByText(`${childType} block 2`))[0]).toBeInTheDocument(); }); it(`shows ${cType} data with no children`, async () => { @@ -148,7 +148,7 @@ describe('', () => { // unit info button expect(await screen.findByRole('button', { name: new RegExp(`${cType} Info`, 'i') })).toBeInTheDocument(); // check all children components are rendered. - expect(await screen.findByText(`This ${cType} is empty`)).toBeInTheDocument(); + expect((await screen.findAllByText(`This ${cType} is empty`))[0]).toBeInTheDocument(); }); it(`can rename ${cType}`, async () => { @@ -157,7 +157,7 @@ describe('', () => { : mockGetContainerMetadata.subsectionId; renderLibrarySectionPage(cId, undefined, cType); expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - expect(await screen.findByText(`Test ${cType}`)).toBeInTheDocument(); + expect((await screen.findAllByText(`Test ${cType}`))[0]).toBeInTheDocument(); const editContainerTitleButton = (await screen.findAllByRole( 'button', @@ -190,7 +190,7 @@ describe('', () => { : mockGetContainerMetadata.subsectionId; renderLibrarySectionPage(cId, undefined, cType); expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - expect(await screen.findByText(`Test ${cType}`)).toBeInTheDocument(); + expect((await screen.findAllByText(`Test ${cType}`))[0]).toBeInTheDocument(); const editContainerTitleButton = (await screen.findAllByRole( 'button', @@ -224,8 +224,8 @@ describe('', () => { axiosMock.onPatch(url).reply(200); renderLibrarySectionPage(undefined, undefined, cType); - // Wait loading of the component - await screen.findByText(`${childType} block 0`); + // Wait loading of the component (on page and in sidebar) + await screen.findAllByText(`${childType} block 0`); const editButton = (await screen.findAllByRole( 'button', @@ -257,8 +257,8 @@ describe('', () => { axiosMock.onPatch(url).reply(400); renderLibrarySectionPage(undefined, undefined, cType); - // Wait loading of the component - await screen.findByText(`${childType} block 0`); + // Wait loading of the component (on page and in sidebar) + await screen.findAllByText(`${childType} block 0`); const editButton = screen.getAllByRole( 'button', @@ -339,7 +339,7 @@ describe('', () => { it(`should open ${childType} page on double click`, async () => { renderLibrarySectionPage(undefined, undefined, cType); - const subsection = await screen.findByText(`${childType} block 0`); + const subsection = (await screen.findAllByText(`${childType} block 0`))[0]; // trigger double click userEvent.click(subsection.parentElement!, undefined, { clickCount: 2 }); expect((await screen.findAllByText(new RegExp(`Test ${childType}`, 'i')))[0]).toBeInTheDocument(); diff --git a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx index 5362f42d1..1b60d5d3a 100644 --- a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx @@ -18,7 +18,6 @@ import { messages, subsectionMessages } from './messages'; import { LibrarySidebar } from '../library-sidebar'; import { LibraryContainerChildren } from './LibraryContainerChildren'; import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers'; -import { ContainerType } from '../../generic/key-utils'; import { ContainerHit } from '../../search-manager'; interface OverflowLinksProps { @@ -54,16 +53,14 @@ const OverflowLinks = ({ children, to }: OverflowLinksProps) => { /** Full library subsection page */ export const LibrarySubsectionPage = () => { const intl = useIntl(); - const { libraryId, subsectionId } = useLibraryContext(); - const { - sidebarComponentInfo, - } = useSidebarContext(); + const { libraryId, containerId } = useLibraryContext(); + const { sidebarItemInfo } = useSidebarContext(); const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); // fetch subsectionData from index as it includes its parent sections as well. const { hits, isLoading, isError, error, - } = useContentFromSearchIndex(subsectionId ? [subsectionId] : []); + } = useContentFromSearchIndex(containerId ? [containerId] : []); const subsectionData = (hits as ContainerHit[])?.[0]; const breadcrumbs = useMemo(() => { @@ -102,9 +99,9 @@ export const LibrarySubsectionPage = () => { ); }, [libraryData, subsectionData, libraryId]); - if (!subsectionId || !libraryId) { + if (!containerId || !libraryId) { // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. - throw new Error('Rendered without subsectionId or libraryId URL parameter'); + throw new Error('Rendered without containerId or libraryId URL parameter'); } // Only show loading if section or library data is not fetched from index yet @@ -142,12 +139,11 @@ export const LibrarySubsectionPage = () => {
} />} + title={} />} breadcrumbs={breadcrumbs} headerActions={( @@ -156,7 +152,7 @@ export const LibrarySubsectionPage = () => { />
- + {
- {!!sidebarComponentInfo?.type && ( + {!!sidebarItemInfo?.type && (
const { navigateTo } = useLibraryRoutes(); const { openComponentEditor } = useLibraryContext(); - const { sidebarComponentInfo } = useSidebarContext(); + const { sidebarItemInfo } = useSidebarContext(); const handleComponentSelection = useCallback((numberOfClicks: number) => { navigateTo({ selectedItemId: block.originalId }); @@ -184,9 +184,9 @@ const ComponentBlock = ({ block, readOnly, isDragging }: ComponentBlockProps) => borderBottom: 'solid 1px #E1DDDB', }} isClickable={!readOnly} - onClick={!readOnly ? (e) => handleComponentSelection(e.detail) : undefined} + onClick={(e) => !readOnly && handleComponentSelection(e.detail)} disabled={readOnly} - cardClassName={sidebarComponentInfo?.id === block.originalId ? 'selected' : undefined} + cardClassName={sidebarItemInfo?.id === block.originalId ? 'selected' : undefined} > {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
', () => { const renderLibraryUnitPage = (unitId?: string, libraryId?: string) => { const libId = libraryId || mockContentLibrary.libraryId; - const uId = unitId || mockGetContainerMetadata.containerId; + const uId = unitId || mockGetContainerMetadata.unitId; render(, { path, routerProps: { @@ -75,14 +75,14 @@ describe('', () => { it('shows the spinner before the query is complete', async () => { // This mock will never return data about the collection (it loads forever): - renderLibraryUnitPage(mockGetContainerMetadata.containerIdLoading); + renderLibraryUnitPage(mockGetContainerMetadata.unitIdLoading); const spinner = screen.getByRole('status'); expect(spinner.textContent).toEqual('Loading...'); }); it('shows an error component if no unit returned', async () => { // This mock will simulate incorrect unit id - renderLibraryUnitPage(mockGetContainerMetadata.containerIdError); + renderLibraryUnitPage(mockGetContainerMetadata.unitIdError); const errorMessage = 'Not found'; expect(await screen.findByRole('alert')).toHaveTextContent(errorMessage); }); @@ -113,7 +113,7 @@ describe('', () => { )[0]; // 0 is the Unit Title, 1 is the first component on the list fireEvent.click(editUnitTitleButton); - const url = getLibraryContainerApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerApiUrl(mockGetContainerMetadata.unitId); axiosMock.onPatch(url).reply(200); await waitFor(() => { @@ -144,7 +144,7 @@ describe('', () => { )[0]; // 0 is the Unit Title, 1 is the first component on the list fireEvent.click(editUnitTitleButton); - const url = getLibraryContainerApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerApiUrl(mockGetContainerMetadata.unitId); axiosMock.onPatch(url).reply(400); await waitFor(() => { @@ -275,7 +275,7 @@ describe('', () => { renderLibraryUnitPage(); const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0]; axiosMock - .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) + .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId)) .reply(200); verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { @@ -289,7 +289,7 @@ describe('', () => { renderLibraryUnitPage(); const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0]; axiosMock - .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) + .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId)) .reply(200); verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { @@ -303,7 +303,7 @@ describe('', () => { renderLibraryUnitPage(); const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0]; axiosMock - .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) + .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId)) .reply(500); verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { @@ -314,7 +314,7 @@ describe('', () => { }); it('should remove a component & restore from component card', async () => { - const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(200); renderLibraryUnitPage(); @@ -334,7 +334,7 @@ describe('', () => { // @ts-ignore const restoreFn = mockShowToast.mock.calls[0][1].onClick; - const restoreUrl = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const restoreUrl = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onPost(restoreUrl).reply(200); // restore collection restoreFn(); @@ -345,7 +345,7 @@ describe('', () => { }); it('should show error on remove a component', async () => { - const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(404); renderLibraryUnitPage(); @@ -363,7 +363,7 @@ describe('', () => { }); it('should show error on restore removed component', async () => { - const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(200); renderLibraryUnitPage(); @@ -383,7 +383,7 @@ describe('', () => { // @ts-ignore const restoreFn = mockShowToast.mock.calls[0][1].onClick; - const restoreUrl = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const restoreUrl = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onPost(restoreUrl).reply(404); // restore collection restoreFn(); @@ -394,7 +394,7 @@ describe('', () => { }); it('should remove a component from component sidebar', async () => { - const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId); + const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(200); renderLibraryUnitPage(); diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx index 6fdd14257..1ab46f7b5 100644 --- a/src/library-authoring/units/LibraryUnitPage.tsx +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Breadcrumb, @@ -13,52 +12,28 @@ import SubHeader from '../../generic/sub-header/SubHeader'; import ErrorAlert from '../../generic/alert-error'; import Header from '../../header'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { - COLLECTION_INFO_TABS, COMPONENT_INFO_TABS, UNIT_INFO_TABS, useSidebarContext, -} from '../common/context/SidebarContext'; +import { useSidebarContext } from '../common/context/SidebarContext'; import { useContainer, useContentLibrary } from '../data/apiHooks'; import { LibrarySidebar } from '../library-sidebar'; import { SubHeaderTitle } from '../LibraryAuthoringPage'; import { LibraryUnitBlocks } from './LibraryUnitBlocks'; import messages from './messages'; import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers'; -import { ContainerType } from '../../generic/key-utils'; export const LibraryUnitPage = () => { const intl = useIntl(); const { libraryId, - unitId, + containerId, } = useLibraryContext(); // istanbul ignore if: this should never happen - if (!unitId) { - throw new Error('unitId is required'); + if (!containerId) { + throw new Error('containerId is required'); } - const { - sidebarComponentInfo, - setDefaultTab, - setHiddenTabs, - } = useSidebarContext(); - - useEffect(() => { - setDefaultTab({ - collection: COLLECTION_INFO_TABS.Details, - component: COMPONENT_INFO_TABS.Manage, - unit: UNIT_INFO_TABS.Manage, - }); - setHiddenTabs([COMPONENT_INFO_TABS.Preview, UNIT_INFO_TABS.Preview]); - return () => { - setDefaultTab({ - component: COMPONENT_INFO_TABS.Preview, - unit: UNIT_INFO_TABS.Preview, - collection: COLLECTION_INFO_TABS.Manage, - }); - setHiddenTabs([]); - }; - }, [setDefaultTab, setHiddenTabs]); + const { sidebarItemInfo } = useSidebarContext(); const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); const { @@ -66,11 +41,11 @@ export const LibraryUnitPage = () => { isLoading, isError, error, - } = useContainer(unitId); + } = useContainer(containerId); - if (!unitId || !libraryId) { + if (!containerId || !libraryId) { // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. - throw new Error('Rendered without unitId or libraryId URL parameter'); + throw new Error('Rendered without containerId or libraryId URL parameter'); } // Only show loading if unit or library data is not fetched from index yet @@ -122,11 +97,10 @@ export const LibraryUnitPage = () => {
} />} + title={} />} headerActions={( @@ -136,7 +110,7 @@ export const LibraryUnitPage = () => { />
- + {
- {!!sidebarComponentInfo?.type && ( + {!!sidebarItemInfo?.type && (