import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Badge, Button, Icon, Stack, useToggle, } from '@openedx/paragon'; import { Add, Description } from '@openedx/paragon/icons'; import classNames from 'classnames'; import { useCallback, useContext, useEffect, useState, } from 'react'; import { blockTypes } from '../../editors/data/constants/app'; import DraggableList, { SortableItem } from '../../generic/DraggableList'; import ErrorAlert from '../../generic/alert-error'; import { getItemIcon } from '../../generic/block-type-utils'; import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import { IframeProvider } from '../../generic/hooks/context/iFrameContext'; import { InplaceTextEditor } from '../../generic/inplace-text-editor'; import Loading from '../../generic/Loading'; import TagCount from '../../generic/tag-count'; import { useLibraryContext } from '../common/context/LibraryContext'; import { PickLibraryContentModal } from '../add-content'; import ComponentMenu from '../components'; import { LibraryBlockMetadata } from '../data/api'; import { useContainerChildren, useUpdateContainerChildren, useUpdateXBlockFields, } from '../data/apiHooks'; import { LibraryBlock } from '../LibraryBlock'; import { useLibraryRoutes, ContentType } from '../routes'; import messages from './messages'; import { SidebarActions, SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; import { ToastContext } from '../../generic/toast-context'; import { canEditComponent } from '../components/ComponentEditorModal'; import { useRunOnNextRender } from '../../utils'; /** Components that need large min height in preview */ const LARGE_COMPONENTS = [ COMPONENT_TYPES.advanced, COMPONENT_TYPES.dragAndDrop, COMPONENT_TYPES.discussion, 'lti', 'lti_consumer', ]; interface LibraryBlockMetadataWithUniqueId extends LibraryBlockMetadata { originalId: string; } interface ComponentBlockProps { block: LibraryBlockMetadataWithUniqueId; preview?: boolean; isDragging?: boolean; } /** Component header */ const BlockHeader = ({ block }: ComponentBlockProps) => { const intl = useIntl(); const { showOnlyPublished } = useLibraryContext(); const { showToast } = useContext(ToastContext); const { navigateTo } = useLibraryRoutes(); const { openComponentInfoSidebar, setSidebarAction } = useSidebarContext(); const updateMutation = useUpdateXBlockFields(block.originalId); const handleSaveDisplayName = async (newDisplayName: string) => { try { await updateMutation.mutateAsync({ metadata: { display_name: newDisplayName, }, }); showToast(intl.formatMessage(messages.updateComponentSuccessMsg)); } catch (err) { showToast(intl.formatMessage(messages.updateComponentErrorMsg)); throw err; } }; /* istanbul ignore next */ const scheduleJumpToTags = useRunOnNextRender(() => { // TODO: Ugly hack to make sure sidebar shows manage tags section // This needs to run after all changes to url takes place to avoid conflicts. setTimeout(() => setSidebarAction(SidebarActions.JumpToManageTags), 250); }); /* istanbul ignore next */ const jumpToManageTags = () => { navigateTo({ componentId: block.originalId }); openComponentInfoSidebar(block.originalId); scheduleJumpToTags(); }; return ( <> e.stopPropagation()} > e.stopPropagation()} > {!showOnlyPublished && block.hasUnpublishedChanges && ( )} ); }; /** ComponentBlock to render preview of given component under Unit */ const ComponentBlock = ({ block, preview, isDragging }: ComponentBlockProps) => { const { showOnlyPublished } = useLibraryContext(); const { navigateTo } = useLibraryRoutes(); const { unitId, collectionId, componentId, openComponentEditor, } = useLibraryContext(); const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext(); const handleComponentSelection = useCallback((numberOfClicks: number) => { navigateTo({ componentId: block.originalId }); const canEdit = canEditComponent(block.originalId); if (numberOfClicks > 1 && canEdit) { // Open editor on double click. openComponentEditor(block.originalId); } else { // open current component sidebar openInfoSidebar(block.originalId, collectionId, unitId); } }, [block, collectionId, unitId, navigateTo, canEditComponent, openComponentEditor, openInfoSidebar]); useEffect(() => { if (block.isNew) { handleComponentSelection(1); } }, [block]); /* istanbul ignore next */ const calculateMinHeight = () => { if (LARGE_COMPONENTS.includes(block.blockType)) { return '700px'; } return '200px'; }; const getComponentStyle = useCallback(() => { if (isDragging) { return { outline: '2px dashed gray', maxHeight: '200px', overflowY: 'hidden', }; } return {}; }, [isDragging, componentId, block]); const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.ComponentInfo && sidebarComponentInfo?.id === block.originalId; return ( } actionStyle={{ borderRadius: '8px 8px 0px 0px', padding: '0.5rem 1rem', background: '#FBFAF9', borderBottom: 'solid 1px #E1DDDB', }} isClickable onClick={(e: { detail: number; }) => handleComponentSelection(e.detail)} disabled={preview} cardClassName={selected ? 'selected' : undefined} > {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
e.stopPropagation()} >
); }; interface LibraryUnitBlocksProps { /** set to true if it is rendered as preview * This disables drag and drop */ preview?: boolean; } export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => { const intl = useIntl(); const [orderedBlocks, setOrderedBlocks] = useState([]); const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [hidePreviewFor, setHidePreviewFor] = useState(null); const { showToast } = useContext(ToastContext); const { unitId, readOnly, showOnlyPublished } = useLibraryContext(); const { openAddContentSidebar } = useSidebarContext(); const orderMutator = useUpdateContainerChildren(unitId); const { data: blocks, isLoading, isError, error, } = useContainerChildren(unitId, showOnlyPublished); const handleReorder = useCallback(() => async (newOrder?: LibraryBlockMetadataWithUniqueId[]) => { if (!newOrder) { return; } const usageKeys = newOrder.map((o) => o.originalId); try { await orderMutator.mutateAsync(usageKeys); showToast(intl.formatMessage(messages.orderUpdatedMsg)); } catch (e) { showToast(intl.formatMessage(messages.failedOrderUpdatedMsg)); } }, [orderMutator]); useEffect(() => { // Create new ids which are unique using index. // This is required to support multiple components with same id under a unit. const newBlocks = blocks?.map((block, idx) => { const newBlock: LibraryBlockMetadataWithUniqueId = { ...block, id: `${block.id}----${idx}`, originalId: block.id, }; return newBlock; }); return setOrderedBlocks(newBlocks || []); }, [blocks, setOrderedBlocks]); if (isLoading) { return ; } if (isError) { // istanbul ignore next return ; } return (
{orderedBlocks?.map((block, idx) => ( // A container can have multiple instances of the same block // eslint-disable-next-line react/no-array-index-key ))} {!preview && (
)}
); };