From acee298f6575941bcc381bf62003813ffa765b4c Mon Sep 17 00:00:00 2001
From: Alexandru Munteanu <alexandru.munt@gmail.com>
Date: Tue, 21 Aug 2018 08:54:36 +0300
Subject: [PATCH] feat(visual): file section component

---
 packages/component-faraday-ui/src/File.js     | 27 +++++---
 .../component-faraday-ui/src/FileSection.js   | 48 +++++++++++--
 .../component-faraday-ui/src/FileSection.md   | 69 +++++++++++--------
 .../component-faraday-ui/src/SortableList.md  |  2 +-
 .../component-faraday-ui/src/helpers/index.js |  2 +
 .../src/helpers/withFileSectionDrop.js        | 45 ++++++++++++
 .../src/helpers/withNativeFileDrop.js         | 34 +++++++++
 packages/styleguide/src/Wrapper.js            |  6 +-
 .../styleguide/src/withDragDropContext.js     |  4 ++
 packages/styleguide/styleguide.config.js      |  4 ++
 10 files changed, 197 insertions(+), 44 deletions(-)
 create mode 100644 packages/component-faraday-ui/src/helpers/index.js
 create mode 100644 packages/component-faraday-ui/src/helpers/withFileSectionDrop.js
 create mode 100644 packages/component-faraday-ui/src/helpers/withNativeFileDrop.js
 create mode 100644 packages/styleguide/src/withDragDropContext.js

