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:
Jillian
2025-06-09 13:28:58 -04:00
committed by GitHub
parent be13c18e5d
commit b3605fa1b8
58 changed files with 571 additions and 625 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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