From 5fd29bb0d6fc44a1d0b8c7186771d913876e97e0 Mon Sep 17 00:00:00 2001 From: Greemty <52284320+Greemty@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:36:06 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Dragndrop=20feature=20to=20move=20a?= =?UTF-8?q?=20file=20into=20a=20folder=20(#138)=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Anton SHEPILOV Co-authored-by: montaghanmy --- tdrive/frontend/package.json | 1 + tdrive/frontend/public/locales/en.json | 1 + tdrive/frontend/public/locales/fr.json | 1 + tdrive/frontend/public/locales/ru.json | 1 + .../app/features/dragndrop/hook/draggable.tsx | 22 ++ .../app/features/dragndrop/hook/droppable.tsx | 23 ++ .../app/views/client/body/drive/browser.tsx | 205 ++++++++++++------ .../client/body/drive/documents/common.tsx | 4 + .../body/drive/documents/document-row.tsx | 20 +- .../body/drive/modals/confirm-move/index.tsx | 75 +++++++ tdrive/frontend/yarn.lock | 23 ++ tdrive/package-lock.json | 102 +++++++++ tdrive/package.json | 7 + 13 files changed, 422 insertions(+), 63 deletions(-) create mode 100644 tdrive/frontend/src/app/features/dragndrop/hook/draggable.tsx create mode 100644 tdrive/frontend/src/app/features/dragndrop/hook/droppable.tsx create mode 100644 tdrive/frontend/src/app/views/client/body/drive/modals/confirm-move/index.tsx create mode 100644 tdrive/package-lock.json create mode 100644 tdrive/package.json diff --git a/tdrive/frontend/package.json b/tdrive/frontend/package.json index 9e21423e..be8c9dfd 100644 --- a/tdrive/frontend/package.json +++ b/tdrive/frontend/package.json @@ -69,6 +69,7 @@ "@babel/preset-env": "^7.8.7", "@babel/preset-es2017": "^7.0.0-beta.53", "@craco/craco": "^6.4.3", + "@dnd-kit/core": "^6.0.8", "@fortawesome/fontawesome-svg-core": "^6.1.2", "@fortawesome/free-solid-svg-icons": "^6.1.2", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 077901f6..006ced8e 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -214,6 +214,7 @@ "common.access-level_read":"Read", "common.access-level_write":"Write", "common.access-level_no_access":"No access", + "components.dragndrop_info_move_to":"move to", "components.SelectorModalContent_move_to":"Move to", "components.SelectorModalContent_no_items":"No item selected", "components.SelectorModalContent_select":"Selected", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index 4a3a67a2..0fdc284f 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -198,6 +198,7 @@ "common.access-level_read":"Lecture", "common.access-level_write":"Ecriture", "common.access-level_no_access":"Pas d'accès", + "components.dragndrop_info_move_to":"déplacé vers", "components.SelectorModalContent_move_to":"Déplacer vers", "components.SelectorModalContent_no_items":"Pas de fichier sélectionné", "components.SelectorModalContent_select":"sélectionné(s)", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index b19f0450..d794dc45 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -198,6 +198,7 @@ "common.access-level_read":"Read", "common.access-level_write":"Write", "common.access-level_no_access":"No access", + "components.dragndrop_info_move_to":"move to", "components.SelectorModalContent_move_to":"Move to", "components.SelectorModalContent_no_items":"No item selected", "components.SelectorModalContent_select":"Selected", diff --git a/tdrive/frontend/src/app/features/dragndrop/hook/draggable.tsx b/tdrive/frontend/src/app/features/dragndrop/hook/draggable.tsx new file mode 100644 index 00000000..00014552 --- /dev/null +++ b/tdrive/frontend/src/app/features/dragndrop/hook/draggable.tsx @@ -0,0 +1,22 @@ +import React, { MouseEventHandler } from 'react'; +import {useDraggable} from '@dnd-kit/core'; + +type DraggableProps={ + children: React.ReactNode + id: number +} + +export function Draggable(props:DraggableProps) { + const {attributes, listeners, setNodeRef, transform} = useDraggable({ + id: `draggable-${props.id+1}`, + data: { + child: props.children + }, + }); + + return ( +
+ {props.children} +
+ ); +} \ No newline at end of file diff --git a/tdrive/frontend/src/app/features/dragndrop/hook/droppable.tsx b/tdrive/frontend/src/app/features/dragndrop/hook/droppable.tsx new file mode 100644 index 00000000..c37b97ad --- /dev/null +++ b/tdrive/frontend/src/app/features/dragndrop/hook/droppable.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import {useDroppable} from '@dnd-kit/core'; + +type DroppableProps={ + children: React.ReactNode + id: any +} + +export function Droppable(props:DroppableProps) { + const { isOver, setNodeRef } = useDroppable({ + id: `droppable-${props.id}`, + data: { + child: props.children + }, + }); + + return ( +
+ {props.children} +
+ ); +} + diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index 0bd1bd3b..f9a06dba 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -21,7 +21,7 @@ import { useOnBuildPeopleContextMenu, useOnBuildDateContextMenu, } from './context-menu'; -import { DocumentRow } from './documents/document-row'; +import {DocumentRow, DocumentRowOverlay} from './documents/document-row'; import { FolderRow } from './documents/folder-row'; import HeaderPath from './header-path'; import { ConfirmDeleteModal } from './modals/confirm-delete'; @@ -36,11 +36,19 @@ import useRouteState from 'app/features/router/hooks/use-route-state'; import { SharedWithMeFilterState } from '@features/drive/state/shared-with-me-filter'; import MenusManager from '@components/menus/menus-manager.jsx'; import Languages from 'features/global/services/languages-service'; +import {DndContext, useSensors, useSensor, PointerSensor, DragOverlay} from '@dnd-kit/core'; +import { Droppable } from 'app/features/dragndrop/hook/droppable'; +import { Draggable } from 'app/features/dragndrop/hook/draggable'; +import { useDriveActions } from '@features/drive/hooks/use-drive-actions'; +import { ConfirmModalAtom } from './modals/confirm-move/index'; import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; +import { ConfirmModal } from './modals/confirm-move'; + + export const DriveCurrentFolderAtom = atomFamily< - string, - { context?: string; initialFolderId: string } + string, + { context?: string; initialFolderId: string } >({ key: 'DriveCurrentFolderAtom', default: options => options.initialFolderId || 'root', @@ -158,13 +166,78 @@ export default memo( const buildFileTypeContextMenu = useOnBuildFileTypeContextMenu(); const buildPeopleContextMen = useOnBuildPeopleContextMenu(); const buildDateContextMenu = useOnBuildDateContextMenu(); + const setConfirmModalState = useSetRecoilState(ConfirmModalAtom); + const [activeIndex, setActiveIndex] = useState(null); + const [activeChild, setActiveChild] = useState(null); + const {update} = useDriveActions(); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }) + ); + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + + function handleDragStart(event:any) { + setActiveIndex(event.active.id); + setActiveChild(event.active.data.current.child.props.item); + } + function handleDragEnd(event:any) { + setActiveIndex(null); + setActiveChild(null); + if (event.over){ + setConfirmModalState({ + open: true, + parent_id: inTrash ? 'root' : event.over.data.current.child.props.item.id, + mode: 'move', + title: + Languages.t('components.item_context_menu.move.modal_header') + + ` '${event.active.data.current.child.props.item.name}'`, + onSelected: async ids => { + await update( + { + parent_id: ids[0], + }, + event.active.data.current.child.props.item.id, + event.active.data.current.child.props.item.parent_id, + ); + }, + }) + } + + } + + function draggableMarkup(index: number, child: any) { + const commonProps = { + key: index, + className: + (index === 0 ? 'rounded-t-md ' : '') + + (index === documents.length - 1 ? 'rounded-b-md ' : ''), + item: child, + checked: checked[child.id] || false, + onCheck: (v: boolean) => + setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)), + onBuildContextMenu: () => onBuildContextMenu(details, child), + }; + return ( + isMobile ? ( + + ) : ( + + + + ) + ); + } + return ( <> {viewId == 'shared-with-me' ? ( <> }> - + @@ -197,8 +270,9 @@ export default memo( + }> - +
-
- {folders.length > 0 && ( - <> - {Languages.t('scenes.app.drive.folders')} - {folders.map((child, index) => ( - { - return setParentId(child.id); - }} - checked={checked[child.id] || false} - onCheck={v => - setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)) - } - onBuildContextMenu={() => onBuildContextMenu(details, child)} - /> - ))} -
- - )} + +
+ + {folders.length > 0 && ( + <> + {Languages.t('scenes.app.drive.folders')} - {Languages.t('scenes.app.drive.documents')} + {folders.map((child, index) => ( + + { + return setParentId(child.id); + }} + checked={checked[child.id] || false} + onCheck={v => + setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity)) + } + onBuildContextMenu={() => onBuildContextMenu(details, child)} + /> + + ))} +
+ + )} - {documents.length === 0 && !loading && ( -
- - {Languages.t('scenes.app.drive.nothing')} - - {!inTrash && access != 'read' && ( - <> - {Languages.t('scenes.app.drive.drag_and_drop')} -
- - - )} -
- )} + {Languages.t('scenes.app.drive.documents')} - {documents.map((child, index) => ( - setChecked(_.pickBy({ ...checked, [child.id]: v }, _.identity))} - onBuildContextMenu={() => onBuildContextMenu(details, child)} - /> - ))} -
-
+ {documents.length === 0 && !loading && ( +
+ + {Languages.t('scenes.app.drive.nothing')} + + {!inTrash && access != 'read' && ( + <> + {Languages.t('scenes.app.drive.drag_and_drop')} +
+ + + )} +
+ )} + + {documents.map((child, index) => ( + draggableMarkup(index, child) + ))} + + {activeIndex ? ( + + ): null} + + +
+ +
)} + ); }, diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/common.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/common.tsx index 9d2743a7..fc438413 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/common.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/common.tsx @@ -11,6 +11,10 @@ export type DriveItemProps = { onBuildContextMenu: () => Promise; }; +export type DriveItemOverlayProps = { + item: DriveItem|null; + className: string; +}; export const menuBuilder = async () => {}; export const CheckableIcon = ({ diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx index d362e678..c03a4e52 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/document-row.tsx @@ -19,7 +19,7 @@ import fileUploadApiClient from '@features/files/api/file-upload-api-client'; import { useEffect, useState } from 'react'; import Avatar from '../../../../../atoms/avatar'; import { PublicIcon } from '../components/public-icon'; -import { CheckableIcon, DriveItemProps } from './common'; +import {CheckableIcon, DriveItemOverlayProps, DriveItemProps} from './common'; import { getDevice } from '../../../../../features/global/utils/device'; import './style.scss'; import { useHistory } from 'react-router-dom'; @@ -143,3 +143,21 @@ export const DocumentRow = ({ ); }; + +export const DocumentRowOverlay = ({ + item, + className, + }: DriveItemOverlayProps) => { + return ( +
+
+ {item?.name} +
+
+ )} + diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-move/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-move/index.tsx new file mode 100644 index 00000000..6e13a8ab --- /dev/null +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/confirm-move/index.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from 'react'; +import { Button } from '@atoms/button/button'; +import { Modal, ModalContent } from '@atoms/modal'; +import { useDriveItem } from '@features/drive/hooks/use-drive-item'; +import { DriveItem } from '@features/drive/types'; +import { atom, useRecoilState } from 'recoil'; +import Languages from '@features/global/services/languages-service'; + +export type ConfirmModalType = { + open: boolean; + parent_id: string; + mode: 'move' | 'select-file' | 'select-files'; + title: string; + onSelected: (ids: string[]) => Promise; +}; + +export const ConfirmModalAtom = atom({ + key: 'ConfirmModalAtom', + default: { + open: false, + parent_id: '', + mode: 'move', + title: '', + onSelected: async () => {}, + }, +}); + +const ConfirmModalContent = () => { + const [state, setState] = useRecoilState(ConfirmModalAtom); + const [selected, setSelected] = useState([]); + const [loading, setLoading] = useState(false); + const parentId = state.parent_id; + + const { item: parent } = useDriveItem(parentId); + + useEffect(() => { + if (state.mode === 'select-file' && parent) setSelected([]); + if (state.mode === 'move' && parent) setSelected([parent]); + }, [parentId, parent?.id]); + + const handleClose = async () => { + setLoading(true); + await state.onSelected(selected.map((i) => i.id)); + setState({ ...state, open: false }); + setLoading(false); + }; + + return ( + + + + ); +}; + +export const ConfirmModal = () => { + const [state, setState] = useRecoilState(ConfirmModalAtom); + + const handleClose = () => { + setState({ ...state, open: false }); + }; + + return ( + + + + ); +}; diff --git a/tdrive/frontend/yarn.lock b/tdrive/frontend/yarn.lock index 785e6ab5..7438fffc 100644 --- a/tdrive/frontend/yarn.lock +++ b/tdrive/frontend/yarn.lock @@ -2030,6 +2030,29 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@dnd-kit/accessibility@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c" + integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.0.8": + version "6.0.8" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005" + integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA== + dependencies: + "@dnd-kit/accessibility" "^3.0.0" + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a" + integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA== + dependencies: + tslib "^2.0.0" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" diff --git a/tdrive/package-lock.json b/tdrive/package-lock.json new file mode 100644 index 00000000..2bcd63e4 --- /dev/null +++ b/tdrive/package-lock.json @@ -0,0 +1,102 @@ +{ + "name": "tdrive", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@dnd-kit/core": "^6.0.8", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", + "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", + "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "dependencies": { + "@dnd-kit/accessibility": "^3.0.0", + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", + "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + } + } +} diff --git a/tdrive/package.json b/tdrive/package.json new file mode 100644 index 00000000..10381468 --- /dev/null +++ b/tdrive/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@dnd-kit/core": "^6.0.8", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +}