(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 ;
}
- /* 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 (
+
+
+
+
+ );
+ };
+
const renderedBlocks = orderedBlocks?.map((block) => (
-
-
- {block.displayName}
-
-
-
- {block.hasUnpublishedChanges && (
-
-
-
-
-
-
- )}
-
-
-
- >
- )}
+ actions={}
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 && (
+ )}
));
return (
-
+
{renderedBlocks}
{ !preview && (
diff --git a/src/library-authoring/units/LibraryUnitPage.test.tsx b/src/library-authoring/units/LibraryUnitPage.test.tsx
index db1df10269..daa63b2301 100644
--- a/src/library-authoring/units/LibraryUnitPage.test.tsx
+++ b/src/library-authoring/units/LibraryUnitPage.test.tsx
@@ -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('', () => {
beforeEach(() => {
const mocks = initializeMocks();
@@ -187,4 +199,36 @@ describe('', () => {
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'));
+ });
});
diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx
index fc1eb67161..e362cdb900 100644
--- a/src/library-authoring/units/LibraryUnitPage.tsx
+++ b/src/library-authoring/units/LibraryUnitPage.tsx
@@ -176,8 +176,8 @@ export const LibraryUnitPage = () => {
return ;
}
+ // istanbul ignore if
if (isError) {
- // istanbul ignore next
return ;
}
diff --git a/src/library-authoring/units/messages.ts b/src/library-authoring/units/messages.ts
index e0e288a383..a15eeb8503 100644
--- a/src/library-authoring/units/messages.ts
+++ b/src/library-authoring/units/messages.ts
@@ -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;