From 23bf495a5a49c20e0073dc65345dcf74c1ba53cd Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 9 Feb 2021 05:59:32 +0100 Subject: [PATCH 1/4] List component: added support for drag & drop --- src/components/builder/lists/List.js | 92 +++++++++++++++------- src/components/builder/lists/ListItem.js | 99 ++++++++++++++---------- 2 files changed, 123 insertions(+), 68 deletions(-) diff --git a/src/components/builder/lists/List.js b/src/components/builder/lists/List.js index 7a5f2163b..bfe77b9ed 100644 --- a/src/components/builder/lists/List.js +++ b/src/components/builder/lists/List.js @@ -1,10 +1,11 @@ import { get, isEmpty } from 'lodash'; import React, { memo, useContext } from 'react'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; import { useTranslation } from 'react-i18next'; import { MdAdd } from 'react-icons/md'; import ModalContext from '../../../contexts/ModalContext'; -import { useSelector } from '../../../contexts/ResumeContext'; -import { formatDateRange } from '../../../utils'; +import { useDispatch, useSelector } from '../../../contexts/ResumeContext'; +import { formatDateRange, reorder } from '../../../utils'; import Button from '../../shared/Button'; import EmptyList from './EmptyList'; import styles from './List.module.css'; @@ -24,42 +25,79 @@ const List = ({ const { t, i18n } = useTranslation(); const items = useSelector(path, []); const { emitter } = useContext(ModalContext); + const dispatch = useDispatch(); const handleAdd = () => emitter.emit(event); const handleEdit = (data) => emitter.emit(event, data); + const onDragEnd = (result) => { + const { source, destination } = result; + + if (!destination) { + return; + } + + const sourceDroppableId = source.droppableId; + const destinationDroppableId = destination.droppableId; + if (sourceDroppableId !== destinationDroppableId) { + return; + } + + const itemsReordered = reorder(items, source.index, destination.index); + dispatch({ + type: 'on_input', + payload: { + path, + value: itemsReordered, + }, + }); + }; + return (
{isEmpty(items) ? ( ) : ( - items.map((x, i) => ( - handleEdit(x)} - isFirst={i === 0} - isLast={i === items.length - 1} - /> - )) + + + {(dropProvided) => ( +
+ {items.map((x, i) => ( + handleEdit(x)} + isFirst={i === 0} + isLast={i === items.length - 1} + index={i} + /> + ))} + {dropProvided.placeholder} +
+ )} +
+
)}
diff --git a/src/components/builder/lists/ListItem.js b/src/components/builder/lists/ListItem.js index 7dfd406b5..46b47f928 100644 --- a/src/components/builder/lists/ListItem.js +++ b/src/components/builder/lists/ListItem.js @@ -1,5 +1,6 @@ import { Menu, MenuItem } from '@material-ui/core'; import React, { memo, useState } from 'react'; +import { Draggable } from 'react-beautiful-dnd'; import { useTranslation } from 'react-i18next'; import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io'; import { MdMoreVert } from 'react-icons/md'; @@ -15,6 +16,7 @@ const ListItem = ({ isFirst, isLast, onEdit, + index, }) => { const dispatch = useDispatch(); const { t } = useTranslation(); @@ -66,49 +68,64 @@ const ListItem = ({ }; return ( -
-
- {title} - - {subtitle && ( - {subtitle} - )} - - {text && ( - {text} - )} -
- -
- - + {(dragProvided) => ( +
-
- - - - - - +
+ {title} + + {subtitle && ( + + {subtitle} + + )} + + {text && ( + + {text} + + )}
- {t('shared.buttons.edit')} - - - {t('shared.buttons.delete')} - - -
-
-
+ +
+ + +
+ + + + + + +
+ + {t('shared.buttons.edit')} + + + + {t('shared.buttons.delete')} + + +
+
+
+ )} + ); }; From 2fa4ff8d9dd1c9ada169e364b64e6ffcb30c664c Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 9 Feb 2021 09:36:36 +0100 Subject: [PATCH 2/4] Builder page: added unit test for drag & drop of skill --- src/components/builder/lists/ListItem.js | 5 ++ .../app/__tests__/builder.skills.test.js | 63 +++++++++++++++++++ src/pages/app/__tests__/helpers/builder.js | 34 ++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/pages/app/__tests__/builder.skills.test.js diff --git a/src/components/builder/lists/ListItem.js b/src/components/builder/lists/ListItem.js index 46b47f928..fa51b7316 100644 --- a/src/components/builder/lists/ListItem.js +++ b/src/components/builder/lists/ListItem.js @@ -7,6 +7,8 @@ import { MdMoreVert } from 'react-icons/md'; import { useDispatch } from '../../../contexts/ResumeContext'; import styles from './ListItem.module.css'; +const dataTestIdPrefix = 'list-item-'; + const ListItem = ({ title, subtitle, @@ -73,6 +75,7 @@ const ListItem = ({
@@ -130,3 +133,5 @@ const ListItem = ({ }; export default memo(ListItem); + +export { dataTestIdPrefix }; diff --git a/src/pages/app/__tests__/builder.skills.test.js b/src/pages/app/__tests__/builder.skills.test.js new file mode 100644 index 000000000..1677ac366 --- /dev/null +++ b/src/pages/app/__tests__/builder.skills.test.js @@ -0,0 +1,63 @@ +import { get } from 'lodash'; +import { screen } from '@testing-library/react'; + +import { DatabaseConstants } from 'gatsby-plugin-firebase'; + +import { dataTestIdPrefix as listItemDataTestIdPrefix } from '../../../components/builder/lists/ListItem'; + +import { + setupAndWait, + expectDatabaseUpdateToHaveCompleted, + dragAndDropDirectionDown, + dragAndDropListItem, +} from './helpers/builder'; + +const testTimeoutInMilliseconds = 20000; +jest.setTimeout(testTimeoutInMilliseconds); + +test('allows to drag & drop', async () => { + const resumeId = DatabaseConstants.demoStateResume1Id; + const { resume, mockDatabaseUpdateFunction } = await setupAndWait( + resumeId, + true, + true, + ); + + const now = new Date().getTime(); + + const dataPath = 'skills.items'; + const dataItems = get(resume, dataPath); + expect(dataItems.length).toBeGreaterThan(1); + + const listItems = screen.getAllByTestId( + `${listItemDataTestIdPrefix}${dataPath}`, + ); + const firstListItem = listItems[0]; + expect(firstListItem).toHaveTextContent(dataItems[0].name); + const secondListItem = listItems[1]; + expect(secondListItem).toHaveTextContent(dataItems[1].name); + + dragAndDropListItem(firstListItem, dragAndDropDirectionDown); + + const actualListItems = screen.getAllByTestId( + `${listItemDataTestIdPrefix}${dataPath}`, + ); + const actualFirstListItem = actualListItems[0]; + expect(actualFirstListItem).toHaveTextContent(dataItems[1].name); + const actualSecondListItem = actualListItems[1]; + expect(actualSecondListItem).toHaveTextContent(dataItems[0].name); + + await expectDatabaseUpdateToHaveCompleted(mockDatabaseUpdateFunction); + const mockDatabaseUpdateFunctionCallArgument = + mockDatabaseUpdateFunction.mock.calls[0][0]; + expect(mockDatabaseUpdateFunctionCallArgument.id).toBe(resumeId); + expect(mockDatabaseUpdateFunctionCallArgument.skills.items[0]).toEqual( + dataItems[1], + ); + expect(mockDatabaseUpdateFunctionCallArgument.skills.items[1]).toEqual( + dataItems[0], + ); + expect( + mockDatabaseUpdateFunctionCallArgument.updatedAt, + ).toBeGreaterThanOrEqual(now); +}); diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index b05eca1b7..0a8f3a8b6 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -1,5 +1,6 @@ import React from 'react'; import { + fireEvent, render, screen, waitFor, @@ -46,6 +47,36 @@ const expectDatabaseUpdateToHaveCompleted = async ( ); }; +const dragAndDropDirectionUp = 'DND_DIRECTION_UP'; +const dragAndDropDirectionDown = 'DND_DIRECTION_DOWN'; + +const dragAndDropListItem = (listItemElement, direction) => { + const spaceKey = { keyCode: 32 }; + const arrowUpKey = { keyCode: 38 }; + const arrowDownKey = { keyCode: 40 }; + const getKeyForDirection = () => { + switch (direction) { + case dragAndDropDirectionUp: + return arrowUpKey; + case dragAndDropDirectionDown: + return arrowDownKey; + default: + throw new Error('Unhandled `direction`!'); + } + }; + + listItemElement.focus(); + + // enable keyboard dragging + fireEvent.keyDown(listItemElement, spaceKey); + + // move element based on direction + fireEvent.keyDown(listItemElement, getKeyForDirection()); + + // disable keyboard dragging + fireEvent.keyDown(listItemElement, spaceKey); +}; + // eslint-disable-next-line no-underscore-dangle async function _setup( resumeId, @@ -124,4 +155,7 @@ export { setupAndWait, waitForDatabaseUpdateToHaveCompletedFn as waitForDatabaseUpdateToHaveCompleted, expectDatabaseUpdateToHaveCompleted, + dragAndDropDirectionUp, + dragAndDropDirectionDown, + dragAndDropListItem, }; From 8ce73a38ea4c11e96a4e8d10ab9a9dfd373cce9c Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 9 Feb 2021 11:52:48 +0100 Subject: [PATCH 3/4] Builder unit tests helper: comments correction --- src/pages/app/__tests__/helpers/builder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index 0a8f3a8b6..e63c93468 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -67,13 +67,13 @@ const dragAndDropListItem = (listItemElement, direction) => { listItemElement.focus(); - // enable keyboard dragging + // start the drag fireEvent.keyDown(listItemElement, spaceKey); // move element based on direction fireEvent.keyDown(listItemElement, getKeyForDirection()); - // disable keyboard dragging + // drop fireEvent.keyDown(listItemElement, spaceKey); }; From bd1bd2fc9712f977e9a5497173b341bf7454a1e2 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 9 Feb 2021 12:31:54 +0100 Subject: [PATCH 4/4] List component, drag handler: added check for source and destination id being the same --- src/components/builder/lists/List.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/builder/lists/List.js b/src/components/builder/lists/List.js index bfe77b9ed..51beb3367 100644 --- a/src/components/builder/lists/List.js +++ b/src/components/builder/lists/List.js @@ -44,6 +44,10 @@ const List = ({ return; } + if (source.index === destination.index) { + return; + } + const itemsReordered = reorder(items, source.index, destination.index); dispatch({ type: 'on_input',