From 1f9c6ac8f736153bca68ecc4b2d44c30c5f8bd74 Mon Sep 17 00:00:00 2001
From: Fedor Katurov <gotham48@gmail.com>
Date: Wed, 16 Oct 2019 11:00:33 +0700
Subject: [PATCH] refactored upload component

---
 src/components/editors/EditorPanel/index.tsx  |   9 +-
 .../editors/EditorUploadButton/index.tsx      | 132 ++++++++++++++++--
 src/components/editors/ImageEditor/index.tsx  |  20 +--
 src/components/editors/ImageGrid/index.tsx    |  38 +++--
 src/containers/dialogs/EditorDialog/index.tsx |  99 +------------
 5 files changed, 151 insertions(+), 147 deletions(-)

diff --git a/src/components/editors/EditorPanel/index.tsx b/src/components/editors/EditorPanel/index.tsx
index 6932ef1d..475b0bb2 100644
--- a/src/components/editors/EditorPanel/index.tsx
+++ b/src/components/editors/EditorPanel/index.tsx
@@ -1,4 +1,4 @@
-import React, { FC, ChangeEventHandler } from 'react';
+import React, { FC } from 'react';
 import * as styles from './styles.scss';
 import { INode } from '~/redux/types';
 import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
@@ -6,12 +6,13 @@ import { EditorUploadButton } from '~/components/editors/EditorUploadButton';
 interface IProps {
   data: INode;
   setData: (val: INode) => void;
-  onUpload: ChangeEventHandler<HTMLInputElement>;
+  temp: string[];
+  setTemp: (val: string[]) => void;
 }
 
