Răsfoiți Sursa

Merge pull request #5087 from weseek/feat/82939-drag-and-drop-with-react-dnd

Feat/82939 drag and drop with react dnd
cao 4 ani în urmă
părinte
comite
f3c740ce0d

+ 3 - 1
packages/app/package.json

@@ -122,6 +122,7 @@
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
+    "p-retry": "^4.0.0",
     "passport": "^0.5.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
@@ -130,10 +131,11 @@
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-twitter": "^1.0.4",
-    "p-retry": "^4.0.0",
     "prom-client": "^13.0.0",
     "re2": "^1.17.1",
     "react-card-flip": "^1.0.10",
+    "react-dnd": "^14.0.5",
+    "react-dnd-html5-backend": "^14.1.0",
     "react-image-crop": "^8.3.0",
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",

+ 5 - 1
packages/app/src/client/app.jsx

@@ -2,6 +2,8 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
 
 import { SWRConfig } from 'swr';
 
@@ -160,7 +162,9 @@ const renderMainComponents = () => {
           <ErrorBoundary>
             <SWRConfig value={swrGlobalConfiguration}>
               <Provider inject={injectableContainers}>
-                {componentMappings[key]}
+                <DndProvider backend={HTML5Backend}>
+                  {componentMappings[key]}
+                </DndProvider>
               </Provider>
             </SWRConfig>
           </ErrorBoundary>

+ 42 - 5
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -4,6 +4,7 @@ import React, {
 import nodePath from 'path';
 import { useTranslation } from 'react-i18next';
 import { pagePathUtils } from '@growi/core';
+import { useDrag, useDrop } from 'react-dnd';
 import { toastWarning } from '~/client/util/apiNotification';
 
 import { ItemNode } from './ItemNode';
@@ -109,6 +110,39 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   const { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
 
+
+  const [{ isDragging }, drag] = useDrag(() => ({
+    type: 'PAGE_TREE',
+    item: { page },
+    collect: monitor => ({
+      isDragging: monitor.isDragging(),
+    }),
+  }));
+
+  const pageItemDropHandler = () => {
+    // TODO: hit an api to rename the page by 85175
+    // eslint-disable-next-line no-console
+    console.log('pageItem was droped!!');
+  };
+
+  const [{ isOver }, drop] = useDrop(() => ({
+    accept: 'PAGE_TREE',
+    drop: pageItemDropHandler,
+    hover: (item, monitor) => {
+      // when a drag item is overlapped more than 1 sec, the drop target item will be opened.
+      if (monitor.isOver()) {
+        setTimeout(() => {
+          if (monitor.isOver()) {
+            setIsOpen(true);
+          }
+        }, 1000);
+      }
+    },
+    collect: monitor => ({
+      isOver: monitor.isOver(),
+    }),
+  }));
+
   const hasChildren = useCallback((): boolean => {
     return currentChildren != null && currentChildren.length > 0;
   }, [currentChildren]);
@@ -180,8 +214,11 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   }, [data, isOpen]);
 
   return (
-    <>
-      <div className={`grw-pagetree-item d-flex align-items-center pr-1 ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}>
+    <div className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}`}>
+      <div
+        ref={(c) => { drag(c); drop(c) }}
+        className={`grw-pagetree-item d-flex align-items-center pr-1 ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
+      >
         <button
           type="button"
           className={`grw-pagetree-button btn ${isOpen ? 'grw-pagetree-open' : ''}`}
@@ -211,7 +248,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       {isEnableActions && (
         <ClosableTextInput
           isShown={isNewPageInputShown}
-          placeholder={t('Input title')}
+          placeholder={t('Input page name')}
           onClickOutside={() => { setNewPageInputShown(false) }}
           onPressEnter={onPressEnterHandler}
           inputValidator={inputValidator}
@@ -219,7 +256,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       )}
       {
         isOpen && hasChildren() && currentChildren.map(node => (
-          <div key={node.page._id} className="grw-pagetree-item-container">
+          <div key={node.page._id} className="grw-pagetree-item-children">
             <Item
               isEnableActions={isEnableActions}
               itemNode={node}
@@ -230,7 +267,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           </div>
         ))
       }
-    </>
+    </div>
   );
 
 };

+ 48 - 27
packages/app/src/styles/_page-tree.scss

@@ -46,46 +46,67 @@ $grw-pagetree-item-padding-left: 10px;
   }
 
   // To realize a hierarchical structure, set multiplied padding-left to each pagetree-item
-
   > .grw-pagetree-item-container {
-    > .grw-pagetree-item {
-      padding-left: $grw-pagetree-item-padding-left;
-    }
-    > .grw-pagetree-item-container {
-      > .grw-pagetree-item {
-        padding-left: $grw-pagetree-item-padding-left * 2;
-      }
+    > .grw-pagetree-item-children {
       > .grw-pagetree-item-container {
         > .grw-pagetree-item {
-          padding-left: $grw-pagetree-item-padding-left * 3;
+          padding-left: $grw-pagetree-item-padding-left;
         }
-        > .grw-pagetree-item-container {
-          > .grw-pagetree-item {
-            padding-left: $grw-pagetree-item-padding-left * 4;
-          }
+        > .grw-pagetree-item-children {
           > .grw-pagetree-item-container {
             > .grw-pagetree-item {
-              padding-left: $grw-pagetree-item-padding-left * 5;
+              padding-left: $grw-pagetree-item-padding-left * 2;
             }
-            > .grw-pagetree-item-container {
-              > .grw-pagetree-item {
-                padding-left: $grw-pagetree-item-padding-left * 6;
-              }
+            > .grw-pagetree-item-children {
               > .grw-pagetree-item-container {
                 > .grw-pagetree-item {
-                  padding-left: $grw-pagetree-item-padding-left * 7;
+                  padding-left: $grw-pagetree-item-padding-left * 3;
                 }
-                > .grw-pagetree-item-container {
-                  > .grw-pagetree-item {
-                    padding-left: $grw-pagetree-item-padding-left * 8;
-                  }
+                > .grw-pagetree-item-children {
                   > .grw-pagetree-item-container {
                     > .grw-pagetree-item {
-                      padding-left: $grw-pagetree-item-padding-left * 9;
+                      padding-left: $grw-pagetree-item-padding-left * 4;
                     }
-                    .grw-pagetree-item-container {
-                      > .grw-pagetree-item {
-                        padding-left: $grw-pagetree-item-padding-left * 10;
+                    > .grw-pagetree-item-children {
+                      > .grw-pagetree-item-container {
+                        > .grw-pagetree-item {
+                          padding-left: $grw-pagetree-item-padding-left * 5;
+                        }
+                        > .grw-pagetree-item-children {
+                          > .grw-pagetree-item-container {
+                            > .grw-pagetree-item {
+                              padding-left: $grw-pagetree-item-padding-left * 6;
+                            }
+                            > .grw-pagetree-item-children {
+                              > .grw-pagetree-item-container {
+                                > .grw-pagetree-item {
+                                  padding-left: $grw-pagetree-item-padding-left * 7;
+                                }
+                                > .grw-pagetree-item-children {
+                                  > .grw-pagetree-item-container {
+                                    > .grw-pagetree-item {
+                                      padding-left: $grw-pagetree-item-padding-left * 8;
+                                    }
+                                    > .grw-pagetree-item-children {
+                                      > .grw-pagetree-item-container {
+                                        > .grw-pagetree-item {
+                                          padding-left: $grw-pagetree-item-padding-left * 9;
+                                        }
+                                        .grw-pagetree-item-children {
+                                          > .grw-pagetree-item-container {
+                                            > .grw-pagetree-item {
+                                              padding-left: $grw-pagetree-item-padding-left * 10;
+                                            }
+                                          }
+                                        }
+                                      }
+                                    }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
                       }
                     }
                   }

+ 3 - 0
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -257,6 +257,9 @@ ul.pagination {
 
   // Pagetree
   .grw-pagetree {
+    .grw-pagetree-is-over {
+      background: $bgcolor-list-hover;
+    }
     .grw-pagetree-item {
       &.grw-pagetree-is-target {
         background: $bgcolor-list-hover;

+ 3 - 0
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -170,6 +170,9 @@ $border-color: $border-color-global;
 
   // Pagetree
   .grw-pagetree {
+    .grw-pagetree-is-over {
+      background: $bgcolor-list-hover;
+    }
     .grw-pagetree-item {
       &.grw-pagetree-is-target {
         background: $bgcolor-list-hover;

+ 57 - 1
yarn.lock

@@ -583,6 +583,13 @@
   dependencies:
     regenerator-runtime "^0.13.2"
 
+"@babel/runtime@^7.9.2":
+  version "7.16.7"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
+  integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/template@^7.1.0":
   version "7.4.0"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b"
@@ -2196,6 +2203,21 @@
     "@promster/metrics" "^9.1.2"
     tslib "2.3.1"
 
+"@react-dnd/asap@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
+  integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==
+
+"@react-dnd/invariant@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
+  integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
+
+"@react-dnd/shallowequal@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
+  integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
+
 "@sematext/gc-stats@1.5.5":
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/@sematext/gc-stats/-/gc-stats-1.5.5.tgz#3461e818454b95de26085b65f0d95417b9f183d6"
@@ -6997,6 +7019,15 @@ dlv@^1.1.3:
   resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
   integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
 
+dnd-core@14.0.1:
+  version "14.0.1"
+  resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e"
+  integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==
+  dependencies:
+    "@react-dnd/asap" "^4.0.0"
+    "@react-dnd/invariant" "^2.0.0"
+    redux "^4.1.1"
+
 doctrine@3.0.0, doctrine@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -9852,7 +9883,7 @@ hogan.js@3.0.2:
     mkdirp "0.3.0"
     nopt "1.0.10"
 
-hoist-non-react-statics@^3.0.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -16498,6 +16529,24 @@ react-copy-to-clipboard@^5.0.1:
     copy-to-clipboard "^3"
     prop-types "^15.5.8"
 
+react-dnd-html5-backend@^14.1.0:
+  version "14.1.0"
+  resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f"
+  integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==
+  dependencies:
+    dnd-core "14.0.1"
+
+react-dnd@^14.0.5:
+  version "14.0.5"
+  resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed"
+  integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==
+  dependencies:
+    "@react-dnd/invariant" "^2.0.0"
+    "@react-dnd/shallowequal" "^2.0.0"
+    dnd-core "14.0.1"
+    fast-deep-equal "^3.1.3"
+    hoist-non-react-statics "^3.3.2"
+
 react-dom@^16.2.0, react-dom@^16.8.3:
   version "16.14.0"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
@@ -17007,6 +17056,13 @@ reduce-component@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/reduce-component/-/reduce-component-1.0.1.tgz#e0c93542c574521bea13df0f9488ed82ab77c5da"
 
+redux@^4.1.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
+  integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+
 reflect-metadata@^0.1.13:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"