From a90e92bb3a6050149b62c76073b34cf42e6a5506 Mon Sep 17 00:00:00 2001 From: ramfox Date: Mon, 11 Nov 2019 16:54:57 -0500 Subject: [PATCH] feat(App): drag and drop adds file to `CreateModal` - adjust `CreateDataset` to take a filePath. We pass that filePath through our `validName` function to create a name that will work with Qri. When a filePath is given, we display all the values in the modal that we know about. Eventually this modal will be removed in favor of desktop making smart assumptions, but before we can do that we need to understand how users can set or change those assumptions, ie the dataset name and default directory locations --- app/components/App.tsx | 36 +++++++++++++++++++------ app/components/modals/CreateDataset.tsx | 31 +++++++++++---------- app/containers/AppContainer.tsx | 2 ++ app/reducers/ui.ts | 4 ++- app/scss/_drag-drop.scss | 24 ++++++++++++++++- 5 files changed, 73 insertions(+), 24 deletions(-) diff --git a/app/components/App.tsx b/app/components/App.tsx index e03e9f70..c8d34e9f 100644 --- a/app/components/App.tsx +++ b/app/components/App.tsx @@ -4,6 +4,10 @@ import { Action } from 'redux' import { CSSTransition } from 'react-transition-group' import { HashRouter as Router } from 'react-router-dom' import { ipcRenderer } from 'electron' +import path from 'path' + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faFileMedical } from '@fortawesome/free-solid-svg-icons' // import components import Toast from './Toast' @@ -21,8 +25,9 @@ import RoutesContainer from '../containers/RoutesContainer' // import models import { ApiAction } from '../store/api' import { Modal, ModalType, HideModal } from '../models/modals' -import { Toast as IToast, Selections } from '../models/store' +import { Toast as IToast, Selections, ToastType } from '../models/store' import { Dataset } from '../models/dataset' +import { ToastTypes } from './chrome/Toast' export const QRI_CLOUD_ROOT = 'https://qri.cloud' @@ -43,6 +48,7 @@ export interface AppProps { datasetDirPath: string qriCloudAuthenticated: boolean toast: IToast + openToast: (type: ToastType, message: string) => Action modal: Modal workingDataset: Dataset exportPath: string @@ -171,6 +177,7 @@ class App extends React.Component { onSubmit={this.props.initDataset} onDismissed={async () => setModal(noModalObject)} setDatasetDirPath={this.props.setDatasetDirPath} + filePath={modal.bodyPath ? modal.bodyPath : ''} /> ) break @@ -269,30 +276,41 @@ class App extends React.Component { event.stopPropagation() event.preventDefault() this.setState({ showDragDrop: true }) - console.log('enter') }} onDragOver={(event) => { event.stopPropagation() event.preventDefault() this.setState({ showDragDrop: true }) - console.log('over') }} onDragLeave={(event) => { event.stopPropagation() event.preventDefault() this.setState({ showDragDrop: false }) - console.log('leave') }} onDrop={(event) => { this.setState({ showDragDrop: false }) - console.log(event.dataTransfer.files[0].name) event.preventDefault() - console.log('drop') + const ext = path.extname(event.dataTransfer.files[0].path) + this.props.closeToast() + if (!(ext === '.csv' || ext === '.json')) { + // open toast for 1 second + this.props.openToast(ToastTypes.error, 'unsupported file format: only json and csv supported') + setTimeout(() => this.props.closeToast(), 2500) + return + } + this.props.setModal({ + type: ModalType.CreateDataset, + bodyPath: event.dataTransfer.files[0].path + }) }} className='drag-drop' id='drag-drop' > - DRAG AND DROP! +
+
Create a new dataset!
+
+
+
You can import csv and json files
) } @@ -311,7 +329,9 @@ class App extends React.Component { return (
{ - this.setState({ showDragDrop: true }) + if (this.props.modal.type === ModalType.NoModal) { + this.setState({ showDragDrop: true }) + } }} style={{ height: '100%', diff --git a/app/components/modals/CreateDataset.tsx b/app/components/modals/CreateDataset.tsx index 9450bcb5..c6ef68ad 100644 --- a/app/components/modals/CreateDataset.tsx +++ b/app/components/modals/CreateDataset.tsx @@ -18,6 +18,7 @@ interface CreateDatasetProps { onSubmit: (path: string, name: string, dir: string, mkdir: string) => Promise datasetDirPath: string setDatasetDirPath: (path: string) => Action + filePath: string } const CreateDataset: React.FunctionComponent = (props) => { @@ -25,11 +26,21 @@ const CreateDataset: React.FunctionComponent = (props) => { onDismissed, onSubmit, datasetDirPath: persistedDatasetDirPath, - setDatasetDirPath: saveDatasetDirPath + setDatasetDirPath: saveDatasetDirPath, + filePath: givenFilePath } = props - const [datasetName, setDatasetName] = React.useState('') + + const validName = (name: string): string => { + // cast name to meet our specification + // make lower case, snakecase, and remove invalid characters + let coercedName = changeCase.lowerCase(name) + coercedName = changeCase.snakeCase(name) + return coercedName.replace(/^[^a-z0-9_]+$/g, '') + } + const [datasetDirPath, setDatasetDirPath] = React.useState(persistedDatasetDirPath) - const [filePath, setFilePath] = React.useState('') + const [filePath, setFilePath] = React.useState(givenFilePath) + const [datasetName, setDatasetName] = React.useState(validName(path.basename(filePath, path.extname(filePath)))) const [dismissable, setDismissable] = React.useState(true) const [buttonDisabled, setButtonDisabled] = React.useState(true) @@ -78,16 +89,6 @@ const CreateDataset: React.FunctionComponent = (props) => { } } - const tryToSetValidName = (name: string) => { - // cast name to meet our specification - // make lower case, snakecase, and remove invalid characters - let coercedName = changeCase.lowerCase(name) - coercedName = changeCase.snakeCase(name) - coercedName = coercedName.replace(/^[^a-z0-9_]+$/g, '') - - setDatasetName(coercedName) - } - const showFilePicker = () => { const window = remote.getCurrentWindow() const filePath: string[] | undefined = remote.dialog.showOpenDialog(window, { @@ -102,7 +103,9 @@ const CreateDataset: React.FunctionComponent = (props) => { const basename = path.basename(selectedPath) const name = basename.split('.')[0] - name && datasetName === '' && tryToSetValidName(name) + if (name && datasetName === '') { + setDatasetName(validName(name)) + } setFilePath(selectedPath) const isDataset = isQriDataset(selectedPath) diff --git a/app/containers/AppContainer.tsx b/app/containers/AppContainer.tsx index 739d27b5..106de291 100644 --- a/app/containers/AppContainer.tsx +++ b/app/containers/AppContainer.tsx @@ -16,6 +16,7 @@ import { import { acceptTOS, setQriCloudAuthenticated, + openToast, closeToast, setModal, setDatasetDirPath, @@ -66,6 +67,7 @@ const AppContainer = connect( addDataset: addDatasetAndFetch, initDataset: initDatasetAndFetch, linkDataset: linkDatasetAndFetch, + openToast, closeToast, pingApi, setWorkingDataset, diff --git a/app/reducers/ui.ts b/app/reducers/ui.ts index 9973c573..8573533a 100644 --- a/app/reducers/ui.ts +++ b/app/reducers/ui.ts @@ -4,6 +4,7 @@ import { ipcRenderer } from 'electron' import store from '../utils/localStore' import { apiActionTypes } from '../utils/actionType' import { SAVE_SUCC, SAVE_FAIL } from '../reducers/mutations' +import { ModalType } from '../models/modals' export const UI_TOGGLE_DATASET_LIST = 'UI_TOGGLE_DATASET_LIST' export const UI_SET_SIDEBAR_WIDTH = 'UI_SET_SIDEBAR_WIDTH' @@ -49,7 +50,8 @@ const initialState = { blockMenus: true, hideCommitNudge: store().getItem(hideCommitNudge) === 'true', datasetDirPath: store().getItem('datasetDirPath') || '', - exportPath: store().getItem('exportPath') || '' + exportPath: store().getItem('exportPath') || '', + modal: { type: ModalType.NoModal } } // send an event to electron to block menus on first load diff --git a/app/scss/_drag-drop.scss b/app/scss/_drag-drop.scss index 3b7c58e7..456159f3 100644 --- a/app/scss/_drag-drop.scss +++ b/app/scss/_drag-drop.scss @@ -1,13 +1,35 @@ .drag-drop { - background: rgba($color: #000000, $alpha: .6); + background: rgba($color: #ffffff, $alpha: .8); position: absolute; width: 100%; height: 100%; display: flex; + flex-direction: column; justify-content: center; align-items: center; top: 0; right: 0; left: 0; bottom: 0; + + .inner { + width: 80%; + height: 70%; + border: .5rem dashed rgba($color: #000000, $alpha: .4); + border-radius: 5px; + display: flex; + flex-direction: column; + justify-content:center; + align-items: center; + + .icon { + color: rgba($color: #000000, $alpha: .4); + } + } + .footer { + position: relative; + bottom: -5%; + background: white; + color: rgba($color: #000000, $alpha: 1); + } } \ No newline at end of file