-const EditorPanel: FC<IProps> = ({ onUpload }) => (
+const EditorPanel: FC<IProps> = ({ data, setData, temp, setTemp }) => (
   <div className={styles.panel}>
-    <EditorUploadButton onUpload={onUpload} />
+    <EditorUploadButton data={data} setData={setData} temp={temp} setTemp={setTemp} />
   </div>
 );
 
diff --git a/src/components/editors/EditorUploadButton/index.tsx b/src/components/editors/EditorUploadButton/index.tsx
index 358e938e..e29115d2 100644
--- a/src/components/editors/EditorUploadButton/index.tsx
+++ b/src/components/editors/EditorUploadButton/index.tsx
@@ -1,21 +1,127 @@
-import React, { FC, ChangeEventHandler } from 'react';
+import React, { FC, useCallback, useEffect, useState } from 'react';
 import * as styles from './styles.scss';
 import { Icon } from '~/components/input/Icon';
+import { IFileWithUUID, INode, IFile } from '~/redux/types';
+import uuid from 'uuid4';
+import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants';
+import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
+import assocPath from 'ramda/es/assocPath';
+import append from 'ramda/es/append';
+import { selectUploads } from '~/redux/uploads/selectors';
+import { connect } from 'react-redux';
 
-interface IProps {
-  onUpload?: ChangeEventHandler<HTMLInputElement>;
-}
+const mapStateToProps = state => {
+  const { statuses, files } = selectUploads(state);
 
-const EditorUploadButton: FC<IProps> = ({
-  onUpload,
-}) => (
-  <div className={styles.wrap}>
-    <input type="file" onChange={onUpload} accept="image/*" multiple />
+  return { statuses, files };
+};
 
-    <div className={styles.icon}>
-      <Icon size={32} icon="plus" />
+const mapDispatchToProps = {
+  uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
+};
+
+type IProps = ReturnType<typeof mapStateToProps> &
+  typeof mapDispatchToProps & {
+    data: INode;
+    setData: (val: INode) => void;
+    temp: string[];
+    setTemp: (val: string[]) => void;
+  };
+
+const EditorUploadButtonUnconnected: FC<IProps> = ({
+  data,
+  setData,
+  temp,
+  setTemp,
+  statuses,
+  files,
+  uploadUploadFiles,
+}) => {
+  const eventPreventer = useCallback(event => event.preventDefault(), []);
+
+  const onUpload = useCallback(
+    (uploads: File[]) => {
+      const items: IFileWithUUID[] = Array.from(uploads).map(
+        (file: File): IFileWithUUID => ({
+          file,
+          temp_id: uuid(),
+          subject: UPLOAD_SUBJECTS.EDITOR,
+          target: UPLOAD_TARGETS.NODES,
+          type: UPLOAD_TYPES.IMAGE,
+        })
+      );
+
+      const temps = items.map(file => file.temp_id);
+
+      setTemp([...temp, ...temps]);
+      uploadUploadFiles(items);
+    },
+    [setTemp, uploadUploadFiles, temp]
+  );
+
+  const onFileAdd = useCallback(
+    (file: IFile) => {
+      setData(assocPath(['files'], append(file, data.files), data));
+    },
+    [data, setData]
+  );
+
+  // const onDrop = useCallback(
+  //   (event: React.DragEvent<HTMLDivElement>) => {
+  //     event.preventDefault();
+
+  //     if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length)
+  //       return;
+
+  //     onUpload(Array.from(event.dataTransfer.files));
+  //   },
+  //   [onUpload]
+  // );
+
+  useEffect(() => {
+    window.addEventListener('dragover', eventPreventer, false);
+    window.addEventListener('drop', eventPreventer, false);
+
+    return () => {
+      window.removeEventListener('dragover', eventPreventer, false);
+      window.removeEventListener('drop', eventPreventer, false);
+    };
+  }, [eventPreventer]);
+
+  useEffect(() => {
+    Object.entries(statuses).forEach(([id, status]) => {
+      if (temp.includes(id) && !!status.uuid && files[status.uuid]) {
+        onFileAdd(files[status.uuid]);
+        setTemp(temp.filter(el => el !== id));
+      }
+    });
+  }, [statuses, files, temp, onFileAdd]);
+
+  const onInputChange = useCallback(
+    event => {
+      event.preventDefault();
+
+      if (!event.target.files || !event.target.files.length) return;
+
+      onUpload(Array.from(event.target.files));
+    },
+    [onUpload]
+  );
+
+  return (
+    <div className={styles.wrap}>
+      <input type="file" onChange={onInputChange} accept="image/*" multiple />
+
+      <div className={styles.icon}>
+        <Icon size={32} icon="plus" />
+      </div>
     </div>
-  </div>
-);
+  );
+};
+
+const EditorUploadButton = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(EditorUploadButtonUnconnected);
 
 export { EditorUploadButton };
diff --git a/src/components/editors/ImageEditor/index.tsx b/src/components/editors/ImageEditor/index.tsx
index 5eb13eed..aecf0a90 100644
--- a/src/components/editors/ImageEditor/index.tsx
+++ b/src/components/editors/ImageEditor/index.tsx
@@ -1,10 +1,12 @@
-import React, { FC, ChangeEventHandler, DragEventHandler } from 'react';
+import React, { FC, ChangeEventHandler, DragEventHandler, useCallback } from 'react';
 import { connect } from 'react-redux';
 import { INode } from '~/redux/types';
 import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
 import { selectUploads } from '~/redux/uploads/selectors';
 import { ImageGrid } from '~/components/editors/ImageGrid';
 import { IUploadStatus } from '~/redux/uploads/reducer';
+import { moveArrItem } from '~/utils/fn';
+import assocPath from 'ramda/es/assocPath';
 
 const mapStateToProps = selectUploads;
 const mapDispatchToProps = {
@@ -21,19 +23,9 @@ type IProps = ReturnType<typeof mapStateToProps> &
     onInputChange: ChangeEventHandler<HTMLInputElement>;
   };
 
-const ImageEditorUnconnected: FC<IProps> = ({
-  data,
-  onFileMove,
-  onInputChange,
-  pending_files,
-}) => (
-  <ImageGrid
-    onFileMove={onFileMove}
-    items={data.files}
-    locked={pending_files}
-    onUpload={onInputChange}
-  />
-);
+const ImageEditorUnconnected: FC<IProps> = ({ data, setData, pending_files }) => {
+  return <ImageGrid data={data} setData={setData} locked={pending_files} />;
+};
 
 const ImageEditor = connect(
   mapStateToProps,
diff --git a/src/components/editors/ImageGrid/index.tsx b/src/components/editors/ImageGrid/index.tsx
index 8b7f5052..7a2ef947 100644
--- a/src/components/editors/ImageGrid/index.tsx
+++ b/src/components/editors/ImageGrid/index.tsx
@@ -1,16 +1,19 @@
-import React, { FC, useCallback, ChangeEventHandler, DragEventHandler } from 'react';
-import { SortableContainer, SortableElement } from 'react-sortable-hoc';
+import React, { FC, useCallback } from 'react';
+import { SortableContainer, SortableElement, SortEvent, SortEnd } from 'react-sortable-hoc';
 import * as styles from './styles.scss';
 import { ImageUpload } from '~/components/upload/ImageUpload';
-import { IFile } from '~/redux/types';
+import { IFile, INode } from '~/redux/types';
 import { IUploadStatus } from '~/redux/uploads/reducer';
 import { getURL } from '~/utils/dom';
+import assocPath from 'ramda/es/assocPath';
+import { moveArrItem } from '~/utils/fn';
 
 interface IProps {
-  items: IFile[];
+  data: INode;
+  setData: (val: INode) => void;
   locked: IUploadStatus[];
-  onFileMove: (o: number, n: number) => void;
-  onUpload?: ChangeEventHandler<HTMLInputElement>;
+  // items: IFile[];
+  // onFileMove: (o: number, n: number) => void;
 }
 
 const SortableItem = SortableElement(({ children }) => (
@@ -18,14 +21,7 @@ const SortableItem = SortableElement(({ children }) => (
 ));
 
 const SortableList = SortableContainer(
-  ({
-    items,
-    locked,
-  }: {
-    items: IFile[];
-    locked: IUploadStatus[];
-    onUpload: ChangeEventHandler<HTMLInputElement>;
-  }) => (
+  ({ items, locked }: { items: IFile[]; locked: IUploadStatus[] }) => (
     <div className={styles.grid}>
       {items.map((file, index) => (
         <SortableItem key={file.id} index={index} collection={0}>
@@ -42,18 +38,20 @@ const SortableList = SortableContainer(
   )
 );
 
-const ImageGrid: FC<IProps> = ({ items, locked, onFileMove, onUpload }) => {
-  const onMove = useCallback(({ oldIndex, newIndex }) => onFileMove(oldIndex, newIndex), [
-    onFileMove,
-  ]);
+const ImageGrid: FC<IProps> = ({ data, setData, locked }) => {
+  const onMove = useCallback(
+    ({ oldIndex, newIndex }: SortEnd) => {
+      setData(assocPath(['files'], moveArrItem(oldIndex, newIndex, data.files), data));
+    },
+    [data, setData]
+  );
 
   return (
     <SortableList
       onSortEnd={onMove}
       axis="xy"
-      items={items}
+      items={data.files}
       locked={locked}
-      onUpload={onUpload}
       pressDelay={window.innerWidth < 768 ? 200 : 0}
       helperClass={styles.helper}
     />
diff --git a/src/containers/dialogs/EditorDialog/index.tsx b/src/containers/dialogs/EditorDialog/index.tsx
index c9332066..8b0a8a53 100644
--- a/src/containers/dialogs/EditorDialog/index.tsx
+++ b/src/containers/dialogs/EditorDialog/index.tsx
@@ -14,12 +14,8 @@ import * as styles from './styles.scss';
 import { selectNode } from '~/redux/node/selectors';
 import { ImageEditor } from '~/components/editors/ImageEditor';
 import { EditorPanel } from '~/components/editors/EditorPanel';
-import { moveArrItem } from '~/utils/fn';
-import { IFile, IFileWithUUID } from '~/redux/types';
-import * as UPLOAD_ACTIONS from '~/redux/uploads/actions';
 import * as NODE_ACTIONS from '~/redux/node/actions';
 import { selectUploads } from '~/redux/uploads/selectors';
-import { UPLOAD_TARGETS, UPLOAD_TYPES, UPLOAD_SUBJECTS } from '~/redux/uploads/constants';
 
 const mapStateToProps = state => {
   const { editor } = selectNode(state);
@@ -29,101 +25,15 @@ const mapStateToProps = state => {
 };
 
 const mapDispatchToProps = {
-  uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles,
   nodeSave: NODE_ACTIONS.nodeSave,
 };
 
 type IProps = IDialogProps & ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
 
-const EditorDialogUnconnected: FC<IProps> = ({
-  onRequestClose,
-  editor,
-  files,
-  statuses,
-
-  uploadUploadFiles,
-  nodeSave,
-}) => {
+const EditorDialogUnconnected: FC<IProps> = ({ onRequestClose, editor, statuses, nodeSave }) => {
   const [data, setData] = useState(editor);
-  const eventPreventer = useCallback(event => event.preventDefault(), []);
   const [temp, setTemp] = useState([]);
 
-  const onUpload = useCallback(
-    (uploads: File[]) => {
-      const items: IFileWithUUID[] = Array.from(uploads).map(
-        (file: File): IFileWithUUID => ({
-          file,
-          temp_id: uuid(),
-          subject: UPLOAD_SUBJECTS.EDITOR,
-          target: UPLOAD_TARGETS.NODES,
-          type: UPLOAD_TYPES.IMAGE,
-        })
-      );
-
-      const temps = items.map(file => file.temp_id);
-
-      setTemp([...temp, ...temps]);
-      uploadUploadFiles(items);
-    },
-    [setTemp, uploadUploadFiles, temp]
-  );
-
-  const onFileMove = useCallback(
-    (old_index: number, new_index: number) => {
-      setData(assocPath(['files'], moveArrItem(old_index, new_index, data.files), data));
-    },
-    [data, setData]
-  );
-
-  const onFileAdd = useCallback(
-    (file: IFile) => {
-      setData(assocPath(['files'], append(file, data.files), data));
-    },
-    [data, setData]
-  );
-
-  const onDrop = useCallback(
-    (event: React.DragEvent<HTMLDivElement>) => {
-      event.preventDefault();
-
-      if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length)
-        return;
-
-      onUpload(Array.from(event.dataTransfer.files));
-    },
-    [onUpload]
-  );
-
-  const onInputChange = useCallback(
-    event => {
-      event.preventDefault();
-
-      if (!event.target.files || !event.target.files.length) return;
-
-      onUpload(Array.from(event.target.files));
-    },
-    [onUpload]
-  );
-
-  useEffect(() => {
-    window.addEventListener('dragover', eventPreventer, false);
-    window.addEventListener('drop', eventPreventer, false);
-
-    return () => {
-      window.removeEventListener('dragover', eventPreventer, false);
-      window.removeEventListener('drop', eventPreventer, false);
-    };
-  }, [eventPreventer]);
-
-  useEffect(() => {
-    Object.entries(statuses).forEach(([id, status]) => {
-      if (temp.includes(id) && !!status.uuid && files[status.uuid]) {
-        onFileAdd(files[status.uuid]);
-        setTemp(temp.filter(el => el !== id));
-      }
-    });
-  }, [statuses, files, temp, onFileAdd]);
-
   const setTitle = useCallback(
     title => {
       setData({ ...data, title });
@@ -141,7 +51,7 @@ const EditorDialogUnconnected: FC<IProps> = ({
 
   const buttons = (
     <Padder style={{ position: 'relative' }}>
-      <EditorPanel data={data} setData={setData} onUpload={onInputChange} />
+      <EditorPanel data={data} setData={setData} temp={temp} setTemp={setTemp} />
 
       <Group horizontal>
         <InputText title="Название" value={data.title} handler={setTitle} autoFocus />
@@ -156,14 +66,11 @@ const EditorDialogUnconnected: FC<IProps> = ({
   return (
     <form onSubmit={onSubmit} className={styles.form}>
       <ScrollDialog buttons={buttons} width={860} onClose={onRequestClose}>
-        <div className={styles.editor} onDrop={onDrop}>
+        <div className={styles.editor}>
           <ImageEditor
             data={data}
             pending_files={temp.filter(id => !!statuses[id]).map(id => statuses[id])}
             setData={setData}
-            onUpload={onInputChange}
-            onFileMove={onFileMove}
-            onInputChange={onInputChange}
           />
         </div>
       </ScrollDialog>