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 (
+
+ )}
+
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"
+ }
+}