diff --git a/packages/component-faraday-ui/src/File.js b/packages/component-faraday-ui/src/File.js
index a9c5252ef..5b870944b 100644
--- a/packages/component-faraday-ui/src/File.js
+++ b/packages/component-faraday-ui/src/File.js
@@ -31,12 +31,13 @@ const hasPreview = (name = '') => {
 }
 
 const FileItem = ({
-  item: file,
   fileSize,
   onPreview,
+  item: file,
   removeFile,
   hasPreview,
   onDownload,
+  onDelete,
   dragHandle = null,
   ...rest
 }) => (
@@ -49,21 +50,22 @@ const FileItem = ({
     {hasPreview && (
       <IconButton
         icon="eye"
-        iconSize={3}
+        iconSize={2}
         ml={1}
         mr={1}
-        onClick={onPreview(file)}
+        onClick={onPreview}
         secondary
       />
     )}
     <IconButton
       icon="download"
-      iconSize={3}
-      ml={1}
+      iconSize={2}
+      ml={hasPreview ? 0 : 1}
       mr={1}
-      onClick={onDownload(file)}
+      onClick={onDownload}
       secondary
     />
+    <IconButton icon="trash" iconSize={2} mr={1} onClick={onDelete} secondary />
   </Root>
 )
 
@@ -80,6 +82,8 @@ FileItem.propTypes = {
   onPreview: PropTypes.func,
   /** Handler for the download button. */
   onDownload: PropTypes.func,
+  /** Handler for the delete button. */
+  onDelete: PropTypes.func,
 }
 
 export default compose(
@@ -88,11 +92,14 @@ export default compose(
     fileSize: parseFileSize(size),
   })),
   withHandlers({
-    onDownload: ({ onDownload }) => file => () => {
-      onDownload(file)
+    onDownload: ({ onDownload, item }) => () => {
+      typeof onDownload === 'function' && onDownload(item)
+    },
+    onPreview: ({ onPreview, item }) => () => {
+      typeof onPreview === 'function' && onPreview(item)
     },
-    onPreview: ({ onPreview }) => file => () => {
-      onPreview(file)
+    onDelete: ({ onDelete, item }) => () => {
+      typeof onDelete === 'function' && onDelete(item)
     },
   }),
 )(FileItem)
diff --git a/packages/component-faraday-ui/src/FileSection.js b/packages/component-faraday-ui/src/FileSection.js
index 2590b3a1c..3901be09f 100644
--- a/packages/component-faraday-ui/src/FileSection.js
+++ b/packages/component-faraday-ui/src/FileSection.js
@@ -2,9 +2,10 @@ import React from 'react'
 import styled from 'styled-components'
 import { th } from '@pubsweet/ui-toolkit'
 import { FilePicker } from '@pubsweet/ui'
-import { compose, withProps } from 'recompose'
+import { compose, withStateHandlers, withProps } from 'recompose'
 
 import { radiusHelpers } from './styledHelpers'
+import { withNativeFileDrop, withFileSectionDrop } from './helpers'
 import {
   Row,
   Item,
@@ -24,21 +25,41 @@ const EXTENSIONS = {
 }
 
 const FileSection = ({
+  error,
   title,
   isLast,
+  listId,
   isFirst,
   required,
-  files = [],
+  moveItem,
+  isFileItemOver,
+  canDropFileItem,
+  connectFileDrop,
   supportedFormats,
+  connectDropTarget,
   allowedFileExtensions,
+  files = [],
+  onFilePick = () => {},
+  onPreview,
+  onDownload,
+  onDelete,
 }) => (
-  <Root isFirst={isFirst} isLast={isLast}>
+  <Root
+    innerRef={instance => {
+      connectFileDrop(instance)
+      connectDropTarget(instance)
+    }}
+    isFileItemOver={isFileItemOver && canDropFileItem}
+    isFirst={isFirst}
+    isLast={isLast}
+  >
+    {error}
     <Row alignItems="center">
       <Item>
         <Label required={required}>{title}</Label>
         <FilePicker
           allowedFileExtensions={allowedFileExtensions}
-          onUpload={file => {}}
+          onUpload={onFilePick}
         >
           <ActionLink icon="plus" size="small">
             UPLOAD FILE
@@ -54,25 +75,42 @@ const FileSection = ({
       )}
     </Row>
     <SortableList
+      beginDragProps={['id', 'index', 'name', 'listId']}
       dragHandle={DragHandle}
       items={files}
+      listId={listId}
       listItem={FileItem}
       mb={1}
+      moveItem={moveItem}
+      onDelete={onDelete}
+      onDownload={onDownload}
+      onPreview={onPreview}
     />
   </Root>
 )
 
 export default compose(
+  withStateHandlers(
+    { error: '' },
+    {
+      setError: () => error => ({
+        error,
+      }),
+    },
+  ),
   withProps(({ allowedFileExtensions = [] }) => ({
     supportedFormats: allowedFileExtensions
       .map(ext => EXTENSIONS[ext.toLowerCase()])
       .join(', '),
   })),
+  withFileSectionDrop,
+  withNativeFileDrop,
 )(FileSection)
 
 // #region styles
 const Root = styled.div`
-  background: ${th('colorBackground')};
+  background: ${props =>
+    props.isFileItemOver ? th('colorFurniture') : th('colorBackground')};
   min-height: calc(${th('gridUnit')} * 22);
   padding: 0 ${th('gridUnit')};
 
diff --git a/packages/component-faraday-ui/src/FileSection.md b/packages/component-faraday-ui/src/FileSection.md
index e67f554e0..c3a1b5f9e 100644
--- a/packages/component-faraday-ui/src/FileSection.md
+++ b/packages/component-faraday-ui/src/FileSection.md
@@ -1,4 +1,4 @@
-A section that shows FileItems. Drag and drop support.
+Sections on top of each other.
 
 ```js
 const files = [
@@ -9,31 +9,7 @@ const files = [
   },
   {
     id: 'file2',
-    name: 'myfile.docx',
-    size: 133127,
-  },
-];
-
-<FileSection
-  files={files}
-  title="Main Manuscript"
-  required
-  allowedFileExtensions={['pdf', 'doc', 'docx']}
-/>
-```
-
-Multiple sections on top of each other.
-
-```js
-const files = [
-  {
-    id: 'file1',
-    name: 'myfile.docx',
-    size: 51312,
-  },
-  {
-    id: 'file2',
-    name: 'myfile.docx',
+    name: 'another_pdf.pdf',
     size: 133127,
   },
 ];
@@ -42,16 +18,55 @@ const files = [
   <FileSection
     isFirst
     required
+    listId="mainManuscript"
     files={files}
     title="Main Manuscript"
     allowedFileExtensions={['pdf', 'doc', 'docx']}
+    onFileDrop={f => console.log('dropped a native file', f)}
+    onFilePick={f => console.log('picked a file', f)}
+    moveItem={
+      (dragIndex, hoverIndex) => console.log('moving the item from', dragIndex, hoverIndex)
+    }
+    changeList={
+      (from, to, fileId) => console.log('change from to', from, to, fileId)
+    }
+    onDelete={f => console.log('delete', f)}
+    onPreview={f => console.log('preview', f)}
+    onDownload={f => console.log('download', f)}
   />
   <FileSection
     required
     title="Cover Letter"
     files={files}
+    listId="coverLetter"
     allowedFileExtensions={['pdf', 'doc', 'docx']}
+    onFileDrop={f => console.log('dropped a native file', f)}
+    onFilePick={f => console.log('picked a file', f)}
+    moveItem={
+      (dragIndex, hoverIndex) => console.log('moving the item from', dragIndex, hoverIndex)
+    }
+    changeList={
+      (from, to, fileId) => console.log('change from to', from, to, fileId)
+    }
+    onDelete={f => console.log('delete', f)}
+    onPreview={f => console.log('preview', f)}
+    onDownload={f => console.log('download', f)}
   />
-  <FileSection title="Supplimental Files" required isLast />
+  <FileSection
+    files={[]}
+    title="Supplimental Files"
+    listId="supplimentalFiles"
+    required isLast onFileDrop={f => console.log('dropped a native file', f)}
+    onFilePick={f => console.log('picked a file', f)}
+    moveItem={
+      (dragIndex, hoverIndex) => console.log('moving the item from', dragIndex, hoverIndex)
+    }
+    changeList={
+      (from, to, fileId) => console.log('change from to', from, to, fileId)
+    }
+    onDelete={f => console.log('delete', f)}
+    onPreview={f => console.log('preview', f)}
+    onDownload={f => console.log('download', f)}
+    />
 </div>
 ```
diff --git a/packages/component-faraday-ui/src/SortableList.md b/packages/component-faraday-ui/src/SortableList.md
index 4db4cfa6c..33a51c4ba 100644
--- a/packages/component-faraday-ui/src/SortableList.md
+++ b/packages/component-faraday-ui/src/SortableList.md
@@ -51,5 +51,5 @@ class Example extends React.Component {
   }
 }
 
-<Example />
+;<Example />
 ```
diff --git a/packages/component-faraday-ui/src/helpers/index.js b/packages/component-faraday-ui/src/helpers/index.js
new file mode 100644
index 000000000..52df3f99c
--- /dev/null
+++ b/packages/component-faraday-ui/src/helpers/index.js
@@ -0,0 +1,2 @@
+export { default as withNativeFileDrop } from './withNativeFileDrop'
+export { default as withFileSectionDrop } from './withFileSectionDrop'
diff --git a/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js b/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js
new file mode 100644
index 000000000..ce997caf0
--- /dev/null
+++ b/packages/component-faraday-ui/src/helpers/withFileSectionDrop.js
@@ -0,0 +1,45 @@
+import { DropTarget } from 'react-dnd'
+
+export default DropTarget(
+  'item',
+  {
+    drop(
+      {
+        files,
+        maxFiles,
+        setError,
+        changeList,
+        listId: toListId,
+        allowedFileExtensions,
+      },
+      monitor,
+    ) {
+      const { listId: fromListId, id, name } = monitor.getItem()
+      const fileExtention = name.split('.')[1]
+
+      if (
+        allowedFileExtensions &&
+        !allowedFileExtensions.includes(fileExtention)
+      ) {
+        setError('Invalid file type.')
+        return
+      }
+
+      if (files.length >= maxFiles) {
+        setError('No more files can be added to this section.')
+        return
+      }
+      if (toListId === fromListId) return
+      changeList(fromListId, toListId, id)
+    },
+    canDrop({ listId: toListId, setError }, monitor) {
+      const { listId: fromListId } = monitor.getItem()
+      return toListId !== fromListId
+    },
+  },
+  (connect, monitor) => ({
+    isFileItemOver: monitor.isOver(),
+    canDropFileItem: monitor.canDrop(),
+    connectDropTarget: connect.dropTarget(),
+  }),
+)
diff --git a/packages/component-faraday-ui/src/helpers/withNativeFileDrop.js b/packages/component-faraday-ui/src/helpers/withNativeFileDrop.js
new file mode 100644
index 000000000..ab635fdbf
--- /dev/null
+++ b/packages/component-faraday-ui/src/helpers/withNativeFileDrop.js
@@ -0,0 +1,34 @@
+import { DropTarget } from 'react-dnd'
+import { NativeTypes } from 'react-dnd-html5-backend'
+
+export default DropTarget(
+  NativeTypes.FILE,
+  {
+    drop(
+      { onFileDrop, files, maxFiles, setError, allowedFileExtensions },
+      monitor,
+    ) {
+      const [file] = monitor.getItem().files
+      const fileExtention = file.name.split('.')[1]
+
+      if (files.length >= maxFiles) {
+        setError('No more files can be added to this section.')
+        return
+      }
+
+      if (
+        allowedFileExtensions &&
+        !allowedFileExtensions.includes(fileExtention)
+      ) {
+        setError('Invalid file type.')
+      }
+
+      typeof onFileDrop === 'function' && onFileDrop(file)
+    },
+  },
+  (connect, monitor) => ({
+    isFileOver: monitor.isOver(),
+    canDropFile: monitor.canDrop(),
+    connectFileDrop: connect.dropTarget(),
+  }),
+)
diff --git a/packages/styleguide/src/Wrapper.js b/packages/styleguide/src/Wrapper.js
index b4b841f14..2fa36592a 100644
--- a/packages/styleguide/src/Wrapper.js
+++ b/packages/styleguide/src/Wrapper.js
@@ -6,13 +6,15 @@ import hindawiTheme from 'hindawi-theme'
 import { ThemeProvider } from 'styled-components'
 import { createStore, combineReducers } from 'redux'
 
+import withDragDropContext from './withDragDropContext'
+
 const store = createStore(
   combineReducers({
     form: reducer,
   }),
 )
 
-export default class Wrapper extends Component {
+class Wrapper extends Component {
   render() {
     return (
       <Provider store={store}>
@@ -23,3 +25,5 @@ export default class Wrapper extends Component {
     )
   }
 }
+
+export default withDragDropContext(Wrapper)
diff --git a/packages/styleguide/src/withDragDropContext.js b/packages/styleguide/src/withDragDropContext.js
new file mode 100644
index 000000000..4eaa72da2
--- /dev/null
+++ b/packages/styleguide/src/withDragDropContext.js
@@ -0,0 +1,4 @@
+import { DragDropContext } from 'react-dnd'
+import HTML5Backend from 'react-dnd-html5-backend'
+
+export default DragDropContext(HTML5Backend)
diff --git a/packages/styleguide/styleguide.config.js b/packages/styleguide/styleguide.config.js
index 74f9330bd..b0d7ed608 100644
--- a/packages/styleguide/styleguide.config.js
+++ b/packages/styleguide/styleguide.config.js
@@ -4,17 +4,21 @@ module.exports = {
   sections: [
     {
       name: 'Hinadwi UI',
+      sectionDepth: 1,
       components: ['../component-faraday-ui/src/[A-Z]*.js'],
     },
     {
       name: 'Modals',
+      sectionDepth: 1,
       components: ['../component-faraday-ui/src/modals/[A-Z]*.js'],
     },
     {
       name: 'Grid Items',
+      sectionDepth: 1,
       components: ['../component-faraday-ui/src/gridItems/[A-Z]*.js'],
     },
   ],
+  pagePerSection: true,
   styleguideComponents: {
     Wrapper: path.join(__dirname, 'src/Wrapper'),
   },
-- 
GitLab