Files
frontend-app-authoring/src/library-authoring/units/LibraryUnitPage.tsx
Rômulo Penido d9dcdfe1e3 feat: add existing components to unit [FC-0083] (#1811)
allows adding existing components to units
2025-04-15 16:49:53 -05:00

241 lines
6.6 KiB
TypeScript

import { useIntl } from '@edx/frontend-platform/i18n';
import {
Breadcrumb,
Button,
Container,
} from '@openedx/paragon';
import { Add, InfoOutline } from '@openedx/paragon/icons';
import { useCallback, useContext, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import Loading from '../../generic/Loading';
import NotFoundAlert from '../../generic/NotFoundAlert';
import SubHeader from '../../generic/sub-header/SubHeader';
import ErrorAlert from '../../generic/alert-error';
import { InplaceTextEditor } from '../../generic/inplace-text-editor';
import { ToastContext } from '../../generic/toast-context';
import Header from '../../header';
import { useLibraryContext } from '../common/context/LibraryContext';
import {
COLLECTION_INFO_TABS, COMPONENT_INFO_TABS, SidebarBodyComponentId, UNIT_INFO_TABS, useSidebarContext,
} from '../common/context/SidebarContext';
import { useContainer, useUpdateContainer, useContentLibrary } from '../data/apiHooks';
import { LibrarySidebar } from '../library-sidebar';
import { SubHeaderTitle } from '../LibraryAuthoringPage';
import { useLibraryRoutes } from '../routes';
import { LibraryUnitBlocks } from './LibraryUnitBlocks';
import messages from './messages';
interface EditableTitleProps {
unitId: string;
}
const EditableTitle = ({ unitId }: EditableTitleProps) => {
const intl = useIntl();
const { libraryId, readOnly } = useLibraryContext();
const { data: container } = useContainer(libraryId, unitId);
const updateMutation = useUpdateContainer(unitId);
const { showToast } = useContext(ToastContext);
const handleSaveDisplayName = (newDisplayName: string) => {
updateMutation.mutateAsync({
displayName: newDisplayName,
}).then(() => {
showToast(intl.formatMessage(messages.updateContainerSuccessMsg));
}).catch(() => {
showToast(intl.formatMessage(messages.updateContainerErrorMsg));
});
};
// istanbul ignore if: this should never happen
if (!container) {
return null;
}
return (
<InplaceTextEditor
onSave={handleSaveDisplayName}
text={container.displayName}
readOnly={readOnly}
/>
);
};
const HeaderActions = () => {
const intl = useIntl();
const { unitId, readOnly } = useLibraryContext();
const {
openAddContentSidebar,
closeLibrarySidebar,
openUnitInfoSidebar,
sidebarComponentInfo,
} = useSidebarContext();
const { navigateTo } = useLibraryRoutes();
// istanbul ignore if: this should never happen
if (!unitId) {
throw new Error('it should not be possible to render HeaderActions without a unitId');
}
const infoSidebarIsOpen = sidebarComponentInfo?.type === SidebarBodyComponentId.UnitInfo
&& sidebarComponentInfo?.id === unitId;
const handleOnClickInfoSidebar = useCallback(() => {
if (infoSidebarIsOpen) {
closeLibrarySidebar();
} else {
openUnitInfoSidebar(unitId);
}
navigateTo({ unitId });
}, [unitId, infoSidebarIsOpen]);
return (
<div className="header-actions">
<Button
className="normal-border"
iconBefore={InfoOutline}
variant="outline-primary rounded-0"
onClick={handleOnClickInfoSidebar}
>
{intl.formatMessage(messages.infoButtonText)}
</Button>
<Button
className="ml-2"
iconBefore={Add}
variant="primary rounded-0"
disabled={readOnly}
onClick={openAddContentSidebar}
>
{intl.formatMessage(messages.addContentButton)}
</Button>
</div>
);
};
export const LibraryUnitPage = () => {
const intl = useIntl();
const {
libraryId,
unitId,
collectionId,
componentId,
} = useLibraryContext();
const {
sidebarComponentInfo,
openInfoSidebar,
setDefaultTab,
setHiddenTabs,
} = useSidebarContext();
useEffect(() => {
setDefaultTab({
collection: COLLECTION_INFO_TABS.Details,
component: COMPONENT_INFO_TABS.Manage,
unit: UNIT_INFO_TABS.Organize,
});
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]);
useEffect(() => {
openInfoSidebar(componentId, collectionId, unitId);
}, [componentId, unitId, collectionId]);
if (!unitId || !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');
}
const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId);
const {
data: unitData,
isLoading,
isError,
error,
} = useContainer(unitId);
// Only show loading if unit or library data is not fetched from index yet
if (isLibLoading || isLoading) {
return <Loading />;
}
if (!libraryData || !unitData) {
return <NotFoundAlert />;
}
if (isError) {
// istanbul ignore next
return <ErrorAlert error={error} />;
}
const breadcrumbs = (
<Breadcrumb
ariaLabel={intl.formatMessage(messages.breadcrumbsAriaLabel)}
links={[
{
label: libraryData.title,
to: `/library/${libraryId}`,
},
// Adding empty breadcrumb to add the last `>` spacer.
{
label: '',
to: '',
},
]}
linkAs={Link}
/>
);
return (
<div className="d-flex">
<div className="flex-grow-1">
<Helmet><title>{libraryData.title} | {process.env.SITE_NAME}</title></Helmet>
<Header
number={libraryData.slug}
title={libraryData.title}
org={libraryData.org}
contextId={libraryId}
isLibrary
containerProps={{
size: undefined,
}}
/>
<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={<EditableTitle unitId={unitId} />} />}
headerActions={<HeaderActions />}
breadcrumbs={breadcrumbs}
hideBorder
/>
</div>
<Container className="px-4 py-4">
<LibraryUnitBlocks />
</Container>
</Container>
</div>
{!!sidebarComponentInfo?.type && (
<div
className="library-authoring-sidebar box-shadow-left-1 bg-white"
data-testid="library-sidebar"
>
<LibrarySidebar />
</div>
)}
</div>
);
};