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
This commit is contained in:
@@ -771,7 +771,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="header-actions">
|
||||
@@ -153,7 +153,7 @@ const LibraryAuthoringPage = ({
|
||||
showOnlyPublished,
|
||||
extraFilter: contextExtraFilter,
|
||||
} = useLibraryContext();
|
||||
const { sidebarComponentInfo } = useSidebarContext();
|
||||
const { sidebarItemInfo } = useSidebarContext();
|
||||
|
||||
const {
|
||||
insideCollections,
|
||||
@@ -333,7 +333,7 @@ const LibraryAuthoringPage = ({
|
||||
</Container>
|
||||
{!componentPickerMode && <StudioFooterSlot containerProps={{ size: undefined }} />}
|
||||
</div>
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
{!!sidebarItemInfo?.type && (
|
||||
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
|
||||
<LibrarySidebar />
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { LibrarySectionPage, LibrarySubsectionPage } from './section-subsections
|
||||
|
||||
const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const {
|
||||
libraryId, collectionId, unitId, sectionId, subsectionId,
|
||||
libraryId, collectionId, containerId,
|
||||
} = useParams();
|
||||
|
||||
if (libraryId === undefined) {
|
||||
@@ -30,11 +30,11 @@ const LibraryLayoutWrapper: React.FC<React.PropsWithChildren> = ({ children }) =
|
||||
|
||||
return (
|
||||
<LibraryProvider
|
||||
/** NOTE: We need to pass the collectionId or unitId as key to the LibraryProvider to force a re-render
|
||||
* when we navigate to a collection or unit page. This is necessary to make the back/forward navigation
|
||||
/** NOTE: We need to pass the collectionId or containerId as key to the LibraryProvider to force a re-render
|
||||
* when we navigate to a collection or container page. This is necessary to make the back/forward navigation
|
||||
* work correctly, as the LibraryProvider needs to rebuild the state from the URL.
|
||||
* */
|
||||
key={collectionId || sectionId || subsectionId || unitId}
|
||||
key={collectionId || containerId}
|
||||
libraryId={libraryId}
|
||||
/** NOTE: The component picker modal to use. We need to pass it as a reference instead of
|
||||
* directly importing it to avoid the import cycle:
|
||||
|
||||
@@ -48,10 +48,10 @@ const render = (collectionId?: string) => {
|
||||
),
|
||||
});
|
||||
};
|
||||
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(<AddContent />, {
|
||||
path: '/library/:libraryId/unit/:unitId?',
|
||||
path: '/library/:libraryId/unit/:containerId?',
|
||||
params,
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
@@ -328,7 +328,7 @@ describe('<AddContent />', () => {
|
||||
|
||||
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('<AddContent />', () => {
|
||||
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('<AddContent />', () => {
|
||||
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);
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -53,13 +53,13 @@ const getIdFromContext = (context: ContextType) => {
|
||||
const render = (context: ContextType) => baseRender(
|
||||
<PickLibraryContentModal isOpen onClose={onClose} />,
|
||||
{
|
||||
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 }) => (
|
||||
<LibraryProvider
|
||||
|
||||
@@ -41,9 +41,7 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
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<PickLibraryContentModalProps> = (
|
||||
} = 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<PickLibraryContentModalProps> = (
|
||||
.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<PickLibraryContentModalProps> = (
|
||||
insideSubsection,
|
||||
insideUnit,
|
||||
collectionId,
|
||||
sectionId,
|
||||
subsectionId,
|
||||
unitId,
|
||||
containerId,
|
||||
]);
|
||||
|
||||
// determine filter an visibleTabs based on current location
|
||||
@@ -123,8 +114,8 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
}
|
||||
|
||||
// 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 (
|
||||
|
||||
@@ -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(<CollectionDetails />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={library.id}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: collectionId,
|
||||
type: SidebarBodyComponentId.CollectionInfo,
|
||||
type: SidebarBodyItemId.CollectionInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(<CollectionInfo
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: collectionId,
|
||||
type: SidebarBodyComponentId.CollectionInfo,
|
||||
type: SidebarBodyItemId.CollectionInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -445,9 +445,10 @@ describe('<LibraryCollectionPage />', () => {
|
||||
])('$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('<LibraryCollectionPage />', () => {
|
||||
// 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}"]`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
</Container>
|
||||
{!componentPickerMode && <StudioFooterSlot containerProps={{ size: undefined }} />}
|
||||
</div>
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
{!!sidebarItemInfo?.type && (
|
||||
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
|
||||
<LibrarySidebar />
|
||||
</div>
|
||||
|
||||
@@ -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<LibraryContextData>(() => {
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string>(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<string>(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<string>(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<SidebarInfoTab>;
|
||||
setHiddenTabs: (tabs: ComponentInfoTab[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -106,7 +107,7 @@ const SidebarContext = createContext<SidebarContextData | undefined>(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<SidebarComponentInfo | undefined>(
|
||||
initialSidebarComponentInfo,
|
||||
const [sidebarItemInfo, setSidebarItemInfo] = useState<SidebarItemInfo | undefined>(
|
||||
initialSidebarItemInfo,
|
||||
);
|
||||
|
||||
const [defaultTab, setDefaultTab] = useState<DefaultTabs>({
|
||||
component: COMPONENT_INFO_TABS.Preview,
|
||||
unit: UNIT_INFO_TABS.Preview,
|
||||
collection: COLLECTION_INFO_TABS.Manage,
|
||||
});
|
||||
const [defaultTab, setDefaultTab] = useState<DefaultTabs>(DEFAULT_TAB);
|
||||
const [hiddenTabs, setHiddenTabs] = useState<Array<SidebarInfoTab>>([]);
|
||||
|
||||
const [sidebarTab, setSidebarTab] = useStateWithUrlSearchParam<SidebarInfoTab>(
|
||||
@@ -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<SidebarContextData>(() => {
|
||||
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;
|
||||
|
||||
@@ -19,9 +19,9 @@ import messages from './messages';
|
||||
export const ComponentAdvancedAssets: React.FC<Record<never, never>> = () => {
|
||||
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');
|
||||
|
||||
@@ -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 }) => (
|
||||
<LibraryProvider libraryId={libraryId} showOnlyPublished={showOnlyPublished}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -23,9 +23,9 @@ import { ComponentAdvancedAssets } from './ComponentAdvancedAssets';
|
||||
const ComponentAdvancedInfoInner: React.FC<Record<never, never>> = () => {
|
||||
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');
|
||||
|
||||
@@ -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(<ComponentDetails />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: sidebarComponentUsageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(<ComponentInfoH
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(<ComponentMa
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId || mockContentLibrary.libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(<ComponentPreview />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: usageKey,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
<BaseCard
|
||||
|
||||
@@ -38,9 +38,9 @@ interface Props {
|
||||
|
||||
const ComponentDeleter = ({ usageKey, ...props }: Props) => {
|
||||
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 () => {
|
||||
|
||||
@@ -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 }) => {
|
||||
<Dropdown.Item onClick={updateClipboardClick}>
|
||||
<FormattedMessage {...messages.menuCopyToClipboard} />
|
||||
</Dropdown.Item>
|
||||
{unitId && (
|
||||
{containerId && (
|
||||
<Dropdown.Item onClick={removeFromContainer}>
|
||||
<FormattedMessage {...messages.removeComponentFromUnitMenu} />
|
||||
</Dropdown.Item>
|
||||
|
||||
@@ -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
|
||||
<Dropdown.Item onClick={openContainer}>
|
||||
<FormattedMessage {...messages.menuOpen} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={confirmDelete} disabled={containerType !== 'unit'}>
|
||||
<Dropdown.Item onClick={confirmDelete}>
|
||||
<FormattedMessage {...messages.menuDeleteContainer} />
|
||||
</Dropdown.Item>
|
||||
{insideCollection && (
|
||||
@@ -96,7 +95,7 @@ export const ContainerMenu = ({ containerKey, containerType, displayName } : Con
|
||||
<FormattedMessage {...messages.menuRemoveFromCollection} />
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item onClick={showManageCollections} disabled={containerType !== 'unit'}>
|
||||
<Dropdown.Item onClick={showManageCollections}>
|
||||
<FormattedMessage {...messages.menuAddToCollection} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
@@ -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 (
|
||||
<BaseCard
|
||||
@@ -287,7 +264,6 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
) : (
|
||||
<ContainerMenu
|
||||
containerKey={containerKey}
|
||||
containerType={itemType}
|
||||
displayName={hit.displayName}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<DeleteModal
|
||||
|
||||
145
src/library-authoring/containers/ContainerInfo.test.tsx
Normal file
145
src/library-authoring/containers/ContainerInfo.test.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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 ContainerInfo from './ContainerInfo';
|
||||
import { getLibraryContainerApiUrl, getLibraryContainerPublishApiUrl } from '../data/api';
|
||||
import { SidebarBodyItemId, SidebarProvider } from '../common/context/SidebarContext';
|
||||
|
||||
mockGetContainerMetadata.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockGetContainerMetadata.applyMock();
|
||||
mockGetContainerChildren.applyMock();
|
||||
|
||||
// TODO Remove this to un-skip section/subsection tests, when implemented
|
||||
const testIf = (condition) => (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(<ContainerInfo />, {
|
||||
path: '/library/:libraryId/:selectedItemId?',
|
||||
params,
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
showOnlyPublished={showOnlyPublished}
|
||||
>
|
||||
<SidebarProvider
|
||||
initialSidebarItemInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyItemId.ContainerInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
describe('<ContainerInfo />', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<>
|
||||
<Dropdown id="unit-info-dropdown">
|
||||
<Dropdown id="container-info-dropdown">
|
||||
<Dropdown.Toggle
|
||||
id="unit-info-menu-toggle"
|
||||
id="container-info-menu-toggle"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt={intl.formatMessage(componentMessages.containerCardMenuAlt)}
|
||||
data-testid="unit-info-menu-toggle"
|
||||
data-testid="container-info-menu-toggle"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={confirmDelete}>
|
||||
@@ -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 <LibraryUnitBlocks unitId={containerId} readOnly />;
|
||||
}
|
||||
return <LibraryContainerChildren containerKey={containerId} readOnly />;
|
||||
};
|
||||
|
||||
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}
|
||||
</Tab>
|
||||
);
|
||||
}, [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 (
|
||||
<Stack>
|
||||
<div className="d-flex flex-wrap">
|
||||
{showOpenUnitButton && (
|
||||
{showOpenButton && (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
className="m-1 text-nowrap flex-grow-1"
|
||||
as={Link}
|
||||
to={`/library/${libraryId}/unit/${unitId}`}
|
||||
to={`/library/${libraryId}/${containerType}/${containerId}`}
|
||||
>
|
||||
{intl.formatMessage(messages.openUnitButton)}
|
||||
{intl.formatMessage(messages.openButton)}
|
||||
</Button>
|
||||
)}
|
||||
{!componentPickerMode && !readOnly && (
|
||||
@@ -148,9 +166,9 @@ const UnitInfo = () => {
|
||||
{intl.formatMessage(messages.publishContainerButton)}
|
||||
</Button>
|
||||
)}
|
||||
{showOpenUnitButton && ( // Check: should we still show this on the unit page?
|
||||
<UnitMenu
|
||||
containerId={unitId}
|
||||
{showOpenButton && (
|
||||
<ContainerMenu
|
||||
containerId={containerId}
|
||||
displayName={container.displayName}
|
||||
/>
|
||||
)}
|
||||
@@ -158,20 +176,28 @@ const UnitInfo = () => {
|
||||
<Tabs
|
||||
variant="tabs"
|
||||
className="my-3 d-flex justify-content-around"
|
||||
defaultActiveKey={defaultTab.unit}
|
||||
defaultActiveKey={defaultContainerTab}
|
||||
activeKey={tab}
|
||||
onSelect={handleTabChange}
|
||||
>
|
||||
{renderTab(
|
||||
UNIT_INFO_TABS.Preview,
|
||||
<LibraryUnitBlocks unitId={unitId} readOnly />,
|
||||
CONTAINER_INFO_TABS.Preview,
|
||||
intl.formatMessage(messages.previewTabTitle),
|
||||
<ContainerPreview containerId={containerId} />,
|
||||
)}
|
||||
{renderTab(
|
||||
CONTAINER_INFO_TABS.Manage,
|
||||
intl.formatMessage(messages.manageTabTitle),
|
||||
<ContainerOrganize />,
|
||||
)}
|
||||
{renderTab(
|
||||
CONTAINER_INFO_TABS.Settings,
|
||||
intl.formatMessage(messages.settingsTabTitle),
|
||||
// TODO: container settings component
|
||||
)}
|
||||
{renderTab(UNIT_INFO_TABS.Manage, <ContainerOrganize />, intl.formatMessage(messages.manageTabTitle))}
|
||||
{renderTab(UNIT_INFO_TABS.Settings, 'Unit Settings', intl.formatMessage(messages.settingsTabTitle))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnitInfo;
|
||||
export default ContainerInfo;
|
||||
@@ -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(<ContainerInfoHeader />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.UnitInfo,
|
||||
type: SidebarBodyItemId.ContainerInfo,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
initialSidebarItemInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.ComponentInfo,
|
||||
type: SidebarBodyItemId.ComponentInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -77,12 +77,12 @@ describe('<ContainerOrganize />', () => {
|
||||
...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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 (
|
||||
<div className="header-actions">
|
||||
|
||||
@@ -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(<UnitInfo />, {
|
||||
path: '/library/:libraryId/:unitId?',
|
||||
params,
|
||||
extraWrapper: ({ children }) => (
|
||||
<LibraryProvider
|
||||
libraryId={libraryId}
|
||||
showOnlyPublished={showOnlyPublished}
|
||||
>
|
||||
<SidebarProvider
|
||||
initialSidebarComponentInfo={{
|
||||
id: containerId,
|
||||
type: SidebarBodyComponentId.UnitInfo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</LibraryProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
describe('<UnitInfo />', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -472,7 +472,7 @@ mockGetCollectionMetadata.applyMock = () => {
|
||||
*/
|
||||
export async function mockGetContainerMetadata(containerId: string): Promise<api.Container> {
|
||||
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<api
|
||||
message: 'Not found.',
|
||||
path: api.getLibraryContainerApiUrl(containerId),
|
||||
});
|
||||
case mockGetContainerMetadata.containerIdLoading:
|
||||
case mockGetContainerMetadata.unitIdLoading:
|
||||
case mockGetContainerMetadata.sectionIdLoading:
|
||||
case mockGetContainerMetadata.subsectionIdLoading:
|
||||
return new 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<api
|
||||
return Promise.resolve(mockGetContainerMetadata.containerData);
|
||||
}
|
||||
}
|
||||
mockGetContainerMetadata.containerId = 'lct:org:lib:unit:test-unit-9a207';
|
||||
mockGetContainerMetadata.unitId = 'lct:org:lib:unit:test-unit-9a207';
|
||||
mockGetContainerMetadata.sectionId = 'lct:org:lib:section:test-section-1';
|
||||
mockGetContainerMetadata.subsectionId = 'lb:org1:Demo_course:subsection:subsection-0';
|
||||
mockGetContainerMetadata.sectionIdEmpty = 'lct:org:lib:section:test-section-empty';
|
||||
mockGetContainerMetadata.subsectionIdEmpty = 'lb:org1:Demo_course:subsection:subsection-empty';
|
||||
mockGetContainerMetadata.containerIdError = 'lct:org:lib:unit:container_error';
|
||||
mockGetContainerMetadata.unitIdError = 'lct:org:lib:unit:container_error';
|
||||
mockGetContainerMetadata.sectionIdError = 'lct:org:lib:section:section_error';
|
||||
mockGetContainerMetadata.subsectionIdError = 'lct:org:lib:section:section_error';
|
||||
mockGetContainerMetadata.containerIdLoading = 'lct:org:lib:unit:container_loading';
|
||||
mockGetContainerMetadata.unitIdLoading = 'lct:org:lib:unit:container_loading';
|
||||
mockGetContainerMetadata.sectionIdLoading = 'lct:org:lib:section:section_loading';
|
||||
mockGetContainerMetadata.subsectionIdLoading = 'lct:org:lib:subsection:subsection_loading';
|
||||
mockGetContainerMetadata.containerIdForTags = mockContentTaxonomyTagsData.containerTagsId;
|
||||
mockGetContainerMetadata.containerIdWithCollections = 'lct:org:lib:unit:container_collections';
|
||||
mockGetContainerMetadata.unitIdForTags = mockContentTaxonomyTagsData.containerTagsId;
|
||||
mockGetContainerMetadata.unitIdWithCollections = 'lct:org:lib:unit:container_collections';
|
||||
mockGetContainerMetadata.containerData = {
|
||||
id: 'lct:org:lib:unit:test-unit-9a2072',
|
||||
containerType: ContainerType.Unit,
|
||||
@@ -541,7 +541,7 @@ mockGetContainerMetadata.subsectionData = {
|
||||
} satisfies api.Container;
|
||||
mockGetContainerMetadata.containerDataWithCollections = {
|
||||
...mockGetContainerMetadata.containerData,
|
||||
id: mockGetContainerMetadata.containerIdWithCollections,
|
||||
id: mockGetContainerMetadata.unitIdWithCollections,
|
||||
collections: [{ title: 'My first collection', key: 'my-first-collection' }],
|
||||
} satisfies api.Container;
|
||||
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
|
||||
@@ -557,7 +557,7 @@ mockGetContainerMetadata.applyMock = () => {
|
||||
export async function mockGetContainerChildren(containerId: string): Promise<api.LibraryBlockMetadata[]> {
|
||||
let numChildren: number;
|
||||
switch (containerId) {
|
||||
case mockGetContainerMetadata.containerId:
|
||||
case mockGetContainerMetadata.unitId:
|
||||
case mockGetContainerMetadata.sectionId:
|
||||
case mockGetContainerMetadata.subsectionId:
|
||||
numChildren = 3;
|
||||
|
||||
@@ -85,10 +85,10 @@ describe('<ManageCollections />', () => {
|
||||
});
|
||||
|
||||
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(<ManageCollections
|
||||
opaqueKey={mockGetContainerMetadata.containerIdWithCollections}
|
||||
opaqueKey={mockGetContainerMetadata.unitIdWithCollections}
|
||||
collections={[{ title: 'My first collection', key: 'my-first-collection' }]}
|
||||
useUpdateCollectionsHook={useUpdateContainerCollections}
|
||||
/>);
|
||||
|
||||
@@ -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]: <AddContent />,
|
||||
[SidebarBodyComponentId.Info]: <LibraryInfo />,
|
||||
[SidebarBodyComponentId.ComponentInfo]: <ComponentInfo />,
|
||||
[SidebarBodyComponentId.CollectionInfo]: <CollectionInfo />,
|
||||
[SidebarBodyComponentId.UnitInfo]: <UnitInfo />,
|
||||
[SidebarBodyItemId.AddContent]: <AddContent />,
|
||||
[SidebarBodyItemId.Info]: <LibraryInfo />,
|
||||
[SidebarBodyItemId.ComponentInfo]: <ComponentInfo />,
|
||||
[SidebarBodyItemId.CollectionInfo]: <CollectionInfo />,
|
||||
[SidebarBodyItemId.ContainerInfo]: <ContainerInfo />,
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
const headerComponentMap = {
|
||||
[SidebarBodyComponentId.AddContent]: <AddContentHeader />,
|
||||
[SidebarBodyComponentId.Info]: <LibraryInfoHeader />,
|
||||
[SidebarBodyComponentId.ComponentInfo]: <ComponentInfoHeader />,
|
||||
[SidebarBodyComponentId.CollectionInfo]: <CollectionInfoHeader />,
|
||||
[SidebarBodyComponentId.UnitInfo]: <ContainerInfoHeader />,
|
||||
[SidebarBodyItemId.AddContent]: <AddContentHeader />,
|
||||
[SidebarBodyItemId.Info]: <LibraryInfoHeader />,
|
||||
[SidebarBodyItemId.ComponentInfo]: <ComponentInfoHeader />,
|
||||
[SidebarBodyItemId.CollectionInfo]: <CollectionInfoHeader />,
|
||||
[SidebarBodyItemId.ContainerInfo]: <ContainerInfoHeader />,
|
||||
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 (
|
||||
<Stack gap={4} className="p-3 text-primary-700">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,11 +84,12 @@ const ContainerRow = ({ container, readOnly }: ContainerRowProps) => {
|
||||
</Badge>
|
||||
)}
|
||||
<TagCount size="sm" count={container.tagsCount} />
|
||||
<ContainerMenu
|
||||
containerKey={container.originalId}
|
||||
containerType={container.containerType}
|
||||
displayName={container.displayName}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<ContainerMenu
|
||||
containerKey={container.originalId}
|
||||
displayName={container.displayName}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
@@ -99,8 +100,8 @@ export const LibraryContainerChildren = ({ containerKey, readOnly }: LibraryCont
|
||||
const intl = useIntl();
|
||||
const [orderedChildren, setOrderedChildren] = useState<LibraryContainerMetadataWithUniqueId[]>([]);
|
||||
const { showOnlyPublished, readOnly: libReadOnly } = useLibraryContext();
|
||||
const { navigateTo, insideSection, insideSubsection } = useLibraryRoutes();
|
||||
const { sidebarComponentInfo } = useSidebarContext();
|
||||
const { navigateTo, insideSection } = useLibraryRoutes();
|
||||
const { sidebarItemInfo } = useSidebarContext();
|
||||
const [activeDraggingId, setActiveDraggingId] = useState<string | null>(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={(
|
||||
<ContainerRow
|
||||
containerKey={containerKey}
|
||||
|
||||
@@ -15,19 +15,18 @@ import { messages, sectionMessages } from './messages';
|
||||
import { LibrarySidebar } from '../library-sidebar';
|
||||
import { LibraryContainerChildren } from './LibraryContainerChildren';
|
||||
import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers';
|
||||
import { ContainerType } from '../../generic/key-utils';
|
||||
|
||||
/** Full library section page */
|
||||
export const LibrarySectionPage = () => {
|
||||
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 <Loading />;
|
||||
}
|
||||
@@ -91,12 +90,11 @@ export const LibrarySectionPage = () => {
|
||||
<Container className="px-0 mt-4 mb-5 library-authoring-page bg-white">
|
||||
<div className="px-4 bg-light-200 border-bottom mb-2">
|
||||
<SubHeader
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={sectionId} />} />}
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={containerId} />} />}
|
||||
breadcrumbs={breadcrumbs}
|
||||
headerActions={(
|
||||
<HeaderActions
|
||||
containerKey={sectionId}
|
||||
containerType={ContainerType.Section}
|
||||
containerKey={containerId}
|
||||
infoBtnText={intl.formatMessage(sectionMessages.infoButtonText)}
|
||||
addContentBtnText={intl.formatMessage(sectionMessages.newContentButton)}
|
||||
/>
|
||||
@@ -105,7 +103,7 @@ export const LibrarySectionPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<Container className="px-4 py-4">
|
||||
<LibraryContainerChildren containerKey={sectionId} />
|
||||
<LibraryContainerChildren containerKey={containerId} />
|
||||
<FooterActions
|
||||
addContentBtnText={intl.formatMessage(sectionMessages.addContentButton)}
|
||||
addExistingContentBtnText={intl.formatMessage(sectionMessages.addExistingContentButton)}
|
||||
@@ -113,7 +111,7 @@ export const LibrarySectionPage = () => {
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
{!!sidebarItemInfo?.type && (
|
||||
<div
|
||||
className="library-authoring-sidebar box-shadow-left-1 bg-white"
|
||||
data-testid="library-sidebar"
|
||||
|
||||
@@ -132,9 +132,9 @@ describe('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
// 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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
: 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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
: 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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
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('<LibrarySectionPage / LibrarySubsectionPage />', () => {
|
||||
|
||||
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();
|
||||
|
||||
@@ -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 = () => {
|
||||
<Container className="px-0 mt-4 mb-5 library-authoring-page bg-white">
|
||||
<div className="px-4 bg-light-200 border-bottom mb-2">
|
||||
<SubHeader
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={subsectionId} />} />}
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={containerId} />} />}
|
||||
breadcrumbs={breadcrumbs}
|
||||
headerActions={(
|
||||
<HeaderActions
|
||||
containerKey={subsectionId}
|
||||
containerType={ContainerType.Subsection}
|
||||
containerKey={containerId}
|
||||
infoBtnText={intl.formatMessage(subsectionMessages.infoButtonText)}
|
||||
addContentBtnText={intl.formatMessage(subsectionMessages.newContentButton)}
|
||||
/>
|
||||
@@ -156,7 +152,7 @@ export const LibrarySubsectionPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<Container className="px-4 py-4">
|
||||
<LibraryContainerChildren containerKey={subsectionId} />
|
||||
<LibraryContainerChildren containerKey={containerId} />
|
||||
<FooterActions
|
||||
addContentBtnText={intl.formatMessage(subsectionMessages.addContentButton)}
|
||||
addExistingContentBtnText={intl.formatMessage(subsectionMessages.addExistingContentButton)}
|
||||
@@ -164,7 +160,7 @@ export const LibrarySubsectionPage = () => {
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
{!!sidebarItemInfo?.type && (
|
||||
<div
|
||||
className="library-authoring-sidebar box-shadow-left-1 bg-white"
|
||||
data-testid="library-sidebar"
|
||||
|
||||
@@ -135,7 +135,7 @@ const ComponentBlock = ({ block, readOnly, isDragging }: ComponentBlockProps) =>
|
||||
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 */}
|
||||
<div
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
const renderLibraryUnitPage = (unitId?: string, libraryId?: string) => {
|
||||
const libId = libraryId || mockContentLibrary.libraryId;
|
||||
const uId = unitId || mockGetContainerMetadata.containerId;
|
||||
const uId = unitId || mockGetContainerMetadata.unitId;
|
||||
render(<LibraryLayout />, {
|
||||
path,
|
||||
routerProps: {
|
||||
@@ -75,14 +75,14 @@ describe('<LibraryUnitPage />', () => {
|
||||
|
||||
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('<LibraryUnitPage />', () => {
|
||||
)[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('<LibraryUnitPage />', () => {
|
||||
)[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('<LibraryUnitPage />', () => {
|
||||
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('<LibraryUnitPage />', () => {
|
||||
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('<LibraryUnitPage />', () => {
|
||||
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('<LibraryUnitPage />', () => {
|
||||
});
|
||||
|
||||
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('<LibraryUnitPage />', () => {
|
||||
// @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('<LibraryUnitPage />', () => {
|
||||
});
|
||||
|
||||
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('<LibraryUnitPage />', () => {
|
||||
});
|
||||
|
||||
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('<LibraryUnitPage />', () => {
|
||||
// @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('<LibraryUnitPage />', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
<Container className="px-0 mt-4 mb-5 library-authoring-page bg-white">
|
||||
<div className="px-4 bg-light-200 border-bottom mb-2">
|
||||
<SubHeader
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={unitId} />} />}
|
||||
title={<SubHeaderTitle title={<ContainerEditableTitle containerId={containerId} />} />}
|
||||
headerActions={(
|
||||
<HeaderActions
|
||||
containerKey={unitId}
|
||||
containerType={ContainerType.Unit}
|
||||
containerKey={containerId}
|
||||
infoBtnText={intl.formatMessage(messages.infoButtonText)}
|
||||
addContentBtnText={intl.formatMessage(messages.addContentButton)}
|
||||
/>
|
||||
@@ -136,7 +110,7 @@ export const LibraryUnitPage = () => {
|
||||
/>
|
||||
</div>
|
||||
<Container className="px-4 py-4">
|
||||
<LibraryUnitBlocks unitId={unitId} />
|
||||
<LibraryUnitBlocks unitId={containerId} />
|
||||
<FooterActions
|
||||
addContentBtnText={intl.formatMessage(messages.newContentButton)}
|
||||
addExistingContentBtnText={intl.formatMessage(messages.addExistingContentButton)}
|
||||
@@ -144,7 +118,7 @@ export const LibraryUnitPage = () => {
|
||||
</Container>
|
||||
</Container>
|
||||
</div>
|
||||
{!!sidebarComponentInfo?.type && (
|
||||
{!!sidebarItemInfo?.type && (
|
||||
<div
|
||||
className="library-authoring-sidebar box-shadow-left-1 bg-white"
|
||||
data-testid="library-sidebar"
|
||||
|
||||
Reference in New Issue
Block a user