feat: reorder components in unit page [FC-00083] (#1816)
Reorders components in unit page via drag and drop. This PR also refactors and moves draggable list and sortable item components to appropriate location. Course authors will be affected by this change.
This commit is contained in:
@@ -45,12 +45,12 @@ import HighlightsModal from './highlights-modal/HighlightsModal';
|
||||
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import PublishModal from './publish-modal/PublishModal';
|
||||
import PageAlerts from './page-alerts/PageAlerts';
|
||||
import DraggableList from '../generic/drag-helper/DraggableList';
|
||||
import DraggableList from './drag-helper/DraggableList';
|
||||
import {
|
||||
canMoveSection,
|
||||
possibleUnitMoves,
|
||||
possibleSubsectionMoves,
|
||||
} from '../generic/drag-helper/utils';
|
||||
} from './drag-helper/utils';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
import { getTagsExportFile } from './data/api';
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
@import "./drag-helper/SortableItem";
|
||||
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
moveUnitOver,
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
} from '../generic/drag-helper/utils';
|
||||
} from './drag-helper/utils';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
@@ -13,8 +13,8 @@ import classNames from 'classnames';
|
||||
import { setCurrentItem, setCurrentSection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import { DragContext } from '../drag-helper/DragContextProvider';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
|
||||
@@ -15,8 +15,8 @@ import CourseOutlineSubsectionCardExtraActionsSlot from '../../plugin-slots/Cour
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import { DragContext } from '../drag-helper/DragContextProvider';
|
||||
import { useClipboard, PasteComponent } from '../../generic/clipboard';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
|
||||
@@ -10,7 +10,7 @@ import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutl
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import TitleLink from '../card-header/TitleLink';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@openedx/paragon';
|
||||
import { Add, SpinnerSimple } from '@openedx/paragon/icons';
|
||||
import Placeholder from '../editors/Placeholder';
|
||||
import DraggableList, { SortableItem } from '../editors/sharedComponents/DraggableList';
|
||||
import DraggableList, { SortableItem } from '../generic/DraggableList';
|
||||
import ErrorAlert from '../editors/sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
@@ -22,6 +24,9 @@ const DraggableList = ({
|
||||
setState,
|
||||
updateOrder,
|
||||
children,
|
||||
renderOverlay,
|
||||
activeId,
|
||||
setActiveId,
|
||||
}) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
@@ -30,7 +35,7 @@ const DraggableList = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const handleDragEnd = useCallback((event) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over.id) {
|
||||
let updatedArray;
|
||||
@@ -44,13 +49,19 @@ const DraggableList = ({
|
||||
});
|
||||
updateOrder()(updatedArray);
|
||||
}
|
||||
};
|
||||
setActiveId?.(null);
|
||||
}, [updateOrder, setActiveId]);
|
||||
|
||||
const handleDragStart = useCallback((event) => {
|
||||
setActiveId?.(event.active.id);
|
||||
}, [setActiveId]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
@@ -59,10 +70,22 @@ const DraggableList = ({
|
||||
>
|
||||
{children}
|
||||
</SortableContext>
|
||||
{renderOverlay && createPortal(
|
||||
<DragOverlay>
|
||||
{renderOverlay(activeId)}
|
||||
</DragOverlay>,
|
||||
document.body,
|
||||
)}
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
DraggableList.defaultProps = {
|
||||
renderOverlay: undefined,
|
||||
activeId: null,
|
||||
setActiveId: () => {},
|
||||
};
|
||||
|
||||
DraggableList.propTypes = {
|
||||
itemList: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -70,6 +93,9 @@ DraggableList.propTypes = {
|
||||
setState: PropTypes.func.isRequired,
|
||||
updateOrder: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
renderOverlay: PropTypes.func,
|
||||
activeId: PropTypes.string,
|
||||
setActiveId: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DraggableList;
|
||||
@@ -17,6 +17,7 @@ const SortableItem = ({
|
||||
children,
|
||||
isClickable,
|
||||
onClick,
|
||||
disabled,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
@@ -31,6 +32,9 @@ const SortableItem = ({
|
||||
} = useSortable({
|
||||
id,
|
||||
animateLayoutChanges: () => false,
|
||||
disabled: {
|
||||
draggable: disabled,
|
||||
},
|
||||
});
|
||||
|
||||
const style = {
|
||||
@@ -52,6 +56,7 @@ const SortableItem = ({
|
||||
>
|
||||
<ActionRow style={actionStyle}>
|
||||
{actions}
|
||||
{!disabled && (
|
||||
<IconButtonWithTooltip
|
||||
key="drag-to-reorder-icon"
|
||||
ref={setActivatorNodeRef}
|
||||
@@ -64,6 +69,7 @@ const SortableItem = ({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
)}
|
||||
</ActionRow>
|
||||
{children}
|
||||
</Card>
|
||||
@@ -76,6 +82,7 @@ SortableItem.defaultProps = {
|
||||
actionStyle: null,
|
||||
isClickable: false,
|
||||
onClick: null,
|
||||
disabled: false,
|
||||
};
|
||||
SortableItem.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -85,6 +92,7 @@ SortableItem.propTypes = {
|
||||
componentStyle: PropTypes.shape({}),
|
||||
isClickable: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@@ -11,6 +11,5 @@
|
||||
@import "./tag-count/TagCount";
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/SortableItem";
|
||||
@import "./block-type-utils";
|
||||
@import "./modal-iframe"
|
||||
|
||||
@@ -126,4 +126,15 @@ describe('library data API', () => {
|
||||
await api.addComponentsToContainer(containerId, [componentId]);
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should update container children', async () => {
|
||||
const { axiosMock } = initializeMocks();
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = api.getLibraryContainerChildrenApiUrl(containerId);
|
||||
|
||||
axiosMock.onPatch(url).reply(200);
|
||||
|
||||
await api.updateLibraryContainerChildren(containerId, ['test']);
|
||||
expect(axiosMock.history.patch[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -670,3 +670,17 @@ export async function updateContainerCollections(containerId: string, collection
|
||||
collection_keys: collectionKeys,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update library container's children.
|
||||
*/
|
||||
export async function updateLibraryContainerChildren(
|
||||
containerId: string,
|
||||
children: string[],
|
||||
): Promise<LibraryBlockMetadata[]> {
|
||||
const { data } = await getAuthenticatedHttpClient().patch(
|
||||
getLibraryContainerChildrenApiUrl(containerId),
|
||||
{ usage_keys: children },
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
useRestoreContainer,
|
||||
useContainerChildren,
|
||||
useAddComponentsToContainer,
|
||||
useUpdateContainerChildren,
|
||||
} from './apiHooks';
|
||||
|
||||
let axiosMock;
|
||||
@@ -266,4 +267,24 @@ describe('library api hooks', () => {
|
||||
|
||||
expect(axiosMock.history.post[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should update container children', async () => {
|
||||
const containerId = 'lct:org:lib1';
|
||||
const url = getLibraryContainerChildrenApiUrl(containerId);
|
||||
|
||||
axiosMock.onPatch(url).reply(200);
|
||||
const { result } = renderHook(() => useUpdateContainerChildren(containerId), { wrapper });
|
||||
await result.current.mutateAsync([]);
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.patch[0].url).toEqual(url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not attempt request if containerId is not defined', async () => {
|
||||
const { result } = renderHook(() => useUpdateContainerChildren(), { wrapper });
|
||||
await result.current.mutateAsync([]);
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.patch.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
restoreContainer,
|
||||
getLibraryContainerChildren,
|
||||
updateContainerCollections,
|
||||
updateLibraryContainerChildren,
|
||||
} from './api';
|
||||
import { VersionSpec } from '../LibraryBlock';
|
||||
|
||||
@@ -696,3 +697,28 @@ export const useUpdateContainerCollections = (containerId: string) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update container children
|
||||
*/
|
||||
export const useUpdateContainerChildren = (containerId?: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (usageKeys: string[]) => {
|
||||
if (!containerId) {
|
||||
return undefined;
|
||||
}
|
||||
return updateLibraryContainerChildren(containerId, usageKeys);
|
||||
},
|
||||
onSettled: () => {
|
||||
if (!containerId) {
|
||||
return;
|
||||
}
|
||||
// NOTE: We invalidate the library query here because we need to update the library's
|
||||
// container list.
|
||||
const libraryId = getLibraryId(containerId);
|
||||
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
|
||||
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Badge, Button, Icon, Stack, useToggle,
|
||||
ActionRow, Badge, Button, Icon, IconButton, Stack, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { Add, Description } from '@openedx/paragon/icons';
|
||||
import { Add, Description, DragIndicator } from '@openedx/paragon/icons';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { ContentTagsDrawerSheet } from '../../content-tags-drawer';
|
||||
import { blockTypes } from '../../editors/data/constants/app';
|
||||
import DraggableList, { SortableItem } from '../../editors/sharedComponents/DraggableList';
|
||||
import DraggableList, { SortableItem } from '../../generic/DraggableList';
|
||||
|
||||
import ErrorAlert from '../../generic/alert-error';
|
||||
import { getItemIcon } from '../../generic/block-type-utils';
|
||||
@@ -20,11 +20,12 @@ import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { PickLibraryContentModal } from '../add-content';
|
||||
import ComponentMenu from '../components';
|
||||
import { LibraryBlockMetadata } from '../data/api';
|
||||
import { libraryAuthoringQueryKeys, useContainerChildren } from '../data/apiHooks';
|
||||
import { libraryAuthoringQueryKeys, useContainerChildren, useUpdateContainerChildren } from '../data/apiHooks';
|
||||
import { LibraryBlock } from '../LibraryBlock';
|
||||
import { useLibraryRoutes } from '../routes';
|
||||
import messages from './messages';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
|
||||
/** Components that need large min height in preview */
|
||||
const LARGE_COMPONENTS = [
|
||||
@@ -35,17 +36,53 @@ const LARGE_COMPONENTS = [
|
||||
'lti_consumer',
|
||||
];
|
||||
|
||||
interface BlockHeaderProps {
|
||||
block: LibraryBlockMetadata;
|
||||
onTagClick: () => void;
|
||||
}
|
||||
|
||||
/** Component header, split out to reuse in drag overlay */
|
||||
const BlockHeader = ({ block, onTagClick }: BlockHeaderProps) => (
|
||||
<>
|
||||
<Stack direction="horizontal" gap={2} className="font-weight-bold">
|
||||
<Icon src={getItemIcon(block.blockType)} />
|
||||
{block.displayName}
|
||||
</Stack>
|
||||
<ActionRow.Spacer />
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
{block.hasUnpublishedChanges && (
|
||||
<Badge
|
||||
className="px-2 pt-1"
|
||||
variant="warning"
|
||||
>
|
||||
<Stack direction="horizontal" gap={1}>
|
||||
<Icon className="mb-1" size="xs" src={Description} />
|
||||
<FormattedMessage {...messages.draftChipText} />
|
||||
</Stack>
|
||||
</Badge>
|
||||
)}
|
||||
<TagCount size="sm" count={block.tagsCount} onClick={onTagClick} />
|
||||
<ComponentMenu usageKey={block.id} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
interface LibraryUnitBlocksProps {
|
||||
/** set to true if it is rendered as preview
|
||||
* This disables drag and drop
|
||||
*/
|
||||
preview?: boolean;
|
||||
}
|
||||
|
||||
export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) => {
|
||||
export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
const intl = useIntl();
|
||||
const [orderedBlocks, setOrderedBlocks] = useState<LibraryBlockMetadata[]>([]);
|
||||
const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false);
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
|
||||
const [hidePreviewFor, setHidePreviewFor] = useState<string | null>(null);
|
||||
const { navigateTo } = useLibraryRoutes();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
|
||||
const {
|
||||
unitId,
|
||||
@@ -60,6 +97,7 @@ export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) =
|
||||
} = useSidebarContext();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const orderMutator = useUpdateContainerChildren(unitId);
|
||||
const {
|
||||
data: blocks,
|
||||
isLoading,
|
||||
@@ -78,11 +116,14 @@ export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) =
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
const handleReorder = () => (newOrder: LibraryBlockMetadata[]) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('LibraryUnitBlocks newOrder: ', newOrder);
|
||||
// TODO: update order of components in unit
|
||||
const handleReorder = () => async (newOrder: LibraryBlockMetadata[]) => {
|
||||
const usageKeys = newOrder.map((o) => o.id);
|
||||
try {
|
||||
await orderMutator.mutateAsync(usageKeys);
|
||||
showToast(intl.formatMessage(messages.orderUpdatedMsg));
|
||||
} catch (e) {
|
||||
showToast(intl.formatMessage(messages.failedOrderUpdatedMsg));
|
||||
}
|
||||
};
|
||||
|
||||
const onTagSidebarClose = () => {
|
||||
@@ -103,44 +144,45 @@ export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) =
|
||||
return '200px';
|
||||
};
|
||||
|
||||
const renderOverlay = (activeId: string | null) => {
|
||||
if (!activeId) {
|
||||
return null;
|
||||
}
|
||||
const block = orderedBlocks?.find((val) => val.id === activeId);
|
||||
if (!block) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ActionRow className="bg-light-200 border border-light-500 p-2 rounded">
|
||||
<BlockHeader block={block} onTagClick={openManageTagsDrawer} />
|
||||
<IconButton
|
||||
src={DragIndicator}
|
||||
variant="light"
|
||||
iconAs={Icon}
|
||||
alt=""
|
||||
/>
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
const renderedBlocks = orderedBlocks?.map((block) => (
|
||||
<IframeProvider key={block.id}>
|
||||
<SortableItem
|
||||
id={block.id}
|
||||
componentStyle={null}
|
||||
actions={(
|
||||
<>
|
||||
<Stack direction="horizontal" gap={2} className="font-weight-bold">
|
||||
<Icon src={getItemIcon(block.blockType)} />
|
||||
{block.displayName}
|
||||
</Stack>
|
||||
<ActionRow.Spacer />
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
{block.hasUnpublishedChanges && (
|
||||
<Badge
|
||||
className="px-2 pt-1"
|
||||
variant="warning"
|
||||
>
|
||||
<Stack direction="horizontal" gap={1}>
|
||||
<Icon className="mb-1" size="xs" src={Description} />
|
||||
<FormattedMessage {...messages.draftChipText} />
|
||||
</Stack>
|
||||
</Badge>
|
||||
)}
|
||||
<TagCount size="sm" count={block.tagsCount} onClick={openManageTagsDrawer} />
|
||||
<ComponentMenu usageKey={block.id} />
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
actions={<BlockHeader block={block} onTagClick={openManageTagsDrawer} />}
|
||||
actionStyle={{
|
||||
borderRadius: '8px 8px 0px 0px',
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#FBFAF9',
|
||||
borderBottom: 'solid 1px #E1DDDB',
|
||||
outline: hidePreviewFor === block.id && '2px dashed gray',
|
||||
}}
|
||||
isClickable
|
||||
onClick={() => handleComponentSelection(block)}
|
||||
disabled={preview}
|
||||
>
|
||||
{hidePreviewFor !== block.id && (
|
||||
<div className={classNames('p-3', {
|
||||
'container-mw-md': block.blockType === blockTypes.video,
|
||||
})}
|
||||
@@ -151,13 +193,21 @@ export const LibraryUnitBlocks = ({ preview = false }: LibraryUnitBlocksProps) =
|
||||
minHeight={calculateMinHeight(block)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SortableItem>
|
||||
</IframeProvider>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="library-unit-page">
|
||||
<DraggableList itemList={orderedBlocks} setState={setOrderedBlocks} updateOrder={handleReorder}>
|
||||
<DraggableList
|
||||
itemList={orderedBlocks}
|
||||
setState={setOrderedBlocks}
|
||||
updateOrder={handleReorder}
|
||||
renderOverlay={renderOverlay}
|
||||
activeId={hidePreviewFor}
|
||||
setActiveId={setHidePreviewFor}
|
||||
>
|
||||
{renderedBlocks}
|
||||
</DraggableList>
|
||||
{ !preview && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { act } from 'react';
|
||||
import {
|
||||
initializeMocks,
|
||||
fireEvent,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from '../../testUtils';
|
||||
import { getLibraryContainerApiUrl } from '../data/api';
|
||||
import { getLibraryContainerApiUrl, getLibraryContainerChildrenApiUrl } from '../data/api';
|
||||
import {
|
||||
mockContentLibrary,
|
||||
mockXBlockFields,
|
||||
@@ -20,12 +21,13 @@ import {
|
||||
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
|
||||
import { mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import LibraryLayout from '../LibraryLayout';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
|
||||
const path = '/library/:libraryId/*';
|
||||
const libraryTitle = mockContentLibrary.libraryData.title;
|
||||
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast: (message: string) => void;
|
||||
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
|
||||
|
||||
mockClipboardEmpty.applyMock();
|
||||
mockGetContainerMetadata.applyMock();
|
||||
@@ -36,6 +38,16 @@ mockContentLibrary.applyMock();
|
||||
mockXBlockFields.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
|
||||
const closestCenter = jest.fn();
|
||||
jest.mock('@dnd-kit/core', () => ({
|
||||
...jest.requireActual('@dnd-kit/core'),
|
||||
// Since jsdom (used by jest) does not support getBoundingClientRect function
|
||||
// which is required for drag-n-drop calculations, we mock closestCorners fn
|
||||
// from dnd-kit to return collided elements as per the test. This allows us to
|
||||
// test all drag-n-drop handlers.
|
||||
closestCenter: () => closestCenter(),
|
||||
}));
|
||||
|
||||
describe('<LibraryUnitPage />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
@@ -187,4 +199,36 @@ describe('<LibraryUnitPage />', () => {
|
||||
userEvent.click(closeButton);
|
||||
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should call update order api on dragging component', async () => {
|
||||
renderLibraryUnitPage();
|
||||
const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0];
|
||||
axiosMock
|
||||
.onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId))
|
||||
.reply(200);
|
||||
closestCenter.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1' }]);
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(firstDragHandle, { code: 'Space' });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(firstDragHandle, { code: 'Space' });
|
||||
});
|
||||
await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Order updated'));
|
||||
});
|
||||
|
||||
it('should show toast error message on update order failure', async () => {
|
||||
renderLibraryUnitPage();
|
||||
const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0];
|
||||
axiosMock
|
||||
.onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId))
|
||||
.reply(500);
|
||||
closestCenter.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1' }]);
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(firstDragHandle, { code: 'Space' });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(firstDragHandle, { code: 'Space' });
|
||||
});
|
||||
await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Failed to update components order'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,8 +176,8 @@ export const LibraryUnitPage = () => {
|
||||
return <NotFoundAlert />;
|
||||
}
|
||||
|
||||
// istanbul ignore if
|
||||
if (isError) {
|
||||
// istanbul ignore next
|
||||
return <ErrorAlert error={error} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Failed to update container.',
|
||||
description: 'Message displayed when container update fails',
|
||||
},
|
||||
orderUpdatedMsg: {
|
||||
id: 'course-authoring.library-authoring.unit-component.order-updated-msg.text',
|
||||
defaultMessage: 'Order updated',
|
||||
description: 'Toast message displayed when components are successfully reordered in a unit',
|
||||
},
|
||||
failedOrderUpdatedMsg: {
|
||||
id: 'course-authoring.library-authoring.unit-component.failed-order-updated-msg.text',
|
||||
defaultMessage: 'Failed to update components order',
|
||||
description: 'Toast message displayed when components are successfully reordered in a unit',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
Reference in New Issue
Block a user