Skip to content

Commit c3e9a60

Browse files
committed
Add new FileDrop implementation.
1 parent ab736c1 commit c3e9a60

File tree

7 files changed

+319
-146
lines changed

7 files changed

+319
-146
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
"react-dnd": "^2.5.1",
139139
"react-dnd-html5-backend": "^2.5.1",
140140
"react-dom": "^16.8.1",
141-
"react-dropzone": "^4.1.2",
142141
"react-icons": "^2.2.1",
143142
"react-redux": "^5.0.7",
144143
"react-suber": "1.0.4",
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright (c) 2002-2019 "Neo4j,"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Neo4j is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
import React, { useState } from 'react'
22+
import { connect } from 'react-redux'
23+
import { withBus } from 'react-suber'
24+
import SVGInline from 'react-svg-inline'
25+
26+
import * as editor from 'shared/modules/editor/editorDuck'
27+
import { addFavorite } from 'shared/modules/favorites/favoritesDuck'
28+
import { parseGrass } from 'shared/services/grassUtils'
29+
import { updateGraphStyleData } from 'shared/modules/grass/grassDuck'
30+
import {
31+
showErrorMessage,
32+
executeCommand
33+
} from 'shared/modules/commands/commandsDuck'
34+
35+
import {
36+
StyledFileDrop,
37+
StyledFileDropInner,
38+
StyledFileDropContent,
39+
StyledFileDropActions,
40+
StyledFileDropActionButton
41+
} from './styled'
42+
import icon from 'icons/task-list-download.svg'
43+
44+
export function FileDrop (props) {
45+
const [fileHoverState, setFileHoverState] = useState(false)
46+
const [userSelect, setUserSelect] = useState(false)
47+
const [file, setFile] = useState(null)
48+
const { saveCypherToFavorites, importGrass, dispatchErrorMessage } = props
49+
50+
const resetState = () => {
51+
setFileHoverState(false)
52+
setUserSelect(false)
53+
setFile(null)
54+
}
55+
56+
const pasteInEditor = content => {
57+
props.bus && props.bus.send(editor.SET_CONTENT, editor.setContent(content))
58+
}
59+
60+
const fileLoader = (file, callback) => {
61+
const reader = new FileReader()
62+
reader.onload = fileEvent => {
63+
callback(fileEvent.target.result)
64+
resetState()
65+
}
66+
reader.onerror = () => {
67+
dispatchErrorMessage(`Something wen't wrong when reading the file`)
68+
resetState()
69+
}
70+
reader.readAsText(file)
71+
}
72+
73+
const handleDragOver = event => {
74+
event.stopPropagation()
75+
event.preventDefault()
76+
77+
if (!fileHoverState) {
78+
setFileHoverState(true)
79+
}
80+
}
81+
82+
const handleDragLeave = event => {
83+
// Check if we're leaving the browser window
84+
if (
85+
event.clientX <= 0 ||
86+
event.clientY <= 0 ||
87+
window.innerHeight < event.clientY ||
88+
window.innerWidth < event.clientX
89+
) {
90+
resetState()
91+
}
92+
}
93+
94+
const handleDrop = event => {
95+
const files = event.dataTransfer.files
96+
if (files.length === 1) {
97+
event.stopPropagation()
98+
event.preventDefault()
99+
100+
setFile(files[0])
101+
102+
const extension = ((files[0] || {}).name || '').split('.').pop()
103+
if (['cyp', 'cypher', 'cql', 'txt'].includes(extension)) {
104+
setUserSelect(true)
105+
} else if (extension === 'grass') {
106+
fileLoader(files[0], importGrass)
107+
const action = executeCommand(':style')
108+
props.bus.send(action.type, action)
109+
} else {
110+
dispatchErrorMessage(`'.${extension}' is not a valid file extension`)
111+
resetState()
112+
}
113+
}
114+
}
115+
116+
const className = ['filedrop']
117+
if (fileHoverState) {
118+
className.push('has-file-hovering')
119+
}
120+
121+
if (userSelect) {
122+
className.push('has-user-select')
123+
}
124+
125+
return (
126+
<StyledFileDrop
127+
className={className.join(' ')}
128+
onDragOver={handleDragOver}
129+
onDragLeave={handleDragLeave}
130+
onDrop={handleDrop}
131+
>
132+
{props.children}
133+
<StyledFileDropInner onClick={resetState}>
134+
<StyledFileDropContent>
135+
<SVGInline svg={icon} accessibilityLabel={'Import'} width={'14rem'} />
136+
{userSelect && (
137+
<StyledFileDropActions>
138+
<StyledFileDropActionButton
139+
className='filedrop-save-to-favorites'
140+
onClick={() => {
141+
fileLoader(file, saveCypherToFavorites)
142+
}}
143+
>
144+
Save to favorites
145+
</StyledFileDropActionButton>
146+
<StyledFileDropActionButton
147+
className='filedrop-paste-in-editor'
148+
onClick={() => {
149+
fileLoader(file, pasteInEditor)
150+
}}
151+
>
152+
Paste in editor
153+
</StyledFileDropActionButton>
154+
</StyledFileDropActions>
155+
)}
156+
</StyledFileDropContent>
157+
</StyledFileDropInner>
158+
</StyledFileDrop>
159+
)
160+
}
161+
162+
const mapDispatchToProps = dispatch => {
163+
return {
164+
saveCypherToFavorites: file => {
165+
dispatch(addFavorite(file))
166+
},
167+
importGrass: file => {
168+
const parsedGrass = parseGrass(file)
169+
if (parsedGrass) {
170+
dispatch(updateGraphStyleData(parsedGrass))
171+
} else {
172+
dispatch(showErrorMessage('Could not parse grass data'))
173+
}
174+
},
175+
dispatchErrorMessage: message => dispatch(showErrorMessage(message))
176+
}
177+
}
178+
179+
export default withBus(
180+
connect(
181+
null,
182+
mapDispatchToProps
183+
)(FileDrop)
184+
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2002-2019 "Neo4j,"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Neo4j is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
import styled from 'styled-components'
22+
23+
export const StyledFileDrop = styled.div``
24+
25+
export const StyledFileDropInner = styled.div`
26+
display: flex;
27+
flex-direction: column;
28+
justify-content: center;
29+
align-items: center;
30+
height: 100vh;
31+
width: 100vw;
32+
opacity: 0;
33+
overflow: hidden;
34+
position: absolute;
35+
left: 0;
36+
right: 0;
37+
top: 0;
38+
bottom: 0;
39+
pointer-events: none;
40+
41+
.has-file-hovering & {
42+
color: #fff;
43+
background-color: rgba(0, 0, 0, 0.6);
44+
opacity: 1;
45+
transition: opacity 0.2s ease-in-out;
46+
z-index: 1000;
47+
}
48+
49+
.has-user-select & {
50+
pointer-events: all;
51+
}
52+
`
53+
54+
export const StyledFileDropContent = styled.div`
55+
position: relative;
56+
`
57+
58+
export const StyledFileDropActions = styled.div`
59+
display: block;
60+
visibility: hidden;
61+
position: absolute;
62+
left: 50%;
63+
top: calc(100% + 1rem);
64+
transform: translateX(-50%);
65+
width: 100vw;
66+
text-align: center;
67+
pointer-events: none;
68+
69+
.has-user-select & {
70+
visibility: visible;
71+
pointer-events: all;
72+
}
73+
`
74+
75+
export const StyledFileDropActionButton = styled.button`
76+
border: 0;
77+
border-radius: 5px;
78+
color: #000;
79+
background-color: #fff;
80+
padding: 5px 10px;
81+
font-weight: 600;
82+
margin: 0 5px;
83+
`
Lines changed: 1 addition & 0 deletions
Loading

src/browser/modules/App/App.jsx

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import { getExperimentalFeatures } from 'shared/modules/experimentalFeatures/exp
7373
import FeatureToggleProvider from '../FeatureToggle/FeatureToggleProvider'
7474
import { inWebEnv, URL_ARGUMENTS_CHANGE } from 'shared/modules/app/appDuck'
7575
import useDerivedTheme from 'browser-hooks/useDerivedTheme'
76+
import FileDrop from 'browser-components/FileDrop/FileDrop'
7677

7778
export function App (props) {
7879
const [derivedTheme, setEnvironmentTheme] = useDerivedTheme(
@@ -128,57 +129,59 @@ export function App (props) {
128129
<ErrorBoundary>
129130
<ThemeProvider theme={themeData}>
130131
<FeatureToggleProvider features={experimentalFeatures}>
131-
<StyledWrapper>
132-
<DocTitle titleString={props.titleString} />
133-
<UserInteraction />
134-
<DesktopIntegration
135-
integrationPoint={props.desktopIntegrationPoint}
136-
onArgumentsChange={props.onArgumentsChange}
137-
onMount={(
138-
activeGraph,
139-
connectionsCredentials,
140-
context,
141-
getKerberosTicket
142-
) => {
143-
props.setInitialConnectionData(
132+
<FileDrop>
133+
<StyledWrapper className='app-wrapper'>
134+
<DocTitle titleString={props.titleString} />
135+
<UserInteraction />
136+
<DesktopIntegration
137+
integrationPoint={props.desktopIntegrationPoint}
138+
onArgumentsChange={props.onArgumentsChange}
139+
onMount={(
144140
activeGraph,
145141
connectionsCredentials,
146142
context,
147143
getKerberosTicket
148-
)
149-
detectDesktopThemeChanges(null, context)
150-
}}
151-
onGraphActive={props.switchConnection}
152-
onGraphInactive={props.closeConnectionMaybe}
153-
onColorSchemeUpdated={detectDesktopThemeChanges}
154-
/>
155-
<Render if={loadExternalScripts}>
156-
<Intercom appID='lq70afwx' />
157-
</Render>
158-
<Render if={syncConsent && loadExternalScripts && loadSync}>
159-
<BrowserSyncInit
160-
authStatus={browserSyncAuthStatus}
161-
authData={browserSyncMetadata}
162-
config={browserSyncConfig}
144+
) => {
145+
props.setInitialConnectionData(
146+
activeGraph,
147+
connectionsCredentials,
148+
context,
149+
getKerberosTicket
150+
)
151+
detectDesktopThemeChanges(null, context)
152+
}}
153+
onGraphActive={props.switchConnection}
154+
onGraphInactive={props.closeConnectionMaybe}
155+
onColorSchemeUpdated={detectDesktopThemeChanges}
163156
/>
164-
</Render>
165-
<StyledApp>
166-
<StyledBody>
167-
<ErrorBoundary>
168-
<Sidebar openDrawer={drawer} onNavClick={handleNavClick} />
169-
</ErrorBoundary>
170-
<StyledMainWrapper>
171-
<Main
172-
cmdchar={cmdchar}
173-
activeConnection={activeConnection}
174-
connectionState={connectionState}
175-
errorMessage={errorMessage}
176-
useBrowserSync={loadSync}
177-
/>
178-
</StyledMainWrapper>
179-
</StyledBody>
180-
</StyledApp>
181-
</StyledWrapper>
157+
<Render if={loadExternalScripts}>
158+
<Intercom appID='lq70afwx' />
159+
</Render>
160+
<Render if={syncConsent && loadExternalScripts && loadSync}>
161+
<BrowserSyncInit
162+
authStatus={browserSyncAuthStatus}
163+
authData={browserSyncMetadata}
164+
config={browserSyncConfig}
165+
/>
166+
</Render>
167+
<StyledApp>
168+
<StyledBody>
169+
<ErrorBoundary>
170+
<Sidebar openDrawer={drawer} onNavClick={handleNavClick} />
171+
</ErrorBoundary>
172+
<StyledMainWrapper>
173+
<Main
174+
cmdchar={cmdchar}
175+
activeConnection={activeConnection}
176+
connectionState={connectionState}
177+
errorMessage={errorMessage}
178+
useBrowserSync={loadSync}
179+
/>
180+
</StyledMainWrapper>
181+
</StyledBody>
182+
</StyledApp>
183+
</StyledWrapper>
184+
</FileDrop>
182185
</FeatureToggleProvider>
183186
</ThemeProvider>
184187
</ErrorBoundary>

0 commit comments

Comments
 (0)